feat: context protocol, HTTP API, and agent-aware handoff#51
feat: context protocol, HTTP API, and agent-aware handoff#51JordanCoin wants to merge 1 commit intofeat/skills-frameworkfrom
Conversation
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>
There was a problem hiding this comment.
Pull request overview
Adds new interfaces for exposing codemap’s “intelligence” to external tools by introducing a JSON context envelope (codemap context), an HTTP API server (codemap serve), and agent-aware handoff history persisted in the handoff artifact.
Changes:
- Add
codemap contextto emit a standardized JSON “context envelope” (optionally compact and/or intent-aware). - Add
codemap serveto expose context, skills, and working-set information via HTTP endpoints. - Persist per-session agent history into the handoff artifact and cap it to the last 20 entries.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| main.go | Dispatch context and serve subcommands before global flag parsing. |
| cmd/context.go | Implements JSON context envelope generation and CLI output. |
| cmd/serve.go | Implements HTTP API server and JSON response helpers. |
| cmd/hooks.go | Records agent session metadata into handoff artifact history. |
| handoff/types.go | Extends persisted handoff schema with AgentEntry and AgentHistory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // GET /api/context — full context envelope | ||
| mux.HandleFunc("/api/context", func(w http.ResponseWriter, r *http.Request) { | ||
| prompt := r.URL.Query().Get("intent") | ||
| compact := r.URL.Query().Get("compact") == "true" | ||
| envelope := buildContextEnvelope(absRoot, prompt, compact) | ||
| writeJSON(w, envelope) | ||
| }) | ||
|
|
||
| // GET /api/skills — list available skills | ||
| mux.HandleFunc("/api/skills", func(w http.ResponseWriter, r *http.Request) { | ||
| idx, err := skills.LoadSkills(absRoot) | ||
| if err != nil { | ||
| writeError(w, http.StatusInternalServerError, err.Error()) | ||
| return | ||
| } | ||
|
|
||
| // Optional filters | ||
| lang := r.URL.Query().Get("language") | ||
| category := r.URL.Query().Get("category") | ||
|
|
||
| if category != "" || lang != "" { | ||
| var langs []string | ||
| if lang != "" { | ||
| langs = []string{lang} | ||
| } | ||
| matches := idx.MatchSkills(category, nil, langs, 10) | ||
| writeJSON(w, matches) | ||
| return | ||
| } | ||
|
|
||
| // Return all skill metadata (no bodies) | ||
| type skillMeta struct { | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Source string `json:"source"` | ||
| Keywords []string `json:"keywords,omitempty"` | ||
| Languages []string `json:"languages,omitempty"` | ||
| Priority int `json:"priority,omitempty"` | ||
| } | ||
| var meta []skillMeta | ||
| for _, s := range idx.Skills { | ||
| meta = append(meta, skillMeta{ | ||
| Name: s.Meta.Name, | ||
| Description: s.Meta.Description, | ||
| Source: s.Source, | ||
| Keywords: s.Meta.Keywords, | ||
| Languages: s.Meta.Languages, | ||
| Priority: s.Meta.Priority, | ||
| }) | ||
| } | ||
| writeJSON(w, meta) | ||
| }) | ||
|
|
||
| // GET /api/skills/:name — get full skill | ||
| mux.HandleFunc("/api/skills/", func(w http.ResponseWriter, r *http.Request) { | ||
| name := strings.TrimPrefix(r.URL.Path, "/api/skills/") | ||
| if name == "" { | ||
| writeError(w, http.StatusBadRequest, "skill name required") | ||
| return | ||
| } | ||
|
|
||
| idx, err := skills.LoadSkills(absRoot) | ||
| if err != nil { | ||
| writeError(w, http.StatusInternalServerError, err.Error()) | ||
| return | ||
| } | ||
|
|
||
| skill, ok := idx.ByName[name] | ||
| if !ok { | ||
| writeError(w, http.StatusNotFound, fmt.Sprintf("skill %q not found", name)) | ||
| return | ||
| } | ||
| writeJSON(w, skill) | ||
| }) |
There was a problem hiding this comment.
New HTTP handlers in RunServe (context/skills/working-set/health) are not covered by tests, while cmd already has extensive automated tests. Consider adding httptest-based tests to verify status codes and response shapes (e.g., /api/skills/<name> 404 vs 200, and /api/context?compact=true omits large fields).
| if os.Getenv("CODEX") == "1" { | ||
| return "codex" | ||
| } | ||
| // Cursor sets CURSOR=1 |
There was a problem hiding this comment.
The comment says "Cursor sets CURSOR=1", but the detection logic actually checks CURSOR_SESSION_ID. Please update the comment to match the environment signal being used (or update the detection if CURSOR=1 is the intended contract).
| // Cursor sets CURSOR=1 | |
| // Cursor sets CURSOR_SESSION_ID |
| // Calculate budget | ||
| data, _ := json.Marshal(envelope) | ||
| envelope.Budget.TotalBytes = len(data) | ||
|
|
There was a problem hiding this comment.
Budget.TotalBytes is computed via json.Marshal(envelope) (no indentation), but RunContext outputs the envelope using an indented encoder. That makes total_bytes systematically under-report the actual bytes written to stdout. Consider computing the budget from the same encoding format you output (e.g., encode into a bytes.Buffer with the same indent settings), and handle the marshal/encode error instead of ignoring it.
| // Detect languages from file extensions | ||
| langSet := make(map[string]bool) | ||
| if info != nil { | ||
| for file := range info.Importers { | ||
| if lang := scanner.DetectLanguage(file); lang != "" { | ||
| langSet[lang] = true | ||
| } | ||
| } | ||
| } | ||
| for lang := range langSet { | ||
| ctx.Languages = append(ctx.Languages, lang) | ||
| } |
There was a problem hiding this comment.
Languages is built by ranging over a map (langSet), which yields a nondeterministic order. Since this command is meant to be consumed by tools, unstable ordering can cause noisy diffs/caches and make tests flaky. Consider sorting ctx.Languages before returning.
| fs := flag.NewFlagSet("serve", flag.ExitOnError) | ||
| port := fs.Int("port", 9471, "Port to listen on") | ||
| if err := fs.Parse(args); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| os.Exit(1) | ||
| } |
There was a problem hiding this comment.
RunServe uses flag.NewFlagSet(..., flag.ExitOnError) but then checks if err := fs.Parse(args); err != nil { ... }. With ExitOnError, Parse exits the process on error, so this block is effectively unreachable. Consider switching to flag.ContinueOnError for controlled errors/usage output (and to validate fs.NArg()), or remove the redundant error check.
| // GET /api/health — simple health check | ||
| mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { | ||
| writeJSON(w, map[string]string{"status": "ok", "root": absRoot}) | ||
| }) |
There was a problem hiding this comment.
The server binds to all interfaces by default (addr := fmt.Sprintf(":%d", *port)), but the startup message implies localhost-only, and endpoints like /api/context and /api/health can expose sensitive local filesystem/project info (e.g., root). Consider defaulting to 127.0.0.1 (or adding a --host/--listen flag), and avoid returning absRoot in the health response unless explicitly requested.
| if err := http.ListenAndServe(addr, mux); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| os.Exit(1) | ||
| } |
There was a problem hiding this comment.
http.ListenAndServe with the default http.Server has no timeouts, which makes the endpoint more vulnerable to slow-client/resource-exhaustion scenarios. Consider constructing an http.Server with at least ReadHeaderTimeout (and optionally ReadTimeout/WriteTimeout/IdleTimeout) and calling server.ListenAndServe().
| // Record this agent session in handoff history | ||
| agentEntry := handoff.AgentEntry{ | ||
| AgentID: detectAgentID(), | ||
| StartedAt: sessionStartTime(state), | ||
| EndedAt: time.Now(), | ||
| } | ||
| if state != nil { | ||
| seen := make(map[string]bool) | ||
| for _, e := range state.RecentEvents { | ||
| if !seen[e.Path] { | ||
| seen[e.Path] = true | ||
| agentEntry.FilesEdited = append(agentEntry.FilesEdited, e.Path) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Carry over history from previous artifact and append current session | ||
| if prev, err := handoff.ReadLatest(root); err == nil && prev != nil { | ||
| artifact.AgentHistory = prev.AgentHistory | ||
| } | ||
| artifact.AgentHistory = append(artifact.AgentHistory, agentEntry) | ||
|
|
||
| // Cap history to last 20 entries | ||
| if len(artifact.AgentHistory) > 20 { | ||
| artifact.AgentHistory = artifact.AgentHistory[len(artifact.AgentHistory)-20:] | ||
| } |
There was a problem hiding this comment.
Agent history recording/capping logic in writeSessionHandoff is new behavior but doesn’t appear to have test coverage (the cmd package has extensive hook tests). Consider adding tests that verify: (1) agent ID detection from env vars, (2) FilesEdited deduping from RecentEvents, and (3) history is capped to the last 20 entries when prior artifacts already contain history.
| fs := flag.NewFlagSet("context", flag.ExitOnError) | ||
| forPrompt := fs.String("for", "", "Pre-classify intent for this prompt") | ||
| compact := fs.Bool("compact", false, "Minimal output for token-constrained agents") | ||
| if err := fs.Parse(args); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| os.Exit(1) | ||
| } |
There was a problem hiding this comment.
RunContext uses flag.NewFlagSet(..., flag.ExitOnError) but then checks if err := fs.Parse(args); err != nil { ... }. With ExitOnError, Parse will call os.Exit(2) on error, so this error-handling block is effectively unreachable. Consider switching to flag.ContinueOnError (and printing a concise usage), or keep ExitOnError and remove the redundant err check.
Also consider rejecting extra positional args (e.g. fs.NArg() > 1) instead of silently ignoring them.
| func buildContextEnvelope(root, prompt string, compact bool) ContextEnvelope { | ||
| projCfg := config.Load(root) | ||
| info := getHubInfoNoFallback(root) | ||
|
|
||
| envelope := ContextEnvelope{ | ||
| Version: 1, | ||
| GeneratedAt: time.Now(), | ||
| Project: buildProjectContext(root, info), | ||
| Budget: BudgetInfo{Compact: compact}, | ||
| } | ||
|
|
There was a problem hiding this comment.
New codemap context output paths/filters and compact vs full behavior aren't covered by tests in cmd (this package already has substantial test coverage). Consider adding unit tests around buildContextEnvelope to lock down: (1) compact omits skills/handoff, (2) intent classification is included when --for is provided, and (3) output fields like Languages are stable.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 924abeabaf
ℹ️ 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".
| writeJSON(w, map[string]string{"status": "ok", "root": absRoot}) | ||
| }) | ||
|
|
||
| addr := fmt.Sprintf(":%d", *port) |
There was a problem hiding this comment.
Bind serve listener to loopback by default
The server address is built as ":<port>", which makes http.ListenAndServe bind on all network interfaces, not just localhost. In environments like shared dev boxes, CI runners, or remote VMs, this exposes /api/context and /api/skills to the network even though the startup message says http://localhost.... Use an explicit loopback bind (for example 127.0.0.1:<port>) or add a host flag with a safe default.
Useful? React with 👍 / 👎.
| for file := range info.Importers { | ||
| if lang := scanner.DetectLanguage(file); lang != "" { | ||
| langSet[lang] = true |
There was a problem hiding this comment.
Derive project languages from more than importer keys
Language detection iterates only over info.Importers keys, but that map only contains files that are imported by at least one other file. Repos with entrypoints/scripts or sparse dependency edges (for example a single-file project) will report languages: [] even when source files exist, making the context envelope inaccurate for downstream routing and tool selection.
Useful? React with 👍 / 👎.
- 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>
…andoff (#52) * feat: add skills framework — builtin skills, matching, CLI, and MCP tools 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> * docs: update README with skills framework, intelligent routing, and roadmap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add context protocol, HTTP API, and agent-aware handoff 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> * fix: address PR #51 review comments - 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> * docs: update README with context protocol, HTTP API, and agent handoff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix gofmt alignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Superseded by #52 which combined Phase 2+3 and is now merged to main. |
Summary
Phase 3 — the final phase. Makes codemap's intelligence consumable by any AI tool.
codemap context— Universal JSON EnvelopeOutput includes project info, intent classification, working set, matched skills, and handoff reference — everything an AI tool needs in one call.
codemap serve— HTTP APIEnables VS Code extensions, CI pipelines, web dashboards, and non-MCP AI tools to consume codemap intelligence.
Agent-Aware Handoff History
AgentEntryrecords which agent worked, when, and what it editedTest plan
go test ./... -race— all 10 packages passcodemap context --for "refactor auth"returns full envelope with intent + skillscodemap context --compactstrips skills/handoff, limits working set🤖 Generated with Claude Code