Skip to content

Commit b0bf359

Browse files
committed
Consolidate MCP tools from 13 to 9 tools, merging redundant endpoints and simplifying parameters
1 parent b4b9528 commit b0bf359

8 files changed

Lines changed: 55 additions & 129 deletions

File tree

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ ts: 2026-06-14T16:10:00-07:00
242242

243243
All three construct the Service identically via a small `wire()` that picks adapters (native vs ripgrep search, real vs fake store) — keep composition in one place.
244244

245-
**Session resolution (adapter-side, see ADR 0003).** `core` stays pure: every session-scoped method takes an explicit `SessionID`. *Which* session an adapter is acting for is discovered at the edge by `harness.ResolveSessionID(explicit, allowDefault)`, with precedence `explicit param/flag → CLAUDE_CODE_SESSION_ID (set by Claude Code in each session's env) → DOSSIER_SESSION → sess_default`. The MCP adapter calls it with `allowDefault=false` and **degrades visibly** (`harness_capability_unavailable`) rather than silently sharing the `sess_default` bucket across concurrent sessions; the CLI calls it with `allowDefault=true` for manual use. This is the bridge that lets an in-session agent call `dossier_switch` with only a slug. The **TUI carries no session identity at all** (it does not expose `Switch`/`Active`) — it is a read/edit viewer over the store; see [ADR 0004](docs/adr/0004-tui-no-session.md).
245+
**Session resolution (adapter-side, see ADR 0003).** `core` stays pure: every session-scoped method takes an explicit `SessionID`. *Which* session an adapter is acting for is discovered at the edge by `harness.ResolveSessionID(explicit, allowDefault)`, with precedence `explicit param/flag → CLAUDE_CODE_SESSION_ID (set by Claude Code in each session's env) → DOSSIER_SESSION → sess_default`. The MCP adapter calls it with `allowDefault=false` and **degrades visibly** (`harness_capability_unavailable`) rather than silently sharing the `sess_default` bucket across concurrent sessions; the CLI calls it with `allowDefault=true` for manual use. This is the bridge that lets an in-session agent call `dossier_session` with only a slug. The **TUI carries no session identity at all** (it does not expose `Switch`/`Active`/`Session` binding) — it is a read/edit viewer over the store; see [ADR 0004](docs/adr/0004-tui-no-session.md).
246246

247247
**MCP**: use the official Go MCP SDK over stdio. Each `dossier_*` tool (SPEC §8.1) is a ~10-line handler: parse input → call one Service method → marshal `Result` into the §8.2 envelope. Map typed errors → §8.2 codes in **one** place (`mcp/errors.go`).
248248

BUILD-DECISIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ This document closes the implementation-discovery questions so the build can sta
2929

3030
These are smaller inconsistencies a dev agent *will* hit. Resolved here; mechanics in `ARCHITECTURE.md` §"Concurrency & Revisions".
3131

32-
1. **`base_revision` does not belong in Dossier frontmatter.** SPEC §4.1 lists it as a (optional) frontmatter field, but a revision is a *session-side* fact, not a property of the Dossier. **Remove it from frontmatter.** Instead: `dossier_recall`, `dossier_active`, and `dossier_switch` **return** the current revision; the agent passes it back as `base_revision` on `dossier_save`. The session binding persists it as `last_seen_revision`.
32+
1. **`base_revision` does not belong in Dossier frontmatter.** SPEC §4.1 lists it as a (optional) frontmatter field, but a revision is a *session-side* fact, not a property of the Dossier. **Remove it from frontmatter.** Instead: `dossier_recall` and `dossier_session` **return** the current revision; the agent passes it back as `base_revision` on `dossier_save`. The session binding persists it as `last_seen_revision`.
3333

3434
2. **Recall output must include the revision.** SPEC §14.2 / §8 don't show recall returning a revision, but the agent needs it to save later. Recall returns `{distilled_state, frontmatter, revision, token_estimate, warnings}`.
3535

