Skip to content

The Professor: drafting agent persona for the handbook knowledge layer - #238

Merged
tucktuck101 merged 23 commits into
launchpadfrom
task/9-the-professor-persona
Aug 20, 2026
Merged

tucktuck101 merged 23 commits into
launchpadfrom
task/9-the-professor-persona

Conversation

@serina-mcfall

@serina-mcfall serina-mcfall commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Adds The Professor: a persona pack that drafts handbook pages, wired to a real 5-tool MCP server (read-only) against the handbook's own provenance contract, plus a real drafting attempt that produced and gate-validated a two-claim page end-to-end.

Related issue

Closes #9

Issue type

Task


Agent provenance

Field Value
Harness / provider Claude Code
Model claude-sonnet-5
Session reference N/A - harness does not expose a stable run id/URL for this session
Initiating human @serina-mcfall

Objective

A .persona.md persona pack for The Professor, wired to a read-only MCP tool server against the handbook's page contract, proven against a real drafting attempt.

Impacted components

launchpad/agents/the-professor/personas/the-professor.persona.md
launchpad/agents/the-professor/tools/server.py
launchpad/agents/the-professor/tools/check_server.py
launchpad/agents/the-professor/skills/draft-page/SKILL.md
launchpad/agents/the-professor/.mcp.json
launchpad/agents/the-professor/.plugin/plugin.json
launchpad/agents/the-professor/README.md
launchpad/AGENTS.md
launchpad/Research/the-professor-design.md
launchpad/plans/2026-08-19-issue-9-the-professor-persona.md

Approach and rejected alternatives

Built a real MCP tool server (official mcp Python SDK, uv run --script shebang) rather than trusting the model to resolve pins/paths from memory — the design doc's own identified SHA-hallucination risk. Route 2 (plain buzz-acp configured from the environment) was chosen over Route 1 (recreating the desktop app's config, not scriptable/testable) — see README's Runtime Route section for the full reasoning, including the explicit specification-vs-configuration caveat. Scratch-path draft output was chosen over a real write-path/PR-against-handbook flow, since #9's bounded acceptance does not require one and the design doc names that as a separate, deferred decision (Open Question 2, Route 3).

Verification

Command run:

just test-unit

Raw output (tail):

  Passed (8):
    pass buzz-core tests
    pass buzz-auth unit tests
    pass buzz-voice tests
    pass buzz-cli tests
    pass buzz-db unit tests
    pass buzz-conformance tests
    pass buzz-push-gateway tests
    pass buzz-backend-kubernetes tests

[run-tests] All tests passed!

Command run:

uv run --script /tmp/verify_draft.py   # calls check_page via the real MCP server, same protocol path as check_server.py

Raw output:

{
  "findings": [],
  "unchecked": [],
  "skipped": [],
  "page_index": {
    "ok": true,
    "errors": []
  }
}

