feat: skills framework, context protocol, HTTP API, and agent-aware handoff#52
feat: skills framework, context protocol, HTTP API, and agent-aware handoff#52JordanCoin merged 6 commits intomainfrom
Conversation
…ools Phase 2 of codemap's evolution. Skills are markdown files with YAML frontmatter that provide context-aware guidance to AI agents. They're matched against the user's intent, mentioned files, and detected languages. New package: skills/ - types.go — SkillMeta, Skill, SkillIndex with fast lookup by name/keyword/language - loader.go — Multi-source loading (builtin → project → global), YAML frontmatter parsing, deduplication (later sources override), weighted matching - embed.go — go:embed for builtin skills - 5 builtin skills: hub-safety, refactor, test-first, explore, handoff New: cmd/skill.go — CLI subcommand (list, show, init) New: MCP tools — list_skills (metadata only) and get_skill (full body) Hook integration: - hookPromptSubmit matches skills against intent + files + languages - Top 3 skills injected with <!-- codemap:skills --> structured marker - Budget-capped at 8KB to prevent context blowup Bug fixes from parallel reviews: - [P1] Priority boost only applies after real signal (codex finding) - [P2] Fallback name derived from filename for frontmatter-less skills - [P1] showSessionProgress now counts unique hub files, not hub events (Claude code-reviewer finding from Phase 1 worktree review) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oadmap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 of codemap's evolution into a code-aware AI coding system. New: codemap context - Universal JSON envelope any AI tool can consume - --for "prompt" pre-classifies intent with matched skills - --compact mode for token-constrained agents - Includes project info, intent, working set, skills, handoff ref New: codemap serve --port 9471 - GET /api/context?intent=refactor+auth&compact=true - GET /api/skills?language=go&category=refactor - GET /api/skills/<name> - GET /api/working-set - GET /api/health - Uses net/http stdlib, no new dependencies New: Agent-aware handoff history - AgentEntry type records agent ID, timestamps, files edited - Session-stop auto-detects agent (Claude Code, Codex, Cursor) - History carried across sessions, capped at 20 entries - Enables true multi-agent continuity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bind HTTP server to 127.0.0.1 by default (security: P1) - Add --host flag for explicit network bind control - Add server timeouts (ReadHeader, Read, Write, Idle) - Remove absRoot from /api/health response - Use flag.ContinueOnError for controlled error handling - Sort Languages output for deterministic JSON - Detect languages from imports + hubs, not just importers - Fix comment: Cursor detection uses CURSOR_SESSION_ID Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Phase 2/3 building blocks for codemap’s evolution into an agent-integrated system by introducing a skills framework, a standard “context envelope” protocol, and new integration surfaces (MCP + HTTP), plus agent-aware handoff history.
Changes:
- Introduces
skills/package with frontmatter parsing, builtin embedding, multi-source loading, and intent/file/language matching (+ tests and builtin skill docs). - Adds new CLI subcommands:
codemap skill ...,codemap context ..., andcodemap serve ...(HTTP API endpoints). - Extends MCP server with
list_skills/get_skilltools; extends handoff artifacts with agent session history and updates hooks to inject matched skills.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/types.go | Defines skill metadata, skill model, and index structures. |
| skills/loader.go | Implements skill loading (builtin/project/global), frontmatter parsing, and matching. |
| skills/embed.go | Embeds builtin skills into the binary and loads them at runtime. |
| skills/loader_test.go | Adds tests for parsing, loading, indexing, and matching behavior. |
| skills/builtin/hub-safety.md | Adds builtin “hub-safety” guidance content. |
| skills/builtin/refactor.md | Adds builtin “refactor” guidance content. |
| skills/builtin/test-first.md | Adds builtin “test-first” guidance content. |
| skills/builtin/explore.md | Adds builtin “explore” guidance content. |
| skills/builtin/handoff.md | Adds builtin “handoff” guidance content. |
| cmd/skill.go | Adds `codemap skill list |
| cmd/context.go | Adds codemap context JSON envelope generator. |
| cmd/serve.go | Adds codemap serve HTTP API endpoints for context/skills/working-set/health. |
| cmd/hooks.go | Injects matched skills into prompt-submit output; records agent session history in handoff; adjusts hub edit counting. |
| cmd/hooks_test.go | Updates session-progress test expectation for unique hub files counting. |
| mcp/main.go | Adds MCP tools to list skills and fetch skill bodies. |
| handoff/types.go | Adds AgentEntry and AgentHistory to persisted handoff artifact. |
| main.go | Routes new subcommands before global flag parsing. |
| go.mod | Adds YAML dependency for frontmatter parsing. |
| README.md | Documents skills framework and updates command list/roadmap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| body := m.Skill.Body | ||
| if totalBytes+len(body) > maxSkillOutputBytes { | ||
| remaining := maxSkillOutputBytes - totalBytes | ||
| if remaining > 100 { | ||
| body = body[:remaining] + "\n... (skill truncated)" | ||
| } else { | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
body = body[:remaining] truncates by bytes and can split a UTF-8 rune, producing invalid text output. If these skill files may contain non-ASCII, consider truncating at a rune boundary (e.g., via utf8.ValidString check / rune iteration) before printing.
| // Detect languages from all known files (importers + imports + working set) | ||
| langSet := make(map[string]bool) | ||
| if info != nil { | ||
| for file := range info.Importers { | ||
| if lang := scanner.DetectLanguage(file); lang != "" { | ||
| langSet[lang] = true | ||
| } | ||
| } | ||
| for file := range info.Imports { | ||
| if lang := scanner.DetectLanguage(file); lang != "" { | ||
| langSet[lang] = true | ||
| } |
There was a problem hiding this comment.
ctx.Languages is built by iterating over a map, so the output order is nondeterministic across runs. If the context envelope is meant to be stable/cachable/diff-friendly, sort ctx.Languages (and potentially TopHubs) before returning.
|
|
||
| addr := fmt.Sprintf("%s:%d", *host, *port) | ||
| fmt.Printf("codemap serve — listening on http://%s\n", addr) | ||
| fmt.Printf(" GET /api/context?intent=refactor+auth&compact=true\n") | ||
| fmt.Printf(" GET /api/skills?language=go&category=refactor\n") | ||
| fmt.Printf(" GET /api/skills/<name>\n") | ||
| fmt.Printf(" GET /api/working-set\n") | ||
| fmt.Printf(" GET /api/health\n") | ||
| fmt.Println() | ||
|
|
||
| server := &http.Server{ | ||
| Addr: addr, |
There was a problem hiding this comment.
This server binds to all network interfaces by default (":<port>"). Since these endpoints expose local repo paths and other potentially sensitive project context, consider binding to localhost by default (e.g., "127.0.0.1") and/or adding an explicit --bind flag to opt into external exposure.
| func buildContextEnvelope(root, prompt string, compact bool) ContextEnvelope { | ||
| projCfg := config.Load(root) | ||
| info := getHubInfoNoFallback(root) | ||
|
|
||
| envelope := ContextEnvelope{ |
There was a problem hiding this comment.
buildContextEnvelope is a new protocol surface with multiple branches (compact vs full, prompt vs no prompt, skills/working-set/handoff). There are no unit tests covering these cases yet; adding focused tests would help prevent regressions as the protocol evolves.
| if totalBytes+len(body) > maxSkillOutputBytes { | ||
| remaining := maxSkillOutputBytes - totalBytes | ||
| if remaining > 100 { | ||
| body = body[:remaining] + "\n... (skill truncated)" | ||
| } else { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| fmt.Println() | ||
| fmt.Printf("📘 Skill: %s\n", m.Skill.Meta.Name) | ||
| fmt.Println(body) | ||
| totalBytes += len(body) |
There was a problem hiding this comment.
The truncation logic can exceed maxSkillOutputBytes because it slices to remaining and then appends the "... (skill truncated)" suffix without accounting for the suffix bytes. This defeats the intended output budget cap; consider reserving space for the suffix (or updating totalBytes based on the pre-suffix remaining) so the printed content never exceeds the cap.
| func runSkillInit(root string) { | ||
| skillsDir := root + "/.codemap/skills" | ||
| if err := os.MkdirAll(skillsDir, 0755); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error creating skills directory: %v\n", err) | ||
| os.Exit(1) | ||
| } |
There was a problem hiding this comment.
Path construction uses string concatenation (root + "/.codemap/skills"), which is not portable on Windows and can introduce double slashes. Prefer filepath.Join(root, ".codemap", "skills") here to match other cmd code.
| path := skillsDir + "/my-skill.md" | ||
| if _, err := os.Stat(path); err == nil { |
There was a problem hiding this comment.
Similarly, path := skillsDir + "/my-skill.md" should use filepath.Join for cross-platform correctness and to avoid edge cases with trailing slashes in root.
| // matchPath does a simple glob-like match: supports * and ** patterns, | ||
| // and falls back to substring matching. | ||
| func matchPath(pattern, path string) bool { | ||
| // Simple substring match for non-glob patterns | ||
| if !strings.ContainsAny(pattern, "*?") { | ||
| return strings.Contains(path, pattern) | ||
| } |
There was a problem hiding this comment.
The comment says this supports ** patterns, but the implementation uses filepath.Match, which does not treat ** specially. Either implement ** support (e.g., via a doublestar-style matcher) or update the comment so callers don’t assume recursive globbing works.
| golang.org/x/oauth2 v0.30.0 // indirect | ||
| golang.org/x/sys v0.38.0 // indirect | ||
| golang.org/x/text v0.3.8 // indirect | ||
| gopkg.in/yaml.v3 v3.0.1 // indirect |
There was a problem hiding this comment.
gopkg.in/yaml.v3 is imported directly (skills/loader.go), so it should be a direct dependency in go.mod (remove the // indirect comment). Running go mod tidy should fix this.
| gopkg.in/yaml.v3 v3.0.1 // indirect | |
| gopkg.in/yaml.v3 v3.0.1 |
| if !strings.Contains(out, "1 hub edits") { | ||
| t.Errorf("expected '1 hub edits' (unique hub files, not events), got %q", out) |
There was a problem hiding this comment.
The expected output string uses incorrect pluralization ("1 hub edits"). Consider updating the production output to handle singular vs plural (e.g., "1 hub edit") and adjust this assertion accordingly.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9fdde7e14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
cmd/serve.go
Outdated
| writeJSON(w, map[string]string{"status": "ok", "root": absRoot}) | ||
| }) | ||
|
|
||
| addr := fmt.Sprintf(":%d", *port) |
There was a problem hiding this comment.
Bind the HTTP server to loopback by default
Using addr := fmt.Sprintf(":%d", *port) makes ListenAndServe bind on all interfaces, so codemap serve is reachable from the local network even though the log message implies localhost-only access. In environments where this runs on shared hosts, the API can expose repository metadata and handoff context to other machines; defaulting to 127.0.0.1 (or requiring an explicit bind address flag) avoids this unintended exposure.
Useful? React with 👍 / 👎.
| matched, _ := filepath.Match(pattern, path) | ||
| if matched { | ||
| return true | ||
| } | ||
| // Also try matching against just the filename |
There was a problem hiding this comment.
Implement actual
** handling in path pattern matching
matchPath documents support for * and **, but it relies on filepath.Match, which does not implement recursive ** semantics. Any skill using patterns like src/**/handlers/*.go will never match and therefore won't activate, which breaks custom skill routing for nested paths.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Phases 2 & 3 of codemap's evolution into a code-aware AI coding system. Builds on #49 (merged).
Phase 2: Skills Framework
skills/package — YAML frontmatter parser, multi-source loader (builtin → project → global), weighted matchinghub-safety,refactor,test-first,explore,handoffcodemap skill list|show|initCLIlist_skills/get_skillMCP toolsPhase 3: Context Protocol & HTTP API
codemap context— universal JSON envelope for any AI tool (--for,--compact)codemap serve --port 9471— REST API:/api/context,/api/skills,/api/working-set,/api/healthAgentEntryrecords which agent worked, auto-detects Claude/Codex/CursorREADME updated
Test plan
go test ./... -race— all 10 packages passcodemap skill list— shows 5 builtinscodemap context --for "refactor auth"— returns full envelopecodemap serve— all 6 endpoints verified with curl🤖 Generated with Claude Code