HANDOFF.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ Precedence when docs disagree: `BUILD-DECISIONS.md` > `SPEC.md` (mechanics) > `P
1919
> **Stage: All Milestones Completed (Project Fully Finished) + Lead Tracking + TUI Enhancements.** The entire Dossier durable memory layer (codename *chainlink*) is fully implemented, verified, and integrated across all surfaces (CLI, MCP, and the Bubble Tea TUI).
2020
> - **Milestone 1–5:** Core file store, CLI, recall, warnings, lexical search/suggestions, promote/link flow, and the MCP stdio server are implemented.
2121
> - **Milestone 6:** Active session binding, hook installation for Claude Code, confirmation prompts, capability detection, non-clobbering configurations, and the interactive **Rich TUI** (dashboard, detail recall view with native markdown rendering and fsnotify hot-refreshing, status/priority/next-action inline editing, ambiguity link resolution, and syntax-highlighted merge conflict resolving) are fully completed.
22-
> - **Lead Tracking & Accountability:** A newly added `Lead` field allows assigning team members to specific dossiers. Supported end-to-end through `dossier promote --lead`, `dossier lead`, `dossier_set_lead` MCP tool, and visually grouped in the TUI with an inline `a` (assign) editor.
22+
> - **Lead Tracking & Accountability:** A newly added `Lead` field allows assigning team members to specific dossiers. Supported end-to-end through `dossier promote --lead`, `dossier lead`, `dossier_update` MCP tool, and visually grouped in the TUI with an inline `a` (assign) editor.
2323
> - **Milestone 7:** Optimistic concurrency control, non-overlapping frontmatter auto-merging, DP LCS unified diff body conflict generation (writing to `conflicts/`), and `dossier merge` CLI/Service commands are verified.
2424
> - **Milestone 8:** The final Distillation Guide is authored in `assets/guide.md` and embedded in the binary to be written to `~/.dossier/context/guide.md` upon initialization. It has been upgraded to employ rigorous linguistic compression (syntactic pruning, lexical density, and negative space framing). All dogfooding validations, test sweeps, and PRD success metrics have been fully met.
2525
> - **Stable Install & Auto-MCP Configuration:** Implemented stable binary self-install path command (`dossier install`, default `~/.local/bin/dossier`), volatile path detection on `init`, and auto-registration of both the MCP stdio server and lifecycle hooks in Claude Code's user/global configuration files (preserving existing third-party configs and backing up changed files).
2626
> - **v1 rescoped to Claude Code only (2026-06-16):** Codex and Antigravity harnesses removed from code and docs. Claude Code is the single supported harness — it provides the full capability set; the degraded Tier 2/3 levels other harnesses reached were insufficient. The `Harness` interface/registry remain for possible future harnesses.
27-
> - **In-session Dossier switching fixed (2026-06-16, ADR 0003):** Previously the MCP `dossier_switch`/`dossier_active` tools required a `session_id` the agent had no way to obtain, so an agent could not change (or even read) its active Dossier from inside Claude Code. Root cause + fix: Claude Code sets `CLAUDE_CODE_SESSION_ID` in the MCP server's env (verified identical to the transcript UUID and hook stdin `session_id`). A shared `harness.ResolveSessionID` now resolves the session at the adapter edge (precedence: explicit → `CLAUDE_CODE_SESSION_ID``DOSSIER_SESSION``sess_default`); MCP degrades visibly instead of falling back to the shared bucket. An agent can now call `dossier_switch` with just a slug; per-session isolation is preserved. See `docs/harness-capabilities.md` (§ MCP Session Identity) and ADR 0003.
27+
> - **In-session Dossier switching fixed (2026-06-16, ADR 0003):** Previously the MCP `dossier_session` tool (consolidating switch/active) required a `session_id` the agent had no way to obtain, so an agent could not change (or even read) its active Dossier from inside Claude Code. Root cause + fix: Claude Code sets `CLAUDE_CODE_SESSION_ID` in the MCP server's env (verified identical to the transcript UUID and hook stdin `session_id`). A shared `harness.ResolveSessionID` now resolves the session at the adapter edge (precedence: explicit → `CLAUDE_CODE_SESSION_ID``DOSSIER_SESSION``sess_default`); MCP degrades visibly instead of falling back to the shared bucket. An agent can now call `dossier_session` with just a slug; per-session isolation is preserved. See `docs/harness-capabilities.md` (§ MCP Session Identity) and ADR 0003.
2828
> - **Hardening (2026-06-17):** Addressed critical system stability and performance risks. Prevented OOM crashes by streaming artifact frontmatter instead of loading large bodies. Fixed severe O(N) performance degradation by bypassing full YAML parsing with fast-path string lookups during directory scans. Ensured strict history preservation, bounded LCS diff matrices for massive conflict files, resolved symlink-destructive writes in the harness adapter, and fortified non-interactive stdin blocking. Enhanced `Doctor` with comprehensive provenance validation, artifact origin checks, and unresolved conflict reporting. Improved `SessionEnd` safety by adding explicit audit logging for missing payload scenarios.
2929
> - **TUI de-sessioned (2026-06-17, ADR 0004):** The TUI no longer resolves or carries a session identity and no longer exposes the per-session active binding. Removed the `a` ("make active") key, the `` active marker/column, the `Session:`/`Active:` header fields, and the standalone-session footer warning — these only ever acted on the `sess_default` bucket that no live agent session reads (the "make-active does nothing for me" / "fixed Session value" confusion). The TUI is now purely a reactive browse/edit viewer (list, markdown recall with live hot-refreshing, status/priority/next-action inline editing, link, syntax-highlighted merge). `Service.Switch`/`Active` are unchanged and remain the CLI/MCP surface for per-session binding. Narrows B9 (see ADR 0004); supersedes ADR 0002 and obsoletes `docs/tui-plan.md`'s catch-up section.
3030

PRD.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,14 @@ This guide is a prompt asset to iterate on like code — its quality is a primar
178178
All tools are namespaced with a **`dossier_` prefix** so they're unambiguously identifiable as Dossier's amid other MCP servers in a harness.
179179

180180
- `dossier_list` — open-work list for in-session refresh (deterministic surfacing is the hook's job, §4.1)
181-
- `dossier_recall(id)` — token-targeted resume (§4.2)
181+
- `dossier_recall(id)` — token-targeted resume, returns the filesystem path of the dossier (§4.2)
182182
- `dossier_search(query, dossier_id?)`
183183
- `dossier_save(id)` — governed write, no gate (§4.4, §4.11)
184184
- `dossier_promote()` — promote current session
185185
- `dossier_link(id?)` — with suggestions
186186
- `dossier_merge(a, b)` — conflicts surfaced
187-
- `dossier_active()` / `dossier_switch(id)` — inspect or change the Dossier bound to the current agent session
188-
- `dossier_set_status` — lifecycle status change
189-
- `dossier_update(id, next_action?, open_questions?, importance?, urgency?, due_date?)` — update any metadata fields in one call
190-
- `dossier_path(id?)` — returns the active or specified Dossier directory path for inspection in the user's own tools
187+
- `dossier_session(id?)` — inspect or change the Dossier bound to the current agent session
188+
- `dossier_update(id, name?, status?, lead?, next_action?, open_questions?, importance?, urgency?, due_date?)` — update any metadata or frontmatter fields in one call
191189

192190
Write tools **commit without a human gate** (D4), but writes are governed by the Distillation Guide and fire on a deterministic cadence (§4.11) — not at the agent's discretion. Safety is structural: nothing is deleted, the audit log records every write, and the user can edit any Distilled State after the fact. Exceptions: ambiguous link targets and merge conflict resolution require human disambiguation; that's contradiction/target resolution, not a distillation review gate.
193191

@@ -201,7 +199,7 @@ Write tools **commit without a human gate** (D4), but writes are governed by the
201199
- Generates/refreshes a **context file** per Dossier for harnesses without MCP.
202200

203201
### 5.3 Slash command (in-session)
204-
- **`/dossier`** — lists Dossiers grouped by `status`, in surfacing order (§4.1), without leaving the agent session. Optional args: `/dossier active`, `/dossier blocked`, etc. to filter to one status. It also supports selecting/switching the session's active Dossier from the list. Thin wrapper over `dossier_list` + `dossier_switch`; this is the quick "what's on my plate right now" view mid-conversation.
202+
- **`/dossier`** — lists Dossiers grouped by `status`, in surfacing order (§4.1), without leaving the agent session. Optional args: `/dossier active`, `/dossier blocked`, etc. to filter to one status. It also supports selecting/switching the session's active Dossier from the list. Thin wrapper over `dossier_list` + `dossier_session`; this is the quick "what's on my plate right now" view mid-conversation.
205203

206204
### 5.4 Hooks (deterministic behavior)
207205
Some behavior must not depend on the agent choosing to act; hooks make it deterministic where the harness supports them (e.g. Claude Code), with the context-file/skill fallback elsewhere.
@@ -290,7 +288,7 @@ That makes the behavior automatic without any user-visible ceremony at session s
290288
The confirmation itself should be lightweight:
291289
> "I see a couple of Dossiers that look related — *Auth refactor (active, last updated 3 days ago)* and *Login flow cleanup (blocked)*. Is one of these the right one to continue, or is this a separate thread?"
292290
293-
If the user picks one, bind to it (`dossier_switch`) and resume. If none fit, proceed with creation.
291+
If the user picks one, bind to it (`dossier_session`) and resume. If none fit, proceed with creation.
294292

295293
**Design notes:**
296294
- The check is zero-latency because the library is already in context — this is not a search call, just an in-context scan.

SPEC.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ Binding fields:
287287
}
288288
```
289289

