Skip to content

Releases: rezzminator/professor

v0.78.0

Choose a tag to compare

@rezzminator rezzminator released this 26 Sep 16:55
8b446f8

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 chat and harvester MCP servers merge into one server keyed professor, registered on Claude, Codex and OpenCode as the stdio command pfm mcp serve --stdio, which forwards to the daemon and keeps each caller's identity. Claude and Codex now see mcp__professor__harvester_read, mcp__professor__chat_inject and so on (OpenCode: professor_harvester_read); issue_servicedesk becomes servicedesk. The daemon serves /mcp/professor and the family views /mcp/professor/harvester and /mcp/professor/chat; /mcp/harvester and /mcp/chat answer 404. pfm mcp chat serve, pfm mcp harvester serve and bare pfm mcp are removed. The update's pfm install removes pfm's own legacy chat/harvester entries by exact shape in every Claude registry, ~/.mcp.json, each Codex config.toml and 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 (replaces readPage, parseLocalDocuments and readWork; takes urls, files and publications in one call), download_file (replaces download), search_literature (replaces findWorks; kind→type, count→limit) and search_web (replaces webSearch). The archive member readers, searchCache, fetchImage and the fetch prompt are gone. Result fields are renamed: size_only→include_content, ocr_lang→ocr_language, method/route→via, partial→a gaps list, cache_status→cached, http_status→status. The CLI follows: pfm harvest download-file, a new pfm harvest search, --include-content=false, --ocr-language, and --json uses the MCP field names. No aliases. The remote gateway serves the same tools under their harvester_ names; download_file there answers a signed ten-minute link, never a server path; harvest.maxDownloadBytes and harvest.maxResourceBytes are 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-live or /flights:orchestrate-cross-harness → flights-orchestrator → one fresh flights-mechanical-executor or flights-smart-executor per 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, the scheduler and architect agents and the wave-builder skill are removed; the update's pfm install retires 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.md and agents/per-project/qa.md are gone upstream; the machine-global flights cast does their work and reads the project's test law from the new template commands/per-project/testing-manual.md (/per-project:testing-manual in the template tree), installed as .claude/commands/{project}-testing-manual.md (testing-manual.md for 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.md and docs-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 install wires pfm internal git-guard as a machine-wide PreToolUse Bash hook in every Claude account: every caller except an agent of type gitter — 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...
Read more

v0.77.2

Choose a tag to compare

@github-actions github-actions released this 14 Sep 16:38
9bb80e4

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 update run from the source clone, as the update prompt directs, ended on NOT-MANAGED with the missing-baseline advice to pfm init a 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 check inside 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.md names the private reporting channel and scope; .github/dependabot.yml updates workflow actions and Go modules weekly, PRs targeting develop; every workflow action is pinned to a full commit SHA with its version as a comment. scripts/check-token-pricing.mjs parses the PRICING table as JSON instead of eval-ing it, and Go test fixtures that read as live credentials carry an example- 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.yml now runs that scanner (pinned action SHA, min score 80, fail on high) on every push and PR. No adopter action.

v0.77.1

Choose a tag to compare

@github-actions github-actions released this 14 Sep 10:04
75a09fb

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 doctor runs 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.md Install interview and docs/BLUEPRINT.md say what the installer actually ships — the interview creates .professor/manifest.json (pfm init writes only baseline.json), tracer/architect/scheduler are machine-global agents linked by pfm install rather than root project agents, and the per-project agents are developer and qa; 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 install and pfm update arm the pre-push leak gate (core.hooksPath=.githooks) in the source clone they manage, so pfm doctor run from ~/.professor — where the update prompt sends every adopter — reports pre-push gate=armed instead of exiting 1 with UNWIRED; 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

Choose a tag to compare