This re-verifies (independent of the ACP session transcript) the exact draft content The Professor produced during the real Step 16 drafting attempt: two claims, [upstream] block/buzz@main pinned to 9a1e861ab8507ee28e5f03010b7f36d1b74ec919 (crates/buzz-persona/PERSONA_PACK_SPEC.md), and [launchpad] launchpad-26/buzz@launchpad pinned to 45dd387e316bdd2ad677ce6bcc1d742baec53532 (launchpad/AGENTS.md §3, launchpad/review-agent/'s existence) — both resolved via resolve_pin against the real GitHub API, not recalled from memory.

Command run:

just mobile-test   # via pre-push lefthook, after merging origin/launchpad

Raw output (tail):

04:56 +1461 -4: Some tests failed.
error: Recipe `mobile-test` failed on line 680 with exit code 1

4 pre-existing failures, all in accessibility large-text-size timestamp layout tests (compose_note_page_test.dart, forum_widgets_test.dart, note_card_test.dart) — filed as #237, unrelated to this PR's diff (zero mobile/** files touched). Pushed past this hook with --no-verify on Serina's explicit authorization, with #237 filed first as the paper trail (same category of problem as the precedent at #181).

Command run:

uv run --script /tmp/verify_injection_fix.py

Raw output:

injection attempt (bad path) -> Error executing tool path_exists_at: path_exists_at('block/buzz', '0000000000000000000000000000000000000000', 'crates/buzz-persona/PERSONA_PACK_SPEC.md?ref=main&'): `path` contains '?' or '&', which cannot appear in a real repository path. Refusing rather than risk it being interpreted as part of the request's query string.
non-SHA commit -> Error executing tool path_exists_at: path_exists_at('block/buzz', 'main', 'crates/buzz-persona/PERSONA_PACK_SPEC.md'): `commit` is not a 40-character hex SHA. This tool checks existence at a pinned commit, not a branch or tag -- resolve it with resolve_pin() first.
ALL INJECTION CHECKS BEHAVED AS EXPECTED (see output above)

Confirms the exact exploit review-code identified (a path value smuggling its own ?ref= to override the pinned commit) is now rejected outright, and that a bare branch name in commit (instead of a real pin) is also rejected.

  • Tests or checks were run and the raw output is pasted above
  • The diff is confined to the scope of the linked issue
  • No secrets, keys, tokens or hostnames were added to tracked files

Not verified

Desktop and relay-integration behavior beyond what just test-unit/cargo test -p buzz-cli cover — this PR touches no desktop or relay code, so those suites were not re-run beyond the pre-push hook's own pass. Whether the persona pack format itself (frontmatter shape, MCP wiring) is stable against upstream PERSONA_PACK_SPEC.md changes going forward — only checked against the pinned commit resolved during Step 16. Whether goose's GOOSE_TEMPERATURE/GOOSE_MODEL env-var projection is the correct mechanism long-term, versus a config file — noted in the README's Runtime Route section as an open, stated caveat rather than something this PR resolves.

Security implications

All five MCP tools in server.py are confirmed read-only (no write, push, or commit calls to GitHub or the handbook repo) — independently re-verified by direct code inspection, not just trusted from the implementation report. The credential used to run the agent (an Anthropic API key) is bring-your-own-key per person, documented explicitly in the README's Credential Policy section, and was never read or printed by the controller during setup — only copied between environment variables. No secret, key, or hostname was added to any tracked file.

Escalations

  1. Credential/provider decision — surfaced to Serina as a group decision (shared infra, not a solo call); resolved as bring-your-own-key per individual for Route 2, documented in README's Credential Policy section. Explicitly does not answer Route 3's (unattended-operation) credential question.
  2. Write-path capability gap — the real Step 16 drafting attempt showed The Professor can compose and validate a correct draft, but has no tool that can write it to disk or reply — only this pack's five read-only MCP tools were ever wired into the runtime; goose's built-in shell/write extension was never enabled. The controller extracted and placed the already-validated draft by hand (documented in README's Observed Behaviour section). This is exactly why Route 3 is filed as task: Route 3 projector — resolve The Professor's pack into a real, write-capable runtime #239, a separate follow-up issue, rather than attempted in this PR.
  3. Pre-existing mobile test failures — filed as bug: accessibility timestamp widget tests fail on launchpad tip (4 failures in mobile/) #237, unrelated to this PR's diff; pushed past the pre-push hook with --no-verify on Serina's explicit authorization after filing that issue first.
  4. Merge-to-nav decision (Step 21) — this PR's draft page stays proof-only; it is not merged into the live launchpad-26/handbook nav. The draft's own body states it is a scratch/test artifact, not real handbook content, and the save step above was not fully autonomous.
  5. review-final findings — a full-branch review was run before this PR left draft. One Blocker: the plan's own review-code gate on server.py (real security surface — gh api subprocess calls, gate-script invocation) was never dispatched during the original build. Two Medium findings (a stale model ID in README's Model section contradicting the persona file, and Step 19's required exact scratch-path evidence missing from the README) were deferred as their own follow-up: task: fix two README documentation-accuracy gaps for The Professor #240.
  6. review-code results (the Blocker from # prd-03 — reproducible and hardened Buzz deployment: rebuild the cohort server from a bare Ubuntu host #5, resolved) — dispatched against server.py, independent of the branch's author. Found real issues, now fixed in c55272389: a Blockerpath_exists_at built its gh api request as a hand-concatenated query string, so a path value containing its own ?ref=... could override the pinned commit it was supposed to check against, defeating pin verification entirely; fixed by passing ref via gh api --method GET -f and rejecting any path containing ?/& outright. A Highcheck_page never checked whether check_provenance.py itself exited non-zero, only whether its stdout parsed as JSON, so a failed gate run printing valid-but-empty JSON would report as a clean pass; fixed to raise when a non-zero exit carries no findings/unchecked/skipped. A Medium — the local handbook checkout lived at a predictable, world-writable /tmp path trusted without an ownership check; fixed to verify ownership and wipe-and-reclone if it doesn't match. A Lowpath_exists_at's commit argument was never validated as a real SHA the way resolve_pin's output is; now validated up front. Re-verified against check_server.py's full fixture suite plus a new targeted check confirming the exact injection payload and a bare branch name both now raise.

serina-mcfall and others added 21 commits August 19, 2026 13:48
Twelve-step plan grounded in the closed state of #6/#7/#8/#10/#11, ADR-0015's
hybrid-authoring decision, and the persona pack spec already implemented in
crates/buzz-persona. Surfaces two open calls a builder must not silently
resolve: which runtime proves the drafting behaviour, and whether the proof
page also becomes live handbook content.

Verification: launchpad/plans/2026-08-19-issue-9-the-professor-persona.md was
checked with .claude/skills/plan-issue/check-plan.sh (mechanical checks clean:
one RUNS HERE, all 12 steps within the declared cap and tagged, all six named
sections present). This checks plan structure only, not implementation --
there is no code to test yet.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
An independent review (Opus, via the review-plan skill, run in a fresh
context, not this session's earlier self-check) found 2 Blockers and 4
High findings, all verified against the real tools rather than assumed:

- step 8's findings-empty check passed on a skipped/unchecked page and on
  an empty directory; now also requires skipped:[] and page_index.py's
  frontmatter-completeness check, which check_provenance.py does not run
- triggers: {} does not mean "off" -- it resolves mentions back to its
  built-in default of true; now set explicitly to all-false
- a GitHub rate-limit/network finding looks identical to a page defect;
  step 9 now reads the message before editing anything
- MCP env-var interpolation isn't implemented yet, so step 3 no longer
  claims functional end-to-end wiring, only well-formed config
- buzz pack validate doesn't check skills: paths resolve or that the
  persona body is non-empty; steps 5 and 6 now assert those directly
- step 7's own worked example cited block/buzz under [launchpad], which
  the real gate rejects as a prefix-repo-mismatch; fixed to [upstream]

Also fixed: check_provenance.py isn't self-contained (step 8 now gets a
handbook scripts/ checkout), buzz isn't built/on PATH in a fresh checkout
(step 1 now builds it), and steps 2/10 both claimed to own README.md
(step 10 now extends it instead of overwriting).

Verification: launchpad/plans/2026-08-19-issue-9-the-professor-persona.md
re-checked with bash .claude/skills/plan-issue/check-plan.sh after all
edits (mechanical checks clean). The substantive fixes above respond to
the independent review's evidence, not to this script.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ed branch

launchpad-26/buzz's docs/the-professor-design branch (commit 650108c0b,
2026-08-12) contains a 639-line design document for this exact issue (#9)
that was never merged, never opened as a PR, and never referenced from
issue #9 itself -- found only by noticing an existing git worktree on
that branch while setting up isolation for this session's work.

Per its own account it was independently checked by three reviewers across
three passes on the block/buzz side, and traces (2026-08-12) that no
runtime in this fork currently consumes resolved persona-pack
configuration -- a pack is authorable and validatable but not runnable,
by design rather than omission on the desktop side.

Cherry-picked the single file rather than merging the branch: the branch
is 42816 deletions behind current launchpad and would revert unrelated
work if merged as a whole.

Placed under launchpad/Research/, not launchpad/agents/ (the design's own
diagram): this session's own check-models.sh hook treats any directory
named agents/ as a Claude Code subagent roster and requires every
top-level .md in it to carry name:/model: frontmatter -- a real naming
collision the design doc didn't anticipate, since it predates that hook.
launchpad/Research/ already holds this repo's other design documents
(project-intelligence-layer-design.md).

Verification: bash .claude/skills/plan-issue/check-plan.sh re-run on the
existing plan (mechanical checks clean) as this repo's recognized
suite-by-path -- there is no test suite for a markdown design document
itself.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Replaces the prior 12-step plan (fb18226, fixed at 30540ee) rather than
patching it -- the orphaned design doc (b170781) changes the architecture
enough that a diff would be less honest than a fresh plan.

User-confirmed size increase to a 20-step cap: the design doc requires real
new engineering (a five-tool MCP server: resolve_pin, path_exists_at,
read_contract, list_categories, check_page) to avoid an LLM hallucinating
commit SHAs, not just persona-pack config wiring.

Key architecture changes from v1:
- pack path: launchpad/agents/the-professor/, not launchpad/personas/
- tool server: launchpad/agents/the-professor/tools/ (Python, mirroring
  launchpad/review-agent/'s convention), NOT a new crate under crates/ --
  that directory is upstream's shared workspace and buzz-dev-mcp is a
  generic dev-tool crate, not Professor-specific
- runtime route: Route 2 (plain buzz-acp configured from environment),
  chosen over Route 1 (manual desktop-app recreation) because it's
  scriptable; Route 3 (a projector) filed as its own follow-up issue
- draft destination: scratch-path proof suffices for #9's bounded
  acceptance; a real PR-to-handbook write path is Route-3-scale, deferred
- new: launchpad/AGENTS.md section 3 gets an agents/ row (step 2), folding
  in the "convention" content the design doc wanted at
  launchpad/agents/README.md -- discovered this session that a bare .md
  directly in any agents/ directory trips this session's own
  check-models.sh hook (Claude Code subagent roster scan), so that
  specific file is never created

Verification: bash .claude/skills/plan-issue/check-plan.sh clean (20 steps
within cap, one RUNS HERE at step 3, every step tagged with done-when).

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…view

A second independent review (Opus, fresh context, run via review-plan)
found 3 Blockers and 3 High findings against the v2 replan, all verified
against real tools/APIs rather than assumed:

- check-models.sh actually DOES trigger on the design doc's own literal
  recommendation (nested .../agents/the-professor.persona.md) -- the v2
  plan's own claim that it wouldn't was wrong. Fixed by naming the inner
  persona directory personas/, matching buzz-persona's own test fixtures
  and doc-comment example (pack.rs:10), not the spec's "agents/" example
- Step 6's negative-control fixture (broken-01, an unsourced-behaviour-
  claim) isolates a judgement-engine rule check_provenance.py deliberately
  doesn't implement; it always returns zero findings regardless of whether
  check_page works. Swapped to broken-03 (prefix-repo-mismatch), a real
  script-engine rule, demonstrated firing live
- Step 15 (now 16)'s own worked example cited this plan's own unmerged
  commit, which fails both pin-not-ancestor and pin-path-missing against
  the real gate -- a circular dependency (can't merge until the gate
  passes, can't pass until it cites something merged). Fixed to cite
  already-merged content
- buzz-acp has no offline mode -- relay_url and a private key are
  required on its only runnable path, confirmed in lib.rs/config.rs. User
  decided (asked directly, given the cost) to stand up a real local relay
  rather than switch away from Route 2 -- added as new Step 14, cap raised
  20 -> 21
- .mcp.json was never declared in plugin.json's mcp_config field, so
  nothing would have wired the tool server at all; buzz-acp's mcp_command
  takes one bare executable with no args, so the tool server needs its
  own shebang, not a "python3 <path>" invocation
- Step 10 required an empirical drafting observation before any drafting
  step had run; split into a reasoned starting value (10) and the actual
  observation (now Step 19, after Step 16 drafts something)
- Step 3's done-when asserted exactly 11 categories; the live mkdocs.yml
  nav has 13. Fixed to verify against the live nav at run time, not a
  literal count
- Step 12's justification misattributed its own scratch-path choice to
  the design doc, which argues the opposite (only a PR reaches the gate's
  enforcement boundary); fixed to state the divergence explicitly
- Step 13's needs tag didn't list Steps 11/12, which its own body said
  must precede it

Verification: bash .claude/skills/plan-issue/check-plan.sh clean (21
steps within the user-confirmed cap, one RUNS HERE, every step tagged
with done-when).

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Scaffolds launchpad/agents/the-professor/: .plugin/plugin.json (OPS
manifest, personas + mcp_config declared) and a placeholder
personas/the-professor.persona.md (complete identity frontmatter, model
and temperature deliberately unset until STEP 10, body is a stub marker
for Serina to replace at STEP 13).

Persona directory is named personas/, not agents/, per the plan's own
finding: a nested .../agents/*.md would trip this session's check-models.sh
subagent-roster hook. Verified against crates/buzz-persona's own test
fixtures (pack.rs's make_pack helper and doc comment both use personas/),
not the spec's "agents/pip.persona.md" example, which is illustrative only.

Verification: cargo test -p buzz-cli, run bare (no pipe/chain -- both
disqualify the commit-gate's attribution check), 341 passed, 0 failed.
buzz pack validate launchpad/agents/the-professor: Valid, exit 0.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Add agents/ row to the launchpad/ directory table documenting persona packs
for Buzz-native agents. Add explanatory paragraph clarifying the distinction
between launchpad/AGENTS.md (contributor guide) and launchpad/agents/
(persona packs), with the rule that no bare .md files may sit directly in
launchpad/agents/ due to the check-models.sh hook that scans that pattern
as Claude Code subagent rosters. Pack documentation belongs in subdirectories
(e.g., launchpad/agents/the-professor/README.md).

Step 2 of 21-step implementation plan for The Professor mentor persona (issue #9).

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…t_categories)

Step 3 of the-professor plan: launchpad/agents/the-professor/tools/ is a
plain Python directory (mirroring launchpad/review-agent/'s convention, no
Rust crate) holding an MCP server built on the official Python SDK
(mcp.server.mcpserver.MCPServer). server.py is the executable entry point
via `uv run --script` with inline PEP 723 dependencies, since no pip/pip3
exists in this environment; it exposes exactly two tools:

- read_contract() fetches launchpad-26/handbook's docs/page-contract.md
  live via `gh api`, never quoted into source.
- list_categories() parses the handbook's live mkdocs.yml nav and excludes
  "Home" and "The page contract" (structural entries, not PRD #4 Ruling 3
  categories), leaving the eleven user-need slots.

check_server.py spawns server.py as a real subprocess and talks MCP over
stdio via the official client (initialize, tools/list, tools/call),
verifying the running server rather than the Python functions in isolation.
Confirmed live: list_categories matches a fresh independent mkdocs.yml
fetch exactly, and read_contract's text contains "The claim rule".

resolve_pin, path_exists_at, and check_page are later plan steps and are
not built here.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Step 4 of the-professor plan: resolve_pin(repo, ref) resolves a branch/tag/SHA
to its full 40-char commit SHA via `gh api repos/{repo}/commits/{ref}`, using
the same gh-CLI fetch convention as the existing handbook tools. Distinguishes
a rate-limited/unauthenticated GitHub response (HTTP 401/403/429) from a real
bad-ref/bad-repo defect, and refuses to return anything that isn't exactly 40
hex characters. check_server.py now exercises resolve_pin over stdio and
cross-checks the returned SHA against `git ls-remote` on the real repo.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Adds path_exists_at(repo, commit, path) -> bool, checking GitHub's
contents API at a pinned commit via gh api, same live-fetch convention
as resolve_pin. A 404 is the tool's normal negative answer; auth and
rate-limit statuses are distinguished and raised instead of being
misreported as "path does not exist" -- the same trap resolve_pin
already guards against.

check_server.py now exercises both the true and false cases over real
stdio against a real pinned commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…provenance gate

Step 6 of the-professor plan. check_page() maintains a local git checkout of
launchpad-26/handbook (refreshed via fetch+reset on every call, never left to
go stale) and shells out to its real scripts/check_provenance.py and
scripts/page_index.py as subprocesses against an isolated per-call scratch
directory -- never reimplementing the gate's rules in Python, per the design
doc's own answer to open question 5.

check_server.py now exercises check_page over real stdio with two real
handbook fixtures: compliant.md (zero findings, zero skipped) and
broken-03-prefix-repo-mismatch.md (a prefix-repo-mismatch finding).
broken-01 is deliberately not used as a negative control since it isolates a
judgement-engine rule the script does not implement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Creates launchpad/agents/the-professor/.mcp.json with professor-tools
MCP server configuration, pointing to the tools/server.py executable
entry script. The server reads GitHub auth from gh CLI's environment
(no explicit GITHUB_TOKEN env var needed).

Verified:
- buzz pack validate: OK
- buzz pack inspect: reports 1 MCP server
- tools/check_server.py: ALL CHECKS PASSED

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Teaches the agent to call read_contract, list_categories, resolve_pin,
path_exists_at, and check_page rather than recalling any of their
answers from memory, and to tag behaviour claims with an origin
prefix while attributing opinion claims to the persona's author
field, never both for the same claim.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Add to the-professor persona frontmatter:
- skills: ["./skills/draft-page/"]
- subscribe: [] (explicit empty)
- triggers: {mentions: false, keywords: [], all_messages: false} (all explicit)

This ensures the persona explicitly references the skill written in Step 8
and disables all trigger modes with fully-explicit config (not relying on
built-in defaults).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Step 10 of the-professor plan: add model and temperature to the persona
frontmatter, and document the reasoning in a new README. Temperature
(0.4) is a starting value reasoned from the design doc's tension between
factual-synthesis accuracy and consistent voice, not yet validated
against a real drafting attempt -- that empirical check is Step 19.

buzz pack validate launchpad/agents/the-professor still exits 0.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… 11)

Add "Runtime Route" section to launchpad/agents/the-professor/README.md
explaining why Route 2 (plain buzz-acp configured from environment) was
chosen over Route 1 (desktop app GUI) and Route 3 (projector). Clearly
states the specification-vs-configuration distinction and notes that
Route 3 is filed separately as Step 20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… drafts sufficient for #9

Extend launchpad/agents/the-professor/README.md with a new section documenting
that for #9's bounded acceptance criteria, a scratch-path draft (stored in a
temporary directory, not committed to the handbook, not opened as a PR) is
sufficient. Section explicitly states this diverges from the design doc's own
conclusion and explains why the divergence is acceptable: #9's acceptance does
not require a PR, and write-permission decisions belong to a separate issue.

Also add a new subsection confirming that all five tools in the tool server
(read_contract, list_categories, resolve_pin, path_exists_at, check_page) are
read-only — no writes, pushes, commits, or PR creation against GitHub or the
handbook repository.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Replaces the <!-- VOICE --> stub with the actual persona prompt, drafted
collaboratively with Serina in chat (not by an implementer subagent, per
issue #9's explicit exclusion and the standing rule that creative/voice
content is hers to write).

Identity: The Professor's own discipline (a citation is owed, not
assumed) merged with an investigator's behavioral DNA (don't stop at the
first plausible answer, treat contradictions as interesting, know
verified-fact from inference). Voice: dry, quietly amused by a bad
citation, restrained on serious findings. Scoped explicitly to what the
pack's five tools can actually do today -- no narrated capabilities
(Slack, live systems, arbitrary search) it doesn't have.

The claim rule gets its own section: wit is channeled through two
separate sentences (sourced claim, then attributed opinion), never
blended into one, so the persona's voice can't accidentally produce a
page the gate rejects.

Verification: buzz pack validate exits 0 (Valid.); buzz pack inspect
reports a 4046-char System prompt (was the stub before); the tool
server's own check_server.py -- all five tools exercised live -- passes
unchanged (this step touched only the persona body, not the tools).

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
anthropic:claude-sonnet-4-20250514 404s against the live API (not a
valid model id on this account); discovered during the real Step 16
drafting attempt. Switched to anthropic:claude-sonnet-5.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…haviour

Credential Policy: each person running The Professor locally brings
their own key (BYOK) for Route 2 -- no shared cohort secret, distinct
from ADR-0012's upstream-synthesis CI credential which has a different
blast radius. Notes the Route 3 carve-out explicitly.

Observed Behaviour: the real Step 16 drafting attempt validated the
temperature-0.4 starting choice (accurate, honestly-scoped prose) and
surfaced a genuine capability gap -- goose had no write/shell tool
wired in, so it composed and validated a clean draft but could not
save or reply itself. The controller extracted and placed the
already-validated draft by hand. This is why Route 3 needs to define
a real write path before the pack is load-bearing.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ssor-persona

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
benmitchell11
benmitchell11 previously approved these changes Aug 20, 2026

@benmitchell11 benmitchell11 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Read the full diff, with server.py as the priority given the PR's own escalation #5 flags it as never having gone through review-code before this PR opened.

Security review of server.py (all 5 MCP tools), done independently:

  • Confirmed read-only end to end: read_contract/list_categories/resolve_pin/path_exists_at are all single gh api GET calls; check_page does git fetch+reset --hard against a local temp-dir cache of the handbook (not the real repo) and shells out to the handbook's own check_provenance.py/page_index.py read-only. No push/commit/write call to GitHub or the handbook repo anywhere in the file.
  • All subprocess.run calls use list-form argv, never shell=Truerepo/ref/path/commit args can't achieve shell injection regardless of content.
  • The rate-limit/auth-vs-404 disambiguation in resolve_pin/path_exists_at (checking the JSON status field rather than trusting exit code or falling through) is a real, non-obvious trap correctly avoided — an ordinary 403 could otherwise silently masquerade as "no such ref."
  • _subprocess_env_with_github_token() never logs or returns the token, only places it in the env dict handed to the gate-script subprocess — checked this holds.

Spot-verified, not just trusted:

  • Both pinned SHAs from the Step 16 drafting attempt are real: crates/buzz-persona/PERSONA_PACK_SPEC.md does exist at 9a1e861a... in block/buzz (main has since moved, which is expected — a pin is point-in-time by design, not a claim that it's still HEAD). The launchpad pin (45dd387e3...) is the same commit I independently verified as accurate while reviewing buzz-infrastructure#69.
  • #237, #239, #240 all exist and match what's described. The --no-verify mobile-test bypass is justified — zero mobile/** files in this diff, issue filed first, same precedent as #181.
  • check_server.py's test design is genuinely careful — it deliberately avoids using broken-01 as a negative control because that fixture would pass regardless of whether check_page works (checked this reasoning against check_provenance.py's own documented judgement-vs-script engine split), and checks both the true and false case for path_exists_at rather than only the happy path.

One new finding, not covered by #240: personas/the-professor.persona.md's frontmatter description ("A mentoring persona designed to guide learning through questions and insights.") and the identical copy in .plugin/plugin.json don't match what the persona actually is. The body defines a strict citation-discipline drafting agent scoped to exactly five named tools ("You do not have access to Slack, live systems, or arbitrary search") — nothing about guiding learning through Socratic questions. Reads like unedited template boilerplate carried over from scaffolding, and it's user-facing (plugin listings, marketplace description). #240 only covers the Model section and Observed Behaviour path — this is a third, separate doc-accuracy gap worth folding into that follow-up or its own tiny issue.

Not blocking — approving. The security-critical piece (server.py) checks out independently, and this is a one-line description fix, not a functional defect.

…lure in server.py

Found by an independent serina:review-code pass (the plan's own required
gate for this file, run late after being missed during the original build):

- Blocker: path_exists_at built its gh api request as
  f"repos/{repo}/contents/{path}?ref={commit}" -- a hand-concatenated
  query string. A `path` value containing its own "?ref=..." could inject
  a second ref parameter and get checked against that instead of the
  pinned commit the caller asked about, defeating the tool's whole
  purpose. Fixed by passing ref via `gh api -f`/`--method GET` instead of
  string concatenation, and by rejecting any `path` containing '?' or '&'
  outright as invalid input.
- Low, same function: `commit` was never validated as a real 40-hex SHA
  (resolve_pin validates its own output this way; path_exists_at accepted
  anything, including a branch name). Now validated up front.
- High: check_page only checked whether check_provenance.py's stdout was
  parseable JSON, never whether the script itself exited non-zero. A
  failed gate run that still printed valid-but-empty JSON would have been
  reported as "zero findings" -- a clean pass that wasn't one. Now a
  non-zero exit with no findings/unchecked/skipped raises instead of
  passing through silently.
- Medium: the local handbook checkout lived at a fixed, predictable path
  under the shared system temp directory, trusted without checking who
  created it -- another local process could plant a hostile checkout
  there ahead of time. Now checked for ownership before being trusted;
  anything not owned by this process is wiped and re-cloned fresh.

Verified: check_server.py's full fixture suite passes (including the real
vs. fabricated path_exists_at cases and both check_page fixtures), plus a
new targeted check confirming the exact injection payload from the review
and a bare branch name both now raise instead of silently succeeding.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
README.md's Model section still named claude-sonnet-4-20250514, contradicting
its own Observed Behaviour section which narrates discovering that model's
404 and switching to claude-sonnet-5 (commit 6d585da) -- and contradicting
personas/the-professor.persona.md's actual model: field. Updated to match.

The Observed Behaviour section said Step 16's validated draft was "placed at
the scratch path by hand" without naming it. Step 19 of the original plan
required this exact path as evidence; it existed only in the git-ignored SDD
ledger, invisible to any reviewer of this README. Now states it literally:
/tmp/the-professor-scratch-drafts/persona-packs-upstream-spec-fork-local-tooling.md
(confirmed against the ledger before writing it here, not recalled from
memory).

Verified: cargo test -p buzz-core passes (251 + 2 doctests, 0 failed) --
unaffected by this doc-only change, run to confirm nothing else broke.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
@tucktuck101
tucktuck101 merged commit 96cccf0 into launchpad Aug 20, 2026
22 checks passed
@serina-mcfall
serina-mcfall deleted the task/9-the-professor-persona branch August 31, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

by:agent Filed or authored by an AI agent, not a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

handbook D — The Professor: the drafting agent persona

3 participants