290-
If a harness cannot expose a durable session id, the agent may maintain binding in context only. In that case, Dossier must still support `dossier_switch(id)` and `dossier_active()` during the session, but deterministic recovery of the binding after restart is not guaranteed.
290+
If a harness cannot expose a durable session id, the agent may maintain binding in context only. In that case, Dossier must still support `dossier_session(id?)` during the session, but deterministic recovery of the binding after restart is not guaranteed.
291291

292292
### 5.2 Switching
293293

@@ -444,11 +444,7 @@ Required tools:
444444
- `dossier_promote`
445445
- `dossier_link`
446446
- `dossier_merge`
447-
- `dossier_active`
448-
- `dossier_switch`
449-
- `dossier_path`
450-
- `dossier_set_status`
451-
- `dossier_set_lead`
447+
- `dossier_session`
452448
- `dossier_update`
453449

454450
### 8.2 Tool Contracts
@@ -964,8 +960,7 @@ Checks:
964960

965961
- Each session can bind zero or one active Dossier.
966962
- Two sessions can bind different Dossiers.
967-
- `dossier_active` reports current binding.
968-
- `dossier_switch` changes binding and returns new Distilled State.
963+
- `dossier_session` reports or changes session binding and returns new Distilled State when switching.
969964
- `/clear` removes session context but does not alter Dossier files.
970965

