Skip to content

feat(cli): tighten MCP routing and add dev-aware user-scoped config install#207

Merged
mike-inkeep merged 17 commits intomainfrom
cli-strict-mcp-routing
Apr 21, 2026
Merged

feat(cli): tighten MCP routing and add dev-aware user-scoped config install#207
mike-inkeep merged 17 commits intomainfrom
cli-strict-mcp-routing

Conversation

@mike-inkeep
Copy link
Copy Markdown
Contributor

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 cwd first, otherwise by the client's only advertised root, and otherwise fail clearly instead of guessing from the startup cwd.

It also keeps --port as the explicit single-target override, normalizes cwd values before config/server cache lookups, and simplifies Claude Desktop to the same npx @inkeep/open-knowledge mcp entry as the project-local editors while surfacing --force guidance when an existing MCP entry differs from the recommended config.

Key decisions

  • Project-scoped MCP routing now blocks on roots/list the first time it needs client roots, and invalidates that cache on roots/list_changed, instead of falling back to the startup cwd or inferring from other tool arguments.
  • cwd normalization is centralized as absolute-path plus best-effort realpath canonicalization so config resolution and project-server discovery share stable cache keys across symlinked paths.
  • --port remains the explicit debug/single-target path and bypasses project selection entirely.
  • open-knowledge init no 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 --force when the existing entry differs from current defaults.

Validation

  • cd packages/cli && bunx tsc --noEmit
  • cd packages/cli && bun test src/mcp/server.test.ts src/mcp/server-discovery.test.ts src/config/loader.test.ts src/commands/init.test.ts
  • bun run check still fails on pre-existing open-knowledge-app tests in packages/app/src/editor/source-polish/view-plugin.test.ts and packages/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.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 18, 2026

🦋 Changeset detected

Latest commit: b2bcbd8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@inkeep/open-knowledge Minor
@inkeep/open-knowledge-core Minor
@inkeep/open-knowledge-server Minor
@inkeep/open-knowledge-app Minor
@inkeep/open-knowledge-desktop Minor

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

Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:314 Race condition in concurrent cache access
  • 🟠 Major: normalize-cwd.ts:12 Silent 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:228 Error message prefix inconsistency
  • 🟡 Minor: shared.ts:74 Missing 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

Comment thread packages/cli/src/mcp/server-discovery.ts
Comment thread packages/cli/src/mcp/server-discovery.ts Outdated
Comment thread packages/cli/src/utils/normalize-cwd.ts
Comment thread packages/cli/src/mcp/server-discovery.test.ts Outdated
Comment thread packages/cli/src/mcp/tools/shared.ts
@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 18, 2026
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
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:338 Race condition in createProjectServerUrlResolver concurrent cache access — consider pending-promise deduplication pattern
  • 🟠 normalize-cwd.ts:12 Silent 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:1 Transient review artifact committed to repository
  • 🟡 Minor: loader.ts:153 Same 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:

  1. Remove review.txt from the commit
  2. 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.

Comment thread review.txt Outdated
Comment thread packages/cli/src/config/loader.ts
@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 20, 2026
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
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:338 Race condition in createProjectServerUrlResolver concurrent cache access — consider pending-promise deduplication pattern
  • 🟠 normalize-cwd.ts:12 Silent fallback on realpath failure hides path resolution issues — add logging for non-ENOENT errors
  • 🟡 server-discovery.ts:243 Error message prefix inconsistency — use Error: instead of OK:
  • 🟠 review.txt:1 Transient 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:

🟡 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:97appendFileSync errors when OK_LOG_FILE is set
  • keepalive.ts:173 — WebSocket error events
  • exec.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:97 Silent swallow of appendFileSync errors
  • 🟡 Minor: keepalive.ts:176 WebSocket error listener swallows errors
  • 🟡 Minor: exec.ts:503 enrichPath errors swallowed
  • 🟡 Minor: exec.ts:538 Batch 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:

🟡 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:

💭 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:47 Logger 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:

  1. Remove review.txt from the commit
  2. Add a changeset describing the strict routing behavioral change
  3. 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.

Comment thread packages/cli/src/mcp/logger.ts
Comment thread packages/cli/src/mcp/logger.ts
@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 20, 2026
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
@mike-inkeep
Copy link
Copy Markdown
Contributor Author

