From 906df86ccfeea28be7e2280fbcbe2a6444e9d03d Mon Sep 17 00:00:00 2001 From: Koopa Date: Fri, 3 Jul 2026 10:05:36 +0800 Subject: [PATCH 1/4] fix(mcp): correct brief morning sections doc key 'tasks' to 'todos' The brief tool's morning `sections` filter matches the runSection label "todos", but three agent-facing docs advertised the key as 'tasks', so an agent following them passed an unmatched key and silently received an empty briefing. Correct the key in the catalog tool description, the BriefInput schema tag, and the skills manual, and complete the todos-section field list (add active_todos, plus recurring_todos in the manual) so the enumerated fields match what the section actually emits. Documentation only; no behaviour change. --- internal/mcp/brief.go | 4 ++-- internal/mcp/ops/catalog.go | 2 +- skills/koopa-system/references/tools.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/mcp/brief.go b/internal/mcp/brief.go index 368a0ae6..22c39702 100644 --- a/internal/mcp/brief.go +++ b/internal/mcp/brief.go @@ -71,7 +71,7 @@ func resolveDefaultSections(caller string) []string { // // Morning group → response field mapping: // -// "todos" → overdue_todos, today_todos, recurring_todos, committed_todos, upcoming_todos +// "todos" → overdue_todos, today_todos, active_todos, recurring_todos, committed_todos, upcoming_todos // "goals" → active_goals // "rss" → rss_highlights // "content_pipeline" → content_pipeline @@ -80,7 +80,7 @@ func resolveDefaultSections(caller string) []string { type BriefInput struct { As string `json:"as,omitempty" jsonschema_description:"Caller agent identity (e.g. koopa0-dev)."` Mode string `json:"mode" jsonschema_description:"Briefing mode (required): 'morning' = daily-planning pull (todos/goals/rss/content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (daily plan items + completion counts). brief is a pure planning-state pull and carries no agent memory."` - Sections FlexStringSlice `json:"sections,omitempty" jsonschema_description:"MORNING-ONLY strict filter on which groups to populate (default: all). Ignored in reflection mode. Omit or pass [] to get the full morning briefing. Group key → response fields: 'tasks' → overdue_todos/today_todos/recurring_todos/committed_todos/upcoming_todos; 'goals' → active_goals; 'rss' → rss_highlights; 'content_pipeline' → content_pipeline. Unknown keys silently ignored."` + Sections FlexStringSlice `json:"sections,omitempty" jsonschema_description:"MORNING-ONLY strict filter on which groups to populate (default: all). Ignored in reflection mode. Omit or pass [] to get the full morning briefing. Group key → response fields: 'todos' → overdue_todos/today_todos/active_todos/recurring_todos/committed_todos/upcoming_todos; 'goals' → active_goals; 'rss' → rss_highlights; 'content_pipeline' → content_pipeline. Unknown keys silently ignored."` Date *string `json:"date,omitempty" jsonschema_description:"Target date YYYY-MM-DD (default: today)"` } diff --git a/internal/mcp/ops/catalog.go b/internal/mcp/ops/catalog.go index c142be8d..d6304350 100644 --- a/internal/mcp/ops/catalog.go +++ b/internal/mcp/ops/catalog.go @@ -32,7 +32,7 @@ func Brief() Meta { Writability: ReadOnly, Stability: StabilityStable, Since: since, - Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'tasks' (overdue/today/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", + Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'todos' (overdue/today/active/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", FieldEnums: map[string][]string{ "mode": {"morning", "reflection"}, }, diff --git a/skills/koopa-system/references/tools.md b/skills/koopa-system/references/tools.md index a2b478d6..e83dc529 100644 --- a/skills/koopa-system/references/tools.md +++ b/skills/koopa-system/references/tools.md @@ -62,7 +62,7 @@ Morning mode is filterable via `sections` (omit or pass `[]` for all). Valid key | Section | Returns | |---|---| -| `tasks` | overdue / today / committed / upcoming todos | +| `todos` | overdue / today / active / recurring / committed / upcoming todos | | `goals` | active_goals | | `rss` | rss_highlights — feeds tagged `priority=high`, NOT relevance-ranked; use `search_knowledge` for ranked retrieval | | `content_pipeline` | content_pipeline | From 50dde6c6ac6c11c5c1d7c2af353732ed21ccb4ce Mon Sep 17 00:00:00 2001 From: Koopa Date: Fri, 3 Jul 2026 10:06:42 +0800 Subject: [PATCH 2/4] feat(mcp): add proposals section to morning brief MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose proposals_pending — the summed count of agent-proposed area, goal, and project drafts awaiting owner triage — as a new "proposals" section on brief(mode=morning). This lets the hermes push agent read the triage backlog over the MCP surface (its existing access path) to decide whether to nudge Koopa back to the admin triage queue, instead of holding admin API credentials just to read the count. proposals_pending is a scalar on the morning wire (always emitted, 0 when nothing is pending); the consumer gates its nudge on N > 0. fillProposalsPending reads the goal and project proposed-counts independently and each degrades to its own zero on error: a transient failure on one entity undercounts rather than suppressing the whole signal, which for a "don't let the queue rot" nudge is the safer failure mode than all-or-nothing. Covered by a morning-wire unit test and an integration test asserting the sum across all three proposed-entity kinds plus the section-filter wiring. --- internal/mcp/brief.go | 72 +++++++++++++++++-------- internal/mcp/integration_test.go | 60 +++++++++++++++++++++ internal/mcp/morning_test.go | 46 ++++++++++++++++ internal/mcp/ops/catalog.go | 2 +- skills/koopa-system/references/tools.md | 1 + 5 files changed, 159 insertions(+), 22 deletions(-) diff --git a/internal/mcp/brief.go b/internal/mcp/brief.go index 22c39702..96e5d978 100644 --- a/internal/mcp/brief.go +++ b/internal/mcp/brief.go @@ -75,12 +75,13 @@ func resolveDefaultSections(caller string) []string { // "goals" → active_goals // "rss" → rss_highlights // "content_pipeline" → content_pipeline +// "proposals" → proposals_pending // // Unknown group names are ignored silently (no error, no warning). type BriefInput struct { As string `json:"as,omitempty" jsonschema_description:"Caller agent identity (e.g. koopa0-dev)."` Mode string `json:"mode" jsonschema_description:"Briefing mode (required): 'morning' = daily-planning pull (todos/goals/rss/content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (daily plan items + completion counts). brief is a pure planning-state pull and carries no agent memory."` - Sections FlexStringSlice `json:"sections,omitempty" jsonschema_description:"MORNING-ONLY strict filter on which groups to populate (default: all). Ignored in reflection mode. Omit or pass [] to get the full morning briefing. Group key → response fields: 'todos' → overdue_todos/today_todos/active_todos/recurring_todos/committed_todos/upcoming_todos; 'goals' → active_goals; 'rss' → rss_highlights; 'content_pipeline' → content_pipeline. Unknown keys silently ignored."` + Sections FlexStringSlice `json:"sections,omitempty" jsonschema_description:"MORNING-ONLY strict filter on which groups to populate (default: all). Ignored in reflection mode. Omit or pass [] to get the full morning briefing. Group key → response fields: 'todos' → overdue_todos/today_todos/active_todos/recurring_todos/committed_todos/upcoming_todos; 'goals' → active_goals; 'rss' → rss_highlights; 'content_pipeline' → content_pipeline; 'proposals' → proposals_pending (count of agent-proposed area/goal/project drafts awaiting owner triage). Unknown keys silently ignored."` Date *string `json:"date,omitempty" jsonschema_description:"Target date YYYY-MM-DD (default: today)"` } @@ -93,15 +94,16 @@ type BriefOutput struct { Date string `json:"date"` // Morning fields. - OverdueTodos []todo.PendingDetail `json:"-"` - TodayTodos []todo.PendingDetail `json:"-"` - ActiveTodos []todo.PendingDetail `json:"-"` - RecurringTodos []todo.Item `json:"-"` - CommittedTodos []daily.Item `json:"-"` - UpcomingTodos []todo.PendingDetail `json:"-"` - ActiveGoals []goal.ActiveGoalSummary `json:"-"` - RSSHighlights []RSSHighlight `json:"-"` - ContentPipeline []ContentSummary `json:"-"` + OverdueTodos []todo.PendingDetail `json:"-"` + TodayTodos []todo.PendingDetail `json:"-"` + ActiveTodos []todo.PendingDetail `json:"-"` + RecurringTodos []todo.Item `json:"-"` + CommittedTodos []daily.Item `json:"-"` + UpcomingTodos []todo.PendingDetail `json:"-"` + ActiveGoals []goal.ActiveGoalSummary `json:"-"` + RSSHighlights []RSSHighlight `json:"-"` + ContentPipeline []ContentSummary `json:"-"` + ProposalsPending int `json:"-"` // Reflection fields. PlannedItems []daily.Item `json:"-"` @@ -125,6 +127,11 @@ type briefMorningWire struct { ActiveGoals []goal.ActiveGoalSummary `json:"active_goals"` RSSHighlights []RSSHighlight `json:"rss_highlights"` ContentPipeline []ContentSummary `json:"content_pipeline"` + // ProposalsPending is the summed count of agent-proposed area/goal/project + // drafts awaiting owner triage. Unlike the list fields it is a scalar, so + // it always serialises (0 when nothing is pending) — the push consumer + // gates its nudge on N > 0. + ProposalsPending int `json:"proposals_pending"` } // briefReflectionWire is the wire shape for mode=reflection. Field tags mirror @@ -148,17 +155,18 @@ func (o BriefOutput) MarshalJSON() ([]byte, error) { switch o.Mode { case briefModeMorning: return json.Marshal(briefMorningWire{ - Mode: o.Mode, - Date: o.Date, - OverdueTodos: o.OverdueTodos, - TodayTodos: o.TodayTodos, - ActiveTodos: o.ActiveTodos, - RecurringTodos: o.RecurringTodos, - CommittedTodos: o.CommittedTodos, - UpcomingTodos: o.UpcomingTodos, - ActiveGoals: o.ActiveGoals, - RSSHighlights: o.RSSHighlights, - ContentPipeline: o.ContentPipeline, + Mode: o.Mode, + Date: o.Date, + OverdueTodos: o.OverdueTodos, + TodayTodos: o.TodayTodos, + ActiveTodos: o.ActiveTodos, + RecurringTodos: o.RecurringTodos, + CommittedTodos: o.CommittedTodos, + UpcomingTodos: o.UpcomingTodos, + ActiveGoals: o.ActiveGoals, + RSSHighlights: o.RSSHighlights, + ContentPipeline: o.ContentPipeline, + ProposalsPending: o.ProposalsPending, }) case briefModeReflection: return json.Marshal(briefReflectionWire{ @@ -277,6 +285,7 @@ func (s *Server) fillBriefMorning(ctx context.Context, date time.Time, requested runSection("goals", func(c context.Context) { s.fillGoals(c, out) }) runSection("rss", func(c context.Context) { s.fillRSSHighlights(c, date, out) }) runSection("content_pipeline", func(c context.Context) { s.fillContentPipeline(c, out) }) + runSection("proposals", func(c context.Context) { s.fillProposalsPending(c, out) }) wg.Wait() } @@ -398,6 +407,27 @@ func (s *Server) fillContentPipeline(ctx context.Context, out *BriefOutput) { out.ContentPipeline = toContentSummaries(all) } +// fillProposalsPending sums the agent-proposed area/goal/project drafts still +// in status=proposed — the count Koopa's admin triage badge shows, surfaced +// here so the push consumer (hermes) can decide whether to nudge him back to +// the queue without holding admin credentials. Each count is read +// independently and degrades to its own zero on error, matching the other +// section fillers. +func (s *Server) fillProposalsPending(ctx context.Context, out *BriefOutput) { + var total int64 + if pending, err := s.goals.ProposalsPendingCount(ctx); err == nil { + total += pending.Goals + pending.Areas + } else { + s.logger.Warn("brief: proposed goals+areas count", "error", err) + } + if projects, err := s.projects.ProposedProjectsCount(ctx); err == nil { + total += projects + } else { + s.logger.Warn("brief: proposed projects count", "error", err) + } + out.ProposalsPending = int(total) +} + // fillBriefReflection populates the reflection fields: the day's plan items // plus plan-vs-actual counts. Completion is derived from each planned todo's // CURRENT state (done -> completed, someday -> deferred, anything else -> diff --git a/internal/mcp/integration_test.go b/internal/mcp/integration_test.go index 7e4b45da..6772ee9e 100644 --- a/internal/mcp/integration_test.go +++ b/internal/mcp/integration_test.go @@ -1773,6 +1773,66 @@ func TestIntegration_BriefReflection_CountsFromTodoState(t *testing.T) { } } +// TestIntegration_BriefMorning_ProposalsPending proves the morning brief's +// proposals_pending sums all three proposed-entity kinds (area + goal + +// project) awaiting owner triage — the count hermes reads to decide whether to +// nudge Koopa back to the triage queue. setupServer truncates goals and +// projects (so each starts with zero proposed rows) but preserves the +// migration-002 seed areas, so proposed areas can leak in from other tests; +// clearing them first makes the summed count deterministic. One proposed draft +// of each kind is seeded through the real propose_* write path, so the count +// exercises the actual status='proposed' rows the admin badge reads. +func TestIntegration_BriefMorning_ProposalsPending(t *testing.T) { + s := setupServer(t) + + if _, err := testPool.Exec(t.Context(), `DELETE FROM areas WHERE status = 'proposed'`); err != nil { + t.Fatalf("clearing pre-existing proposed areas: %v", err) + } + + if _, _, err := callHandlerAs(t, "claude", s.proposeArea, ProposeAreaInput{ + Name: "Brief Count Area", + }); err != nil { + t.Fatalf("proposeArea: %v", err) + } + // Area omitted → an unclassified proposed goal; still status='proposed', so + // it counts, without coupling the test to area resolution. + if _, _, err := callHandlerAs(t, "claude", s.proposeGoal, ProposeGoalInput{ + Title: "Brief Count Goal", + }); err != nil { + t.Fatalf("proposeGoal: %v", err) + } + if _, _, err := callHandlerAs(t, "claude", s.proposeProject, ProposeProjectInput{ + Name: "Brief Count Project", + }); err != nil { + t.Fatalf("proposeProject: %v", err) + } + + // Cases pin both the sum (all three proposed kinds) and the section-filter + // wiring: an explicit ["proposals"] must run the filler (a runSection-label + // mismatch would silently return 0), and requesting a different section must + // leave it uncomputed at 0 (the strict filter must exclude it). + tests := []struct { + name string + sections FlexStringSlice + want int + }{ + {name: "all sections (default)", sections: nil, want: 3}, + {name: "proposals section explicitly selected", sections: FlexStringSlice{"proposals"}, want: 3}, + {name: "unrelated section excludes proposals", sections: FlexStringSlice{"goals"}, want: 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, out, err := callHandler(t, s.brief, BriefInput{Mode: "morning", Sections: tt.sections}) + if err != nil { + t.Fatalf("brief(morning, sections=%v): %v", tt.sections, err) + } + if out.ProposalsPending != tt.want { + t.Errorf("ProposalsPending = %d, want %d", out.ProposalsPending, tt.want) + } + }) + } +} + // --- project_progress (read-only PARA momentum/stalled) --- // // These tests pin the load-bearing semantics: the HUMAN-ONLY activity diff --git a/internal/mcp/morning_test.go b/internal/mcp/morning_test.go index e7a7cccf..ed280e71 100644 --- a/internal/mcp/morning_test.go +++ b/internal/mcp/morning_test.go @@ -152,6 +152,7 @@ func TestBriefOutput_MorningWireShape(t *testing.T) { "active_goals", "rss_highlights", "content_pipeline", + "proposals_pending", } type fieldExpectation struct { @@ -324,3 +325,48 @@ func TestBriefOutput_ReflectionWireShape(t *testing.T) { } } } + +// TestBriefOutput_ProposalsPendingWire pins proposals_pending as a scalar on +// the morning wire: it always serialises (present at 0, never omitted) so the +// push consumer can gate its nudge on N > 0, it carries the value set on the +// output through MarshalJSON's morning branch, and it never leaks into the +// reflection envelope (it is a morning-only field). +func TestBriefOutput_ProposalsPendingWire(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + out BriefOutput + want string // exact JSON number expected for proposals_pending + }{ + { + name: "morning zero — present as 0, not omitted", + out: BriefOutput{Mode: briefModeMorning, Date: "2026-05-27"}, + want: "0", + }, + { + name: "morning positive — carries the summed count through MarshalJSON", + out: BriefOutput{Mode: briefModeMorning, Date: "2026-05-27", ProposalsPending: 7}, + want: "7", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + parsed := marshalToKeyMap(t, tt.out) + raw, ok := parsed["proposals_pending"] + if !ok { + t.Fatalf("morning BriefOutput missing proposals_pending key") + } + if string(raw) != tt.want { + t.Errorf("proposals_pending = %s, want %s", raw, tt.want) + } + }) + } + + // Reflection mode must not emit proposals_pending — it is morning-only. + reflection := BriefOutput{Mode: briefModeReflection, Date: "2026-05-27", ProposalsPending: 5} + if _, ok := marshalToKeyMap(t, reflection)["proposals_pending"]; ok { + t.Error("reflection BriefOutput emitted proposals_pending, want absent (morning-only field)") + } +} diff --git a/internal/mcp/ops/catalog.go b/internal/mcp/ops/catalog.go index d6304350..2dff5176 100644 --- a/internal/mcp/ops/catalog.go +++ b/internal/mcp/ops/catalog.go @@ -32,7 +32,7 @@ func Brief() Meta { Writability: ReadOnly, Stability: StabilityStable, Since: since, - Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'todos' (overdue/today/active/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", + Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'todos' (overdue/today/active/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline), 'proposals' (proposals_pending — count of agent-proposed area/goal/project drafts awaiting owner triage). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", FieldEnums: map[string][]string{ "mode": {"morning", "reflection"}, }, diff --git a/skills/koopa-system/references/tools.md b/skills/koopa-system/references/tools.md index e83dc529..469b71d2 100644 --- a/skills/koopa-system/references/tools.md +++ b/skills/koopa-system/references/tools.md @@ -66,6 +66,7 @@ Morning mode is filterable via `sections` (omit or pass `[]` for all). Valid key | `goals` | active_goals | | `rss` | rss_highlights — feeds tagged `priority=high`, NOT relevance-ranked; use `search_knowledge` for ranked retrieval | | `content_pipeline` | content_pipeline | +| `proposals` | proposals_pending — count of agent-proposed area/goal/project drafts awaiting owner triage | Every caller gets all sections by default; pass an explicit `sections` list to narrow the briefing. From b66ddcc2d7aa396c5454a1917c7a2d338d1eecc2 Mon Sep 17 00:00:00 2001 From: Koopa Date: Fri, 3 Jul 2026 10:31:27 +0800 Subject: [PATCH 3/4] fix(mcp): use int64 for proposals_pending, dropping the int64 to int narrowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proposed area/goal/project counts come from count(*) (int64); summing them into an int field required an int64->int conversion that narrows on 32-bit builds. The counts are far too small to overflow in practice, but typing the field as int64 matches the source and removes the conversion entirely rather than guarding an impossible case. No behaviour or wire change — JSON still serialises a bare number. Addresses Augment review on PR #20. --- internal/mcp/brief.go | 9 +++++---- internal/mcp/integration_test.go | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/mcp/brief.go b/internal/mcp/brief.go index 96e5d978..52fdbc65 100644 --- a/internal/mcp/brief.go +++ b/internal/mcp/brief.go @@ -103,7 +103,7 @@ type BriefOutput struct { ActiveGoals []goal.ActiveGoalSummary `json:"-"` RSSHighlights []RSSHighlight `json:"-"` ContentPipeline []ContentSummary `json:"-"` - ProposalsPending int `json:"-"` + ProposalsPending int64 `json:"-"` // Reflection fields. PlannedItems []daily.Item `json:"-"` @@ -130,8 +130,9 @@ type briefMorningWire struct { // ProposalsPending is the summed count of agent-proposed area/goal/project // drafts awaiting owner triage. Unlike the list fields it is a scalar, so // it always serialises (0 when nothing is pending) — the push consumer - // gates its nudge on N > 0. - ProposalsPending int `json:"proposals_pending"` + // gates its nudge on N > 0. int64 matches the count(*) source type, so no + // narrowing conversion is needed. + ProposalsPending int64 `json:"proposals_pending"` } // briefReflectionWire is the wire shape for mode=reflection. Field tags mirror @@ -425,7 +426,7 @@ func (s *Server) fillProposalsPending(ctx context.Context, out *BriefOutput) { } else { s.logger.Warn("brief: proposed projects count", "error", err) } - out.ProposalsPending = int(total) + out.ProposalsPending = total } // fillBriefReflection populates the reflection fields: the day's plan items diff --git a/internal/mcp/integration_test.go b/internal/mcp/integration_test.go index 6772ee9e..c5aa2660 100644 --- a/internal/mcp/integration_test.go +++ b/internal/mcp/integration_test.go @@ -1814,7 +1814,7 @@ func TestIntegration_BriefMorning_ProposalsPending(t *testing.T) { tests := []struct { name string sections FlexStringSlice - want int + want int64 }{ {name: "all sections (default)", sections: nil, want: 3}, {name: "proposals section explicitly selected", sections: FlexStringSlice{"proposals"}, want: 3}, From a2c266d29e46ae51616abc442e032fd9428dcdc5 Mon Sep 17 00:00:00 2001 From: Koopa Date: Fri, 3 Jul 2026 10:32:57 +0800 Subject: [PATCH 4/4] docs(mcp): align morning brief section summaries with the wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The opening morning summaries — in the brief tool description, the mode schema tag, and the skills manual — still listed the pre-proposals section set and omitted active_todos, drifting from what the morning wire actually emits. List active_todos and proposals_pending everywhere the sections are summarised, and correct the BriefInput doc's "empty-slice default" wording to "zero-value default" now that proposals_pending is a scalar (0), noting that an unrequested field's default is not a computed result. Addresses Augment review on PR #20. --- internal/mcp/brief.go | 10 ++++++---- internal/mcp/ops/catalog.go | 2 +- skills/koopa-system/references/tools.md | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/mcp/brief.go b/internal/mcp/brief.go index 52fdbc65..e7a76bfd 100644 --- a/internal/mcp/brief.go +++ b/internal/mcp/brief.go @@ -65,9 +65,11 @@ func resolveDefaultSections(caller string) []string { // // Mode selects the briefing flavour and is required. Sections is a STRICT // filter that applies only in morning mode: when non-empty, only the listed -// groups are populated and every other morning field stays at its empty-slice -// default. Omit Sections (or pass an empty list) to populate every morning -// group. Sections is ignored in reflection mode. +// groups are populated and every other morning field stays at its zero-value +// default ([] for the list fields, 0 for proposals_pending) — an unrequested +// field's default is not a computed result. Omit Sections (or pass an empty +// list) to populate every morning group. Sections is ignored in reflection +// mode. // // Morning group → response field mapping: // @@ -80,7 +82,7 @@ func resolveDefaultSections(caller string) []string { // Unknown group names are ignored silently (no error, no warning). type BriefInput struct { As string `json:"as,omitempty" jsonschema_description:"Caller agent identity (e.g. koopa0-dev)."` - Mode string `json:"mode" jsonschema_description:"Briefing mode (required): 'morning' = daily-planning pull (todos/goals/rss/content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (daily plan items + completion counts). brief is a pure planning-state pull and carries no agent memory."` + Mode string `json:"mode" jsonschema_description:"Briefing mode (required): 'morning' = daily-planning pull (todos/goals/rss/content_pipeline/proposals); 'reflection' = end-of-day plan-vs-actual retrospective (daily plan items + completion counts). brief is a pure planning-state pull and carries no agent memory."` Sections FlexStringSlice `json:"sections,omitempty" jsonschema_description:"MORNING-ONLY strict filter on which groups to populate (default: all). Ignored in reflection mode. Omit or pass [] to get the full morning briefing. Group key → response fields: 'todos' → overdue_todos/today_todos/active_todos/recurring_todos/committed_todos/upcoming_todos; 'goals' → active_goals; 'rss' → rss_highlights; 'content_pipeline' → content_pipeline; 'proposals' → proposals_pending (count of agent-proposed area/goal/project drafts awaiting owner triage). Unknown keys silently ignored."` Date *string `json:"date,omitempty" jsonschema_description:"Target date YYYY-MM-DD (default: today)"` } diff --git a/internal/mcp/ops/catalog.go b/internal/mcp/ops/catalog.go index 2dff5176..0e5f89a4 100644 --- a/internal/mcp/ops/catalog.go +++ b/internal/mcp/ops/catalog.go @@ -32,7 +32,7 @@ func Brief() Meta { Writability: ReadOnly, Stability: StabilityStable, Since: since, - Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'todos' (overdue/today/active/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline), 'proposals' (proposals_pending — count of agent-proposed area/goal/project drafts awaiting owner triage). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", + Description: "Read-only planning-state pull. Pick a mode (required): 'morning' = single-call daily-planning briefing (overdue/today/active/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline, proposals_pending); 'reflection' = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). brief is a pure planning-state pull and carries no agent memory. Morning mode is filterable via the sections parameter (ignored in reflection mode) — valid keys (omit or pass [] for all): 'todos' (overdue/today/active/recurring/committed/upcoming todos), 'goals' (active_goals), 'rss' (rss_highlights — feeds tagged priority=high, NOT relevance-ranked; use search_knowledge for ranked retrieval), 'content_pipeline' (content_pipeline), 'proposals' (proposals_pending — count of agent-proposed area/goal/project drafts awaiting owner triage). Every caller gets all sections by default; pass an explicit sections list to narrow the briefing. Scope is the target date (default today), not since-last-session.", FieldEnums: map[string][]string{ "mode": {"morning", "reflection"}, }, diff --git a/skills/koopa-system/references/tools.md b/skills/koopa-system/references/tools.md index 469b71d2..77ab72e9 100644 --- a/skills/koopa-system/references/tools.md +++ b/skills/koopa-system/references/tools.md @@ -47,7 +47,7 @@ is hand-maintained. | Tool | Key params | Returns | |---|---|---| -| `brief` | `mode` (`morning` / `reflection`), `sections?` (morning only), `date?` | `morning` = single-call daily-planning briefing (overdue/today/active/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline). `reflection` = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). `mode` is required. Read-only; carries no agent memory. Scope is the target date (default today), not since-last-session. | +| `brief` | `mode` (`morning` / `reflection`), `sections?` (morning only), `date?` | `morning` = single-call daily-planning briefing (overdue/today/active/recurring/committed/upcoming todos, active_goals, rss_highlights, content_pipeline, proposals_pending). `reflection` = end-of-day plan-vs-actual retrospective (planned_items + completed/deferred/planned counts + completion_rate). `mode` is required. Read-only; carries no agent memory. Scope is the target date (default today), not since-last-session. | | `search_knowledge` | `query`, `content_type?`, `after?`, `before?`, `limit?` | Hybrid (FTS + pgvector) retrieval over the content corpus. See Search section below. | | `project_progress` | (none) | Live PARA momentum/stalled intelligence for Koopa's projects, goals, and areas, computed at read time. Returns the owner's full PARA (not caller-scoped): `projects[]` with expected_cadence, days_since_human_activity, open_next_action, milestone_done/total, and a `stalled` flag; `goals[]` with milestone progress + stalled-project rollup; `areas[]` with `area_neglected` (no owner activity attributed to the area via any project, goal, or milestone for >14 days). Progress counts owner activity only (`actor='human'`); agent/system actors never count. |