971966
### 14.7 Harness Transparency

internal/core/service.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type RecallResult struct {
3333
Frontmatter Frontmatter `json:"frontmatter"`
3434
Revision Revision `json:"revision"`
3535
TokenEstimate int `json:"token_estimate"`
36+
Path string `json:"path"`
3637
}
3738

3839
// ListItem represents a single summary item for dossier listings.
@@ -49,6 +50,7 @@ type ListItem struct {
4950
DueDate string `json:"due_date,omitempty"`
5051
StalenessDays int `json:"staleness_days"`
5152
PriorityScore int `json:"priority_score"`
53+
Path string `json:"path"`
5254
}
5355

5456
// DoctorReport summarizes integrity checks run by Doctor.
@@ -302,7 +304,7 @@ func (s *Service) Promote(ctx context.Context, req PromoteReq) (Result, error) {
302304
Data: candidates,
303305
NextActions: []NextAction{
304306
`Present the candidates to the user: "I found Dossiers that look related — [for each: Name (status, N days since last update)]. Is one of these the right one to continue, or is this a separate thread?"`,
305-
`If the user picks one: call dossier_switch with its slug to bind it, then dossier_recall to load its state.`,
307+
`If the user picks one: call dossier_session with its slug to bind it, then dossier_recall to load its state.`,
306308
`If the user confirms this is a new topic: call dossier_promote again with force=true.`,
307309
},
308310
}, NewError(ErrAmbiguousTarget, "Multiple likely Dossiers match this promote request.")
@@ -950,9 +952,10 @@ func (s *Service) Recall(ctx context.Context, req RecallReq) (Result, error) {
950952
warnings = append(warnings, Warning(fmt.Sprintf("Distilled State exceeds token target (%d > %d tokens). Consider condensing.", tokens, target)))
951953
}
952954

