Releases: rezzminator/professor
Release list
v0.78.0
v0.78.0 — 2026-09-25
The chat and harvester MCP servers become one professor server and the harvester's tools are renamed for their intent, with no aliases: every permission rule, agent tools: list, prompt or script that names an mcp__harvester__* or mcp__chat__* tool, or an old harvester tool, stops matching the moment the update lands. The wave engine is retired for the flights family, the per-project dev and qa agents give way to a machine-global flights cast, and a new machine-wide git-guard hook lets only the gitter agent write git — the main chat included. pfm update runs the new binary's pfm install --yes, so the MCP re-registration, the new hooks and the global roster land on their own; restart every running chat afterwards, then run pfm update check in each adopted project and apply the template changes listed below. Follow INSTALL.md § Updating for the whole sequence; going back to v0.77.2 needs the repair line under the MCP bullet.
Work every #### → For: line below, not only pfm update --to: an install on v0.76.0 or older runs an update prompt that reads only this note and builds no checklist, so first read each skipped releases/vX.Y.Z.md after your installed version, oldest first, and work their #### → For: lines too — above all v0.77.0's before-update step: attach the detached source clone with git -C "$HOME/.professor" switch -c professor, or pfm update refuses with "source checkout is detached". In each adopted project, a clean pfm update check does not mean the retired surface is gone: pfm update drop only forgets a pin and leaves the local file live, so DELETE every retired file this note lists — the wave/* commands, commands/pfm.md, the documenter* and km files, the per-project developer and qa agents — before you drop it.
Breaking
- pfm: professor MCP server — the
chatandharvesterMCP servers merge into one server keyedprofessor, registered on Claude, Codex and OpenCode as the stdio commandpfm mcp serve --stdio, which forwards to the daemon and keeps each caller's identity. Claude and Codex now seemcp__professor__harvester_read,mcp__professor__chat_injectand so on (OpenCode:professor_harvester_read);issue_servicedeskbecomesservicedesk. The daemon serves/mcp/professorand the family views/mcp/professor/harvesterand/mcp/professor/chat;/mcp/harvesterand/mcp/chatanswer 404.pfm mcp chat serve,pfm mcp harvester serveand barepfm mcpare removed. The update'spfm installremoves pfm's own legacychat/harvesterentries by exact shape in every Claude registry,~/.mcp.json, each Codexconfig.tomland the OpenCode config, and keeps any entry you edited by hand. When one family fails to configure, the other still serves and the failed family's tools answer with an error naming the config path.
→ For: every adopter · after update · running chats — restart every chat that was running during the update (or /reload it): it still holds the old chat/harvester registration, which the new binary no longer serves. Both families ship disabled, so the professor server is registered only while one is on: enable the ones you use with pfm mcp chat enable and/or pfm mcp harvester enable, then pfm install --yes; pfm mcp ls shows each family true, and a restarted chat lists mcp__professor__* tools.
→ For: every adopter · after update · MCP registrations — run pfm doctor; a line doctor: mcp client=… legacy=… with remediation=run pfm install --yes names a legacy entry pfm left because you edited it — remove or rename that entry by hand, then pfm doctor shows no legacy= suffix.
→ For: every adopter · per project · MCP tool names — replace every mcp__harvester__* and mcp__chat__* name in the project's .claude/settings.json permissions, agent tools: lists, commands and scripts with its mcp__professor__harvester_* / mcp__professor__chat_* form; ! grep -rnE 'mcp__(harvester|chat)__' .claude CLAUDE.md then prints nothing and exits 0.
→ For: adopters who roll back to v0.77.2 · after update · MCP registrations — v0.78.0's pfm update refuses a downgrade, so check out v0.77.2 in the source clone, rebuild pfm as INSTALL.md § 2 shows and run pfm install --yes from the clone; v0.77.2's own install re-registers chat/harvester but leaves professor behind: delete the "professor" key under "mcp" in ~/.config/opencode/opencode.jsonc, run claude mcp remove professor -s user once per Claude account that still lists it, and ! grep -ns '"professor"' ~/.claude.json ~/.mcp.json ~/.config/opencode/opencode.jsonc prints nothing and exits 0.
- pfm: harvester tools — the harvester serves four tools named for their intent:
read(replacesreadPage,parseLocalDocumentsandreadWork; takesurls,filesandpublicationsin one call),download_file(replacesdownload),search_literature(replacesfindWorks;kind→type,count→limit) andsearch_web(replaceswebSearch). The archive member readers,searchCache,fetchImageand thefetchprompt are gone. Result fields are renamed:size_only→include_content,ocr_lang→ocr_language,method/route→via,partial→agapslist,cache_status→cached,http_status→status. The CLI follows:pfm harvest download-file, a newpfm harvest search,--include-content=false,--ocr-language, and--jsonuses the MCP field names. No aliases. The remote gateway serves the same tools under theirharvester_names;download_filethere answers a signed ten-minute link, never a server path;harvest.maxDownloadBytesandharvest.maxResourceBytesare new config keys.
→ For: adopters who call the harvester from their own prompts, scripts or remote MCP clients · after update · harvester tool names — rename each old tool, field and CLI flag to its new name above; a call to an old name now fails as an unknown tool.
- Global: flights replaces wave —
/flights:spec(maps the area and grills the user) →flights-speccer(task files) →/flights:orchestrate-nested,/flights:orchestrate-liveor/flights:orchestrate-cross-harness→flights-orchestrator→ one freshflights-mechanical-executororflights-smart-executorper task file →flights-lander(checks, one whole-diff review, adversarial tests) →/flights:audit./wave:builder,/wave:ccc,/wave:live,/wave:orchestrator,/wave:refine,/wave:walker,/wave:walker-invariants, theschedulerandarchitectagents and the wave-builder skill are removed; the update'spfm installretires their dangling links and Codex roles. Reds carry their cause, a second red diagnoses first, a third is BLOCKED; executors run only their affected tests.
→ For: every adopter · after update · wave commands — start new work with /flights:spec where you typed /wave:refine or /wave:builder; ls ~/.claude/commands no longer lists wave.
→ For: source installs that built the wave-walker engine · after update · ~/.professor/engines — optional cleanup: nothing reads the old build tree any more and the clone now ignores it, so rm -rf "$HOME/.professor/engines"; test -e "$HOME/.professor/engines" || echo gone then prints gone.
- Project: flights cast replaces per-project developer and qa —
agents/per-project/developer.mdandagents/per-project/qa.mdare gone upstream; the machine-global flights cast does their work and reads the project's test law from the new templatecommands/per-project/testing-manual.md(/per-project:testing-manualin the template tree), installed as.claude/commands/{project}-testing-manual.md(testing-manual.mdfor a single-project install). The project wave surface goes with them:commands/wave/live.md,commands/wave/orchestrator.md,commands/wave/walker.md,commands/wave/walker-invariants.md,codex/skills/wave-builder/SKILL.md,docs-commands/wave/references/debug-discipline.md,docs-commands/wave/references/fix-core.md,docs-commands/build/references/build-reference.mdanddocs-commands/git/references/gitter-phase-wave.md.
→ For: every adopter · per project · pfm update check — adopt the NEW template project/commands/per-project/testing-manual.md as .claude/commands/{project}-testing-manual.md for each project in the roster (.claude/commands/testing-manual.md for a single-project install), copied from the template with every token filled as at install (docs/PLACEHOLDERS.md) — its frontmatter becomes name: {project}-testing-manual with the project's name for {project}, in a single-project install too — and its sections filled with that project's own test commands, and map it with pfm update pin --template project/commands/per-project/testing-manual.md <local>; for each GONE-UPSTREAM file above delete the local copy first (keep a customized one only if you still use it) and then run pfm update drop <local>; then remove each directory those deletions emptied, deepest first, once it is empty — rmdir docs/commands/wave/references docs/commands/wave .codex/skills/wave-builder .claude/commands/wave for each that exists; an rmdir that refuses names a directory still holding a retired file, so delete that file and run it again. pfm update check then lists none of them, and for p in .claude/commands/wave .claude/agents/developer.md .claude/agents/qa.md .codex/skills/wave-builder docs/commands/wave; do test ! -e "$p" || echo "left: $p"; done prints nothing.
- pfm: guard hooks —
pfm installwirespfm internal git-guardas a machine-widePreToolUseBash hook in every Claude account: every caller except an agent of typegitter— the main chat included — is denied git's shared-state writes (commit, merge, rebase, reset, push, fetch, pull, switch, branch/tag writes, add/rm/clean, whole-tree checkout or restore, whole-repo stash, worktree add/remove, config writes and more); reads and a path-scop...
v0.77.2
v0.77.2 — 2026-09-14
The repository clears the HOL AI Plugin Scanner bar the awesome-ai-plugins catalog gates listings on — 56/F with 11 high findings becomes 88/B with none. Ten of the highs were test fixtures and placeholder names that read as live credentials; the eleventh was a real eval in the token-pricing check, which now parses its table as data. Every workflow action is pinned to a commit SHA, Dependabot keeps those pins and the Go modules current, SECURITY.md names the private reporting channel, and the scanner itself now runs in CI on every push and pull request. After pfm update from the source clone, its closing line names pfm update check in each adopted project as the step that finishes the update, where it used to suggest pfm init — from the next update on, since the update into this release still runs the previous binary.
Verification
The HOL AI Plugin Scanner (plugin-scanner 3.0.123, the version the catalog's CI installs) was first run locally against a clone of main and reproduced the catalog's result exactly — 56/F, 11 high, 17 medium, 4 low — before anything changed; the same scanner scores this release 88/B with 0 critical and 0 high, and the new plugin-scan.yml workflow reproduced 88/B on GitHub's runners against develop. The one real finding, the pricing check's eval, now parses the table as JSON; a single-quoted string, an unquoted identifier and a comment marker inside a string were each fed to it and each fails loudly with PRICING-UNREADABLE (exit 2), never a silent pass. Every pinned action SHA was checked against its tag. The first fenced pfm run caught one fixture renamed at its set site but not in its Authorization header (TestRemoteStaticGateway, 401); fixed, and the full pfm suite ran green inside the dev.sh iso fence (65 packages, none skipped). The candidate review against origin/main found no defects. dev.sh iso all for templates, pfm and walker plus dev.sh iso e2e ran green on both the stable side and this candidate.
The update was rehearsed on two fresh fenced Linux machines, a cheap Codex model playing the adopter: one installed v0.77.1, the other v0.76.0, two releases behind. The rehearsal found one shipped defect, fixed here behind a regression test watched failing against the unfixed code (TestWriteProjectUnmanagedHumanAndJSON): pfm update run from the source clone, as the update prompt directs, ended by advising pfm init. Its next attempt showed the update into this release still prints the old line, because the previous binary runs that update; the note above now says so, and a replay with the v0.77.2 binary printed the new line. The first main-machine attempt is void — the rehearsal brief itself had been corrupted by shell expansion (v0.77.2:releases became v0.77eleases), so the brief is now built without a shell. Final attempts: both machines ended at v0.77.2 exactly on its tag, doctor: clean, and the adopted project's pfm update check clean at exit 0, each re-run by the judge rather than taken from the model; the machine two releases behind read v0.77.0, v0.77.1 and v0.77.2 and worked their merged checklist. The frictions the model still reported were its own — a --repo argument no doc names, and hand-applied template diffs, which is how project templates are designed to update.
Fixed
- pfm: update —
pfm updaterun from the source clone, as the update prompt directs, ended onNOT-MANAGEDwith the missing-baseline advice topfm inita new install, which steers the adopter to scaffold the Professor clone itself. The terminal now says a missing baseline is expected in the source clone and names the step that finishes the update:pfm update checkinside each adopted project. The line comes from the binary that runs the update, so it first appears on the update after this one.
→ For: adopters updating to v0.77.2 — the update into this release still ends on the old NOT-MANAGED — … pfm init scaffolds a new one line, printed by the previous binary; do not pfm init the source clone — run pfm update check inside each adopted project instead.
Changed
- repo: security hygiene —
SECURITY.mdnames the private reporting channel and scope;.github/dependabot.ymlupdates workflow actions and Go modules weekly, PRs targetingdevelop; every workflow action is pinned to a full commit SHA with its version as a comment.scripts/check-token-pricing.mjsparses the PRICING table as JSON instead ofeval-ing it, and Go test fixtures that read as live credentials carry anexample-prefix. The HOL AI Plugin Scanner that gates the awesome-ai-plugins listing moves from 56/F with 11 high findings to 88/B with none, and.github/workflows/plugin-scan.ymlnow runs that scanner (pinned action SHA, min score 80, fail on high) on every push and PR. No adopter action.
v0.77.1
v0.77.1 — 2026-09-14
pfm heal stops making a wedged Codex thread worse: a rollout whose ordinals repeat or skip — the duplicate resume-boundary variant of openai/codex#38792 — is reported as NONCANONICAL and never rebuilt, because on Codex below 0.154.0 the rebuild from zero fails on the same record and the chat that showed one turn shows none; the report names 0.154.0 as the release whose projector recovers both shapes at the next resume. Harvester sends every fetch through one gateway, resolves over DNS-over-HTTPS, and closes the WebSocket and service-worker holes in its browser rung's SSRF guard. Terminals opened by an app that was itself launched from inside a chat open the picker again.
Verification
The candidate was reviewed by area against origin/main, and the two reviewer findings on the fetch gateway — the SSRF pre-check still consulting the system resolver, and the MCP resolver client dialling through a bare transport with its own copy of the private-host check — are fixed here, each behind a regression test watched FAILING against the unfixed code (TestAssertFetchableConsultsTheDoHResolver, TestNewDirectClientDialsThroughTheResolver, TestResolverClientIsPinned, TestAssertPublicURLIsTheHarvestChokepoint; the wire User-Agent test's watched failure is a compile failure against the old constructor). The heal change was proven on a live duplicate-boundary rollout copied into the fence: pfm 0.77.0 emptied the thread's projection rows, this build reports NONCANONICAL and leaves them, and the eight new heal tests plus the command's advice line were watched failing first. The terminal fix's three shim checks were watched failing on the old shim. The full pfm suite, go vet, gofmt and the architecture ratchet ran green inside the dev.sh iso fence after every change; cmd/pfm stays under its line budget (16078 of 16104).
The update was rehearsed twice on two fresh fenced Linux machines, a cheap Codex model playing the adopter from the shipped docs and the update prompt pfm ls opens: one machine installed v0.77.0, the other v0.75.1, two releases behind. Round one found two shipped defects, both fixed here: the install interview promised a manifest, a root tracer and per-project planner/architect files the installer never ships, and pfm doctor run from the source clone — where the update prompt sends the adopter — exited 1 with pre-push gate=UNWIRED because nothing had ever armed core.hooksPath there. Round two ran against this release's code in its final state: both machines ended at v0.77.1 with pre-push gate=armed, doctor: clean and the adopted project's pfm update check clean at exit 0, each re-run by the judge from the clone rather than taken from the model, and the machine two releases behind read v0.76.0, v0.77.0 and v0.77.1 and worked their merged checklist. The three frictions the model still reported in round two were its own: release notes grepped from a v0.75.1 working tree instead of git show, a mechanically mis-parsed report it then verified by hand, and an update run without the --skip-harvest that machine required — whose failure rolled the clone back to v0.77.0, as designed.
Changed
-
pfm: heal — a wedged/midline projection cursor whose rollout is not canonically ordinalled (a repeated, regressed, or skipped ordinal — the openai/codex#38792 duplicate resume-boundary variant) now reports NONCANONICAL instead of WEDGED, and one whose rollout could not be read end to end reports UNSCANNED; both are new verdicts that are never deleted, report or
--apply, because a rebuild from zero fails on the same record until Codex >= 0.154.0 (PR #42369) projects past it. No adopter action. -
B: pfm — Harvester sends every fetch through ONE gateway. The generic web ladder and the scholarly provider path were two parallel transports, so a provider page behind a JS/bot wall was terminal while the identical wall on the generic ladder was passed. One gateway now owns the SSRF assertion, headers, cookie jar, redirect re-validation, decoding, the byte ceiling, and a single challenge ladder — caller's client, Chrome impersonation, real browser headless, real browser headed. Headless is always attempted before a visible window; the headed rung is last and is never spent on a binary download. A type-checked source guard fails the build when any other HTTP call shape (a client's Do/Get/Post/Head/PostForm, a RoundTrip, the package-level http helpers) appears outside the gateway, and the web-search health probe
pfm doctorruns enters it too. -
B: pfm — Harvester resolves every dial over DNS-over-HTTPS, with a TTL cache. A network that answers a source host with its own block address made every rung land on a block page and read as the source refusing the request; resolving over HTTPS removes the class. A transport failure or a server-side DNS error falls back to the system resolver and logs it; NXDOMAIN is an answer, returned as "no such host" without asking the system resolver. RFC 6761/6762 special-use names skip DoH. The SSRF guard still runs on every address returned. (cost): the names of the hosts Harvester fetches are resolved through Cloudflare's resolver (cloudflare-dns.com, reached by its fixed addresses) instead of the system's.
-
B: pfm — the real-browser rung is pinned to the same DNS-over-HTTPS answers via Chrome's host-resolver rules, closing the gap where Chrome re-resolved independently. Pinning also narrows the DNS-rebinding window the strict fetchable check documents as a residual risk; a private address is never pinned.
Fixed
-
docs:
docs/SETUP.mdInstall interview anddocs/BLUEPRINT.mdsay what the installer actually ships — the interview creates.professor/manifest.json(pfm initwrites onlybaseline.json),tracer/architect/schedulerare machine-global agents linked bypfm installrather than root project agents, and the per-project agents aredeveloperandqa; the release rehearsal's adopter had invented all three from the old text. No adopter action. -
B: pfm — a later rung's transport error no longer overwrites an earlier rung's answer. A source that served a challenge page was being reported as a DNS failure, which sent the operator to fix the network instead of the wall.
-
B: pfm — the real-browser rung's SSRF guard covers WebSockets and service workers. The route guard saw navigation, redirects, subresources and XHR, but a page's
new WebSocket(…)and its service worker's own fetches never passed through it, so either could reach a private address. WebSocket connections now ask the same fetchable check before connecting (a refusal or a failed check closes the socket), and service workers are blocked for the browser context. -
B: pfm — a Harvester JSON response over its byte ceiling is refused by name. Several scholarly lookups read JSON through a path that silently truncated at the ceiling, so an oversize answer failed as "unexpected end of JSON input"; every JSON read now reports that the response exceeded its byte limit.
-
B: pfm — a terminal opened by an app that was itself launched from inside a chat opens the pfm picker again. Such an app (a launcher or window manager a chat relaunched) hands every process it starts the chat's CLAUDECODE / session markers, so each new VS Code terminal looked like a shell inside a chat and auto-open silently stepped aside. The Professor extension's terminal and the fallback PFM profile now drop those markers, and a terminal whose profile still passes them through (a hand-edited profile, an extension not yet reloaded) clears them itself and opens the picker, with one stderr line naming the leak.
-
B: pfm —
pfm installandpfm updatearm the pre-push leak gate (core.hooksPath=.githooks) in the source clone they manage, sopfm doctorrun from~/.professor— where the update prompt sends every adopter — reportspre-push gate=armedinstead of exiting 1 withUNWIRED; a clone without the shipped hook is a named skip. Found by this release's rehearsal on both machines. No adopter action.
v0.77.0
v0.77.0 — 2026-09-14
Harvester stops caching a JavaScript app's empty shell as the page and gains Google Scholar discovery, the Professor VS Code extension ships inside pfm, global agents reach every Claude account, and pfm ls groups resumable chats and stops showing unanswered OpenCode sessions. This is also the first release rehearsed as an adopter would live it: a cheap Codex model installed the previous release on a fresh fenced machine, then updated it — once from v0.76.0 and once from five releases behind — and every defect it tripped on is fixed here, starting with a source install that never recorded its clone and an update that refused the detached checkout the install recipe leaves.
Verification
Six sonnet reviewers read origin/main...release/v0.77.0 by area (six reports for six dispatched); every defect they raised was checked against the code, and each fixed one carries a regression test watched FAILING against the unfixed code first — the detached-source update, the three release-notes listing branches, the missing-marker install line, chat find across an 8 MB line, the retired MCP tools' absence, the update prompt reading every skipped release, the pre-release version ordering, the no-clone doctor row, resumable name groups, and the OpenCode size and unanswered-session rows. The full pfm suite, go vet and the architecture ratchet ran green inside the dev.sh iso fence after every change; cmd/pfm stays under its line budget by extraction (16060 of 16104), never by raising it.
The update was rehearsed five times on two fresh fenced Linux machines, a cheap Codex model playing the adopter and following only the shipped docs and the update prompt pfm ls opens: one machine installed v0.76.0, the other v0.73.0, five releases behind. The first two rounds rolled the update back: a Codex-only host failed the post-update doctor on Claude Code's absence (the claude launcher shim, its hooks, its global agents and its projects root were counted as warnings), and the rollback then stranded the host because the older pfm refused the fleet database the new one had migrated. Both are fixed here: without Claude Code those rows are named and not counted, and the fleet database stays at schema 8 with an additive column. Those rounds also found that a source install never recorded its clone and that the update refused the detached checkout the install recipe leaves; each now has its fix or its #### → For: line. In rounds three to five pfm update completed on both machines, pfm doctor ended clean and the adopted project's pfm update check exited 0; on the machine five releases behind the update read all six release notes, v0.73.1 through v0.77.0, and worked their merged checklist. Round five ran against this release's final tree, and its judge re-ran every end-state check on both machines rather than taking the model's verdict.
Added
-
B: pfm — Harvester adds Google Scholar discovery alongside Unpaywall and PubMed Central. CLI and MCP results use complete exported artifacts and opaque discovery handles, keeping acquisition URLs, methods, internal paths, and fallback traces private. Provider failures remain explicit, and internal cache files cannot be fetched through the public surface.
-
B: pfm — the Professor VS Code extension ships inside pfm:
pfm install --vscodelinks it into theextensions/of every VS Code product present (desktop, Insiders, VSCodium, Remote-SSH server, a portable install) and adds its Professor terminal profile to the + dropdown. The default terminal stays pfm's ownPFMsettings profile: a window reload rebuilds each restored terminal through VS Code's contributed-default lookup, which hands it to the default's extension for a brand-new terminal instead of reattaching — with an extension profile as the default, every reload dropped the open terminals (the pty host shut them down five minutes later). Each new integrated terminal opens the chat picker and carries its chat's live name. The ledger records every link; later installs and updates keep it reconciled without the flag; uninstall removes only links still pointing at pfm's copy. An owned default holdingProfessormoves back toPFM, and an older ledger gets the extension linked on the next ordinary install. VS Code settings writes now go THROUGH a symlinkedsettings.jsoninstead of replacing the link with a copy, and a portable install's settings path is$VSCODE_PORTABLE/user-data/User/settings.json(it had a straydata/). (cost): writes a symlink into each present VS Codeextensions/directory.
→ For: VS Code users — run pfm install --yes --vscode once (or any install if you opted in before), then Developer: Reload Window.
-
B: pfm — the MCP daemon restarts itself onto a new build. It exits (status 75) when its own binary is replaced, after in-flight calls finish, and launchd's KeepAlive or systemd's
Restart=on-failurebrings it back on the binary now on disk — so an install path that only swaps the binary no longer leaves the daemon serving the old one for days. -
A: quality family —
/quality:description, the law for the one field every session pays for: adescription:is injected into every chat and every sub-agent spawn whether or not its body loads. Fixed component order ([TOKEN] {function ≤5 words} — {when}. [Returns {shape}.] [Not for x → home.]), measured caps (280 chars for a body-bearing entry, 400 when the overrun is entry points / family neighbours / Returns, 600 per MCP tool, 900 per server instruction block), a cut order that takes the mechanism first (with rumdl,via midrun.js, a model tier), naming law (the name is not respent in the description), family law (afamily:*chain is legible from descriptions alone, carried by exactly one member), USER-ONLY declared twice (disable-model-invocation: trueplus the token, since a prompt clause is advisory and the harness flag is not), a## MCP toolssection for body-less entries (one inline example call with the schema's real field names, the sibling hand-off, the audience boundary, empty-vs-error distinguished), five WRONG→RIGHT pairs measured on live text, and an 11-check Approval gate emittingAPPROVED/REJECTED — checks {n,…}/UNREAD./pfm§ Descriptions and its file-type laws collapse to a pointer here. -
A: quality family —
/quality:md-forlint, markdown lint and format mechanics for any project:check [path],fmt [path],prompt-safe <file>,audit,profile <path>. Onerumdlconfig at the repo root decides every rule for every path, and its[per-file-ignores]table IS the category map — prompt (.claude/**,CLAUDE.md, templates, injected prompt assets), doc (docs/**, engine specs), public (README,INSTALL,CHANGELOG), generated (mirrors — format the source and recompile), record (releases/, ledgers,testdata/— never reformatted). Measured on this framework: paragraph-collapse reflow −242 bytes / −161 lines over 23 prompt files where sentence-per-line costs +1,048 / +690 and a 100-column wrap costs +2,289 / +1,602, so never wrap; stripping table column padding (MD060.style = "compact") takes 7.4% off adocs/tree; the whole professor-owned markdown surface lands −93,404 bytes / −1,524 lines with one word-level change, a mis-numbered ordered list corrected. Carries the gotchas that make or break it: rumdl resolves[per-file-ignores]against the CURRENT DIRECTORY (run it from the repo root or every exemption silently misses), an invalid rule option does not fail but printsUsing default valuesand formats with the default, and four rules rewrite what they must not —MD056/MD075delete cells from a malformed table andMD018/MD020read a line-initial#20(a wave, issue or PR number) as a heading missing its space and re-level every real heading below it, so all four are held inunfixable;MD001/MD025would demote a prompt's own H1 sections, which are addressing, not hierarchy. The record category is drawn the same way: a file whose bytes another test asserts is not prose — a staged prompt asset (prompts_asset_test.goasserts it equals its template twin,embed_test.gopins its SHA-256), a verbatim capture of another system's prompt, aCHANGELOG.mdwhose#### → For:markers the release flow copies verbatim, and a per-run wave train are all excluded. -
B: pfm —
pfm installprovisionsrumdl(pinned) through the harvest-provisioneduv,pfm doctorcarries its row, andpfm initscaffolds the adopter's.rumdl.toml. Every terminal branch is a distinct line — already present, dry-run plan, no uv, uv failure, offline — and none of them fails the install.
Changed
-
T: markdown lint policy — the public
README.mdis exempt from MD033 (inline HTML), in.rumdl.tomland the shippedrumdl-policy.toml: GitHub renders only allowlisted HTML (p align,picture,img,details) for layout, so a centered hero, a badge row, and collapsible sections exist only as inline HTML. The exemption is anchored to the root README; inline HTML anywhere else still reports MD033. -
B: docs — README rewritten as a walk-through of what Professor visibly does — the fleet and its sky, chats that message each other across harnesses, rules that refuse and name their unlock, Harvester reading what the web hides from bots — every transcript real output from this repository, names redacted.
-
T: prompts — the Professor fleet prompt gains a Voice and a Stance section: signature phrases and a cut-on-sight vocabulary list, an emoji budget (one at the Verdict, one in the body, 🚫 alone for sacred ground), observational humor with self-deprecation aimed only at the fleet's own past mistakes, anecdotes sourced from the fleet's real history (an invented career is a hallucination in costume), a gear-change to zero charm when the leak or publication line is in play or the user swears, dissent-once-then-execute, own-mistake-first-sentence, and t...
v0.76.0
v0.76.0 — 2026-09-11
Hiding a live chat ends it, /exit and e close the terminal tab, chat new is born on the calling chat's engine, and the TUI renders nothing VS Code's WebGL terminal cannot paint. This is also the first release cut through the develop → main pull request: main is release-only from here on.
Verification
The full pfm Go suite (.claude/scripts/dev.sh test pfm, every package, no filter) passed on macOS (Darwin 25.5.0, Go 1.24) after each change and three consecutive times at the end of the suite-reliability pass; the three flaky timing tests it fixed were each reproduced under mutation before the fix. Every regression test in this release was watched FAILING against the unfixed code first: the WebGL glyph guard failed on 18 real hits before going green; the hide→exit tests failed with the manager gate reverted; the caller-engine tests failed with the helper's precedence swapped and with its call removed; the exit-close reload guard failed with the closer running mid-reboot; the quiet-block tests failed against the exit-2 banner. The reload/exit-close interaction was additionally reproduced live (a /reload --1h closed its own VS Code tab) and confirmed fixed with the installed binary.
Not run for this release: the isolated Linux suite and the harvestpy archive-provisioning gate. The GitHub ruleset and pre-push branch guard were exercised by this release's own PR.
Added
-
A: pfm —
/exitand a newe//eshortcut now close the TERMINAL, not just the chat. A fleet seat is exec'd, so its pane already dies with the engine; what survived was the shell that SPAWNEDtmux attachas a child (a VS Code tab's zsh), left sitting at a prompt that IS the tab still open. Two hooks close it:pfm internal exit-close(SessionEnd) hangs up each tmux client's parent shell when the human ends the chat with/exit, andpfm internal exit-intercept(UserPromptSubmit) turns a prompt of exactlyeor/eintochat kill self --exitin-process, blocking the prompt so it never costs a model turn. The intercept sends/exit, so both spellings converge on the one closer and cannot drift. Acts only on reasonprompt_input_exit—/clearfalls through, because that chat keeps running — and only on a fleet socket, which is what keeps the hook off a human's own tmux; a parent that is not a terminal shell, is pid <= 1, or is the hook itself is skipped BY NAME rather than silently, and every path fails open. Claude only: Codex has noUserPromptSubmithook, so a Codex seat'sestill routes through the model. A reload's own/exitis exempt:pfm chat reloadholds its pane mutex for the whole reboot and the hook probes it (reload.InFlight) before touching any terminal, so/reloadreboots the pane in place instead of closing the tab it lives in. Both intercept hooks now swallow a prompt they ran successfully with Claude Code's quiet JSON block (no banner, no prompt echo); only a failed reload or close keeps the exit-2 banner with the front's text.→ For: run
pfm install --yesonce to wire the two hooks. They reach NEW chats only — Claude reads its hook registry at session start. (cost: one newSessionEndand one newUserPromptSubmithook on every configured Claude account'ssettings.json) -
A: pfm —
pfm chat reloadaccepts--model Mand--effort E, pinning what the REBORN pane is born with on both engines (--effortfor Claude,-c model_reasoning_effort="…"for Codex). Effort is validated against the rosterschat newalready uses — the former inline checks are replaced by exportedaction.ClaudeEffort/action.CodexEffort/action.CodexEffortArg, so the two commands cannot drift into two dialects of the same flags. The/reloadcard teaches both. -
A: pfm —
/handoffgains--branch: it writes the same handoff file, then starts a SEPARATE detached chat seeded with it viapfm chat newand leaves the calling pane and conversation completely untouched — no reboot, no hide — so the human opens the successor themselves from the picker. The default mode still reboots in place and hides the caller. The skill's "in-flight sub-agents die with the reboot" warning is now scoped to the default path, where it is true. -
A: pfm — hiding a LIVE chat now ends it:
pfm chat kill <id|self>, the picker's ⌃X, and MCPchat_killall run the detached exit choreography (/exitor/quit, kill-pane, close viewports, sweep crumbs, refresh, reap teammates) whenever the target resolves to a live tmux socket+pane;--exitis the explicit form. A hide of a resumable-only conversation stays a store write, andpfm chat reload --new --hideis unaffected because it never carries a live address.→ For:
chat kill/ ⌃X on a running chat closes its pane and process — usepfm chat unkillto bring the conversation back as resumable, not as live.
Changed
-
B: pfm —
pfm chat reload --freshis renamed--new; behavior is unchanged (reboot into a brand-new session id in the same pane). The retired--freshspelling is refused BY NAME and pointed at--new, matching how the legacyswapalias was retired.--hidenow reads--hide needs --new.→ For: replace
--freshwith--newin any saved command or alias; the old spelling now errors rather than running. -
B: pfm — the Limits page draws every engine on ONE scale: the bar fills by usage and the number reads
% used. Codex rows were rendered on an inverted% leftscale, so a Codex row at 76% consumed showed24% leftbeside the shortest bar on the page, meaning the opposite of the identical-looking Claude row above it. -
B: pfm —
pfm chat newwithout--engineis born on the CALLING chat's engine (Claude session env wins over an inherited Codex thread env; neither → config default), sharing one helper withchat branch./handoff --branchfrom a Claude chat therefore spawns a Claude successor; the skill states the rule.
Fixed
- A: pfm — the TUI renders nothing from the Unicode ranges VS Code's WebGL terminal draws as "custom glyphs" (Block Elements, Braille, Powerline, Progress, Git Branch, Legacy Computing): the Limits bars are
[▰▰▱▱], the Stats sparkline a_⎽⎼⎻⎺¯scan-line ladder, the carousel caps◖ ◗, and both statusline gauges▰▱. Underterminal.integrated.gpuAcceleration: autothose glyphs left stale, blank, and ghosted cells on every live refresh.cmd/pfm/webgl_glyph_guard_test.goscans every Go string/rune literal and every staged asset for the banned ranges (Box Drawing stays allowed for borders; the cosmos braille canvas is the one named exemption).
Removed
- B: pfm — the
chat_branchMCP tool is retired, so a model can no longer fork a conversation on its own initiative; the CLIpfm chat branchis unaffected. Its absence is pinned by name in the roster test, per the reasoning already recorded there: a tool that merely vanished from the advertised list would also pass if it came back renamed.→ For: restart the MCP daemon (
launchctl kickstart -k gui/$UID com.professor.pfm.mcp) — a running daemon keeps advertising the tool until it does.
v0.75.1
v0.75.1 — 2026-09-11
pfm install no longer restarts a launch agent whose plist did not change, so an ordinary install stops taking the MCP daemon down with it.
Verification
The full pfm Go suite ran on macOS (Darwin 25.5.0, Go 1.24) across all 64 packages in the module — the three slices' package lists were unioned and diffed against go list ./..., with no package left uncovered by any slice. internal/installer, the only package this release changes, passed. Five tests fail, in three packages, every one environmental rather than a regression: two in shim (the host's ~/.zshenv sources a missing .cargo/env inside the test's fake HOME), two in internal/headless/run (macOS resolves /var to /private/var), and one pre-existing 5-second timing flake in internal/deps.
The five new regression tests were each watched FAILING against the unfixed code, including the headline one — a loaded launch agent with an unchanged plist must issue no bootout and no bootstrap at all. The fix was additionally proven live: a full pfm install --yes reported "already loaded, plist unchanged — left running" for both agents and left the running MCP daemon's pid and start time untouched, where the previous build had stopped that daemon and left it down. The release binary was built twice with -trimpath and produced an identical SHA-256 both times.
Not run for this release: the isolated Linux suite and the harvestpy archive-provisioning gate. The launchd behavior is covered by fake-runner unit tests plus the live install above; no test drives real launchctl.
Fixed
- Patch:
pfm install— a launch agent whose plist did not change is no longer torn down and re-registered.launchctl bootoutSTOPS the running job, and the installer ran it unconditionally on every install, so an ordinarypfm installrestarted the user'spfm mcp serveas a side effect — and when the immediately-followingbootstraplost the race against the still-in-flight teardown (EIO / exit 5), it left the daemon DOWN, reporting only "agent file is installed but service is not loaded". A loaded job with an unchanged plist is now left running untouched; when the plist did move, the bootstrap is retried while launchd finishes the teardown (no wait at all when the first attempt succeeds); and a bootstrap that fails after its job was stopped now says the service is DOWN and names thelaunchctl bootstrapthat restores it, distinct from the case where nothing was running to begin with.
v0.75.0
v0.75.0 — 2026-09-11
Every Claude process pfm starts now disables Claude Code's own output style, so the staged professor prompt is the only persona layer; pfm chat reload works from a chat with no tmux ancestry; rr persists every answer; and the Limits page, the usage-hook banner and scripts/dev.sh's log scanner each stop lying in their own way.
Verification
The full pfm Go suite ran on macOS (Darwin 25.5.0, Go 1.24) across all 64 of its packages, split into three slices whose package lists reconcile against go list ./... with none left out. Five tests fail, in three packages, and every one is environmental rather than a regression: two in shim (the host's own ~/.zshenv sources a missing .cargo/env inside the test's fake HOME), two in internal/headless/run (macOS resolves /var to /private/var, so a scratch-path comparison mismatches), and one pre-existing 5-second timing flake in internal/deps. Every new regression test was watched FAILING against the unfixed code before it was accepted. The release binary was built twice with -trimpath and produced an identical SHA-256 both times.
The whole change range was reviewed before tagging. That review found four defects; three were fixed inside this release, each with a regression test of its own: the "usage recovered" banner asserting a measured 0% for a window whose reset had already passed, the spawn doors silently overriding a caller's own --settings, and pfm chat reload accepting --pane and then resolving as though it were absent.
Not run for this release: the isolated Linux suite, the harvestpy archive-provisioning gate, and any REAL-SESSION flow needing a live claude or codex process. Known gap carried forward rather than fixed here: the Limits page's Codex window path does not apply the reset-passed honesty rule the Claude path now has, so an all-rolled-over Codex payload can still be served as fresh.
Added
- Minor:
rragent — the fast-research agent now SAVES its full synthesis to.professor/RR/<slug>-<YYYY-MM-DD>.mdand returns that path on the first line of its reply, the answer inline beneath it, so a research answer outlives the chat that asked for it and the parent gets a file it can hand on. The directory is the calling project's.professor/RR/when that project has a.professor/dir, else the framework's own~/.professor/RR/; the agent never creates.professor/in an unmanaged project and writes nowhere else. A failed write is reported on that first line, never silently skipped. (cost: therragent gains theWritetool, scoped by its prompt to that one RR file)
Changed
- Minor:
pfmlaunch env — every Claude process pfm starts (new, resume, reload, theclaudelauncher shim, agent-open, headless chats,pfm headless exec,pfm ask) now carriesCLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION=9007199254740991, lifting Claude Code's 200-WebSearch-per-session cap that silently stalled long research chats. Declared once as the Claude engine descriptor'sLaunchEnvand carried by both spawn doors (action.ClaudeSpawn, the generic headless runner). (cost: no WebSearch ceiling per session — each search still bills normally) - Minor:
pfmlaunch args — every Claude process pfm starts (interactive spawn, resume, reload, theclaudelauncher shim, agent-open, probes, headless chats,pfm headless exec,pfm ask) now carries--settings '{"outputStyle":"default"}', disabling Claude Code's own output style so the staged professor system prompt (--system-prompt-file) is the only persona layer; a project or useroutputStylesetting no longer double-applies on top of it. Declared once as the Claude engine descriptor'sLaunchArgsand carried by both spawn doors (action.ClaudeSpawn, the generic headless runner), which drop any such flag the caller already spells for itself rather than appending after it — a user runningclaude --settings mine.jsonthrough the launcher keeps their own file instead of having it silently overridden by the flag appended behind it;pfm doctor's spawn audit now reports a launch that carries the staged prompt without this flag as its own outcome, distinct from injecting nothing at all — a VIOLATION when the seat was born after the prompt layer was staged, and PREDATES-LAYER (“reload to carry it”) when it predates the layer, so upgrading never accuses every chat already running on the host. (cost: none) - Minor:
/rnd— moves from a machine-global command (with its/rnd:hammerand/rnd:refereesub-commands) to a project-scope lifecycle command that opens, continues, verifies and lands research runs under.professor/RND/<call>/<N>-<slug>/, spawning the new root agentrndierto execute one run. SETUP now always writesrndierbesidegitter,mono-documenterandtracer, and substitutes the AI-service placeholders into both files.
→ For: adopters — pfm install drops the global /rnd, /rnd:hammer and /rnd:referee; run pfm update check, hand-apply the NEW commands/rnd.md and agents/rndier.md, then pfm update pin.
- Minor:
deep-rrskill — the description now opens with a USER-ONLY gate: the skill launches only on the user's explicit request for it, never on the model's own initiative and never as a step inside another task. A run fans out a background Workflow of many agents, so an unasked-for launch is expensive in a way no other skill's is; the gate lives in thedescription:because that is the text the router reads before the body is ever loaded.
Fixed
- Patch:
scripts/dev.sh— frontend startup unsets inheritedFORCE_COLORbefore settingNO_COLOR, and a single severity-aware log scanner replaces broadERRsubstring matching in both status summaries. StructuredINFOpayload fields no longer appear as failures; numeric/string error and fatal levels plus standalone failure signatures remain visible. The scanner reports unreadable logs as scan failures instead of “no errors.” - Patch:
pfmusage hook — the critical banner names the window that actually crossed the threshold and says only what is true of it: the 5-hour window keeps "finish the in-flight step, then /reload", the 7-day account cap says to /reload, and a model-scoped 7-day cap (opus / fable) says to keep working and route that tier's spawns elsewhere — instead of printing a session-wide stop order on every prompt while the 5-hour window sits nearly empty. - Patch:
pfmLimits page + usage hook — the picker's Claude usage fetch ran at a 5 s TTL and rate-limited both accounts (one with a server-sent 3600 s Retry-After); it now polls once per 60 s. The prompt hook ages its shared cache by the recordedfetched_atinstead of file mtime (a backoff-only write no longer re-warns on hours-old usage; a future-stamped record is untrusted and falls back to mtime), andresets_atis honoured on every window: a window whose reset has passed renders—with a "reset passed · awaiting refetch" note, forces a refetch, and the hook's warn line says "reset passed" instead of a stale percentage. All-expired cached payloads are never reused unless a backoff is active, and the hook's “usage recovered” banner no longer reports a reset-passed window as a measured0%— it says5h — (reset passed · awaiting refetch), the same as the warn line, because an expired window collapsing to 0 is exactly what drives that banner. Scope: the Claude usage path; the Codex window path keeps its existing reset handling. - Patch:
pfm chat reload— the scheduler now hands its detached worker the resolved tmux socket and pane (--sock/--pane) instead of making the worker re-identify the calling chat; a chat served by a Claude Codebg-spareprocess (no$TMUX, identity only through process ancestry) reloaded nothing and logged "this chat is not inside tmux".--sock SERVER --pane PANEnow also disambiguates a multi-pane server from the command line instead of being accepted and then refused with “has multiple panes”.
v0.74.0
v0.74.0 — 2026-09-10
Harvester settings move into one validated harvester.config.json, the MCP daemon becomes one process with a loopback port and an opt-in authenticated external port, a self-hosted SearXNG works again (GitHub #21), and the chat MCP drops chat_load.
Verification
The PFM build, vet, and full test suite passed in the isolated Linux environment (57 packages, 0 failures), plus the harvestpy conversion corpus byte-exact against the pinned interpreter. New regression tests were each watched failing against the unfixed code. The host migration (config.json → pfm.config.json, port 8377 → 18377, client re-wiring) was applied and verified on a Linux host. Not run for this release: a native macOS runtime suite, the harvestpy archive-provisioning gate, and an end-to-end two-listener pfm mcp serve test (each listener is covered on its own).
Breaking
- Major: pfm harvester config — every Harvester setting moves out of the process environment into
~/.config/pfm/harvester.config.json(enabled,external,search,scholarly,fetch,convert,cache,output), validated at load (bad URL, negative TTL, an external gateway withoutpublicURL+ auth, a world-readable file holding a secret = load error naming the key); the machine config is renamedconfig.json→pfm.config.json; an interrupted migration's leftoverconfig.jsonis parked on the next install, never read as done.pfm mcp serveis now ONE process with two ports: the loopback port (default 18377, was 8377; chat + harvester, unauthenticated) and an opt-in authenticated external port (default 18378) that serves the harvester only, behind a mandatory passphrase-OAuth/static-bearer wall with reads confined to the cache. Search trusts exactly the configured SearXNG origin (a loopback/LAN SearXNG works; redirects refused; fetch keeps the SSRF guard) and reports each backend's own error (GitHub #21). Fixed en route: the daemon cached into~/.cache(cwd-relative) while the CLI used~/.professor/.cache; local-read confinement failed OPEN when no root resolved; MCPfindWorksran without the configured scholarly keys. Retired:pfm mcp harvester serve --transport httpand its 8081/8082 pair,--allow-unauthenticated,PFM_HARVEST_PYTHON(broke conversion when set),HARVESTER_LOCAL_ROOTS(never honored).→ For: every host — run
pfm install --yesonce: it renames the config, moves the harvester flag intoharvester.config.json, moves an init-written port 8377 → 18377, and re-wires every MCP client in the same pass. Then move any exportedSEARXNG_URL/BRAVE_API_KEY/HARVESTER_*intoharvester.config.json—pfm doctornames each one still set and its key. (cost: MCP port change; env → config)
Removed
- Major: pfm chat MCP —
chat_loadandpfm chat loadare retired end to end (MCP tool, CLI verb,internal/chatload); a model reads files with its own tools, and a transcript loads throughchat_read. The chat MCP's server instructions andchat_injectnow state the server is for cross-chat communication between independent chats only — parent/child agent traffic uses the harness's native agent tools.→ For: callers of
chat_loadorpfm chat load— read the files with the harness's own file tools; the CLI verb now exits 2 as an unknown command.
v0.73.1
v0.73.1 — 2026-09-09
Includes the features and fixes in v0.73.0, with a correction to its cancellation test. Runtime behavior is unchanged.
Fixed
- Patch: headless cancellation verification — wait for descendant exit and distinguish a killed zombie from a live process, eliminating the CI race while preserving detection of surviving descendants.
Verification
The original isolated cancellation test failed 4 of 100 repetitions when a killed child remained briefly as a zombie. The corrected test passed all 100 repetitions. The isolated PFM build, vet, and full test suite passed.
macOS limitations
Darwin builds and installer CI passed for v0.73.0. A full native macOS runtime suite has not been run, and the reported second-account keychain case remains unresolved.
v0.73.0
v0.73.0 — 2026-09-09
Shared headless execution and fixes for credential recovery, usage reporting, process handling, and fleet diagnostics.
Verification and macOS limitations
Release checks passed in the isolated Linux environment: the PFM build, vet, and full test suite; Walker build, verification, typecheck, and all 375 tests; and template, mirror, and self-hosted manifest gates. Darwin builds and installer CI passed for the merged PRs. A full native macOS runtime suite has not been run; the reported second-account keychain case remains unresolved. The historical scope claims in the ledger entries below do not establish full macOS compatibility.
Added
- Minor: PFM headless — shared Claude/Codex execution interface with system prompts, schemas, configurable timeouts, normalized results, and native streaming; prepared-source asks, harness capture, credential refresh, and Walker equivalence route through one internal process runner, with explicit errors for unsupported engine capabilities.
Fixed
-
Major: pfm Limits tab — a Claude account whose
.credentials.jsonis ABSENT (the normal shape wherever Claude keeps its credentials in the OS keychain rather than on disk) rendered an EMPTY Limits card instead of the provider-confirmed windows sitting in itscc-rate-limitsstatusline snapshot. The missing-credentials read recorded a sixty-second provider backoff even though it makes no request at all, and the replay path revives a cached backoff aserrors.New(record.Backoff.Message)— a flat string error that cannot satisfy theerrors.Is(err, os.ErrNotExist)gatingFetchClaude's statusline fallback. The fallback therefore fired only on the first sample after each backoff expired and was suppressed for the rest of every window, which on such a host is effectively always. An absent credentials file now records no backoff, and — because this cache is SHARED across every pfm process, so a record written by a peer or an older build is normal rather than anomalous — an EMPTY backoff replay for an account with no credentials file on disk falls through to the free local path that returns the properly wrapped sentinel. A backoff carrying usable windows, such as a 429's cached quota, still serves them unchanged. The identity-matched snapshot therefore keeps serving the card on every sample, whatever else is running on the box. -
Major: pfm Limits tab — the model-SCOPED Fable window now survives the
cc-rate-limitsstatusline snapshot (fable_used/fable_resets_at). The harness reports Fable inside itslimitsarray rather than as a flat window, and althoughwindowsAtfolds it into the resolved window map before the snapshot is written, the writer copied out only the two flat windows. On a host that has fallen back to that snapshot — at the time, every macOS host, since pfm could not yet read the OS keychain — Fable was therefore structurally unrenderable in the Limits tab however faithfully the statusline drew it inline. The reader reconstitutes it by appending a synthesizedweekly_scopedentry tousage.Limits, leavingusagehook.fableWindowthe single opinion on which scoped limit is Fable. -
Major: pfm Limits tab — a Claude account with NEITHER a credentials file NOR an identity-matched statusline snapshot now gets the same hidden one-turn Haiku probe (
-p ACK --max-turns 1, at most once per account per sampler) that the credential-rejection path already used, and then re-reads both sources; such a seat previously stayed blank until the user sent it a prompt by hand. A snapshot belonging to a PREVIOUS account identity is still never adopted after the probe. When the probe itself fails, its reason is remembered and reported on every later refresh — an OAuth session too old to refresh needs an interactive re-login, which a bare "credentials file missing" does not convey. -
Major: pfm usage credentials — pfm now resolves a Claude account's OAuth credential from the credentials FILE first and the macOS login keychain second, closing the root cause behind every blank Claude Limits card on macOS. Claude Code stores the credential as a generic-password item keyed by config directory (
Claude Code-credentials-<first 4 bytes of sha256(configDir), hex>) and writes no.credentials.jsonat all, so pfm's file-only reader reported every fully-logged-in account as having no credentials, and the provider usage API — the only source of the model-scoped Fable window, and of confirmed quota generally — was permanently unreachable there. The Limits tab, the prompt hook's usage warning and the shared usage cache all now query the real endpoint on such a host. The service name is DERIVED per config directory and never discovered by scanning, because a host accumulates one entry per config dir it has ever used and a scan could attribute one account's quota to another. The item is read through/usr/bin/security(pfm buildsCGO_ENABLED=0, so a subprocess is the only door), and onlyerrSecItemNotFoundcounts as absence: a locked keychain or a denied ACL surfaces as the failure it is rather than as "no credentials here". The credential is re-resolved on every fetch, so the existing 401 → ACK-refresh → retry chain picks up the rotated token Claude Code writes back. -
Major: pfm Limits tab — a SIGNED-OUT account (a credential present in either source but holding an empty access token, the shape Claude Code leaves after a refresh token expires) is now distinguished from an account with no credential at all. It reports the one repair that works, an interactive
claude /loginfor that seat, instead of ano such file or directorythat sends the reader hunting for a file a keychain host is never supposed to have. It still counts as unavailable for fallback purposes, so an identity-matched statusline snapshot keeps serving its card, and it is no longer spent on the hidden Haiku probe — no headless turn can mint a token from an empty refresh token, so the probe could only re-derive a diagnosis already in hand and bury it under a probe-failure suffix. -
Patch: credential and picker verification — registry aliases honor their declared executable path, keychain reads obey cancellation and a bounded timeout, keyboard wake polling stays responsive independently of idle identity probes, and warned reconciliation remains eligible for retry.
-
Patch: Claude usage cache identity — the hook stamps each record with its config directory, and the hook, Limits tab, and statusline reject unbound or mismatched records when an account number is reused. Recovery warnings also remain scoped to the originating directory. A valid Fable window persists to the quota snapshot even when ordinary windows are empty or expired.
-
Patch: process cleanup — a procfs ESRCH during process-group inspection is recognized as an exited process, alongside ENOENT; other read failures and reused process identities still stop cleanup.
-
pfm: the transcript index no longer mistakes a detached turn for a background session —
sessionKind:"bg"is stamped per record on every turn Claude Code produces while nobody is attached to the pane, which is how the fleet drives its own named chats throughpfm chat inject, so reading that marker retroactively reclassified deep interactive chats as machine work and dropped them from the picker with no visible signal. Provenance (sdkSpawned) is now the only background signal, andclaude_parser_versionbumps to 3 so the next index pass re-derives every stored row. -
pfm:
pfm chat killworks on macOS — the detach helper resolvedsetsidunconditionally, a util-linux binary absent from every Mac, so the spawn failed after the kill had already been recorded and left chats hidden in the picker while their pane and process stayed alive. A shareddeps.DetachLaunchernow preferssetsid -fand falls back tonohupwith an explicit release of the child, and it names both failures when neither is available; the duplicate resolution insideinjectcollapses into the same helper. -
pfm: tmux window titles converge instead of being assumed — five servers sitting at
set-titles offwere reported as a deliberate host-owned preference, so a VS Code terminal tab could disagree with the chat and tmux label indefinitely.pfm name-syncnow reads each live server's real option state, applies the configured policy, and nudges the title string, reporting a per-server transition and a converged count;pfm doctornames divergence, counts it, and treats a server whose state could not be read as an unanswered question rather than claiming either answer for it. -
pfm: ancestry recovery works on macOS —
newNativeProcFSaccepted a ProcFS root and discarded it, so the darwin path ignoredPFM_PROC_ROOTwhile documenting that it honoured it. The root is now honoured behind a directory guard, which is load-bearing because path resolution hands/procto macOS too, and theEnvironfailure namesKERN_PROCARGS2instead of implying a missing file. -
pfm: the harvester's browser check stops reporting a healthy install as tampered — it compared a resolved path against an unresolved one, so
/varversus/private/varon macOS read as a mismatch. Both sides now canonicalise before the compare. -
pfm: a dependency whose version probe outruns its bound reads as a timeout, not as broken — the version path mapped every execution failure, cancellation included, into the same bucket as a corrupt binary, so an installed and working tool on a loaded box, a cold binary, or a network mount sent the reader after an install defect that did not exist. A new
timeoutstate names the enforced bound and says no fault was established, while a genuinely broken tool still reads broken; the required-dependency warning arithmetic is deliberately unchanged, so no gate weakened. The sibling self-doctor path had drawn this line since it was written. -
pfm: five test fixtures stopped encoding an idle Linux box, each of which had ...