diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index 10a6106..9824e4d 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -645,3 +645,56 @@ If `Edit` or `Write` (or another tool an execution agent needs) is missing from the target project's allowlist, the agent will report `TASK_BLOCKED` with an actionable error message explaining what permissions are needed. The user must update the project's permissions config before retrying. + +## Knowledge Base Tools + +### `get_knowledge_base` + +Returns all knowledge-base docs for a project in a single call. Used by +planning, brainstorming, and debugging skills to load architectural context. + +**Input:** + +| Field | Required | Description | +| --------- | -------- | ----------------------------------------- | +| `project` | yes | Project name | +| `repo` | no | Repo name; defaults to the primary repo | + +**Response:** + +```json +{ + "project": "my-project", + "repo": "primary", + "docs": { + "architecture.md": "...", + "code-structure.md": "...", + "api-documentation.md": "...", + "glossary.md": "..." + }, + "summaries": { + "architecture.md": "Short summary extracted from the doc's ## Summary section.", + "code-structure.md": "Short summary...", + "api-documentation.md": "", + "glossary.md": "Short summary..." + }, + "meta": { "...": "..." } +} +``` + +**`summaries` field:** a map from doc name to the text of its first `## Summary` +section. Empty string when a doc has no `## Summary` section. Always present +(never `null`) — an empty object `{}` means no docs have summaries yet. + +**Usage pattern for agents:** when `summaries` is non-empty, read each doc's +summary to judge relevance to the current task, then retain full content from +`docs` only for the relevant docs. When `summaries` is empty or a doc has no +entry, load all docs as before (current fallback behaviour). + +### `read_knowledge_doc` + +Read a single KB doc by name. Use when you need only one doc and want to avoid +loading the full payload. + +**Input:** `project` (required), `repo` (optional), `doc` (required — one of +`architecture.md`, `code-structure.md`, `api-documentation.md`, `glossary.md`). diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index 2982a9f..26c64c8 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -1206,10 +1206,11 @@ type getKnowledgeBaseInput struct { } type getKnowledgeBaseOutput struct { - Project string `json:"project"` - Repo string `json:"repo"` - Docs map[string]string `json:"docs"` - Meta board.KnowledgeRepoMeta `json:"meta"` + Project string `json:"project"` + Repo string `json:"repo"` + Docs map[string]string `json:"docs"` + Summaries map[string]string `json:"summaries"` + Meta board.KnowledgeRepoMeta `json:"meta"` } func registerGetKnowledgeBase(server *mcp.Server, svc *service.CardService) { @@ -1227,15 +1228,20 @@ func registerGetKnowledgeBase(server *mcp.Server, svc *service.CardService) { out.Docs = map[string]string{} } + if out.Summaries == nil { + out.Summaries = map[string]string{} + } + if out.Meta.Docs == nil { out.Meta.Docs = map[string]board.KnowledgeDocMeta{} } return nil, getKnowledgeBaseOutput{ - Project: out.Project, - Repo: out.Repo, - Docs: out.Docs, - Meta: out.Meta, + Project: out.Project, + Repo: out.Repo, + Docs: out.Docs, + Summaries: out.Summaries, + Meta: out.Meta, }, nil }) } diff --git a/internal/service/service_knowledge.go b/internal/service/service_knowledge.go index 52c0087..3ebd99f 100644 --- a/internal/service/service_knowledge.go +++ b/internal/service/service_knowledge.go @@ -237,10 +237,43 @@ func (s *CardService) WriteKnowledgeDocs(ctx context.Context, in WriteKnowledgeD // KnowledgeBaseRead is returned by ReadKnowledgeBase. type KnowledgeBaseRead struct { - Project string `json:"project"` - Repo string `json:"repo"` - Docs map[string]string `json:"docs"` - Meta board.KnowledgeRepoMeta `json:"meta"` + Project string `json:"project"` + Repo string `json:"repo"` + Docs map[string]string `json:"docs"` + Summaries map[string]string `json:"summaries"` + Meta board.KnowledgeRepoMeta `json:"meta"` +} + +// extractSummary returns the text content of the first ## Summary section in +// the given markdown content. It scans for the first line that is exactly +// "## Summary", collects all following lines until the next ##-level heading +// or EOF, then returns the trimmed result. Returns an empty string if no +// ## Summary section is found. +func extractSummary(content string) string { + lines := strings.Split(content, "\n") + + inSummary := false + + var summaryLines []string + + for _, line := range lines { + if !inSummary { + if line == "## Summary" { + inSummary = true + } + + continue + } + + // Stop at the next ##-level heading. + if strings.HasPrefix(line, "## ") { + break + } + + summaryLines = append(summaryLines, line) + } + + return strings.TrimSpace(strings.Join(summaryLines, "\n")) } // KnowledgeDocRead is returned by ReadKnowledgeDoc. @@ -319,7 +352,7 @@ func (s *CardService) ReadKnowledgeBase(ctx context.Context, project, repo strin // primary, store I/O) must propagate so callers see the real // failure. if errors.Is(err, errNoReposConfigured) { - return &KnowledgeBaseRead{Project: project, Docs: map[string]string{}}, nil + return &KnowledgeBaseRead{Project: project, Docs: map[string]string{}, Summaries: map[string]string{}}, nil } return nil, err @@ -331,6 +364,7 @@ func (s *CardService) ReadKnowledgeBase(ctx context.Context, project, repo strin } docs := map[string]string{} + summaries := map[string]string{} for _, name := range board.KnowledgeDocNames { exists, err := s.store.KnowledgeDocExists(ctx, project, resolvedRepo, name) @@ -348,13 +382,15 @@ func (s *CardService) ReadKnowledgeBase(ctx context.Context, project, repo strin } docs[name] = string(data) + summaries[name] = extractSummary(string(data)) } return &KnowledgeBaseRead{ - Project: project, - Repo: resolvedRepo, - Docs: docs, - Meta: meta.Repos[resolvedRepo], + Project: project, + Repo: resolvedRepo, + Docs: docs, + Summaries: summaries, + Meta: meta.Repos[resolvedRepo], }, nil } diff --git a/internal/service/service_knowledge_test.go b/internal/service/service_knowledge_test.go index db1e65b..f0bdcc9 100644 --- a/internal/service/service_knowledge_test.go +++ b/internal/service/service_knowledge_test.go @@ -568,6 +568,93 @@ func TestWriteKnowledgeDocs_InvalidDocNameReturnsSentinel(t *testing.T) { assert.ErrorIs(t, err, storage.ErrInvalidKnowledgeDoc) } +func TestExtractSummary(t *testing.T) { + tests := []struct { + name string + content string + expected string + }{ + { + name: "well-formed doc with summary section", + content: `# Architecture + +## Summary +This document describes the system architecture. +It covers components and data flow. + +## Components +Details here. +`, + expected: "This document describes the system architecture.\nIt covers components and data flow.", + }, + { + name: "doc missing summary section", + content: "# No Summary Here\n\n## Components\nDetails.\n", + expected: "", + }, + { + name: "summary followed by next section stops at heading", + content: `## Summary +First summary line. +Second summary line. +## NextSection +Should not be included. +`, + expected: "First summary line.\nSecond summary line.", + }, + { + name: "two summary headings returns first one only", + content: `## Summary +First summary content. + +## Other +## Summary +Second summary content should be ignored. +`, + expected: "First summary content.", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := extractSummary(tc.content) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestReadKnowledgeBase_PopulatesSummaries(t *testing.T) { + svc, _, cleanup := setupTest(t) + defer cleanup() + + ctx := context.Background() + content := "# Architecture\n\n## Summary\nBrief description of the architecture.\n\n## Details\nMore here.\n" + + _, err := svc.WriteKnowledgeDocs(ctx, WriteKnowledgeDocsInput{ + Project: "test-project", + Repo: "core", + Docs: map[string]string{"architecture.md": content}, + Source: KnowledgeWriteSourceRefresh, + HeadCommit: "abc", + AgentID: "human:t", + }) + require.NoError(t, err) + + out, err := svc.ReadKnowledgeBase(ctx, "test-project", "core") + require.NoError(t, err) + assert.NotNil(t, out.Summaries) + assert.Equal(t, "Brief description of the architecture.", out.Summaries["architecture.md"]) +} + +func TestReadKnowledgeBase_SummariesNonNilWhenEmpty(t *testing.T) { + svc, _, cleanup := setupTest(t) + defer cleanup() + + out, err := svc.ReadKnowledgeBase(context.Background(), "test-project", "") + require.NoError(t, err) + assert.NotNil(t, out.Summaries, "Summaries must be non-nil even when no docs exist") +} + func TestBuildRefreshPlan_ReasonsOnlyMissingOrScheduled(t *testing.T) { svc, _, cleanup := setupTest(t) defer cleanup() diff --git a/workflow-skills/brainstorming.md b/workflow-skills/brainstorming.md index c791f92..92d3cf1 100644 --- a/workflow-skills/brainstorming.md +++ b/workflow-skills/brainstorming.md @@ -98,6 +98,12 @@ You MUST complete each of these in order: `glossary.md` to use the project's vocabulary correctly. Reference them when discussing architecture, decomposition, or naming. If empty, note that to the user when relevant and proceed. + + If `summaries` is non-empty, use each doc's summary to judge + relevance to the current task before loading its full content from + `docs`. Retain in active context only the docs whose summary + indicates relevance. If `summaries` is empty or a doc has no entry, + load all docs. 2. **Explore project context** — read files referenced in the card and anything the KB doesn't cover (recent commits, files mentioned in the body). Don't re-derive what the KB already states. diff --git a/workflow-skills/create-plan.md b/workflow-skills/create-plan.md index decf503..794da20 100644 --- a/workflow-skills/create-plan.md +++ b/workflow-skills/create-plan.md @@ -186,6 +186,12 @@ Hold this claim through Phase 5. `glossary.md` to use the project's vocabulary correctly. If empty, proceed. + If `summaries` is non-empty, use each doc's summary to judge + relevance to the current task before loading its full content from + `docs`. Retain in active context only the docs whose summary + indicates relevance. If `summaries` is empty or a doc has no entry, + load all docs. + 2. **Review card details.** Read the card details provided above. If the card body already contains a `## Plan` section, use it as a starting point — do not discard previous planning work. Only call diff --git a/workflow-skills/refresh-knowledge.md b/workflow-skills/refresh-knowledge.md index b920f4b..5934e0a 100644 --- a/workflow-skills/refresh-knowledge.md +++ b/workflow-skills/refresh-knowledge.md @@ -205,6 +205,8 @@ via the Task tool. Pass: Collect each sub-agent's output as the new doc content. +Every generated doc MUST begin with `## Summary` as its first level-2 heading (150–300 tokens, prose describing the doc's purpose and major sections). + After each sub-agent returns, call `update_refresh_progress` so the UI shows progress: @@ -241,6 +243,10 @@ in local mode (no UI-side job to update) — proceed regardless. ```markdown # System Architecture +## Summary + +[150–300 token prose paragraph: what this doc covers, what questions it answers, and the major sections inside — written for an agent deciding which full docs to load into context.] + ## System Overview [2-4 paragraph high-level description of what the system does, who calls @@ -296,6 +302,10 @@ where it is enforced so the agent can verify before changing nearby code.] ```markdown # Code Structure +## Summary + +[150–300 token prose paragraph: what this doc covers, what questions it answers, and the major sections inside — written for an agent deciding which full docs to load into context.] + ## Build System - **Type**: [go modules / npm / pyproject / cargo / make-driven / etc.] @@ -357,6 +367,10 @@ without REST/MCP/CLI). Note the skip in your output to the user. ```markdown # API Documentation +## Summary + +[150–300 token prose paragraph: what this doc covers, what questions it answers, and the major sections inside — written for an agent deciding which full docs to load into context.] + ## Overview [Which surfaces this repo exposes: REST, MCP, CLI, gRPC, webhooks. Where @@ -412,6 +426,10 @@ each is registered in the codebase.] ```markdown # Glossary +## Summary + +[150–300 token prose paragraph: what this doc covers, what questions it answers, and the major sections inside — written for an agent deciding which full docs to load into context.] + ## Domain terms ### [Term] diff --git a/workflow-skills/run-autonomous.md b/workflow-skills/run-autonomous.md index f8aa1bc..4bf6137 100644 --- a/workflow-skills/run-autonomous.md +++ b/workflow-skills/run-autonomous.md @@ -80,6 +80,11 @@ If docs are returned, hold them in your reasoning context: use boundaries, `api-documentation.md` to avoid breaking public surfaces, and `glossary.md` to use the project's vocabulary correctly. If empty, proceed. +If `summaries` is non-empty, use each doc's summary to judge relevance to the +current task before loading its full content from `docs`. Retain in active +context only the docs whose summary indicates relevance. If `summaries` is +empty or a doc has no entry, load all docs. + Immediately after the KB call, log the outcome: ``` diff --git a/workflow-skills/systematic-debugging.md b/workflow-skills/systematic-debugging.md index eff97f8..f6c1dd4 100644 --- a/workflow-skills/systematic-debugging.md +++ b/workflow-skills/systematic-debugging.md @@ -79,6 +79,11 @@ You MUST complete each phase before proceeding to the next. - Translate user-facing names to internal names (`glossary.md`). - If empty, proceed without — fall back to grepping for symbols and reading `CLAUDE.md`. + - If `summaries` is non-empty, use each doc's summary to judge + relevance to the current bug before loading its full content from + `docs`. Retain in active context only the docs whose summary + indicates relevance. If `summaries` is empty or a doc has no + entry, load all docs. 2. **Read the card body carefully.** - Quote any stack traces, error messages, error codes, or log lines the