955+
dossierPath := filepath.Join(s.cfg.DossierHome, d.Frontmatter.Slug)
953956
return Result{
954957
OK: true,
955-
Data: RecallResult{DistilledState: d.DistilledState.Body, Frontmatter: d.Frontmatter, Revision: rev, TokenEstimate: tokens},
958+
Data: RecallResult{DistilledState: d.DistilledState.Body, Frontmatter: d.Frontmatter, Revision: rev, TokenEstimate: tokens, Path: dossierPath},
956959
Warnings: warnings,
957960
}, nil
958961
}
@@ -1003,10 +1006,11 @@ func (s *Service) List(ctx context.Context, req ListReq) (Result, error) {
10031006
var items []ListItem
10041007
for _, sItem := range scored {
10051008
daysSinceTouched := int(now.Sub(sItem.fm.LastTouchedAt).Hours() / 24)
1006-
if daysSinceTouched < 0 {
1009+
if daysSinceTouched < 0 {
10071010
daysSinceTouched = 0
10081011
}
10091012

1013+
dossierPath := filepath.Join(s.cfg.DossierHome, sItem.fm.Slug)
10101014
items = append(items, ListItem{
10111015
ID: sItem.fm.ID,
10121016
Name: sItem.fm.Name,
@@ -1020,6 +1024,7 @@ if daysSinceTouched < 0 {
10201024
DueDate: sItem.fm.DueDate,
10211025
StalenessDays: daysSinceTouched,
10221026
PriorityScore: sItem.score,
1027+
Path: dossierPath,
10231028
})
10241029
}
10251030

@@ -1471,7 +1476,7 @@ func (s *Service) SessionStart(ctx context.Context, sessionID string) (string, e
14711476
sb.WriteString("No active Dossier is bound to this session.\n\n")
14721477
sb.WriteString("When the user names a topic to work on, check the Open Dossiers list above before creating anything:\n")
14731478
sb.WriteString("1. If a close match exists, surface it: \"I see [Name] ([status], last touched N days ago) — is that the one to continue, or is this a new thread?\"\n")
1474-
sb.WriteString("2. If the user confirms an existing one, call dossier_switch with its slug.\n")
1479+
sb.WriteString("2. If the user confirms an existing one, call dossier_session with its slug.\n")
14751480
sb.WriteString("3. If none match or the user says it's new, call dossier_promote — it will run a similarity check and flag any missed candidates before creating.\n")
14761481
}
14771482

0 commit comments

Comments
 (0)