Addressed the current review pass in fa64d6a7: added in-flight per-cwd dedupe in both routed resolvers, switched MCP spawn failures to the established Error: prefix, warned on non-ENOENT normalizeCwd() failures and OK_LOG_FILE write failures, removed review.txt, moved the auto-start tests next to server-discovery.ts, added direct shared-helper / preview-url error-path / logger / normalize-cwd coverage, and added .changeset/strict-mcp-routing.md. Verification: bun test "src/mcp/server-discovery.test.ts" "src/config/loader.test.ts" "src/mcp/tools/shared.test.ts" "src/mcp/tools/get-preview-url.test.ts" "src/utils/normalize-cwd.test.ts" "src/mcp/logger.test.ts", bun run typecheck, and bunx biome check on the touched files. For the logger API-shape suggestion, I kept the message-first MCP logger and documented the intentional divergence instead of changing every CLI call site in this PR.

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
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Clean routing abstractioncreateProjectRoutingResolver provides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR, MULTIPLE_ROOTS_ERROR, ROOTS_UNAVAILABLE_ERROR)

  2. Race condition prevention — Both createProjectServerUrlResolver and createProjectConfigResolver use the pending-promise deduplication pattern, preventing duplicate server spawns and config loads

  3. 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

  4. Structured logging — The new McpLogger provides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions

  5. 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:176 WebSocket error event lacks diagnostic context
  • 💭 Consider: server.ts:106 Error 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 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:

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
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
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Clean routing abstractioncreateProjectRoutingResolver provides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR, MULTIPLE_ROOTS_ERROR, ROOTS_UNAVAILABLE_ERROR)

  2. Race condition prevention — Both createProjectServerUrlResolver and createProjectConfigResolver use the pending-promise deduplication pattern, preventing duplicate server spawns and config loads

  3. 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.

  4. Structured logging — The new McpLogger provides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions

  5. 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 and errorType field 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.

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
  • bootServer extraction 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

  1. Clean routing abstractioncreateProjectRoutingResolver provides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR, MULTIPLE_ROOTS_ERROR, ROOTS_UNAVAILABLE_ERROR)

  2. Race condition prevention — Both createProjectServerUrlResolver and createProjectConfigResolver use the pending-promise deduplication pattern, preventing duplicate server spawns and config loads

  3. Comprehensive test coverage — New test files (server-discovery.test.ts, logger.test.ts, normalize-cwd.test.ts, editors.test.ts) provide excellent coverage

  4. 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.

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
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
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Clean routing abstractioncreateProjectRoutingResolver provides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR, MULTIPLE_ROOTS_ERROR, ROOTS_UNAVAILABLE_ERROR)

  2. Race condition prevention — Both createProjectServerUrlResolver and createProjectConfigResolver use the pending-promise deduplication pattern, preventing duplicate server spawns and config loads

  3. Comprehensive test coverage — The new test files (server-discovery.test.ts at 699 lines, server.test.ts at 395 lines, logger.test.ts at 93 lines) provide excellent coverage of the new functionality

  4. Structured logging — The new McpLogger provides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions

  5. 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.

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
@mike-inkeep mike-inkeep changed the title fix(cli): enforce strict MCP routing feat(cli): tighten MCP routing and add dev-aware user-scoped config install Apr 21, 2026
Copy link
Copy Markdown

@inkeep-internal-ci inkeep-internal-ci Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/main bringing in unrelated changes (Electron desktop infrastructure, UI components, bootServer extraction)
  • 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

  1. Clean routing abstractioncreateProjectRoutingResolver provides a single source of truth for cwd resolution with clear error states (NO_CLIENT_ROOTS_ERROR, MULTIPLE_ROOTS_ERROR, ROOTS_UNAVAILABLE_ERROR)

  2. Race condition prevention — Both createProjectServerUrlResolver and createProjectConfigResolver use the pending-promise deduplication pattern, preventing duplicate server spawns and config loads

  3. 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), and editors.test.ts (170 lines)

  4. Structured logging — The new McpLogger provides JSON-structured logs with session correlation IDs, improving debuggability for MCP stdio sessions

  5. 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() with errorType in log context
    • ✅ Changeset added — strict-mcp-routing.md properly 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.

@github-actions github-actions Bot deleted a comment from inkeep-internal-ci Bot Apr 21, 2026
@mike-inkeep mike-inkeep merged commit 1451548 into main Apr 21, 2026
9 checks passed
@mike-inkeep mike-inkeep deleted the cli-strict-mcp-routing branch April 21, 2026 18:34
amikofalvy added a commit that referenced this pull request Apr 22, 2026
…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>
miles-kt-inkeep pushed a commit that referenced this pull request Apr 22, 2026
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.
miles-kt-inkeep added a commit that referenced this pull request Apr 22, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant