Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ All notable changes to this project are documented here. Format loosely follows

### Added
- **Mothball's destructive API is now unreachable from Modore by mechanism, not by intention.** Absorbing Mothball absorbed two different deletion disciplines. Modore destroys nothing without a preview that issues a single-use 64-byte approval token, a 15-minute owner-only manifest, a remeasure at the destructive boundary, and a receipt. MothballCore's `ArchiveOrchestrator.archive()` is careful in its own right — it refuses `/` and `$HOME`, verifies the archive before touching the original, and moves to Trash rather than unlinking — but it is an *in-process* Swift call that takes no token and consumes no manifest, and the vendor dependency already compiles it into Modore's binary. Nothing but review stood between a future `try orchestrator.archive(…)` in a view action and a second deletion path the approval chain never sees. Two tests now pin the boundary: any Modore source file that names `ArchiveOrchestrator`, `Restorer`, `ArchiveRun`, or `trashItem` must also carry the approval-token symbol, so a diff cannot introduce one half without showing the other; and `MothballService` is pinned to the read-only scan-and-classify surface it actually uses. Wiring the archive path stays allowed — wiring it *around* the token does not.
- **File-access reverse index — `scripts/fileaccess.py`.** Absorbed from canary's `get_file_access` / `getFileAccessAggregates` (that repository is a frozen paper archive; this was the capability Modore had no equivalent for). Inverts the evidence scree already reads: for each path, how many reads, writes, and shell references it received, from how many sessions, and when last — with agent rule and config surfaces (CLAUDE.md, AGENTS.md, settings.json, anything under `~/.claude` or `~/.codex`) sorted first and returned by default, because a silently edited rule file is the case the view exists for. The rule-surface taxonomy is carried over verbatim. **The content contract is stricter than the original's**: canary attached a 200-character excerpt of the shell command to every row, and that excerpt is command content, so it is dropped here — a path extracted from a command is metadata about which file was touched, the command that touched it is not. Only tool names and paths are retained; assistant text, tool results, and non-path tool inputs are discarded in the same pass that reads them, and nested subagent transcripts are never opened, matching scree's collector. Paths are canonicalised before aggregation, so a file reached as `/Users/x/.claude/settings.json` from a tool input and as `~/.claude/settings.json` from a shell command is one row rather than several. Dedupe is keyed on the tool_use block id — Claude streams one assistant message as multiple lines sharing a message id, so a path-keyed dedupe would silently collapse a genuine second read. Exposed as the `file_access` MCP tool.
- **File-access reverse index — `scripts/fileaccess.py`.** Absorbed from canary's `get_file_access` / `getFileAccessAggregates` (that repository is a frozen paper archive; this was the capability Modore had no equivalent for). Inverts the evidence scree already reads: for each path, how many reads, writes, and shell references it received, from how many sessions, and when last — with agent rule and config surfaces (CLAUDE.md, AGENTS.md, settings.json, anything under `~/.claude` or `~/.codex`) sorted first and returned by default, because a silently edited rule file is the case the view exists for. The rule-surface taxonomy is carried over verbatim. **The content contract is stricter than the original's**: canary attached a 200-character excerpt of the shell command to every row, and that excerpt is command content, so it is dropped here — a path extracted from a command is metadata about which file was touched, the command that touched it is not. Only tool names and paths are retained; assistant text, tool results, and non-path tool inputs are discarded in the same pass that reads them, and nested subagent transcripts are never opened, matching scree's collector. Paths are canonicalised before aggregation, so a file reached as `/Users/x/.claude/settings.json` from a tool input and as `~/.claude/settings.json` from a shell command is one row rather than several. Dedupe is keyed on the tool_use block id — Claude streams one assistant message as multiple lines sharing a message id, so a path-keyed dedupe would silently collapse a genuine second read. Exposed as the `agent_file_access` MCP tool.
- **Hugging Face cache audit — `scripts/hfscan.py`.** Absorbed from decant's `ContextProbe.swift` (that repository is being retired; this was one of two capabilities Modore did not already have). Derives each cached model's identifier from its hub directory name (`models--Qwen--Qwen2.5-Coder-1.5B-Instruct` → the `Qwen/…` slug and the bare leaf) and searches the given roots for any occurrence, case-insensitively, so that over-catching keeps a model rather than losing one. **The original's central defect is inverted here.** decant returned "unreferenced" whenever the search could not run — a missing search root, a failed grep, a mistyped `--projects` argument all produced the same answer as a genuinely exhaustive search that found nothing, so one typo could mark an entire hub cache safe to delete. Absence of evidence is only evidence of absence when the search actually happened: a root that does not exist, a file cap reached, or a subtree that could not be read now yields `unknown` for every model with the reason stated, and `--allow-missing-roots` / `--ignore-unreadable` widen the verdict only when the operator asks. Metadata-only: file contents are matched in memory and discarded, and the only paths emitted are the files that *do* reference a model, masked through `scree.mask_text`.
- **MCP config hygiene — `scripts/mcpaudit.py`.** Absorbed from decant's `MCPHygiene.swift`. Reads `~/.claude.json` (including servers nested under per-project blocks), both Claude Desktop config locations, and `~/.mcp.json`, and reports entries that cannot start: `dead` (command does not resolve, or a script argument points at a path that is gone), `duplicate` (same command and args as another entry), `manual-review` (an `env` block is present), `unknown` (no command at all). Two deliberate deviations from the original: servers are named rather than anonymised to `server#N`, because a hygiene report the operator cannot act on is not one, and a server name is configuration metadata of the same kind scree already emits — while `env` is reported as a key *count*, never as keys or values. And a verdict that depends on PATH is withheld as `unknown` when PATH is unusable rather than reported as `dead`, the same fail-safe rule hfscan applies to its search. Read-only in the strong sense: the module has no code path that edits a config, disables a server, or starts one, pinned by a test.
- **Two more read-only MCP tools — `hf_orphans`, `mcp_hygiene`.** The audits above, exposed through the existing surface on the same terms as `scree_report` and `friction_scan`: allowlisted by name, annotated read-only and non-destructive, forwarding what the judgment script prints rather than reimplementing it. `hf_orphans` restates `search_complete` as a top-level `verdicts_withheld` flag so an incomplete search cannot be misread as "no orphans found", and its `roots` argument — the one place a caller supplies a path — is bounded to 8 entries and screened so a value can never be read as an option by the script it is passed to.
- **Two more read-only MCP tools — `model_residue_report`, `mcp_hygiene`.** The audits above, exposed through the existing surface on the same terms as `agent_state_report` and `operator_friction_report`: allowlisted by name, annotated read-only and non-destructive, forwarding what the judgment script prints rather than reimplementing it. `model_residue_report` restates `search_complete` as a top-level `verdicts_withheld` flag so an incomplete search cannot be misread as "no orphans found", and its `roots` argument — the one place a caller supplies a path — is bounded to 8 entries and screened so a value can never be read as an option by the script it is passed to.
- **Leave-behind audit — `scripts/moraine.py`.** Reads the two records that outlive an uninstall — macOS installer receipts (`pkgutil`) and the admin/user trust domains (`security dump-trust-settings`) — and correlates them into a verdict neither produces alone: a trusted root certificate whose installing package has no payload left on disk. First live run on the development Mac found exactly that — `INNORIX.CA`, a self-signed `CA:TRUE` root valid to 2028-07-07, trusted unconditionally in the admin domain, installed 2026-05-01 by a Korean file-transfer plugin whose app, LaunchAgent, and processes are all gone. Receipts are judged `present`/`partial`/`vanished`/`no_payload` with any file sampling reported; roots with no owning receipt are `unattributed` (MDM profiles and enterprise Wi-Fi legitimately have none), never accused. Read-only: nothing is written and nothing deleted — removing a trust root is an admin act that stays a human decision. Exposed through the MCP surface as `uninstall_residue_report`.