@github-actions github-actions released this 14 Sep 04:53
56d97f8

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 --vscode links it into the extensions/ 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 own PFM settings 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 holding Professor moves back to PFM, and an older ledger gets the extension linked on the next ordinary install. VS Code settings writes now go THROUGH a symlinked settings.json instead 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 stray data/). (cost): writes a symlink into each present VS Code extensions/ 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-failure brings 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: a description: 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 (a family:* chain is legible from descriptions alone, carried by exactly one member), USER-ONLY declared twice (disable-model-invocation: true plus the token, since a prompt clause is advisory and the harness flag is not), a ## MCP tools section 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 emitting APPROVED / 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>. One rumdl config 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 a docs/ 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 prints Using default values and formats with the default, and four rules rewrite what they must not — MD056/MD075 delete cells from a malformed table and MD018/MD020 read 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 in unfixable; MD001/MD025 would 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.go asserts it equals its template twin, embed_test.go pins its SHA-256), a verbatim capture of another system's prompt, a CHANGELOG.md whose #### → For: markers the release flow copies verbatim, and a per-run wave train are all excluded.

  • B: pfm — pfm install provisions rumdl (pinned) through the harvest-provisioned uv, pfm doctor carries its row, and pfm init scaffolds 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.md is exempt from MD033 (inline HTML), in .rumdl.toml and the shipped rumdl-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...

Read more

v0.76.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 17:39
cccc9d1

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 — /exit and a new e / /e shortcut 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 SPAWNED tmux attach as 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, and pfm internal exit-intercept (UserPromptSubmit) turns a prompt of exactly e or /e into chat kill self --exit in-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 reason prompt_input_exit — /clear falls 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 no UserPromptSubmit hook, so a Codex seat's e still routes through the model. A reload's own /exit is exempt: pfm chat reload holds its pane mutex for the whole reboot and the hook probes it (reload.InFlight) before touching any terminal, so /reload reboots 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 --yes once to wire the two hooks. They reach NEW chats only — Claude reads its hook registry at session start. (cost: one new SessionEnd and one new UserPromptSubmit hook on every configured Claude account's settings.json)

  • A: pfm — pfm chat reload accepts --model M and --effort E, pinning what the REBORN pane is born with on both engines (--effort for Claude, -c model_reasoning_effort="…" for Codex). Effort is validated against the rosters chat new already uses — the former inline checks are replaced by exported action.ClaudeEffort / action.CodexEffort / action.CodexEffortArg, so the two commands cannot drift into two dialects of the same flags. The /reload card teaches both.

  • A: pfm — /handoff gains --branch: it writes the same handoff file, then starts a SEPARATE detached chat seeded with it via pfm chat new and 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 MCP chat_kill all run the detached exit choreography (/exit or /quit, kill-pane, close viewports, sweep crumbs, refresh, reap teammates) whenever the target resolves to a live tmux socket+pane; --exit is the explicit form. A hide of a resumable-only conversation stays a store write, and pfm chat reload --new --hide is unaffected because it never carries a live address.

    → For: chat kill / ⌃X on a running chat closes its pane and process — use pfm chat unkill to bring the conversation back as resumable, not as live.

Changed

  • B: pfm — pfm chat reload --fresh is renamed --new; behavior is unchanged (reboot into a brand-new session id in the same pane). The retired --fresh spelling is refused BY NAME and pointed at --new, matching how the legacy swap alias was retired. --hide now reads --hide needs --new.

    → For: replace --fresh with --new in 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 % left scale, so a Codex row at 76% consumed showed 24% left beside the shortest bar on the page, meaning the opposite of the identical-looking Claude row above it.

  • B: pfm — pfm chat new without --engine is born on the CALLING chat's engine (Claude session env wins over an inherited Codex thread env; neither → config default), sharing one helper with chat branch. /handoff --branch from 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 ▰▱. Under terminal.integrated.gpuAcceleration: auto those glyphs left stale, blank, and ghosted cells on every live refresh. cmd/pfm/webgl_glyph_guard_test.go scans 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_branch MCP tool is retired, so a model can no longer fork a conversation on its own initiative; the CLI pfm chat branch is 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

Choose a tag to compare

@github-actions github-actions released this 11 Sep 13:21

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 bootout STOPS the running job, and the installer ran it unconditionally on every install, so an ordinary pfm install restarted the user's pfm mcp serve as a side effect — and when the immediately-following bootstrap lost 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 the launchctl bootstrap that restores it, distinct from the case where nothing was running to begin with.

v0.75.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 11:13

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: rr agent — the fast-research agent now SAVES its full synthesis to .professor/RR/<slug>-<YYYY-MM-DD>.md and 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: the rr agent gains the Write tool, scoped by its prompt to that one RR file)

