Add the friction scan and a read-only MCP surface - #77
Merged
Conversation
canary is frozen as a paper archive and its tool layer moves here. Comparing the two, canary was already a subset of Modore -- session/project join and unpushed-worktree sole-copy judgment are scree's, and retention forecast, orphan detection, and preserve export exist only here. The one capability that did not overlap was the operator-friction scan, and the one practical reason to keep canary alive was that its judgments were reachable from an MCP client mid-session while Modore's were terminal-only. Both are closed. friction.py ports canary/lib/sessions/friction.ts: the nine-category pushback taxonomy at severity 1-3, keyword and tone matching only, no model in the judgment path. Claude Code and Codex sessions come from scree's own collectors rather than a second traversal. Gemini CLI chats and Claude Desktop local-agent sessions are collected here because scree does not traverse either -- its Gemini collector reads only ~/.gemini/projects.json, and it has no Claude Desktop collector at all, so the brief's premise that all four stores were already walked did not hold. Gemini transcripts carry no cwd, so the workspace is joined through the project registry's alias. Reading user turns is the second deliberate exception to scree's no-content contract, after preserve, and is bounded the same way: user-authored turns only, quotes capped at 200 characters and masked through scree.mask_text by default, --raw-quotes as the explicit opt-out, nothing written. mcp_server.py is a zero-dependency stdio JSON-RPC server exposing three read-only tools -- scree_report, friction_scan, system_scan_summary. Each runs scree.py --json or friction.py --json and forwards the output, so no judgment is reimplemented and the CLI, the app, and this surface cannot disagree. Cleanup, deletion, and scan execution are absent: Modore gates destruction on an approval a human grants on screen, and an agent-reachable bypass would void that. A test exercises every tool and asserts the only processes ever spawned are the two judgment scripts. Results are fenced as untrusted machine-derived data. AirMCP was read as the precedent for tool descriptors, read-only annotations, and untrusted-content fencing, but its server is TypeScript on the MCP SDK; this repository ships no Node toolchain and pins zero runtime dependencies, so the protocol layer is stdlib Python instead. Neither script joins the signed app runtime -- no Swift view calls either -- so both are listed in the runtime-completeness exclusions with the condition for moving them in.
Correcting the previous commit message: AirMCP does have a Swift MCP server. ios/Sources/AirMCPServer/MCPServer.swift is a real one -- initialize, tools/list, tools/call, ping, protocolVersion 2025-03-26, over HTTP, with a JSONRPCTypes layer and Calendar/Contacts/Health/Location/Reminder tools. The TypeScript server (the npm package) is the macOS one; the Swift server is the iOS one. Saying AirMCP "is not a Swift MCP server" was wrong. Reading it changes the design in one place worth taking. Its registerTool consults IOSPreviewContract (ios/Sources/AirMCPServer/PreviewTools.swift) and refuses to register anything that is not on an explicit name allowlist and annotated read-only and non-destructive -- failing closed at registration rather than hiding the tool from tools/list. This surface had the same property only by intention plus a test asserting nobody had broken it. Now it is a mechanism: contract_allows gates the handler map and the descriptor list, a rejected tool is unreachable rather than unlisted, and REJECTED_TOOLS makes the rejection visible in --tools instead of silent. The language choice stands and is in fact closer to the precedent than the previous message claimed: the Swift server is 229 lines of hand-rolled dispatch over a small tool table with no SDK, which is exactly the shape of this one. Python remains right here because Modore's judgments are Python scripts and this repository pins zero runtime dependencies.
The previous commit's message retracted an accurate finding. Checked again, with the evidence this time: .mcp.json -> npx [email protected] (TypeScript) app/.../ServerManager.swift -> node <entry> --http / npx (TypeScript) npm package "airmcp" (TypeScript) ios/Sources/AirMCPServer -> Swift, Hummingbird HTTP, README: "iOS runtime is in preview" So AirMCP has two MCP servers. The one in operation -- the one its own app spawns, the one this session's airmcp tools come from -- is TypeScript, which is what the first commit said. The Swift server exists and is real, but it is the preview iOS runtime; finding it does not make the original statement wrong, it makes it incomplete. The docstring now states both, and says which one each borrowed pattern came from: descriptors, read-only annotations, and untrusted fencing from the TypeScript server; the hand-rolled dispatch and the registration-time contract from the Swift one. cb37e4d's message cannot be fixed without a force push, so the correction is recorded in the PR body instead. The code it added stands on its own.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
canary(
~/IdeaProjects/Paper/canary)는 논문 아카이브로 동결됐고, 도구 계층을 Modore로 옮긴다. 두 레포를 대조하면 canary는 Modore의 부분집합이었다 — 세션↔프로젝트 조인과 미푸시 워크트리 유일본 판정은 이미scree.py가 하고 있고, retention forecast·orphan 탐지·preserve 내보내기는 Modore에만 있다. 겹치지 않는 고유 기능은 friction scanner 하나였고, canary를 존치시켜온 실용적 이유는 그 판정이 세션 중 MCP로 질의 가능했다는 점 하나였다. 둘 다 닫는다.scripts/friction.pycanary/lib/sessions/friction.ts의 결정론적 포팅. 9종 분류 × 심각도 1–3, 키워드·톤 매칭만 쓰고 판정 경로에 모델이 없다.scree.collect_claude/collect_codex를 그대로 재사용한다(중복 순회 없음).collect_gemini는~/.gemini/projects.json레지스트리만 읽고(트랜스크립트 없음), Claude Desktop 수집기는 아예 없다. Gemini 트랜스크립트에는cwd가 없어 워크스페이스는 레지스트리의 alias로 조인한다.preserve에 이은 scree 무내용 계약의 두 번째 명시적 예외다. 같은 방식으로 제한한다: 사용자 작성 턴만, 인용 200자 상한, 기본scree.mask_text마스킹,--raw-quotes가 명시적 opt-out, 아무것도 쓰지 않음.--json으로 구조화 출력. 30일 창 실측: 세션 200개 / 사용자 턴 3,563 / 발견 430건(rage 247 · irritation 60 · correction 123).분류 근거 수치(2026-07, 2,630턴/515건 인간 감사)는 canary 기록에서 인용만 하고 재도출하지 않았다.
scripts/mcp_server.py의존성 0의 stdio JSON-RPC 서버. 읽기 전용 도구 셋:
scree_reportfriction_scansystem_scan_summaryscree.py --json/friction.py --json을 실행해 출력을 전달할 뿐이다. 판정 재구현이 없으므로 CLI·Mac 앱·MCP가 서로 다른 사실을 말할 수 없다.설계가 브리핑과 어긋난 지점
scree가 gemini/desktop 트랜스크립트를 순회한다는 전제는 사실이 아니었다 (위 참조). 셋 중 둘을 friction 쪽에 새로 만들었고, 이미 배포되어 Swift UI가 소비하는 scree 리포트를 넓히지는 않았다.
AirMCP의 MCP 서버는 둘이고, 운영 중인 쪽은 TypeScript다.
.mcp.json이 등록하는 것, 자기 macOS 앱의ServerManager.swift가node/npx로 띄우는 것, npm에 배포되는 것 전부 TS 서버(@modelcontextprotocol/sdk, 260+ 도구)다. 이 PR이 따른 도구 서술자·readOnlyHint애노테이션·untrusted 펜싱 패턴은 여기서 왔다. 별개로ios/Sources/AirMCPServer에 Swift MCP 서버가 하나 더 있다 —MCPServer.swift229줄, initialize·tools/list·tools/call·ping, protocolVersion 2025-03-26, Hummingbird HTTP. README가 "iOS runtime is in preview"로 표시하는 프리뷰 런타임이다. 이 PR의 프로토콜 계층 형태(SDK 없이 손으로 짠 dispatch + 작은 도구 테이블)와 아래 등록 시점 게이트는 이쪽에서 가져왔다.Modore가 어느 쪽 언어도 따라가지 않은 이유는 언어 자체가 아니라, 판정이 Python 스크립트이고 이 레포가 런타임 외부 의존성 0을 명시적으로 고정하고 있기 때문이다.
읽기 전용 계약을 등록 시점으로 옮겼다 (커밋
cb37e4d). Swift 서버의registerTool은IOSPreviewContract를 참조해 이름 allowlist에 없거나 read-only·non-destructive 애노테이션이 없는 도구를 등록 자체에서 거부한다 —tools/list에서 숨기는 게 아니라. 이 표면은 같은 성질을 "그런 도구를 안 만들었다"는 의도 + 테스트로만 갖고 있었는데, 이제contract_allows가 핸들러 맵과 서술자 목록을 게이트하고, 거부된 도구는 미노출이 아니라 도달 불가이며,REJECTED_TOOLS가--tools출력에 드러난다.scree.py의_extract_turn은 Codex 파일에서 동작하지 않는다 (별건, 이 PR에서 고치지 않음). Codexevent_msg는payload.message가 문자열이고response_item은input_text/output_text블록을 쓰는데,_extract_turn은content리스트의type == "text"만 본다. 따라서 Codex 세션에preserve를 걸면 "no recognizable turns"가 나온다. friction의 Codex 파서는 두 채널을 모두 처리한다.검증
python3 -I -B -m pytest tests/ -q→ 444 passed (friction 39 + MCP 34 신규)python3 -m compileall -q scripts/,python3 -I -B scripts/release_smoke.py→ 통과Swift UI 노출은 범위 밖이라 두 스크립트 모두 서명 앱 런타임에 넣지 않았고, 런타임 완결성 테스트의 제외 목록에 "Swift 호출자가 생기면 RUNTIME_FILES로 옮긴다"는 조건과 함께 등록했다.