feat(cli): tighten MCP routing and add dev-aware user-scoped config install#207
feat(cli): tighten MCP routing and add dev-aware user-scoped config install#207mike-inkeep merged 17 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: b2bcbd8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
PR Review Summary
(4) Total Issues | Risk: Medium
🟠⚠️ Major (2) 🟠⚠️
🟠 1) server-discovery.ts:312-318 Race condition in concurrent cache access
Issue: The createProjectServerUrlResolver function has a potential race condition where concurrent calls can interleave between checking the cache and setting it. Two calls for the same effectiveCwd arriving within the same tick could both miss the cache, both call ensure(), and one result could overwrite the other.
Why: While the impact is limited (the server URL should be deterministic for the same cwd), concurrent auto-starts could spawn duplicate server processes before the first one writes its lock file. This is especially relevant during bursty MCP tool usage.
Fix: Consider using a pending-promise pattern (similar to pendingRootsLoad in createProjectRoutingResolver) to deduplicate in-flight resolutions:
const pending = new Map<string, Promise<string | undefined>>();
return async (cwd?: string): Promise<string | undefined> => {
const effectiveCwd = await normalizeCwd(cwd ?? opts.startupCwd);
const now = Date.now();
const cached = cache.get(effectiveCwd);
if (cached && cached.expiresAt > now) return cached.url;
// Dedupe concurrent resolutions
let pendingResolve = pending.get(effectiveCwd);
if (!pendingResolve) {
pendingResolve = (async () => {
// ... resolution logic
})().finally(() => pending.delete(effectiveCwd));
pending.set(effectiveCwd, pendingResolve);
}
return pendingResolve;
};Refs:
🟠 2) normalize-cwd.ts:12-18 Silent fallback on realpath failure obscures issues
Issue: The normalizeCwd function catches all realpath errors silently and falls back to the absolute path. While the JSDoc documents this behavior, no logging occurs when the fallback is triggered.
Why: When symlink resolution fails (e.g., broken symlinks, permission issues, or paths that don't exist), operators have no visibility into why cache keys might not be stable across calls. This can cause subtle cache misses where the same logical path sometimes uses realpath and sometimes doesn't, depending on transient filesystem state.
Fix: Add a debug-level log when the fallback path is taken:
export async function normalizeCwd(cwd: string): Promise<string> {
const absolute = resolve(cwd);
try {
return await realpath(absolute);
} catch (err) {
// Fallback to absolute path when realpath fails (path doesn't exist, broken symlink, etc.)
// This is intentional per the JSDoc, but worth logging for debugging cache key stability.
if (process.env.DEBUG) {
console.error(`[normalize-cwd] realpath failed for ${absolute}: ${err instanceof Error ? err.message : err}`);
}
return absolute;
}
}Refs:
Inline Comments:
- 🟠 Major:
server-discovery.ts:314Race condition in concurrent cache access - 🟠 Major:
normalize-cwd.ts:12Silent fallback without logging
🟡 Minor (2) 🟡
🟡 1) server-discovery.ts:228,250,253,265 Inconsistent error message prefix
Issue: Error messages in ensureServerRunning use OK: prefix (e.g., OK: spawn failed:, OK: server did not start) while most other error messages in the codebase use Error: prefix.
Why: Inconsistent error prefixes make log aggregation and error categorization harder. Users may not recognize OK: as an error prefix.
Fix: Consider aligning to a consistent prefix pattern across the codebase.
Refs:
🟡 2) shared.ts:74-91 New helper functions lack unit tests
Issue: resolveProjectConfigContext and resolveProjectServerContext are new exported functions in shared.ts but there are no corresponding unit tests for these functions.
Why: These functions encapsulate the core cwd → config → server resolution logic used by all MCP tools. Edge cases (thrown errors, missing config, etc.) should have explicit test coverage.
Fix: Add unit tests covering: (1) successful resolution, (2) resolveCwd throwing, (3) resolveConfig throwing, (4) resolveServerUrl throwing.
Refs:
Inline Comments:
- 🟡 Minor:
server-discovery.ts:228Error message prefix inconsistency - 🟡 Minor:
shared.ts:74Missing unit tests for helper functions
💭 Consider (1) 💭
💭 1) server-discovery.test.ts:1 Test organization could be improved
Issue: Tests for server-discovery.ts are well-written but could benefit from grouping into nested describe blocks by function (decideAutoStart, ensureServerRunning, createProjectServerUrlResolver).
Why: Nested describes improve test discoverability and make it easier to run subsets of tests during development.
Refs:
💡 APPROVE WITH SUGGESTIONS
Summary: This PR implements a clean strict routing contract for MCP project selection. The architecture is sound — using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches. The main concerns are around a potential race condition in concurrent cache access (fixable with the pending-promise pattern already used elsewhere in this PR) and silent error handling in normalizeCwd. Neither blocks merge, but addressing the race condition before heavy multi-client usage would be wise.
Discarded (3)
| Location | Issue | Reason Discarded |
|---|---|---|
server-discovery.ts:328 |
Host value uses config.server.host instead of opts.host |
This is intentional — opts.host is for CLI-level decisions while config-resolved host is correct for per-project server discovery |
server.ts:322 |
effectiveCwd fallback to resolveCwd() without explicit cwd |
This is by design — the routing resolver handles the fallback logic with proper error messages |
init.ts |
MCP config diffing uses JSON.stringify | Deep equality via stringify is appropriate here given the simple object shapes involved |
Reviewers (6)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-standards |
4 | 1 | 0 | 0 | 1 | 0 | 2 |
pr-review-tests |
2 | 1 | 1 | 0 | 1 | 0 | 0 |
pr-review-consistency |
2 | 1 | 0 | 0 | 1 | 0 | 1 |
pr-review-errors |
2 | 1 | 0 | 0 | 1 | 0 | 1 |
pr-review-appsec |
1 | 0 | 0 | 0 | 0 | 0 | 1 |
pr-review-types |
1 | 0 | 0 | 0 | 0 | 0 | 1 |
| Total | 12 | 4 | 1 | 0 | 4 | 0 | 6 |
Resolve the CLI merge by keeping the strict per-tool MCP routing model while carrying forward compatible init updates like Codex support and Claude Desktop setup polish. Made-with: Cursor
There was a problem hiding this comment.
PR Review Summary
(4) Total Issues | Risk: Medium
🕐 Pending Recommendations (2)
Prior review flagged these issues — they remain ACTIVE (code unchanged since last review):
- 🟠
server-discovery.ts:338Race condition increateProjectServerUrlResolverconcurrent cache access — consider pending-promise deduplication pattern - 🟠
normalize-cwd.ts:12Silent fallback on realpath failure hides path resolution issues — add logging for non-ENOENT errors
🟡 Minor (1) 🟡
🟡 1) server-discovery.ts:243,270,288 Error message prefix inconsistent with MCP tool convention
Issue: Error messages use OK: prefix (e.g., OK: spawn failed:, OK: server did not start within...) while all other MCP tool error messages use Error: prefix.
Why: The Error: prefix is used across 50+ occurrences in MCP tool handlers. Agents may pattern-match on this prefix to identify tool-level failures. The OK: prefix is novel and undocumented.
Fix: Replace OK: with Error: in the three throw statements at lines 243, 270, and 287-289.
Refs:
Inline Comments:
- 🟠 Major:
review.txt:1Transient review artifact committed to repository - 🟡 Minor:
loader.ts:153Same cache race pattern as server-discovery.ts
💭 Consider (2) 💭
💭 1) mcp/server-discovery.test.ts Test file organization
Issue: Tests for decideAutoStart and ensureServerRunning live in commands/mcp.test.ts, not co-located with the implementation in server-discovery.ts.
Why: Developers modifying server-discovery.ts and running only its co-located test file would miss regressions in these critical functions. The functions ARE tested (thoroughly) — they're just in a different file.
Fix: Either move those tests to server-discovery.test.ts or add a comment pointing to mcp.test.ts.
💭 2) shared.ts:76-120 New helper functions lack direct unit tests
Issue: resolveProjectConfigContext and resolveProjectServerContext have no direct unit tests. They're exercised transitively through tool integration tests.
Why: These are the centralized error-handling wrappers all tools use. Edge cases (resolveCwd throws, resolveConfig throws) may not be directly covered.
Fix: Add ~30 lines of unit tests covering success/failure paths for both functions.
Refs:
💡 APPROVE WITH SUGGESTIONS
Summary: This PR implements a clean strict routing contract for MCP project selection. The architecture is sound — using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches. The prior review's two MAJOR findings (race condition in server URL resolver, silent realpath fallback) remain unaddressed but are not blockers. The new finding is a review.txt file that was accidentally committed and should be removed before merge.
Before merging:
- Remove
review.txtfrom the commit - Consider addressing the pending race condition suggestion in
server-discovery.ts(or accept it as low-risk given server lock file protection)
Discarded (3)
| Location | Issue | Reason Discarded |
|---|---|---|
review.txt findings |
Claims tools don't expose cwd parameter |
Outdated — all 19 routed tools now have cwd via ROUTED_CWD_DESCRIPTION |
init.ts conflict detection |
Claims user-managed fields cause false conflicts | Outdated — isCompatible now checks only managed fields via hasMatchingManagedFields |
keepalive never attaches |
Claims keepalive fails in multi-project mode | Needs verification — getKeepaliveCwd returns activeProjectCwd after any tool call succeeds |
Reviewers (4)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-standards |
2 | 0 | 0 | 0 | 1 | 0 | 1 |
pr-review-tests |
8 | 0 | 2 | 0 | 0 | 0 | 6 |
pr-review-errors |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-consistency |
2 | 1 | 0 | 0 | 1 | 0 | 0 |
| Total | 12 | 1 | 2 | 0 | 2 | 0 | 7 |
Note: pr-review-errors returned 0 findings because all significant error handling issues were already covered by the prior review.
Use one global Open Knowledge MCP registration across supported editors now that per-call cwd routing makes project-local setup unnecessary. Add global path detection, legacy local-config warnings, and updated docs/tests for the new init behavior. Made-with: Cursor
There was a problem hiding this comment.
PR Review Summary
(6) Total Issues | Risk: Medium
🕐 Pending Recommendations (4)
Prior review flagged these issues — they remain ACTIVE (code unchanged since last review):
- 🟠
server-discovery.ts:338Race condition increateProjectServerUrlResolverconcurrent cache access — consider pending-promise deduplication pattern - 🟠
normalize-cwd.ts:12Silent fallback on realpath failure hides path resolution issues — add logging for non-ENOENT errors - 🟡
server-discovery.ts:243Error message prefix inconsistency — useError:instead ofOK: - 🟠
review.txt:1Transient review artifact committed to repository — remove before merge
🟠⚠️ Major (1) 🟠⚠️
🟠 1) .changeset/ Missing changeset for CLI public surface changes
Issue: This PR makes significant changes to the CLI's MCP routing behavior (strict routing contract, project-scoped tool calls, cwd normalization) but has no changeset describing these changes. The deleted claude-desktop-init-cwd.md was from a different merged PR (#221).
Why: Per release engineering conventions, public surface changes require a changeset. The CLI's MCP routing behavior is user-facing — agents relying on the previous fallback-to-startup-cwd behavior will now receive errors. The PR description notes "tool calls now route by explicit cwd first, otherwise by the client's only advertised root, and otherwise fail clearly instead of guessing" which is a behavioral change requiring documentation.
Fix: Create a new changeset via bun run changeset:
---
"@inkeep/open-knowledge": minor
---
feat(cli): enforce strict MCP routing — tool calls now require explicit `cwd` parameter or single client root, instead of falling back to startup cwd
Refs:
- changeset-bot comment — "No Changeset found"
🟡 Minor (3) 🟡
🟡 1) logger.ts, keepalive.ts, exec.ts Silent error swallowing in multiple catch blocks
Issue: Several catch blocks silently swallow errors without any logging:
logger.ts:97—appendFileSyncerrors whenOK_LOG_FILEis setkeepalive.ts:173— WebSocket error eventsexec.ts:503,538— enrichPath/enrichDirectory and batch backlink fetch failures
Why: When these operations fail, operators have no visibility into what went wrong. This makes debugging significantly harder, especially for the logging subsystem itself where silent failures defeat the purpose of logging.
Fix: Add console.debug or console.warn logging before swallowing each error.
Inline Comments:
- 🟡 Minor:
logger.ts:97Silent swallow of appendFileSync errors - 🟡 Minor:
keepalive.ts:176WebSocket error listener swallows errors - 🟡 Minor:
exec.ts:503enrichPath errors swallowed - 🟡 Minor:
exec.ts:538Batch backlink fetch failure swallowed
🟡 2) normalize-cwd.ts, logger.ts Missing co-located unit tests for new utilities
Issue: The new normalize-cwd.ts utility and logger.ts module have no co-located test files, unlike other utilities in the same directories (frontmatter.test.ts, server-discovery.test.ts).
Why: The codebase convention is "Tests co-located with source: foo.test.ts next to foo.ts". These are central utilities for the new routing logic.
Fix: Add normalize-cwd.test.ts covering: (1) absolute path resolution, (2) symlink canonicalization, (3) ENOENT fallback. Add logger.test.ts covering debug gating and child logger behavior.
Refs:
- packages/cli/src/utils/frontmatter.test.ts — sibling test file pattern
🟡 3) get-preview-url.test.ts Missing error path test coverage
Issue: The new tests verify that buildGetPreviewUrlResult passes cwd to config resolvers, but do not test the error handling when resolveCwd throws (e.g., no client roots and no explicit cwd).
Why: If resolveCwd throws, the tool should return a user-friendly error, not crash. The test shows the happy path but not the failure path.
Fix: Add a test case: resolveCwd: async () => { throw new Error('No client roots'); } and verify the result contains {ok: false, ...} with the expected error message.
Refs:
- shared.ts:76-93 — resolveProjectConfigContext error handling
💭 Consider (2) 💭
💭 1) logger.ts:47 Logger method signature inconsistent with server package
Issue: McpLogger uses (msg, err?, ctx?) signature while server's PinoLogger uses (data, message) matching Pino convention.
Why: Cross-package consistency reduces cognitive load. Consider documenting the intentional divergence.
Inline Comments:
- 💭 Consider:
logger.ts:47Logger signature inconsistency
💭 2) biome.jsonc Unrelated tooling bump bundled with feature PR
Issue: Biome schema version bumped from 2.4.11 to 2.4.12 as a transitive lockfile update.
Why: Bundling unrelated dependency updates with feature PRs makes it harder to diagnose regressions. Consider splitting or noting explicitly.
💡 APPROVE WITH SUGGESTIONS
Summary: This PR implements a clean strict routing contract for MCP project selection. The architecture is sound — using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches. The prior review's two MAJOR findings (race condition in server URL resolver, silent realpath fallback) remain unaddressed in code but are documented as pending recommendations. The main new finding is a missing changeset for this behavioral change — agents will start receiving errors instead of fallback behavior, which warrants release documentation.
Before merging:
- Remove
review.txtfrom the commit - Add a changeset describing the strict routing behavioral change
- Consider addressing the pending race condition suggestion (or accept it as low-risk given server lock file protection)
Discarded (5)
| Location | Issue | Reason Discarded |
|---|---|---|
loader.ts:59 |
YAML parse error truncates line info | Low impact — yaml library's error.message already includes position info in most cases |
server.ts:98 |
roots/list error type not distinguished | Acceptable — the error is logged and re-thrown with a user-facing message |
exec.ts:385 |
stat failure in classifyPaths silent | ENOENT is expected for non-existent paths; other errors are rare edge cases |
editors.test.ts |
Missing isCompatible/mergeManagedFields tests | Covered transitively through init.test.ts integration tests |
server.test.ts:67 |
Test verifies schema not routing behavior | Unit tests for routing resolver provide adequate coverage |
Reviewers (5)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-standards |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-consistency |
4 | 0 | 1 | 0 | 1 | 0 | 2 |
pr-review-errors |
8 | 1 | 0 | 0 | 4 | 0 | 3 |
pr-review-tests |
5 | 1 | 0 | 0 | 0 | 0 | 4 |
pr-review-devops |
3 | 1 | 1 | 0 | 0 | 0 | 1 |
| Total | 20 | 3 | 2 | 0 | 5 | 0 | 10 |
Note: pr-review-standards returned 0 findings because all major issues were already covered in prior reviews.
Deduplicate per-project MCP resolution, surface routing/logging failures more clearly, and add focused coverage plus a changeset for the strict routing behavior. Made-with: Cursor
|
Addressed the current review pass in |
Keep `init --dev-mcp` consistent by pointing Claude preview launches at the local dist build, and stop tracking generated project-local editor config files in the repo. Made-with: Cursor
Made-with: Cursor # Conflicts: # README.md
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This is a well-executed refactor that implements a strict routing contract for MCP project selection. The architecture is sound: using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches with proper pending-promise deduplication.
What was done well
-
Clean routing abstraction —
createProjectRoutingResolverprovides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR,MULTIPLE_ROOTS_ERROR,ROOTS_UNAVAILABLE_ERROR) -
Race condition prevention — Both
createProjectServerUrlResolverandcreateProjectConfigResolveruse the pending-promise deduplication pattern, preventing duplicate server spawns and config loads -
Comprehensive test coverage — The new test files (
server-discovery.test.ts,logger.test.ts,normalize-cwd.test.ts,editors.test.ts) provide good coverage of the new functionality -
Structured logging — The new
McpLoggerprovides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions -
Prior feedback addressed — All issues from previous reviews have been resolved:
- ✅ Race condition in concurrent cache access — fixed with pending-promise pattern
- ✅ Silent realpath fallback — now warns on non-ENOENT errors
- ✅ Error message prefix — changed to
Error:consistently - ✅ review.txt artifact — removed
- ✅ Shared helper tests — added
- ✅ Auto-start tests moved — co-located with implementation
💭 Consider (2) 💭
Inline Comments:
- 💭 Consider:
keepalive.ts:176WebSocket error event lacks diagnostic context - 💭 Consider:
server.ts:106Error type classification in roots/list failure
🟡 Minor (4) — Test Coverage Opportunities 🟡
These are opportunities to strengthen test coverage for edge cases. None are blocking.
🟡 1) server-discovery.test.ts Missing test for synchronous spawn throw path
Issue: The implementation has a try/catch around the spawn call (lines 230-238) to handle EACCES or PATH resolution errors that throw synchronously. The test suite doesn't explicitly exercise this sync-throw path.
Why: If the sync-throw path breaks during refactoring, the error message formatting could regress without detection.
Fix: Add a test with spawn: () => { throw new Error('EACCES'); } and verify the error message format.
🟡 2) server-discovery.test.ts Missing test for async spawn error event path
Issue: The implementation attaches a child.on('error') handler to capture async spawn errors. The test suite doesn't exercise this path where an error event fires during polling.
Why: Async spawn errors (especially ENOENT when npx/node isn't found) are a real failure mode.
Fix: Add a test with a mock child that emits an 'error' event during the poll loop.
🟡 3) server.test.ts No test for concurrent roots resolution deduplication
Issue: The pendingRootsLoad pattern deduplicates concurrent root resolution calls. While server-discovery.test.ts validates the same pattern for server URL resolution, there's no equivalent test for resolveCwd.
Why: The pattern could regress during refactoring. Lower priority since the pattern is validated elsewhere.
🟡 4) loader.test.ts Missing test for config cache expiration
Issue: The createProjectConfigResolver has a cacheMs parameter for TTL, but no test verifies cache entries actually expire and reload.
Why: The TTL logic could regress, though this is low-impact since config rarely changes mid-session.
✅ APPROVE
Summary: This PR delivers a clean, well-tested implementation of strict MCP project routing. All prior review feedback has been addressed. The remaining suggestions are minor test coverage enhancements that don't block merge. Ship it! 🚀
Discarded (1)
| Location | Issue | Reason Discarded |
|---|---|---|
search.ts:116 |
buildSearchResult throws instead of returning structured error | Consistency with exec tool's structured errors is a valid point, but this is a pre-existing pattern and the PR's scope is routing — not error response normalization |
Reviewers (4)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-standards |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-tests |
4 | 4 | 0 | 0 | 0 | 0 | 0 |
pr-review-consistency |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-errors |
3 | 0 | 2 | 0 | 2 | 0 | 1 |
| Total | 7 | 4 | 2 | 0 | 2 | 0 | 1 |
| if (opts.bypassProjectSelection) { | ||
| const cwd = await startupCwdPromise; | ||
| opts.logger?.debug('cwd resolved', { cwd, routing: 'bypass' }); | ||
| return cwd; |
There was a problem hiding this comment.
💭 Consider: Error type classification in roots/list failure
Issue: The loadRoots() catch block logs the error generically as "roots/list unavailable" and rethrows ROOTS_UNAVAILABLE_ERROR. This loses the distinction between expected errors (client doesn't support roots/list) and unexpected ones (network timeout, protocol error).
Why: During debugging, operators seeing "roots/list unavailable" cannot tell if the client simply doesn't support the feature (expected, fallback to explicit cwd) or if there's a transport-layer problem that might affect other MCP calls.
Fix: Consider adding error type classification to the log context:
catch (err) {
const errorType = err instanceof Error && 'code' in err
? (err as any).code
: 'unknown';
opts.logger?.warn('roots/list unavailable', {
error: err instanceof Error ? err.message : String(err),
errorType,
});
throw new ProjectRoutingError(ROOTS_UNAVAILABLE_ERROR);
}Refs:
Add routing and keepalive diagnostics where the latest PR review asked for more context, and cover the remaining resolver and spawn edge cases with focused tests. Made-with: Cursor
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This PR implements a clean strict routing contract for MCP project selection. The architecture is sound: using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches with proper pending-promise deduplication.
What was done well
-
Clean routing abstraction —
createProjectRoutingResolverprovides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR,MULTIPLE_ROOTS_ERROR,ROOTS_UNAVAILABLE_ERROR) -
Race condition prevention — Both
createProjectServerUrlResolverandcreateProjectConfigResolveruse the pending-promise deduplication pattern, preventing duplicate server spawns and config loads -
Comprehensive test coverage — The new test files (
server-discovery.test.ts,logger.test.ts,normalize-cwd.test.ts,editors.test.ts) provide excellent coverage. The latest commit (0d3e3b6) adds tests for both sync spawn throws and async spawn error events. -
Structured logging — The new
McpLoggerprovides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions -
All prior feedback addressed:
- ✅ Race condition in concurrent cache access — fixed with pending-promise pattern
- ✅ Silent realpath fallback — now warns on non-ENOENT errors
- ✅ Error message prefix — changed to
Error:consistently - ✅ review.txt artifact — removed
- ✅ Shared helper tests — added
- ✅ Auto-start tests moved — co-located with implementation
- ✅ Error type classification in roots/list failure — added
classifyRootsLoadError() - ✅ WebSocket error event diagnostics — now emits debug breadcrumb
- ✅ Sync spawn throw test — added
- ✅ Async spawn error event test — added
Changes Since Last Review
Commit 0d3e3b6 ("fix(cli): follow up on routing review suggestions") addressed the remaining minor test coverage suggestions from the prior APPROVED review:
- Added
classifyRootsLoadError()helper anderrorTypefield in roots/list error logging - Added debug breadcrumb in WebSocket error handler instead of silent swallow
- Added test for synchronous spawn throw path (EACCES)
- Added test for async spawn error event path (ENOENT)
- Extended keepalive and server routing test coverage
✅ APPROVE
Summary: This is a well-executed refactor that delivers a clean, well-tested implementation of strict MCP project routing. All prior review feedback has been addressed, including the minor test coverage enhancements suggested in the last review. The routing contract is clear (explicit cwd → single client root → error), the caching strategy prevents race conditions, and the logging infrastructure supports operator debugging. Ship it! 🚀
Reviewers (1)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
orchestrator |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Note: This is a re-review following an APPROVED status. All prior findings have been resolved.
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This PR implements a clean strict routing contract for MCP project selection. The prior automated review (2026-04-21T01:33:32Z) approved this PR after all feedback was addressed.
Changes Since Last Review
The latest commit (2c495f5) is a merge from origin/main bringing in unrelated changes:
- Electron desktop app infrastructure (new
packages/desktop/) - UI components (OkBlob rage-click, ConnectingBanner grace period)
bootServerextraction from CLI to server package (D35)- CI matrix sharding for quality gate
Impact on routing changes: None. The CLI routing implementation (server.ts, server-discovery.ts, normalize-cwd.ts, tools/*) is unchanged since approval.
What Was Done Well
-
Clean routing abstraction —
createProjectRoutingResolverprovides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR,MULTIPLE_ROOTS_ERROR,ROOTS_UNAVAILABLE_ERROR) -
Race condition prevention — Both
createProjectServerUrlResolverandcreateProjectConfigResolveruse the pending-promise deduplication pattern, preventing duplicate server spawns and config loads -
Comprehensive test coverage — New test files (
server-discovery.test.ts,logger.test.ts,normalize-cwd.test.ts,editors.test.ts) provide excellent coverage -
All prior feedback addressed:
- ✅ Race condition in concurrent cache access — fixed with pending-promise pattern
- ✅ Silent realpath fallback — now warns on non-ENOENT errors
- ✅ Error message prefix — changed to
Error:consistently - ✅ review.txt artifact — removed
- ✅ Shared helper tests — added
- ✅ Auto-start tests — co-located with implementation
- ✅ Error type classification in roots/list — added
classifyRootsLoadError()
✅ APPROVE
Summary: This is a well-executed refactor that delivers a clean, strict routing contract for MCP project selection. All prior review feedback has been addressed. The merge from main does not affect the routing implementation. Ship it! 🚀
Reviewers (1)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
orchestrator |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Note: This is a re-review following an APPROVED status. The merge from main introduced no changes to the routing implementation under review.
Declare the app's ws types explicitly and remove stale dependency drift so clean-install build and knip checks pass again. Made-with: Cursor
Made-with: Cursor # Conflicts: # knip.config.ts
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This PR implements a clean strict routing contract for MCP project selection. The prior automated reviews (3 APPROVED verdicts) have all confirmed that previous feedback was addressed.
Changes Since Last Approval
The latest commits (de36c77, b75811b, 2c495f5) are merge commits from origin/main plus a housekeeping commit that:
- Reorders dependencies alphabetically in
package.json - Narrows export visibility (makes
resolveCodexHomePath,EditorMcpResult,LaunchJsonResult, etc. private) - Removes unused re-exports from
mcp.ts
Impact on routing changes: None. The CLI routing implementation (server.ts, server-discovery.ts, normalize-cwd.ts, tools/*) is unchanged since the last approval.
What Was Done Well
-
Clean routing abstraction —
createProjectRoutingResolverprovides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR,MULTIPLE_ROOTS_ERROR,ROOTS_UNAVAILABLE_ERROR) -
Race condition prevention — Both
createProjectServerUrlResolverandcreateProjectConfigResolveruse the pending-promise deduplication pattern, preventing duplicate server spawns and config loads -
Comprehensive test coverage — The new test files (
server-discovery.test.tsat 699 lines,server.test.tsat 395 lines,logger.test.tsat 93 lines) provide excellent coverage of the new functionality -
Structured logging — The new
McpLoggerprovides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions -
All prior feedback addressed:
- ✅ Race condition in concurrent cache access — fixed with pending-promise pattern
- ✅ Silent realpath fallback — now warns on non-ENOENT errors
- ✅ Error message prefix — changed to
Error:consistently - ✅ review.txt artifact — removed
- ✅ Shared helper tests — added
- ✅ Auto-start tests — co-located with implementation
- ✅ Error type classification in roots/list — added
classifyRootsLoadError()
Prior Feedback Status
The one previously ACTIVE automated comment at server.ts:114 about error type classification in roots/list failure has been addressed:
// Line 121-124: Now includes errorType in log context
opts.logger?.warn('roots/list unavailable', {
error: err instanceof Error ? err.message : String(err),
errorType: classifyRootsLoadError(err),
});✅ APPROVE
Summary: This is a well-executed refactor that delivers a clean, strict routing contract for MCP project selection. All prior review feedback has been addressed. The merge from main does not affect the routing implementation. The architecture is sound — using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches with proper pending-promise deduplication. Ship it! 🚀
Reviewers (1)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
orchestrator |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Note: This is a re-review following 3 prior APPROVED verdicts. The routing implementation is unchanged since the last approval — only merge commits from main have been added.
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This PR implements a clean strict routing contract for MCP project selection. This is a re-review following 4 prior APPROVED verdicts — all prior feedback has been addressed.
Changes Since Last Approval
The latest commits (b2bcbd8, 2b81347) since the last approval (0d3e3b6) are:
- Merge commits from
origin/mainbringing in unrelated changes (Electron desktop infrastructure, UI components,bootServerextraction) - Housekeeping: narrowed export visibility (made
DecideAutoStartInput,EnsureServerRunningOptions,CreateProjectServerUrlResolverOptions,ProjectRoutingResolver, etc. private instead of exported) - Fixed a flaky test
Impact on routing changes: None. The CLI routing implementation (server.ts, server-discovery.ts, normalize-cwd.ts, tools/*) is unchanged since the last approval.
What Was Done Well
-
Clean routing abstraction —
createProjectRoutingResolverprovides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR,MULTIPLE_ROOTS_ERROR,ROOTS_UNAVAILABLE_ERROR) -
Race condition prevention — Both
createProjectServerUrlResolverandcreateProjectConfigResolveruse the pending-promise deduplication pattern, preventing duplicate server spawns and config loads -
Comprehensive test coverage — ~1,400 lines of new tests across
server-discovery.test.ts(699 lines),server.test.ts(395 lines),logger.test.ts(93 lines),normalize-cwd.test.ts(39 lines), andeditors.test.ts(170 lines) -
Structured logging — The new
McpLoggerprovides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions -
All prior feedback addressed:
- ✅ Race condition in concurrent cache access — fixed with pending-promise pattern
- ✅ Silent realpath fallback — now warns on non-ENOENT errors
- ✅ Error message prefix — changed to
Error:consistently - ✅ review.txt artifact — removed
- ✅ Shared helper tests — added
- ✅ Auto-start tests — co-located with implementation
- ✅ Error type classification in roots/list — added
classifyRootsLoadError()witherrorTypein log context - ✅ Changeset added —
strict-mcp-routing.mdproperly describes the behavioral change
Prior Feedback Status
The one previously ACTIVE automated comment at server.ts:114 about error type classification has been addressed:
// Lines 121-124 now include errorType in log context:
opts.logger?.warn('roots/list unavailable', {
error: err instanceof Error ? err.message : String(err),
errorType: classifyRootsLoadError(err),
});✅ APPROVE
Summary: This is a well-executed refactor that delivers a clean, strict routing contract for MCP project selection. All prior review feedback has been addressed. The merge commits from main do not affect the routing implementation. The architecture is sound — using MCP roots/list for project context, normalizing paths with realpath, and maintaining TTL-based caches with proper pending-promise deduplication. Ship it! 🚀
Reviewers (1)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
orchestrator |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Note: This is a re-review following 4 prior APPROVED verdicts. The routing implementation is unchanged since the last approval — only merge commits from main and export visibility housekeeping have been added.
…les owns presence (#261) * docs(triage): refresh bug-bash status against PRs merged/opened 2026-04-16..21 Refresh coverage column against the last 5 days of commits + PRs: - #1 (presence icon overlap) ⬜→🔵 — PR #246 (open) fixes directly - #5 (Cmd+R orange banner flash) ⬜→✅ — PR #231 (merged) - #15 (install MCP for all agents) ⬜→🟡 — PRs #221 + #207 merged; spec 2026-04-18-mcp-install-harness-coverage + draft PR #223 open - V5 (bun x vs npx confusion) ⬜→🔵 — PR #225 merged (`ok` alias), PR #227 open (install-UX docs reconciliation) - V13 (`open-knowledge clone` not permitted) ⬜→🔵 — PR #244 open (shadow-repo single mode + auto-git-init + R6 fail-fast copy) - §4 multi-agent presence row → PR #246 open (no longer Draft-only) - Cluster A prerequisite updated with PR #246 status - Fix stale [[specs/2026-04-17-multi-agent-presence/SPEC]] redlinks at 6 sites; the active spec path is 2026-04-21-multi-agent-presence - Last updated 2026-04-20 → 2026-04-21 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(triage): link PR #222 (agent-identity foundation) + Miles owns presence - Item #1 Notes: add PR #222 (Nick's draft spec handed off to Miles, `specs/2026-04-18-agent-identity-attribution-foundation/`) — per-session CRDT identity (F1) is the substrate presence depends on - Item #1 Owner: Andrew → Miles - Item #18 Owner: (empty) → Miles - Cluster A: add PR #222 to prerequisite; declare Miles as owner - §4 row "Multi-agent presence": include PR #222 + Miles ownership - §5 next action #3: note Miles owns presence + agent-identity foundation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidates existing system, user scenarios, options considered (PRs #186/#191/#207, V0-14 spec, STORY §14), open tensions, and invariants into a single spec-ready context artifact. §12 addendum captures deep verification findings beyond the initial synthesis — attribution gaps in rename/rollback/save-version, leaked sessions on MCP subprocess exit, V0-16 cleanup residue at package boundaries, origin-system details (seven literals, structural-only paired check), fuzzer coverage gap for V0-14, and other edge cases. Non-prescriptive: topology mapping only, no recommendations.
* worldmodel: agent identity + attribution topology (pre-spec) Consolidates existing system, user scenarios, options considered (PRs #186/#191/#207, V0-14 spec, STORY §14), open tensions, and invariants into a single spec-ready context artifact. §12 addendum captures deep verification findings beyond the initial synthesis — attribution gaps in rename/rollback/save-version, leaked sessions on MCP subprocess exit, V0-16 cleanup residue at package boundaries, origin-system details (seven literals, structural-only paired check), fuzzer coverage gap for V0-14, and other edge cases. Non-prescriptive: topology mapping only, no recommendations. * spec: agent identity + attribution foundation (scaffold) SPEC.md + evidence/ + meta/_changelog.md for specs/2026-04-18-agent-identity-attribution-foundation/. Scope: F1 (per-session origin objects) + F2 (session lifecycle + cleanup) foundation; derives V0-14 per-session Agent UMs, attribution completeness sweep, shadow ref topology with classified writers (file-system / git-upstream / git-branch-switch / openknowledge-service), main-git save-version attribution (principal-author + Co-Authored-By trailers), transaction-effect capture (y-lite), principal representation (stable UUID + git config display), burst-grouping shared utility, keepalive-WS cleanup hooks. 20 decisions locked (D1-D20) with evidence citations. 11 open questions tracked (Q1-Q11) for iterative loop phase. Directive applied throughout: greenfield / no deferred tech debt / architecturally best / clean codebase / best product UX without over-engineering. Evidence files copied from worldmodel report: - crdt-to-git-translation.md (end-to-end pipeline trace, Option A/B/C analysis, subsystem interactions) - yjs-attribution-verification.md (C1-C8 verified against v13.6.30 and v14-rc; PermanentUserData + v14 AttributionManager as context) * spec: iterate loop D21-D54 — evidence-backed decisions locked Evidence from 3 parallel Opus investigations: - evidence/um-mechanics.md (Y.UndoManager internals, effect-diff, doc-op edges) - evidence/session-lifecycle.md (keepalive correlation, race, origin threading) - evidence/shadow-git-and-sweep.md (GPG split, refs, migration, L2 drain, park) 34 decisions locked under directive (greenfield / no-deferred-debt / architecturally best). Highlights: - Per-session UM scope = [ytext, metaMap, activityMap], captureTimeout 500, trackedOrigins writes-only, captureTransaction defense, deep-freeze origin - Effect-diff via YTextEvent.delta; Y.Map('agent-activity') storage; 30d/500-entry eviction - Keepalive ?connectionId= URL query + 30s cancellable WS-close grace; getSession in-flight promise dedup (latent race fix) - Drop 'human-' prefix from refs (principalId already 'principal-UUID'); delete legacy 'server' refs on first-run; git-identity sanitization - L2 drain per-writer partition; park mutex moved; park on session refs; applyExternalChange attributes to file-system writer - FR-5 covers 9 mutating endpoints + meta-test enforcement - Product: always-new on subprocess restart; AgentFocus fires on undo + rollback (not rename); closed-session removes from presence; non-git mode shadow-only; principal.json gitignored; 30d per-writer GC TTL All initial Q1-Q11 CLOSED. Residual Q100-Q105 (mostly P2) tracked for implementation-time resolution. * spec: naming rename + D45/D49 corrections under directive Nick flagged D45 as pragmatism-mode (corrected) and pressure-tested "shadow" as imprecise. Under greenfield + clean-precedents directive: D45 rewrite: save-version is graceful, not disabled in non-git mode. History checkpoint always lands; parent-git tag is best-effort; transitions heal forward; no user-facing run-git-init prompt. Matches existing code's non-fatal wrap at api-extension.ts:1877-1897. D49 rewrite: Activity log = server-side store + CC1-broadcast invalidation + REST fetch, not Y.Map replication. Avoids Y.Doc state bloat; matches backlink-index precedent. D55 (naming rename): "shadow" → "history" throughout. Aligns with existing /api/history + UI History panel + user mental model. File renames: shadow-repo.ts → history-repo.ts, shadow-lock.ts → history-lock.ts, shadow-branch-gc.ts → history-branch-gc.ts, shadow-repo-layout.ts → history-repo-layout.ts. Symbol renames: ShadowHandle → HistoryHandle, initShadowRepo → initHistoryRepo, etc. Log prefix [shadow] → [history]. D56 (unified state dir): .open-knowledge/ for all metadata. Subdirs: config.yml, principal.json, history/, *.lock. Eliminates the .openknowledge/ vs .open-knowledge/ naming drift and the integrated/standalone bifurcation. First-run migration from legacy locations. 52+ shadow references in SPEC replaced to history. Evidence file shadow-git-and-sweep.md renamed to history-and-sweep.md. ASK_FIRST stale items cleaned (all resolved by D21-D54). Residual "shadow" in SPEC only in D55/D56 (describing the rename transition itself). * spec: audit findings applied (D26/D35/D42/FR-11 corrections + D57 Y.Map rename) 4 parallel Opus audits dispatched against D21-D56 with eng:audit skill. All findings evaluated via eng:assess-findings at high fidelity — every HIGH + MEDIUM finding verified against code with adversarial stance. Applied corrections: HIGH findings (all verified + fixed): - D26 rationale: was factually wrong. Hocuspocus.unloadDocument DOES call document.destroy() (Hocuspocus.ts:580), triggering UM auto-destroy. Real hazard: DC blocking shouldUnloadDocument. Load-bearing primitive is dc.disconnect(). - FR-11: still referenced transaction.changed — contradicts D22's YTextEvent.delta lock. Rewrote FR-11 to align. - D42/FR-5: missing handleSyncTrigger/handleSyncSetEnabled/ handleSyncAbortMerge (3 mutating POST handlers at api-extension.ts:4012,4040,4165). Expanded enumeration 9→12. - D35 legacy ref sweep: regex $NF=="server" missed human-server refs actively written by parkBranch(sessionId='server'). Switched to parseWriterId classification-based sweep. - D19 refactor cost ownership: D39 framing of "one-line reorder" understated the parkBranch per-session refactor. D19 now explicitly owns the signature/loop changes; D39 owns only mutex ordering. - D57 NEW: Y.Map('activity') → Y.Map('agent-flash'). Disambiguates three-way naming conflation (Y.Map flash side-channel, D49 activity log, D25 UM-scope activityMap). 6 code sites + D25 update. PRAGMATISM finding: - D39: "known tolerated microsecond-late transact" was pragmatism. Upgraded to transact-wrapped isolation via new PARK_SNAPSHOT_ORIGIN. Race eliminated, not tolerated. MEDIUM findings applied: - D25: ignoreRemoteMapChanges: true added (multi-agent Y.Map safety) - D32: session context threading + STOP rule vs dc.transact(fn) - D43: AgentFocusEntry.writeKind enum extended for undo + rollback - D51 subsumed by D56 (directory-wide gitignore) - D41: extend existing recordContributor (no new function) - D55: UI "History panel" claim corrected (actual: TimelinePanel) 0 findings dismissed. 0 reopens. 0 escalations needed. Spec state: 57 decisions locked (D1-D57). Ready for Task 11 finalize. * spec: round-2 audit + challenger applied via assess-findings Fresh auditor + independent challenger on post-correction spec. Used eng:assess-findings high-fidelity to triage. Result: 9 CORRECT applied, 7 DISMISS with rationale, 3 ESCALATE for user. Key corrections: - R2-H1: §7 reverted to pre-rename filenames (describes CURRENT state, not target). My earlier bulk rename was overbroad. - R2-H2: D35 switched from parseWriterId (only knows legacy taxonomy) to explicit allowlist sweep. Previous approach would have catastrophically deleted new-taxonomy refs. - R2-H3: D57 scope expanded to 7 sites + ActivityEntry type rename. - R2-M: FR-3 updated to Y.Map('agent-flash') + ignoreRemoteMapChanges. - R2-M: NFR-6 locked delete-not-rewrite. - Ch-M4: Added FR-20 for agent-type aggregation render contract. Dismissed findings: - Ch-H2 D6 vs journeys: re-read; journeys are Option A compatible. - Ch-M5 "history" vs "journal": already adjudicated. - Ch-M6 D57 prose-only alt: pragmatism under directive. - Ch-M7 D56 git self-containment: D56 adds gitignore entry for same effect. - Ch-M8 live per-line attribution: rejected per NG1. Escalated for user: - E1 (Ch-H1): D49 server-side+CC1+REST vs bounded Y.Map ring-buffer. UX-scope dependent. - E2 (Ch-H3): D1 actor tuple — add delegator? slot now for Q103 agents-of-agents (deferred P2)? - E3 (Ch-M5): "history" vs "journal" naming reopen (already adjudicated). * spec: E1/E2/E3 adjudicated — D49 reverted to Y.Map ring-buffer After /analyze of audit round-2 escalations + user decisions: E1 REOPENED: D49 reverted to bounded Y.Map ring-buffer - Y.Map('agent-effects') on each doc, keyed by sessionId:transactIdx - Bounded at 50 entries total per doc (oldest-by-timestamp eviction) - No CC1, no /api/activity-log REST endpoint, no server-side store - Clients sync + subscribe via standard Y.Doc + Y.Map.observe - ~10KB per doc bounded storage Reason: journey re-read (P1-P5) showed no consumer needs retention beyond hours. Historical attribution uses shadow/history git commit bodies (D13), separate path. Server-side + CC1 + REST was over-engineered for the actual scope. FR-11 updated to match new D49 shape. UM scope (FR-3) unchanged — agent-effects NOT in UM scope so undo doesn't revert effect-diff log entries (undo-transacts create their own entries; timeline renders write + undo narrative). E2 KEEP DEFERRED (user decision): D1 stays two-slot. Q103 nested agents remains P2. Additive schema evolution is safe either way. E3 KEEP "history" (prior decision stands): challenger reopen dismissed; alignment with /api/history + user mental model holds. Spec state: 57 decisions + FR-20. All high/medium audit + challenger findings closed. Ready for Task 11 (Verify and finalize). * spec: round-3 audit findings applied (10 findings, 0 remain) H1: persistence.ts:405 → 388 across §1 + D31 H2: D27 keepalive close-handler moved from start.ts to boot.ts M3: FR-18 parseWriterId + WRITER_ID_RE prerequisite called out M4: FR-5 enumerates full 12-handler taxonomy per D42 M5: D42 line citations refreshed M6: getSession citation 179-219 → 188-219 M7: TiptapEditor.tsx:236 → 257; test-harness.ts:538 → 635 M8: FR-20 added to §13 In scope roll-up L9: Decision log gaps signposted in numbering notes L10: D45 null projectDir path clarified Audit artifact committed at specs/.../meta/audit-findings-2026-04-21.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [US-001] D55: Rename shadow-* → history-* across codebase Rename all shadow-repo files/symbols/log-prefixes to history-* so the naming reflects user-facing semantics (history repo backs /api/history and the Timeline panel) and eliminates the accidental 'shadow' drift. - packages/core/src/shadow-repo-layout.ts → history-repo-layout.ts ShadowContributor → HistoryContributor; package.json export updated - packages/server/src/shadow-repo.ts → history-repo.ts ShadowHandle → HistoryHandle, initShadowRepo → initHistoryRepo, shadowGit → historyGit, [shadow] → [history] log prefixes - packages/server/src/shadow-lock.ts → history-lock.ts - packages/server/src/shadow-branch-gc.ts → history-branch-gc.ts gcShadowBranches → gcHistoryBranches - All importers updated: standalone.ts, persistence.ts, api-extension.ts, server-observers.ts, server-observer-extension.ts, boot.ts, timeline-query.ts, hocuspocus-plugin.ts, shadow-log.ts, enrichment.ts - HistorySource runtime values: 'shadow-repo' → 'history-repo', 'shadow-repo-absent' → 'history-repo-absent' - All 4 test files renamed; standalone.test.ts shadowHandle → historyHandle - git ref namespace (refs/wip/*, refs/checkpoints/*) unchanged Quality gate: bun run check — 14/14 tasks green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-002] D57: Rename Y.Map('activity') → Y.Map('agent-flash') + ActivityEntry → AgentFlashEntry Disambiguates from D49's server-side activity log and D25's per-session UndoManager scope. All 7 getMap('activity') call sites updated; type renamed at the definition and all consumers. - packages/core/src/types/awareness.ts: ActivityEntry → AgentFlashEntry - packages/core/src/constants/activity.ts: import + cast updated - packages/core/src/index.ts: re-export updated - packages/server/src/api-extension.ts (3 sites): getMap renamed - packages/app/src/editor/TiptapEditor.tsx: getMap renamed - packages/app/src/editor/plugins/agent-flash-source.ts: getMap + comment - packages/app/tests/integration/test-harness.ts: getMap renamed - packages/app/src/editor/observers.test.ts: getMap renamed - packages/app/src/server/agent-sim.ts: log comment updated Quality gate: bun run check — 14/14 tasks green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-003] D56: Unified .open-knowledge/ state dir + first-run migration History repo always lives at <projectRoot>/.open-knowledge/history/ regardless of project git state. Legacy locations are migrated atomically on first server start. .open-knowledge/ is added to .gitignore in all modes (not just standalone). - history-repo-layout.ts: resolveHistoryDir() returns a string (unified path), removes HistoryRepoMode / ResolvedHistoryDir bifurcation - history-repo.ts: migrateHistoryRepo() checks .git/openknowledge/ and .openknowledge/ and renames to unified path; ensureGitignoreEntry() always writes .open-knowledge/ entry; initHistoryRepo() calls both - history-repo-layout.test.ts: updated getHistoryRepoPath tests to reflect unified path; legacy locations explicitly do not satisfy query - history-repo.test.ts: updated path assertions; added 2 migration tests for integrated-mode and standalone-mode legacy locations - Migration log: [history-migration] relocated history from <old> to <new> Quality gate: bun run check — 14/14 tasks green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-004] Core types foundation — actor.ts, principal.ts, extend writeKind Add shared Actor model types to core so downstream code imports (principal, agent_session) tuple, Principal, SessionRecord shapes without a server dep. - packages/core/src/types/actor.ts: Actor, PrincipalId, SessionId - packages/core/src/types/principal.ts: Principal (id, display_name, display_email, source, created_at) - packages/core/src/types/awareness.ts: AgentFocusEntry.writeKind expanded to 'write' | 'edit' | 'undo' | 'rollback-apply' | null (D43) - packages/core/src/index.ts: re-exports for Actor, PrincipalId, SessionId, Principal Quality gate: bun run check — 14/14 tasks green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-005] Principal representation — principal.ts + sanitizeGitIdentity Introduces a stable per-machine principal identity persisted under .open-knowledge/principal.json, and a sanitization helper applied at all identity input boundaries. - packages/server/src/git-identity-sanitize.ts: strips <>, CR, LF, trims, slices to 128 chars; unit tests in .test.ts - packages/server/src/principal.ts: loadPrincipal() reads/creates principal-<UUID> record; refreshes display fields from git config on each call, keeps id + created_at immutable; tests in .test.ts - packages/server/src/api-extension.ts: extractAgentIdentity now calls sanitizeGitIdentity() for agentName and clientName instead of inline .replace(/[\r\n]/g,'') Quality gate: bun run check — 14/14 tasks green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-002] Fix commitUpstreamImport tests — update to git-upstream writer ID and Git (upstream) display name * [US-007] F1 — Per-session origin objects in AgentSessionManager - Add SessionRecord type (dc, origin, agentId, docName) - createSessionOrigin: deep-frozen PairedWriteOrigin per session (D2, D23) - D30 in-flight promise dedup: concurrent getSession shares one DC - All 4 agent write handlers use session.origin (D32 STOP rule) - Remove stale AGENT_WRITE_ORIGIN import from api-extension.ts - Update 3 test files (api-agent-frontmatter, api-agent-patch, on-agent-write) to use session.dc.document.* instead of dc.document.* - FR-4 integration test updated to track session.origin via server.instance.sessionManager.getSession() instead of shared constant - Add US-007 unit tests: concurrent dedup, deep-freeze TypeError, object identity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-008] Per-session Y.UndoManager on [Y.Text, metaMap, flashMap] SessionRecord gains `um: Y.UndoManager` and `undoOrigin: LocalTransactionOrigin`. UM tracks writes under session.origin across all three Y-types atomically; captureTransaction excludes undoOrigin writes (V0-14 placeholder); ignoreRemoteMapChanges prevents remote agent map updates from polluting the undo stack. All session close paths (closeSession, closeAllForDoc, closeAllForAgent, closeAll) call um.destroy() before dc.disconnect(). Unit tests updated with real Y.Doc mock; integration tests cover S1/S2 independence, atomic multi-type undo, and post-destroy non-tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-009] AGENT_UNDO_ORIGIN + applyAgentUndo handler (V0-14) * [US-010] TQ13 cleanup — remove dangling /api/agent-undo* callers + repoint liveness probe * [US-011] F2 — Keepalive-WS close cleanup (connectionId correlation + 30s grace + closeAllForAgent) * [US-012] Attribution sweep — thread identity through 12 mutating handlers + meta-test - Thread extractAgentIdentity through 15 mutating POST handlers in api-extension.ts: handleAgentWrite, handleAgentWriteMd, handleAgentPatch, handleAgentUndo, handleSaveVersion, handleRollback, handleCreatePage, handleRename, handleRenamePath, handleDeletePath, handleUploadImage, handleSyncTrigger, handleSyncSetEnabled, handleSyncAbortMerge, handleSyncResolveConflict - Update MCP tools rename-document.ts + rollback-to-version.ts to accept identityRef and spread agentId/agentName/clientName/colorSeed into POST body - Pass identityRef to registerRenameDocument + registerRollbackToVersion in index.ts - Add attribution-sweep-coverage.test.ts meta-test: static analysis asserting all required POST handlers call extractAgentIdentity and no untracked handler can be added to the route registry without explicit classification (FR-5, D42) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-013] persistence.ts onStoreDocument origin threading (FR-16) - Export resolveWriterFromOrigin() from persistence.ts: dispatches lastTransactionOrigin → WriterIdentity for all Hocuspocus origin shapes (local+session_id → agent-<id>, local+file-watcher → FILE_SYSTEM_WRITER, local+upstream-import → GIT_UPSTREAM_WRITER, local fallback → SERVICE_WRITER, connection+principalId → principal writer stub) - onStoreDocument destructures lastTransactionOrigin and records contributor for non-service session-bearing origins (safety-net for writes that bypass api-extension.ts handlers) - commitToWipRef uses contributor snapshot dispatch: first writer or SERVICE_WRITER fallback; replaces defaultWriter={id:'server',...} hardcode - Add FILE_SYSTEM_WRITER, GIT_UPSTREAM_WRITER, SERVICE_WRITER constants to history-repo.ts (D34); export from packages/server/src/index.ts - Rename UPSTREAM_WRITER → GIT_UPSTREAM_WRITER in commitUpstreamImport - 11-test resolveWriterFromOrigin unit suite in persistence.test.ts covering all dispatch paths including null/undefined/non-object guard - standalone.test.ts L2-flush test: check for any WIP ref (writer ID varies with module-level contributor state across concurrent tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-014] Per-writer L2 fan-out — shared tree SHA + restoreContributorEntry - Add buildWipTree(shadow, contentRoot): stages content directory into a fresh index, returns tree SHA shared by all per-writer commits in the drain - Add commitWipFromTree(shadow, writer, treeSha, message, branch): creates a commit from a pre-built tree SHA and advances the per-writer WIP ref; no staging, no tmpIndex per writer - Export both from index.ts alongside existing commitWip - Refactor commitToWipRef for per-writer fan-out (FR-7, US-014): - Non-empty snapshot: buildWipTree once → commitWipFromTree per writer - Per-writer failure: restoreContributorEntry(writerId, entry) + metric - All writers failed: bump consecutiveGitFailures + gitAutoSaveFailure - Empty snapshot: SERVICE_WRITER commitWip fallback (D32 unchanged) - Export ContributorEntry type from contributor-tracker.ts; add restoreContributorEntry(agentId, entry) for per-writer attribution recovery on partial fan-out failure (D38) - Add gitWriterCommitFailureCount to ReconciliationMetrics + incrementGitWriterCommitFailure() + resetMetrics() reset - persistence-fan-out.test.ts: two-writer fan-out asserts both refs exist and share the same tree SHA; SERVICE_WRITER fallback asserts any WIP ref Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-015] Structured ok-actor: body + subject-prefix action encoding - Add formatReconcileSubject, formatRollbackSubject, formatParkSubject, formatRenameSubject, formatCheckpointSubject, formatImportSubject to history-repo-layout.ts (D53, FR-13); full test suite in layout.test.ts - Add ok-actor: JSON body line to all history commits: commitUpstreamImport, safetyCheckpoint, parkBranch, saveVersion in history-repo.ts - Add subjectOverride?: string to ContributorEntry; extend recordContributor signature; L2 drain in persistence.ts uses subjectOverride ?? formatWipSubject - Thread rollback: and rename: subject overrides through api-extension.ts recordContributor calls for rollback and managed-rename handlers - Fix timeline-query.ts classifyType to recognize import: prefix as 'upstream' (import: has been the actual prefix since the initial implementation) - Tests: ok-actor round-trip for commitUpstreamImport, safetyCheckpoint, parkBranch, saveVersion; new safetyCheckpoint describe block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-016] applyExternalChange writer = file-system (D41) - Rename ContributorEntry.agentId → writerId in contributor-tracker.ts to accept any writer taxonomy value (agent-<uuid>, file-system, etc.) - external-change.ts: record FILE_SYSTEM_WRITER contributor with reconcile: subject after each file-watcher transact (D41) - persistence.ts: when onStoreDocument finds markdown === reconciledBase but contributors are pending, still call scheduleGitCommit() so the L2 drain fires at graceful shutdown (skipStoreHooks path, FR-6) - persistence-fan-out.test.ts: two new US-016 integration tests — file-system ref exists with reconcile: subject, and concurrent agent + file-watcher commits share the same tree SHA (FR-7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-017] Park serialization isolation + PARK_SNAPSHOT_ORIGIN + service writer PARK_SNAPSHOT_ORIGIN defined in standalone.ts (paired:true, D39); park loop wrapped in doc.transact atomically; setBatchInProgress before loop; park uses SERVICE_WRITER.id (openknowledge-service, D58). Fixed lint: import ordering + line-length in standalone.ts and history-repo.test.ts. * [US-018] Legacy ref sweep — allowlist deletion of server/human-*/upstream (D35, NFR-6) sweepLegacyHistoryRefs() enumerates refs/wip/*/*, deletes only refs whose parseWriterId().classification === 'unknown' AND match the known-legacy allowlist (server, human-*, upstream). New taxonomy refs (agent-*, principal-*, file-system, git-upstream, openknowledge-service) are preserved. Called from initHistoryRepo; idempotent. historyMigrationLegacyRefsDeleted metric added to metrics.ts. 3 unit tests: mixed-ref sweep, idempotence, fresh-repo no-op. * [US-019] Per-writer 30-day TTL GC for session refs on active branches SESSION_WRITER_TTL_MS=30d constant; GcResult.deletedStaleSessionRefs counter; per-writer TTL loop in gcHistoryBranches that GC's stale agent-*/principal-* refs on active project branches while preserving classified writers and fresh session refs. Fixed early-return control flow so TTL runs even when no orphaned branches exist. Tests: stale-session deletion + fresh-session preservation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-020] Save-version main-git Co-Authored-By trailers + principal-author swapContributors() drains attribution atomically at save-version time; agent-*/principal-* contributors become Co-Authored-By trailers in the parent-git commit. Author/Committer identity: body principal > git config > openknowledge fallback. formatCheckpointSubject() wraps subject line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-021] Save-version graceful across project-repo availability (D45, D33) Add git-repo availability check via revparse(--git-dir) before acquiring the parent-git lock. Non-git dir: response 200 with checkpointRef, versionTag omitted, [save-version] parent-git unavailable: warn emitted. Git dir: full flow with Co-Authored-By trailers and ok/vN tag. History checkpoint via commit-tree plumbing always runs regardless. 3 integration tests: non-git, git, and state-transition (non-git → git init → fresh tag). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-024] Browser principal fetch → pool.setTabIdentity (D50) Import tabSessionId from tab-identity.ts in DocumentContext and fetch /api/principal once collabUrl resolves; wire result into pool.setTabIdentity so every subsequent HocuspocusProvider open carries {principalId,tabSessionId} as its auth token — consumed by standalone.ts onAuthenticate to populate connection.context.principalId for per-writer WIP ref attribution. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-025] AgentFocus push-nav on rollback-apply + undo focus test (D43) Wire agentFocusBroadcaster.setFocus(writeKind:'rollback-apply') in handleRollback — the only missing push-nav call per D43. Undo already published focus; managed-rename correctly never fires (structural noise). Add integration test verifying POST /api/agent-undo publishes writeKind=undo to the focus map after a session write. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-027] Fuzzer agent-undo op + Chain E fidelity coverage (FR-17, D18) Extend bridge-convergence.fuzz.test.ts with a 3% agent-undo op kind that calls POST /api/agent-undo against the claude-1 session; adds AGENT_UNDO_ORIGIN → 'agent-undo' to WRITE_SURFACE_TO_OP_KIND so the D18 coverage gate passes. Content oracle treats agent-undo as a content-reset (conservative, avoids false-positive marker-missing fails). Add Chain E to bridge-observer-conversion.test.ts: 3 deterministic tests exercise the post-undo XmlFragment-authoritative composition path (stripFrontmatter → parseWithFallback → updateYFragment → applyFastDiff), mirroring applyAgentUndo's inner logic without networking (PR-tier gate). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-028] Test harness migration — structural isPairedWriteOrigin checks (NFR-4) * [US-029] Integration tests — session-cleanup NFR-5 + persistence fan-out + per-session UM perf (NFR-7, FR-7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [US-030] Docs + precedents — AGENTS.md entries 24/25, agent-write-path.mdx, inline comments (D48) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [docs] AGENTS.md shadow->history rename (D55) + identity-foundation contract US-030 doc sweep: reflect the shipped agent-identity-attribution-foundation SPEC (2026-04-18) in the root AGENTS.md (CLAUDE.md symlinks here). - Server package section: shadow-* filenames -> history-* per D55; shadow repo dir -> .open-knowledge/history/ per D56 unified state dir - Add Writer-ID taxonomy table (agent-/principal-/file-system/git-upstream/ openknowledge-service) per D7/D8/D34 + subject-prefix scheme per D53 - Add ok-actor: structured body (FR-8, D13) + L2 per-writer fan-out (FR-7, D38) - API endpoints: drop removed /api/agent-undo-status + /api/agent-redo; fix /api/agent-undo description for per-session UM; save-version D45 graceful - Key files list: add history-repo, history-lock, history-branch-gc, principal, activity-log, contributor-tracker, agent-focus, git-identity-sanitize; dedupe duplicate agent-sessions entry; note applyAgentUndo landed - Editor architecture diagrams: Y.Map('activity') -> Y.Map('agent-flash') (D57) + add Y.Map('agent-effects') ring-buffer (D49); per-session UM contract - Propagation matrix: V0-14 -> W5 (Agent Undo landed surface) - CC8 shutdown ordering: shadow lock -> history lock - STOP rules: agent-undo future-work rule rewritten as landed contract; syncTextToFragment rule updated to reference applyAgentUndo landed; cleanup of V0-14 "pending" language - Open Knowledge intro section: "shadow-repo activity" -> "history-repo activity"; native Edit falls through as file-system classified writer Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [docs] README shadow->history + identity-foundation surfaces packages/server/README.md: - Intro line: shadow repo -> history repo; add pointer to agent-identity SPEC + precedents #24/#25 in AGENTS.md - saveInMemoryCheckpoint + parseCheckpoint references updated to history-repo.ts / history-repo-layout - Add "Agent-write HTTP surface" section covering extractAgentIdentity, per-session origin contract, applyAgentMarkdownWrite/applyAgentUndo, save-version D45 graceful path, writer-ID taxonomy pointer, and the closeAllForAgent session-lifecycle wiring (D28 grace + D29 reconnect policy) packages/desktop/README.md: - Process-model ASCII diagram: simple-git (shadow repo) -> (history repo) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [docs] docs/content/* shadow->history (D55/D56) + identity-foundation UX User-visible docs updated for the agent-identity-attribution-foundation SPEC (2026-04-18). Terminology rename (D55 + D56): - internals/service-topology.mdx: shadow repo -> history repo across intro, Mermaid diagram (A --> E[History Repo]), Layer 2 header, L2 fan-out bullet, dedicated History repo section (new location: .open-knowledge/history/), WIP ref taxonomy note, ok-actor commit body note, dev-vs-prod row - internals/server-lifecycle.mdx: init phase list + Phase 5 "History release" + destroyHistoryRepo + rescue path <history-gitDir> + historyRef.current - internals/lifecycle.mdx: phase-1-5 description + "history repo" - internals/architecture.mdx: evolution note - overview.mdx: internals card - guides/github-sync.mdx: auto-commit cadence + offline behavior - guides/mcp-integration.mdx: read_document / rollback_to_version / get_history / save_version tool descriptions Identity-foundation UX surfaces: - guides/timeline.mdx: - Writer chip bullet: expanded to cover the five-way writer-ID taxonomy (agent-<connId>, principal-<UUID>, file-system, git-upstream, openknowledge-service) and link to [[agent-write-path]] - Upstream arrow: explicit git-upstream writer credit - New "Save Version and your git log" section: Co-Authored-By trailer shape (FR-9/D12), graceful skip in non-git dirs (D45), fresh agent session per reconnect (D29) No source code, tests, or SPEC edits. Writes routed through OK agent-patch HTTP API (MCP edit_document tools timed out; HTTP is the underlying transport) so edits retain identity attribution; service-topology fell back to native Edit because the Mermaid chart block gets escaped on CRDT round-trip and would visually break the diagram. One code-fence title attribute restored via native Edit for the same fidelity-round-trip reason. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [review] P1 fixes from pre-QA review gate Six issues flagged by the staff-level pre-QA review — all fixed so the branch goes into QA clean. 1. shadow-repo → history-repo in degraded[] (D55 consistency) standalone.ts + test updated; JSDoc refreshed. 2. history-repo-layout.ts header comment — described the OLD 4-prefix taxonomy. Rewrote to match the D34 5-prefix taxonomy the implementation block below already documents. 3. Deep-freeze PARK_SNAPSHOT_ORIGIN + TAB_SESSION_ORIGIN per D23 / precedent #24(b). Shallow Object.freeze leaves context mutable; strict-mode callers would silently succeed at mutating context.origin. 4. boot.ts destroy() now clears pending keepalive-grace timers before destroyHocuspocus(). Prevents grace callbacks firing against a disposed sessionManager / agentFocusBroadcaster during graceful shutdown (was producing spurious error-log spam). 5. Remove gaming extractAgentIdentity({}) / discarded-return calls from the four sync/* handlers (handleSyncTrigger, handleSyncSetEnabled, handleSyncAbortMerge, handleSyncResolveConflict). Moved them to EXEMPT_HANDLERS in the meta-test — they are control-plane orchestrators; commits they trigger land inside SyncEngine under classified writers (git-upstream, file-system, openknowledge-service) and are already attributed correctly there. D42 corrigendum added to SPEC.md §FR-5 per the CLAUDE.md post-ship annotation pattern. 6. FR-8 full actor tuple (principalId, agentType, clientName, clientVersion, label) now threads through ContributorEntry into the ok-actor: body line. Before: all five fields were hardcoded null in persistence.ts L2 drain regardless of input. Now: - ActorMetadata added to contributor-tracker.ts (merge semantics: last non-undefined wins per field within a drain) - recordContributor signature extends (optional 6th arg) - api-extension.ts extractAgentIdentity returns clientVersion + label; buildAgentActor + resolveAgentType helpers populate the tuple at every agent-write / rollback / rename / create-page site - persistence.ts L2 drain reads entry.actor and populates the full OkActorEntry; classified writers still carry nulls as intended Closes the "timeline queryable without session registry" promise for real — downstream git-log consumers now get the full actor tuple from the commit body. Quality gate: 14/14 tasks pass, 202 integration pass, 2 skip, 0 fail. No behavior change for the tier-2 perf gate. No Y.Doc schema touched. * [qa] fix Vite plugin /api/principal gap + add QA-003 trailer test **Vite plugin fix** — e2e S6 stress was failing on a 404 console error for /api/principal. Root cause: hocuspocus-plugin.ts (dev mode, used by Playwright) didn't wire loadPrincipal + getPrincipal into createApiExtension, while standalone.ts (CLI, production) did. Dev/test behavior now mirrors prod. Changes: - export `loadPrincipal` + `Principal` type from server package index - wire `loadPrincipal(CONTENT_DIR)` into plugin init (async, mirrors standalone.ts:931) - pass `getPrincipal: () => loadedPrincipal` to createApiExtension **QA-003 test** — save-version Co-Authored-By trailers + principal-author. Seeds two agent contributors + one principal contributor via contributor-tracker, invokes /api/save-version with principal body, reads the resulting parent-git HEAD commit and asserts: - subject: `checkpoint: <user message>` - trailer per distinct writer: `Co-Authored-By: <display> <id@ok.local>` - Author = principal (not a classified writer) Quality gate: 14/14 turbo tasks PASS, 205 integration tests, 0 fail. Previously-failing e2e S6 now passes via the plugin fix. * fixup! local-review: baseline (pre-review state) * [review] respond to /review-local findings — 1 Major + 2 Minor + 1 Consider Local review (17 parallel reviewers via run-local-review.sh) flagged 4 issues on the identity/attribution branch. All fixed. **Major (cors-wildcard-mutating-api)** — replace `Access-Control-Allow-Origin: *` on /api/* with an Origin allowlist that accepts only loopback (localhost, 127.x.x.x, [::1]) and the opaque `"null"` origin (file:// / packaged Electron per Fetch spec §4.3). Non-loopback Origins get 403. When an allowed Origin is present it's reflected verbatim in ACAO (not `*`) with `Vary: Origin` to prevent cache poisoning. Closes the CSRF door on the unauthenticated mutating routes (/api/agent-write-md, /api/rollback, /api/manage/delete, etc.) without breaking Electron or Vite dev servers. Files: api-extension.ts:isAllowedApiOrigin + onRequest CORS block; desktop-fetch.ts comment refresh. **Minor 1 (principal-display-name-stub)** — `resolveWriterFromOrigin` now accepts an optional `getPrincipal` accessor. When the claimed `ctx.principalId` matches `loadedPrincipal.id`, we emit WriterIdentity with the real `display_name` / `display_email` (git-config user.name / user.email) instead of hardcoded "Local User". Closes the fidelity gap on the D50/US-024 browser-principal path — `ok-actor:` body and Co-Authored-By trailers now mirror git-config identity for human writes. Wired via PersistenceOptions.getPrincipal; standalone.ts + Vite plugin both pass `() => loadedPrincipal`. 3 new unit tests lock in the match / mismatch / null-loaded branches. **Minor 2 (safety-net-metadata-races-handler)** — `hasContributor(writerId)` gate in onStoreDocument safety-net. api-extension handlers record rich identity synchronously after the Y.Doc transact fires; onStoreDocument runs on Hocuspocus's 2s debounce. The safety-net now skips when the handler has already recorded, so the stub `Agent (<short>)` displayName can never overwrite the handler's real `Claude` under any ordering edge case (post-restart replay, test harness, future extension orderings). Browser-principal writes still fall through to the safety-net (no api-extension handler exists for them). **Consider (principalId-token-unverified)** — onAuthenticate pins `ctx.principalId = loadedPrincipal.id` when the browser tab's claim matches. On mismatch, emits a structured `principal-token-mismatch` warn and omits principalId (falls through to SERVICE_WRITER). Closes attribution-forgery on the single-user loopback deployment. When loadedPrincipal is null (pre-load window), claim passes through so writes during the brief async-load gap still get a writer-id. Quality gate: 14/14 PASS, 202 integration tests, 0 fail. * remove added skills * restore elements md * remove artifacts * fix biome lint error * address PR #222 review comments - activity-log: add EFFECT_CAPTURE_ORIGIN (precedent #1) and unobserve on doc destroy to prevent observer leak - activity-log.test: replace unreachable metric assertion with real error-path trigger; add doc-destroy observer cleanup test - persistence: null-check principal display_name/display_email before returning writer identity - agent-sessions: document why Y.Map('agent-flash') is tracked by the UndoManager - shadow-repo: sweep orphaned index-wip-fanout-* temp files in initShadowRepo (pre-lock, safe) - agent-undo.test: replace literal origin shape assertion with real sessionManager-driven inspection Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: pass connectionId to MCP keepalive so session cleanup fires on exit MCP subprocesses were opening the keepalive WS without a connectionId query param, so the server-side close handler in boot.ts silently early-returned and never invoked closeAllForAgent. Ghost agents persisted in awareness (visible in presence bar) until 30-min idle-shutdown or server restart. Fix: pass connectionId: \`agent-\${connectionId}\` to startKeepalive — the prefixed form matches the session-key format used by extractAgentIdentity and closeAllForAgent, matching the session-cleanup.test.ts contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * mutation fix * fix: document read endpoint no longer leaks 'Agent' presence entry GET /api/document previously called sessionManager.getSession(docName) with no identity. That created a cached session with undefined clientName, which made iconFromClientName() return 'bot' and surfaced a generic "Agent" avatar in the presence bar that persisted until the 30-minute idle-shutdown. Swap to a transient hocuspocus.openDirectConnection(docName): reads the Y.Text('source') without touching awareness, then disconnects. No session cache, no ghost presence entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * address additional PR #222 review comments - thread recordContributor + flushDocToGit through handleAgentUndo so per-session undo writes keep shadow-repo attribution (FR-5/D42). No-op undo (empty UM stack) is skipped — nothing to attribute. - close a TOCTOU race in bootServer between keepalive-grace timer callbacks and destroy(): timers short-circuit on a shuttingDown flag, and destroy() awaits any inflight cleanup work via Promise.allSettled. - drain pendingSessions for the agent inside closeAllForAgent so an in-flight getSession() can't register AFTER the key-scan pass. - applyAgentUndo now returns a boolean so the HTTP response and attribution gate can distinguish a real undo from an empty-stack no-op. - add scope='session' integration test — drains multi-frame UM stack, asserts undone:true, verifies empty-stack second call returns undone:false, and checks the bridge invariant post-drain. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix checks * fix rogue commit --------- Co-authored-by: miles-kt-inkeep <miles.kamingthanassi@inkeep.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
This PR removes the CLI MCP server's single-project compatibility behavior and replaces it with a strict routing contract for project-scoped tool calls. Tool calls now route by explicit
cwdfirst, otherwise by the client's only advertised root, and otherwise fail clearly instead of guessing from the startup cwd.It also keeps
--portas the explicit single-target override, normalizes cwd values before config/server cache lookups, and simplifies Claude Desktop to the samenpx @inkeep/open-knowledge mcpentry as the project-local editors while surfacing--forceguidance when an existing MCP entry differs from the recommended config.Key decisions
roots/listthe first time it needs client roots, and invalidates that cache onroots/list_changed, instead of falling back to the startup cwd or inferring from other tool arguments.cwdnormalization is centralized as absolute-path plus best-effortrealpathcanonicalization so config resolution and project-server discovery share stable cache keys across symlinked paths.--portremains the explicit debug/single-target path and bypasses project selection entirely.open-knowledge initno longer silently overwrites existing MCP entries by default. It now distinguishes an identical entry from a conflicting one and tells the user to rerun with--forcewhen the existing entry differs from current defaults.Validation
cd packages/cli && bunx tsc --noEmitcd packages/cli && bun test src/mcp/server.test.ts src/mcp/server-discovery.test.ts src/config/loader.test.ts src/commands/init.test.tsbun run checkstill fails on pre-existingopen-knowledge-apptests inpackages/app/src/editor/source-polish/view-plugin.test.tsandpackages/app/src/editor/source-polish/broken-ref-field.test.ts; no CLI package failures were introduced by this change.Manual QA
No manual QA performed.