Changed

  • Minor: pfm launch env — every Claude process pfm starts (new, resume, reload, the claude launcher shim, agent-open, headless chats, pfm headless exec, pfm ask) now carries CLAUDE_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's LaunchEnv and carried by both spawn doors (action.ClaudeSpawn, the generic headless runner). (cost: no WebSearch ceiling per session — each search still bills normally)
  • Minor: pfm launch args — every Claude process pfm starts (interactive spawn, resume, reload, the claude launcher 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 user outputStyle setting no longer double-applies on top of it. Declared once as the Claude engine descriptor's LaunchArgs and 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 running claude --settings mine.json through 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:hammer and /rnd:referee sub-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 agent rndier to execute one run. SETUP now always writes rndier beside gitter, mono-documenter and tracer, 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-rr skill — 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 the description: because that is the text the router reads before the body is ever loaded.

Fixed

  • Patch: scripts/dev.sh — frontend startup unsets inherited FORCE_COLOR before setting NO_COLOR, and a single severity-aware log scanner replaces broad ERR substring matching in both status summaries. Structured INFO payload 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: pfm usage 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: pfm Limits 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 recorded fetched_at instead 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), and resets_at is 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 measured 0% — it says 5h — (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 Code bg-spare process (no $TMUX, identity only through process ancestry) reloaded nothing and logged "this chat is not inside tmux". --sock SERVER --pane PANE now also disambiguates a multi-pane server from the command line instead of being accepted and then refused with “has multiple panes”.

v0.74.0

Choose a tag to compare

@rezzminator rezzminator released this 10 Sep 15:28

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 without publicURL + auth, a world-readable file holding a secret = load error naming the key); the machine config is renamed config.json → pfm.config.json; an interrupted migration's leftover config.json is parked on the next install, never read as done. pfm mcp serve is 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; MCP findWorks ran without the configured scholarly keys. Retired: pfm mcp harvester serve --transport http and its 8081/8082 pair, --allow-unauthenticated, PFM_HARVEST_PYTHON (broke conversion when set), HARVESTER_LOCAL_ROOTS (never honored).

    → For: every host — run pfm install --yes once: it renames the config, moves the harvester flag into harvester.config.json, moves an init-written port 8377 → 18377, and re-wires every MCP client in the same pass. Then move any exported SEARXNG_URL / BRAVE_API_KEY / HARVESTER_* into harvester.config.json — pfm doctor names each one still set and its key. (cost: MCP port change; env → config)

Removed

  • Major: pfm chat MCP — chat_load and pfm chat load are retired end to end (MCP tool, CLI verb, internal/chatload); a model reads files with its own tools, and a transcript loads through chat_read. The chat MCP's server instructions and chat_inject now 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_load or pfm 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

Choose a tag to compare

@github-actions github-actions released this 09 Sep 20:26

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

Choose a tag to compare

@github-actions github-actions released this 09 Sep 19:59

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.json is 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 its cc-rate-limits statusline 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 as errors.New(record.Backoff.Message) — a flat string error that cannot satisfy the errors.Is(err, os.ErrNotExist) gating FetchClaude'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-limits statusline snapshot (fable_used / fable_resets_at). The harness reports Fable inside its limits array rather than as a flat window, and although windowsAt folds 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 synthesized weekly_scoped entry to usage.Limits, leaving usagehook.fableWindow the 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.json at 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 builds CGO_ENABLED=0, so a subprocess is the only door), and only errSecItemNotFound counts 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 /login for that seat, instead of a no such file or directory that 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 through pfm 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, and claude_parser_version bumps to 3 so the next index pass re-derives every stored row.

  • pfm: pfm chat kill works on macOS — the detach helper resolved setsid unconditionally, 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 shared deps.DetachLauncher now prefers setsid -f and falls back to nohup with an explicit release of the child, and it names both failures when neither is available; the duplicate resolution inside inject collapses into the same helper.

  • pfm: tmux window titles converge instead of being assumed — five servers sitting at set-titles off were reported as a deliberate host-owned preference, so a VS Code terminal tab could disagree with the chat and tmux label indefinitely. pfm name-sync now 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 doctor names 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 — newNativeProcFS accepted a ProcFS root and discarded it, so the darwin path ignored PFM_PROC_ROOT while documenting that it honoured it. The root is now honoured behind a directory guard, which is load-bearing because path resolution hands /proc to macOS too, and the Environ failure names KERN_PROCARGS2 instead 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 /var versus /private/var on 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 timeout state 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 ...

Read more