### Changed
- **The scree pitch no longer claims the unpushed-work verdict is unique.** Orca's workspace cleanup runs the same `rev-list --not --remotes` check against the worktrees it manages, verified in its source. README now states the actual difference — scree sweeps every agent worktree on the machine whatever created it, plus primary checkouts stranded off main and registry entries whose directory vanished — and names the prior art instead of implying none exists.

- **Operator-friction scan — `scripts/friction.py`.** Deterministic port of canary's `lib/sessions/friction.ts` (that repository is now a frozen paper archive; this was its one capability Modore did not already have). Flags the user turns where the operator pushed back on agent behaviour, across nine categories — wrong-action, no-research-assertion, stalling-approval, rule-contamination, over-orchestration-token, stale-repetition, verbosity, tone-attitude, other-ai-friction — at severity 1-3. Keyword and tone matching only, no model in the judgment path. Claude Code and Codex sessions are discovered through scree's own collectors rather than a second traversal; Gemini CLI chats (`~/.gemini/tmp/*/chats`, joined to a workspace through the project registry) and Claude Desktop local-agent sessions are added because scree does not traverse either — its Gemini collector reads only the project registry, and it has no Claude Desktop collector. Content contract: user-authored turns only, quotes capped at 200 characters and masked by default (`--raw-quotes` opts out), nothing written. `--json` serves the structured report. First live run over a 30-day window: 200 sessions, 3,563 user turns, 430 findings (rage 247 / irritation 60 / correction 123).
- **Read-only MCP surface — `scripts/mcp_server.py`.** A zero-dependency stdio JSON-RPC server exposing three tools: `scree_report` (join, retention forecast, sole-copy and orphan verdicts, by section), `friction_scan` (the taxonomy above, filterable by store/category/minimum severity), and `system_scan_summary` (the storage and security scan result already on disk, with its age). Each tool runs `scree.py --json` or `friction.py --json` and forwards the output, so no judgment is reimplemented and the CLI, the Mac app, and the MCP surface cannot disagree. Cleanup, deletion, and scan execution are deliberately not exposed — Modore's destructive path is gated on an on-screen human approval, and an agent-reachable bypass would void it; pinned by a test that exercises every tool and asserts the only processes ever spawned are the two judgment scripts. The read-only contract is enforced where tools are registered, ported from AirMCP's own Swift MCP server (`IOSPreviewContract`): a tool is reachable only if it is on an explicit name allowlist and annotated read-only and non-destructive, so one added without a deliberate edit fails closed rather than being merely unlisted. All results are fenced as untrusted machine-derived data.
- **Read-only MCP surface — `scripts/mcp_server.py`.** A zero-dependency stdio JSON-RPC server exposing three tools: `agent_state_report` (join, retention forecast, sole-copy and orphan verdicts, by section), `operator_friction_report` (the taxonomy above, filterable by store/category/minimum severity), and `system_scan_summary` (the storage and security scan result already on disk, with its age). Each tool runs `scree.py --json` or `friction.py --json` and forwards the output, so no judgment is reimplemented and the CLI, the Mac app, and the MCP surface cannot disagree. Cleanup, deletion, and scan execution are deliberately not exposed — Modore's destructive path is gated on an on-screen human approval, and an agent-reachable bypass would void it; pinned by a test that exercises every tool and asserts the only processes ever spawned are the two judgment scripts. The read-only contract is enforced where tools are registered, ported from AirMCP's own Swift MCP server (`IOSPreviewContract`): a tool is reachable only if it is on an explicit name allowlist and annotated read-only and non-destructive, so one added without a deliberate edit fails closed rather than being merely unlisted. All results are fenced as untrusted machine-derived data.
- **scree lineage: what the sessions remember versus what the disk still holds.** Every work path appearing in session records is classified alive+git / alive+plain / vanished, using existence and a `.git` presence check only; macOS case-variant ghosts (the same path recorded under different casings) are merged by casefold and reported once with their spellings. First live run: 422 paths — 118 alive git repos, 189 alive plain directories, 115 vanished (session records their only trace), 4 case ghosts. Consumers such as balance's lineup-gap diff read `--json .lineage`; scree itself reports universal facts only.
- **Stray primary-checkout judgment (scree).** Agent sessions can leave a repo's *main checkout* parked on a non-default branch with unmerged work — the same unique-work risk as an orphaned worktree, but invisible to worktree listing. `collect_worktrees` now judges the primary checkout of every repo that hosts agent worktrees: a non-main/master branch is reported as `stray_checkout` with the same protected-versus-rebuildable rules, and the report lists each one with its branch and unpushed count. First live run found 11 stranded checkouts, three of them carrying unpushed unique commits.

Expand Down
Loading