Skip to content

Blackbird engine: pitch-only reconciliation + removal cycle (stacked on #34) - #35

Draft
ahueb wants to merge 65 commits into
blackbirdfrom
blackbird-engine-reconciliation
Draft

Blackbird engine: pitch-only reconciliation + removal cycle (stacked on #34)#35
ahueb wants to merge 65 commits into
blackbirdfrom
blackbird-engine-reconciliation

Conversation

@ahueb

@ahueb ahueb commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Was stacked on the prompts+docs branch (PR #34, now merged into blackbird) — this PR now targets blackbird directly and carries only the engine-side commits. Implements the engine half of the pitch-only design and the follow-on removal cycle.

Engine reconciliation

  • Topology enforced in code: startup fails fast unless cohorts are star-shaped; the hub auto-activates an interview thread on every lab top-level post (mention or not); labs' menu is exactly pitch (one per day, enforced pre-LLM).
  • Funding surface removed (⚠️ migration 0026 drops grantbot_posted_foas — archive first per the deploy checklist): GrantBot modules/service/tool/channel and the funding-thread open-to-all participation exception. WRITER_GRANTBOT stays reserved for historical message ids.
  • Collaboration-era paths deleted: :memo:/ proposal handshakes, private-channel refinement, the phase-5 reply action.

Removal cycle (second pass, per updated product direction)

  • Private instructions & confidentiality clauses removed end-to-end (injection, synthesis pipeline, editor routes, prompts, docs). ⚠️ Deploy: archive-and-diff the untracked host profiles/private/blackbird.md before deploying — see the checklist.
  • The hub is reply-only: it never makes a top-level post; the <assessment_json> sidecar rides in its concluding interview reply, is stripped before Slack, and persists to /admin/assessments. CANONICAL post types = {pitch}.
  • All human-PI interaction removed (pi_handler, DM/tag/web-post/email-instruction paths); human-authored rows are structurally inert to bot behavior (activation/pending/priority guards, pinned by tests) while history/observability reads keep them visible. pi_dm_messages + history views kept read-only.
  • Phase 2 fully excised (prompts, builders, engine functions, interesting_posts machinery). Pipeline is 1→3→4→5.

Audit-discovered correction

  • MUST-CONCLUDE was unreachable in production (a prior-count vs ordinal off-by-one meant cap-reaching interviews silently closed as timeouts with no verdict). Fixed: phase4_guidance and the {message_count} token now receive the reply's ordinal; the message-12 concluding reply — verdict inline + sidecar — is real, pinned end-to-end, with an absent-sidecar warning live under default config.

For review

  • Golden-master regenerations are single reviewed commits (be732ff, 8a8fb12) — @andrewsu, these plus the thread_guidance.py §4 adoption on the base branch are the sign-off surface.
  • Deploy runbook: docs/plans/2026-08-12-blackbird-pitch-only-deploy-checklist.md (migration + archive steps, DB purges, host-file hygiene, verification signals).
  • Design + addendum: docs/plans/2026-08-12-pr34-pitch-only-reconciliation-design.md (§15 records the removal cycle's locked decisions and sanctioned survivors).

🤖 Generated with Claude Code

ahueb and others added 30 commits August 12, 2026 14:43
…terminal_only narrowing; per-role menus

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…l, compose, config, drop-table migration)

Deletes src/agent/grantbot.py, funding_rules.py, foa_cache.py,
src/services/grants.py, and src/models/grantbot_posted.py; drops the
retrieve_foa tool, the funding-opportunities channel, the grantbot compose
services, slack_bot_token_grantbot, and the grantbot_posted_foas table
(0026, downgrade mirrors 0012). WRITER_GRANTBOT stays reserved (historical
message ids carry its residue). simulation.py's Phase 3/4/5 funding call
sites are Task 4-6's job and are untouched; its now-dangling foa_cache/
funding_rules imports are replaced with TASK-6-REMOVES no-op stand-ins so
the module keeps importing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
Delete message_log.is_funding_post and MessageLog.is_funding_thread, and
the funding branch inside get_thread_allowed_agents — locked decision:
ex-funding thread roots get no replacement exception, they follow the
same tag/2-party rule as any other thread.

simulation.py's ~11 live call sites onto the deleted methods (Task 3's
stubbing only covered the foa_cache/funding_rules module-level imports,
not these) are neutered inline to `False`, each marked
# TASK-6-REMOVES for Task 6 to delete alongside the rest of the funding
plumbing. agent.py's is_funding_thread param is untouched (Task 5's
territory) since it no longer receives anything but False.

tests/unit/test_cohort_isolation.py: dropped is_funding_thread from the
UNGATED-methods inventory list, and rewrote
test_non_cohort_third_party_cannot_manufacture_priority to pin the new
behavior directly on get_thread_allowed_agents (a moneybag root no
longer grants a third party open access) instead of the old assertion,
which was actually gated by the cohort filter and never exercised the
funding exception at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mpt builders

Removes is_funding_thread/your_prior_messages/thread_activity_summary from
build_phase4_prompt and foa_contexts/thread_foa_contexts/funding_only/
funding_thread_summaries from build_phase5_prompt, along with the
{foa_number}/{funding_thread_context} substitutions and the funding_only
template-surgery regex block (no partial survival, per design §6).
simulation.py's call sites updated to match; the FOA/funding_only assembly
that only fed the removed params is deleted, with TASK-6-REMOVES markers
left for the remaining dead is_funding branches Task 6 owns.

Rewrites the four branch-1-failing prompt-pin tests in test_roles.py to the
current Option A/B templates and pi_lab/scout_hub guidance text, deletes the
two tests whose premise (Baltimore gating prose, funding_only surgery) no
longer exists, and repins the two thread_guidance.py tests to spot-anchors of
the current DECIDE/CONCLUDE text and the 3-criteria gating contract. Deletes
the one funding-dedicated golden master (test_phase4_prompt_pi_context_and_funding_gm)
and its snapshot block.

test_roles.py + test_agent_prompts.py + test_thread_guidance.py are now 100%
green, retiring the last branch-1 unit failures; only the characterization
tier (regenerated in Task 16) stays red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…on; blocked agents narrow to terminal types

Task 6 of the branch2 engine-reconciliation plan: removes every remaining
funding call site in simulation.py/state.py (the FOA/funding_only stub block,
_backfill_foa_cache, Phase 2/3/4 is_funding branches, the funding disjuncts in
the blocked-action bypass) and deletes the phase-5 action == "reply" branch.
Any non-new_post/non-skip action now logs and increments the skip streak via
previous_skips + 1 (never a bare += 1). Renames _non_funding_thread_count ->
_active_thread_count and unreviewed_non_funding_count -> unreviewed_count now
that there's no funding exemption left to carve out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…p-table migration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
private-profile-synthesis.md generated a "Collaboration Preferences"
section (partner-seeking, "wet-lab partners") that contradicts the
pitch-only model: agents pitch their own lab's ideas to BlackbirdBot,
they don't broker collaborations. Rewritten as "Pitch Preferences"
guidance (what to pitch first, evidence bar, what to hold back).

pi-dm-classify.md's standing_instruction example ("Always look for
opportunities with the Wiseman lab") named a cross-lab collaboration
target that no longer exists in this model; replaced with a
pitch-prioritization example.

pi-profile-rewrite.md's merge-target reference to "collaboration
preferences" updated to "pitch preferences" to match the renamed
section.

_FORBIDDEN in test_doc_prompt_sync.py extended with both retired
phrases so this can't regress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m fallback and onboarding template

src/services/llm.py's synthesize_private_profile FileNotFoundError fallback and
src/routers/onboarding.py's brand-new-user default template both hardcoded the
retired "Collaboration Preferences" / "Criteria to Always Explore" section
contract, invisible to the prompts-only phrase guard in
test_doc_prompt_sync.py. Mirror the rewritten private-profile-synthesis.md
Output Format contract (Pitch Preferences / Communication Style / Topic
Priorities) instead, per this deployment's pitch-only model.

Add tests/unit/test_no_collaboration_residue.py as a source-text guard so this
can't regress again outside the prompts/*.md guard's blind spot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expands design §13 into a copy-paste-ready runbook: merge order; the 0026
grantbot_posted_foas drop-table migration (with pg_dump archive option, and a
flag for run_migration.sh's stale --target default); grantbot compose-service
teardown + orphaned /copi/grantbot CloudWatch group; DB purges for legacy
unreviewed proposals and 💰 threads (with interesting_posts called out
as needing none — it's in-memory only); stale profiles/private/blackbird.md
host-file hygiene; cohort star-shape preflight SQL; image rebuild + --fresh
restart; and the full verification signal set (menu, hub auto-activation,
opportunity_assessments, zero funding/phase-2 activity, one-pitch/day cap).

Also documents CLAUDE.md staleness this branch causes (the now-false
retrieve_foa bullet) and a pre-existing app/blackbird-app service-name
discrepancy between CLAUDE.md and the checked-in compose file, worked around
via an explicit $WEB_SVC resolution rather than picked a side silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te-channel collaboration flow

Retires the Slack-driven 📝/checkmark bilateral-proposal handshake and the
collab_private refinement flow it fed, per Task 7 of the branch-2 engine
reconciliation:

- _check_thread_outcome: deleted the checkmark-confirms-memo and
  posted-memo arms; kept the pause-button no-viable-collaboration close and
  general thread conclusion (timeouts, message-12) untouched.
- Deleted _check_private_channel_outcome, _finalize_private_proposal,
  _seed_private_refinements and their call sites/scheduling.
- agent.py: deleted the PRIVATE_CHANNEL_RULES prompt block and its injection
  in _compose_system_prompt; visibility/channel_id plumbing for private
  working-memory segmentation (infrastructure) is unaffected.
- simulation.py: removed the now-dead is_private bypass of blocked_for_regular
  and the is_private_reply/target_post_id logic (only ever reachable via the
  "reply" action Task 6 already removed); kept the private-channel
  turn-taking rule and _finalized_private_channels (still populated from
  legacy DB rows).

outcome="proposal" remains a valid ThreadDecision value for legacy rows and
is still served by _close_thread, the admin/PI dashboard routes, and
ProposalReview — nothing in the live system can produce a new one going
forward.

tests/integration/test_proposal_review.py's shared _conclude_thread fixture
now fabricates a legacy-shaped ThreadDecision row directly for
outcome="proposal" instead of driving the (now nonexistent) live handshake;
outcome="no_proposal" still drives the real path. Replaced
test_a_concluded_thread_records_a_proposal_decision with a control pair
pinning that the pause-button close still writes a decision and that a
checkmark+memo reply no longer does. Deleted TestSeedPrivateRefinements and
TestPrivateChannelFinalization from test_simulation_logic.py, and the
private-channel-rules assertions from test_privacy_scoping.py.

Known casualty (accepted, deferred to Task 16 GM regen): the characterization
test importing PRIVATE_CHANNEL_RULES will need its import/assertions and the
matching .ambr snapshot block removed before regeneration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-level post

Adds a third _phase3_activate_threads loop, gated on the plain
agent.role == "scout_hub" attribute (not self._roles_by_agent(), per
INV-E structural note 4). It opens a hub interview thread on any new
top-level post from an allowed sender (cohort gate), no @-mention
required, mirroring the tag loop's private-channel/_closed_thread_ids/
already-active/get_thread_allowed_agents guards exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hecklist itself

Review found the §3b closure script's untested/dry-run-first caveat only
lived in the task report, not the document — an operator following just the
checklist would run new, never-exercised logic cold against production. Add
the callout directly above the script, pointing at CLAUDE.md's scratch-DB
(copi_xN) instructions for rehearsal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-paper interview note

execute_tool's retrieve_abstract branch now takes a keyword-only own_dois
set; a lookup whose pmid_or_doi contains an own-lab DOI (Agent.own_publication_dois)
bypasses both the per-thread abstracts_other cap check and its increment.
A bare PMID still counts (no DOI to match against — documented limit, design
§10). The single call site (simulation.py's _reply_to_thread tool_executor)
passes agent.own_publication_dois.

Also rewords the phase-4 own-paper injection in build_phase4_prompt from
"was authored by your own lab... Only continue toward a collaboration..." to
"cites a paper your own lab authored... Speak as its author... focus on what
remains unexploited beyond the published scope" — matches the interview
model's framing (the counterpart is the screening hub, not a co-author
candidate) rather than the retired collaboration-proposal language.

No import cycle: tools.py importing _extract_dois from agent.py is safe
since agent.py's own import graph never reaches tools.py at module level
(verified empirically both import orders + via simulation.py, which imports
both).

tests/unit/test_own_authored_papers.py's two reply-prompt-caution assertions
updated to the new wording (unavoidable side effect of the reword: they pin
the literal replaced string).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _run_turn no longer calls _phase2_scan_filter and no longer gates Phase 5
  on a phase2_ran flag; _phase2_scan_filter/_phase2_prune stay on disk
  (uncalled) per design §9, docstrings updated to say so.
- build_phase2_scan_prompt drops the ⚠️ SELF-AUTHORED injection (the
  builder itself, post fencing, and cites_own_paper are unaffected —
  cites_own_paper is still used by the phase-4 reply-prompt warning).
- New settings.lab_daily_post_cap (default 1): pi_lab agents get one pitch
  per day in _phase5_new_post's cap gate; other roles keep daily_post_cap.
- New tests/unit/test_phase2_guard.py pins both behaviors (source-inspection
  on _run_turn + a _drive()-style cap test for pi_lab vs scout_hub).
- Deletes the now-obsolete
  test_agent_turn_gm.py::test_phase2_scan_prompt_flags_self_authored_gm and
  its .ambr block; fixes three brief-unlisted collateral breakages this
  change caused (test_own_authored_papers.py's scan-prompt SELF-AUTHORED
  assertion, and missing lab_daily_post_cap in two other tests' settings
  stubs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-rejection deletion

The comment above previous_skips's capture still described the back-to-back
private-channel post rejection's bare += 1 as the reason for capturing the
pre-reset streak value — but Task 7 deleted that rejection block entirely.
Every remaining rejection path (unsupported action, post-type rejection,
body-mention rejection) already restores the true streak via
`previous_skips + 1`. Rewrite the comment to describe that, not code that no
longer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
Task 12 (branch-2 engine reconciliation). The reply action was retired in
Tasks 6/7, so a PI tagging a bot's channel post could no longer make the bot
join or reply to that thread — handle_channel_tag still promised exactly
that. Repurpose the tag as pitch-shaping context instead:

- build_phase5_prompt gains a keyword-only `pi_flagged` param; when set, it
  appends a `## Your PI flagged this` section (with the authoritative-
  direction sentence) after the rendered template.
- _phase5_new_post builds that string from the agent's current
  pi_priority interesting_posts entries, passes it, and consumes those
  entries once the turn's action resolves (new_post posted successfully, or
  skip) — same turn-scoped semantics as has_pi_directive. The has_pi_priority
  bypass of phase5_skip_probability is unchanged.
- handle_channel_tag's DM copy (both branches) and the two-agent branch's
  seeded pi_context now say the bot can't reply/join and will fold the note
  into its next pitch, instead of promising a reply or a new thread.

New tests/unit/test_pi_flag_flow.py (8 tests, RED->GREEN) covers heading
injection/omission, consume-once on both resolution paths, the new DM copy
for both handle_channel_tag branches, and the pi_priority skip-bypass
regression. tests/unit/test_doc_prompt_sync.py stays green (prompts/*.md
untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sentinel

The prior version of test_tagged_post_activates_exactly_one_thread_no_dupe_with_tag_loop
only checked key cardinality, which can't distinguish "guard skipped a
duplicate activation" from "guard missing, but the overwrite happened to
build an identical ThreadState." Pre-seed a sentinel ThreadState
(message_count=99, a value get_thread_message_count can't produce for this
thread) under the thread id and assert it survives untouched. Verified by
experiment: deleting the hub loop's `if thread_id in
agent.state.active_threads: continue` line makes this fail (sentinel
clobbered to message_count=1); restoring it passes again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add SimulationEngine._validate_star_topology(): for every pi_lab agent with a
non-None cohort gate, flag (a) any other pi_lab agent reachable directly
(labs may only be cohorted with the hub) and (b) no scout_hub agent reachable
at all (hub unreachable, pitch targets unsatisfiable). start() raises
RuntimeError with all violations joined right after the first
_recompute_allowed_sender_ids() call; the shared mid-run recompute path logs
each violation as an ERROR instead, since a live run must not crash on an
admin's transient cohort edit.

tests/integration/test_cohort_engine_live.py's only start()-driving isolated
test used a lab-to-lab cohort with no hub in the roster at all — exactly the
shape this validation now rejects. Gave it a star-shaped topology (a
scout_hub agent, {lab, hub} per lab) and added a sibling test asserting the
old shape now raises through the real start() path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… wording, consumption comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ahueb and others added 27 commits August 12, 2026 20:19
_default_system_prompt() (used only when prompts/agent-system.md and every
role override are missing from disk) still described the lab<->lab
collaboration mesh verbatim: "facilitate scientific collaboration by
engaging with other lab agents", "true complementarity", "concrete first
experiment", "DM other labs' PIs". None of that exists in the pitch-only
hub-and-spoke topology.

Replace it with a short pitch-era fallback, mirroring the shape of the
llm.py fallback fixed in 707b337: the agent represents its lab and pitches
its own ideas to BlackbirdBot (the scouting hub), never proposes
collaborations, and defers PI-intent questions (funding preference, equity
appetite) to the actual PI. Kept intentionally short -- this is an
emergency fallback, not the real prompt.

No test pins the old fallback text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…simulation

tests/unit/test_no_collaboration_residue.py only covered llm.py's private-
profile fallback and onboarding.py's default template. Extend it to the
three other places just fixed in this wave: the welcome email
(src/services/email.py), agent.py's emergency system-prompt fallback
(_default_system_prompt), and simulation.py's memory-synthesis prompt
(_update_agent_memory) -- each scanned for "collaboration opportunities",
"facilitate scientific collaboration", "re-engage to refine", and
"complementarity".

RED/GREEN demonstrated by swapping in the pre-fix (commit ce424f2) blobs
of all three files and re-running: 4 of the 12 new checks failed
(email.py's "re-engage to refine"; agent.py's "facilitate scientific
collaboration" and "complementarity"; simulation.py's "collaboration
opportunities"), confirming the guard catches the exact residue those
fixes removed. Restored the fixed files afterward; full file is green
again (17 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…FOA/grantbot mentions

test_full_run_live.py's `full_run` fixture put all three lab agents in one
shared cohort -- lab-to-lab, exactly the shape `_validate_star_topology`
(design doc §5) now fail-fasts on in `start()`. Add a scout_hub agent
(HUB_AGENT_ID = "blackbird", mirroring test_cohort_engine_live.py:1321's
`roles={"blackbird": "scout_hub"}` approach) and reshape the cohort setup
into one cohort per lab, each pairing the lab with the hub. The hub gets
a NullTransport, not a real Slack client -- there is no fourth probe bot
token provisioned (tests/conftest.py's _PROBE_BOTS is still su/cravatt/
wiseman), so this is the minimal edit that gets the hub into the engine's
roster without provisioning new credentials.

Limits, documented in the fixture docstring: this module's actual
scenario -- three real labs conversing directly to a lab-to-lab
:memo:/✅ handshake -- still assumes the retired mesh model (only a
scout_hub agent replies to top-level posts now), and several downstream
assertions (e.g. `expected_gate = set(AGENTS)`) still assume a full-mesh
gate. Rewriting the scenario is a much larger change this fix doesn't
attempt, and the whole module is gated on ANTHROPIC_API_KEY + live Slack
tokens so it cannot be run here to verify further. Verified `pytest
--co -q` still collects all 4 tests, and ruff is clean on the file.

prompts/daily_audit.md:28 dropped ` grantbot` from the compose logs
command (the service no longer exists).

prompts/pi-dm-classify.md:14-15 replaced two retired-capability examples
("Good catch on that FOA", "Summarize the funding opportunities") with
pitch-era equivalents. Not doc-embedded elsewhere (grepped docs/specs:
only a filename mention in a README's file list, no mirrored content),
so no sibling copy needed updating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-measured with the exact command the ratchet runs (\`ruff check src
--output-format=concise --quiet\`, no E902): 249 findings, down from the
260 ceiling. This wave's fixes (email.py, agent.py, simulation.py
rewrites) paid down some of the pre-existing debt; lock it in rather than
let it drift back up. Never raise this number to make a push pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reopen_proposal defaulted to enable_private_refinement=True: a reopened
proposal on a public-origin thread migrated into a NEW collab_private
channel before posting the PI's guidance there. The engine-side private-
channel collaboration/refinement flow was deleted (design doc §8 -- no
agent converses inside a collab_private channel anymore, hub-and-spoke
only), so a freshly migrated channel is a dead room nothing ever posts
in again. Per the "private-channel collaboration is out" ruling:

- src/config.py: enable_private_refinement default flipped True -> False.
  The flag itself is NOT removed -- src/services/email_inbound.py's
  separate (currently out-of-scope) inbound-email reply path still reads
  it, so it is not dead.
- src/routers/agent_page.py: reopen_proposal's collab_private migration
  branch and the "already private -> 501" branch are both deleted. The
  route now has exactly one path, unconditionally: post the PI's guidance
  into the proposal's origin thread (Slack if the agent has a token, the
  DB inbox otherwise), regardless of that thread's visibility. Legacy
  viewing/discovery code for EXISTING private channels
  (_sync_private_channels_from_db, the /message route's membership check,
  admin views) is untouched.
- templates/agent/dashboard.html: the reopen tab's copy no longer promises
  "the agents will continue the conversation with a fresh set of
  replies" -- that flow doesn't exist any more.

Rewrote every test that drove reopen and asserted on collab_private-
channel creation (tests/integration/test_agent_page.py's whole "private-
channel reopen route" section, tests/integration/test_proposal_review.py's
"Reopen -> private channel" section) to assert on the DB-inbox message /
Slack post the route now makes instead, and added dedicated tests pinning
that reopen never creates a collab_private channel. Also fixed a latent
hermeticity gap this rewrite exposed: `slack_globally_enabled`'s workspace-
wide auto-detect falls back to `Settings.get_slack_tokens()`, which is
keyed by real roster ids -- a dev host with live-tier credentials in its
`.env` (e.g. SLACK_BOT_TOKEN_WISEMAN) made these fictitious-agent tests
take the real-Slack branch and 500. Both files' Slack-off fixtures now
stub that out explicitly.

Gate: python -c "import src.routers.agent_page" passes; the three affected
test files (135 tests) pass in full against a real Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…design doc

Parallel design-coverage audit findings, all surgical:

- §6: "the turn skips" read as if an empty menu were a code-level bypass,
  same as blocked_for_regular's narrowed-empty case. It isn't -- the skip
  is LLM-mediated: _EMPTY_MENU renders skip-only text, phase 5 still
  calls the LLM, and the model itself returns {"action": "skip"}.
  Clarified and contrasted explicitly with the code-bypass case.
- §8: added a note that infrastructure/discovery code for channels that
  already exist (_sync_private_channels_from_db, the /message route's
  membership check, admin views) is retained by design -- only the flow
  that creates new state (seeding, collab_private finalization, and now
  reopen_proposal's migration, fix 9) was removed.
- §11: the quoted confidentiality-rule sentence was a drafted target, not
  the landed text (prompts/agent-system.md's actual wording differs).
  Annotated so a reader doesn't mistake the quote for a live pin -- the
  doc-sync test (test_doc_prompt_sync.py) covers the prompt file itself,
  not this design doc's quotation of it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts resolved deletion-wins: pi-dm-classify.md and pi-profile-rewrite.md
deleted (branch-2 edits superseded); hub role.toml takes branch-1's empty
post_types; the PI-flag paragraph removed from phase5-new-post.md and its doc
mirror. The token-contract test's covered-template list drops the four deleted
phase-2 templates and the deleted hub phase-5 template (builders die in Task 7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ction engine

Private-instructions removal (AUD-PRIV branch-2 layer):
- agent.py: delete private_profile property/_private_profile cache, the
  "## Your Private Instructions" system-prompt injection, update_private_profile,
  persist_private_profile_to_db, and the phase-4 pi_context injection; drop
  build_phase5_prompt's pi_flagged param/injection; own_publication_dois now
  derives from the public profile only (decision 2).
- llm.py: delete synthesize_private_profile + its fallback; profile_pipeline.py:
  delete step 9b (private-profile seed generation); profile_export.py: delete
  export_private_profile + PRIVATE_PROFILES_DIR.
- onboarding.py: delete the private-profile step (GET/POST
  /onboarding/private-profile) and its live/seed/disk/template fallback chain;
  relocate its completion side effects (onboarding_complete flip, welcome
  email, pending-invite/post-login-redirect resume) onto save_profile, now
  the terminal step. Verified RED (reverted the relocation, 4 tests failed)
  then GREEN (restored, full file green — 57 passed).
- Delete prompts/private-profile-synthesis.md and
  templates/onboarding/private_profile.html (now-orphaned readers).

PI-interaction removal (AUD-PI branch-2 layer):
- Delete src/agent/pi_handler.py (PIHandler) outright: DM handling, channel-tag
  handling, thread-conclusion DMs.
- simulation.py: remove PIHandler construction, _load_pi_mappings (+
  delegate_slack_ids fold-in), _seed_pi_dm_cursor, _poll_pi_dms,
  _poll_pi_dms_from_db, _poll_proposal_threads_for_pi, _handle_pi_inbound_entry,
  _check_pi_proposal_review, _reopen_thread, and notify_thread_conclusion
  callers. Rename _poll_slack_for_pi_messages -> _poll_slack_for_human_messages
  and strip its PI-specific branch (human messages are now ingested for
  history only, no action taken); _poll_inbound_from_db's is_bot=False branch
  likewise logs-and-ignores. has_pi_directive/pi_priority/PostRef.pi_context
  removed; phase-5 gate simplifies to has_new_work = has_interesting or
  has_phase4_work; skip/blocked bypasses drop their pi_priority carve-outs.
  ThreadState.pi_context/message_count_offset intentionally KEPT — still
  written by _sync_proposal_reviews_from_db (Task 6's scope).
- email.py: reword the welcome email to drop the "weigh in yourself" PI->bot
  implication (decision 7), keeping read-only Slack spectating.
- email_inbound.py::_handle_instruction: classify-and-ignore (decision 3) —
  no thread post, no channel migration, no review row; classification
  plumbing (classify_reply) kept.
- KEPT per decision 5: pi_inbox.py service, pi_dm_messages model/migrations,
  and src/routers/agent_page.py's private-profile editor + PI-DM/message web
  routes (out of this task's scope — see task-5-report.md's escalations).

Tests: delete test_pi_flag_flow.py and test_slack_pi_live.py; retarget
test_hub_budget_scheduler.py's TestPIHandlerAccounting (2 of the 24 known-red
failures) to TestPhase5CallAccounting, anchored on _phase5_new_post instead of
the deleted PIHandler; rewrite test_no_collaboration_residue.py's obsolete
section-list assertions into absence pins; fix autouse fixtures in
test_onboarding_flow.py/test_agent_page.py that referenced deleted
PRIVATE_PROFILES_DIR/synthesize_private_profile (would have zeroed both
files); scalpel PI-DM tests out of test_message_persistence.py and
test_cohort_engine_live.py; mechanical renames/cleanup across e2e and other
live-tier integration files.

Gate: tests/unit 22 failed / 1274 passed / 2 skipped (down from the 24
known-red baseline — all 22 remaining are Task 6's hub-reply-only territory,
none introduced by this task). ruff: test-suite 0 findings, src/ 240 (ceiling
249, was 249 pre-task). Full tests/ collection (1972 tests) has zero import
errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…cation (Option A)

scout_hub is now hard-gated out of _phase5_new_post (decision 9): the hub's
:mag: Opportunity Assessment is extracted from, and stripped out of, its own
Phase-4 CONCLUDE reply instead of filed as a separate top-level post
(SimulationEngine._reply_to_thread/_capture_hub_assessment). Deletes
TERMINAL_POST_TYPES, available_for's terminal_only narrowing, the
blocked-narrowing plumbing in _phase5_new_post, unreviewed-proposal blocking
+ _sync_proposal_reviews_from_db (nothing on this branch creates a proposal
to review anymore; models/admin stay), and CANONICAL's opportunity_assessment
entry (CANONICAL == {pitch}). Labs at active_thread_threshold now skip
Phase 5 outright, pre-LLM, like the daily-post cap.

Rewrites/deletes the 22 known-red tests this left behind
(test_phase5_terminal_posts.py, test_role_menus.py,
test_post_type_enforcement.py hub cases, test_agent_prompts.py,
test_roles.py) plus the collateral breakage from the CANONICAL/
available_for signature changes (test_post_types.py, test_roles.py's
generic parse_post_types tests, test_hub_budget_scheduler.py's tick-IO
list, test_phase2_guard.py's now-inverted direct-call assertion). Adds new
pins for the Option A relocation: a concluding hub reply with a sidecar
produces an OpportunityAssessment row with the post stripped, a malformed
sidecar still posts with no row and a logged error, and a persistence
failure never crashes the reply — at both the unit level
(test_simulation_logic.py) and the integration level
(test_opportunity_assessment_persistence.py, rewritten from the deleted
Phase-5 driver to a new _reply_to_thread driver).

tests/unit: 1288 passed, 2 skipped (0 failed, was 22).
tests/integration: 484 passed, 1 skipped-irrelevant pre-existing failure
(test_cohort_engine_live.py, confirmed present on the unmodified base commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…_posts cascade

Phase 2 (scan/filter) has been dead code since 7806c1a (_run_turn never called
it) and Task 5 removed its last field writer (pi_handler). Delete it outright:
_phase2_scan_filter/_phase2_prune (simulation.py), build_phase2_scan_prompt/
build_phase2_prune_prompt/build_scan_system_prompt (agent.py), and
interesting_posts_cap (config.py).

Cascade every remaining reader of AgentState.interesting_posts to zero: the
phase-5 available_posts filter loop, its interesting_posts swap-and-restore,
and the private/public visibility resolution it fed (all downstream of a
field with zero writers) collapse to a plain cap -> threshold -> prompt ->
new_post/skip Phase 5; the two pruning sites (_evict_dead_thread,
_apply_cohort_gate_to_state) drop their interesting_posts half; the
AgentState.interesting_posts field and the now-fully-orphaned PostRef
dataclass are deleted from state.py.

Tests: test_phase2_guard.py replaced by test_lab_daily_post_cap.py (its
phase-2 source-inspection pin is vacuous once the function doesn't exist; the
daily-cap tests it also held survive under an accurate name).
test_cohort_isolation.py::TestStatePruning, test_cohort_real_llm.py's four
phase-2-driven real-model tests, and test_cohort_scenarios.py's
interesting_senders-based test deleted with rationale (each traced to
confirm the invariant it protected is still pinned elsewhere, deterministically,
where one exists). test_cohort_engine_live.py's test_phase2_prompt_omits_non_cohort_posts
(the one pre-existing tests/integration failure on this branch) deleted and
re-pinned on the one surviving production call site of the same gated read
(the scout_hub auto-activation branch of Phase 3).

Gate: tests/unit 1283 passed/2 skipped/0 failed (was 1288/2/0 — net -5
deliberate removals); tests/integration (full, real_llm excluded) 538
passed/0 failed (was 484/1/72 — the one failure is gone); characterization
10 failed/57 passed (was 11/57, net -1, remainder pre-existing per the
removal-cycle's own "stays red mid-cycle" constraint). grep for
interesting_posts/phase2/_phase2/scan_system_prompt in src/ is empty. ruff
src/ 240 findings, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
test_scout_hub_never_reaches_the_llm_in_phase_5_even_when_saturated used the
production active_thread_threshold (12) against a 65-thread fixture, so the
generic backpressure check tripped first and the test passed even with the
scout_hub role gate deleted. Raise the threshold to 1000 so only the role
gate can explain the skip; verified by temporarily neutering the gate
(test fails) and restoring it (test passes again).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…rdict

_capture_hub_assessment already warns when an <assessment_json> sidecar is
present but broken; it stays silent when the tag is absent, correctly, since
that's the ordinary case on ~11 of every 12 interview turns. That silence
hid a real gap at the one turn where thread_guidance.py's CONCLUDE branch
requires the hub to either decline (⏸️) or close with a verdict carrying the
sidecar: a reply that does neither produced nothing persistable with no
trace anywhere.

_warn_if_hub_conclude_missing_assessment fires only when the thread is at
thread_guidance's own CONCLUDE point (delegating to phase4_guidance rather
than re-deriving the cutoff from settings.max_thread_messages, since the two
can diverge), the reply does not open with the ⏸️ decline convention, and no
assessment_json tag is present at all. Added a 4-case boundary matrix
covering fire/silent on both sides of each condition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
There was no direct test of email_inbound._handle_instruction at all —
the removal cycle's decision 5 retired every human-PI-to-bot interaction
surface, leaving the function a documented classify-and-ignore no-op, but
nothing enforced that. Drive it directly against real DB fixtures and
assert it returns False, writes no ProposalReview row, writes nothing to
either message store (AgentMessage or PiDmMessage), and logs the ignore
line naming the thread. Verified the test catches a regression (a row
write + True return) before reverting the probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…ount

_reply_to_thread computed thread.message_count as the count of messages
already in the thread and passed it straight to phase4_guidance, whose
documented contract is the ORDINAL of the message being written ("This is
message 12", not "message 11"). Combined with the system-enforced-close
check intercepting at the same prior-count >= max_thread_messages (before
any reply is generated), the CONCLUDE branch was unreachable under the
default config: a reply only ever generates at prior-count <= 11 (DECIDE at
most), and prior-count >= 12 silently closes the thread as a "timeout" with
no verdict. The documented system-enforced concluding reply (verdict +
sidecar when warranted) never executed.

Fix: Agent.build_phase4_prompt now feeds phase4_guidance (and the prompt's
{message_count} substitution, for internal consistency) thread.message_count
+ 1. _warn_if_hub_conclude_missing_assessment's own phase4_guidance call gets
the same correction, since it must agree with the guidance the reply it is
inspecting was actually generated under. The close check itself is
unchanged — it is correctly a check on the prior count — but now documents
the latent coupling between settings.max_thread_messages and
thread_guidance's hardcoded ordinal boundaries.

Added an end-to-end regression pin (TestPhase4OrdinalGuidance) driving a
real (non-mocked) Agent.build_phase4_prompt through a real
SimulationEngine._reply_to_thread call: a thread at prior-count 11 now
receives MUST-CONCLUDE guidance and posts; a thread at prior-count 12 closes
without ever reaching the LLM. Verified RED against the pre-fix code (via
git stash) and GREEN after.

phase4_guidance itself, and every test that calls it directly with an
explicit ordinal (test_thread_guidance.py, test_doc_prompt_sync.py) or drives
build_phase4_prompt without asserting on phase category text, are unaffected
and intentionally untouched. The GM/characterization snapshot tier is
expected to need regeneration (tracked separately) and is out of scope here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
TestHubConcludeMissingAssessmentWarning previously had to inflate
max_thread_messages to 20 to make a CONCLUDE-guided reply reachable at all
— a symptom of the ordinal off-by-one just fixed. Now that
Agent.build_phase4_prompt and _warn_if_hub_conclude_missing_assessment both
use the reply's ordinal, a thread with 11 existing messages reaches the
warning's CONCLUDE check as a genuinely real reply under DEFAULT settings
(11 < max_thread_messages=12, ordinal 12 -> CONCLUDE). Dropped the
threshold-inflation fixture entirely; the 4-case boundary matrix (fires /
silent x each condition) now runs against the real default configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…ent warning's off-by-one

Nine (now ten — grep found one more hyphenated "Phase-2 backlog" mention)
comments across transport.py, slack_client.py, and simulation.py still
described Phase 2 as live after prior tasks removed it outright. Reworded
each to describe what actually consumes get_new_top_level_posts today (the
hub's Phase 3 auto-activation scan) or dropped the phase reference where the
comment was really about a historical bug.

_VALID_GATING_STATES pointed at the deleted phase5-new-post.md; repointed at
the relocated <assessment_json> skeleton in phase4-thread-reply.md.

_warn_if_hub_conclude_missing_assessment logged thread.message_count (the
prior count) while classifying by the ordinal (message_count + 1) — the same
off-by-one 55822a4 fixed for build_phase4_prompt, just missed here. Now logs
message_ordinal. Added an assertion pinning the corrected log line, plus a
new real-path test (test_agent_prompts.py) driving build_phase4_prompt itself
at prior-count 4 to pin the shifted EXPLORE/DECIDE boundary (ordinal 5 ->
DECIDE), which was untested at the engine-wiring level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…es and the migration service they and email once gated

Web dashboard dead ends whose sources were already deleted:
- agent_page.py: the private-profile view/edit/save routes (nothing generates
  or reads a private profile anymore; agent.py's private_profile property and
  llm.py's synthesize_private_profile were already gone) and the PI-web-DM
  route (its only reader, pi_handler.py, was already gone). Removed their
  templates (agent/profile.html; the DM section of agent/conversations.html;
  the "Agent Instructions" card of agent/dashboard.html and the matching
  cross-link in profile/edit.html, which pointed at the now-deleted route).
  Delegate account/dashboard access and all admin/history views are
  untouched.
- src/services/private_channels.py (the public-thread -> collab_private
  channel migration service): zero production callers remain — the web
  reopen route stopped calling it in an earlier fix, and it was the sole
  consumer of AgentSlackClient's create_private_channel/invite_to_channel
  beyond direct unit coverage. Deleted outright, along with the
  enable_private_refinement setting that gated it (config.py) — orphaned
  once neither the reopen route nor email_inbound.py read it any longer
  (verified: zero references in email_inbound.py despite config.py's own
  comment claiming otherwise).
- config.py's unreviewed_proposal_block_count: unreferenced in src/ since an
  earlier task deleted the unreviewed-proposal-blocking mechanism from
  _phase5_new_post; removed the setting and the inert kwargs test fixtures
  were still passing it.

Test fallout: deleted test_private_channel_migration.py's dead-service
coverage, keeping and renaming (to test_slack_private_channel_creation.py)
the parts that test AgentSlackClient's own capabilities rather than the
deleted service; deleted test_slack_private_live.py (live-Slack coverage of
the same deleted service — its module-level import would otherwise break
collection); deleted test_message_persistence.py's migration-flow tests;
deleted test_slack_client_contract.py's pin on the deleted _MAX_POST_CHARS;
updated test_agent_page.py's Ep table (20 -> 16 endpoints) and stale
docstrings referencing the deleted setting/module.

Updated tests/integration/test_onboarding_flow.py's docstring, which still
credited agent_page.py as a current writer of private_profile_md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
… reply-only hub

email_inbound.py::_send_instruction_confirmation was unreachable: the caller
only invoked it when _handle_instruction returned reopened=True, and
_handle_instruction (the removal cycle's classify-and-log-ignore no-op) always
returns False. Deleted the function and the dead conditional around its call
site; the "already emailed the PI an explanation" comment there was also
false (_handle_instruction only logs) and is corrected.

email.py's welcome text still told PIs BlackbirdBot "posts an Opportunity
Assessment to the thread" on reaching a verdict — describing the retired
standalone top-level post. Reworded (plain-text and HTML bodies) to match the
reply-only hub: the verdict is stated directly in BlackbirdBot's concluding
reply in the same thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…xt; fix a stale docstring

ThreadState.pi_context had zero readers or writers anywhere in src/ or
tests/ — its only writer (a web-guidance reopen path) and only reader
(Agent._compose_system_prompt's phase-4 injection) were both already deleted;
its own comment flagged dropping the field as "a separate cleanup." Dropped
it; message_count_offset (same comment, same former writer) stays since
simulation.py still reads it.

pi_inbox.py::record_pi_message's docstring claimed the inbound poller
"routes it through PI handling (proposal-review clear, thread reopen,
pi_context, @bot tags)" — none of that exists any more (confirmed against
_poll_inbound_from_db's own, already-accurate docstring). Corrected.

Extended test_doc_prompt_sync.py's _FORBIDDEN prompt-phrase guard with four
phrases from this cycle's four removals ("your pi flagged", "private
instructions", "dm rules", "phase 2") — verified zero current hits in
prompts/, so this is a pure regression guard, not a fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…gnals; fix CLAUDE.md's private-instructions/hub-posting claims

CLAUDE.md's BlackbirdBot section still described the rubric as loaded from
profiles/private/blackbird.md and injected under a "## Your Private
Instructions" header (deleted this cycle — Agent._compose_system_prompt no
longer does this, and nothing reads profiles/private/{agent_id}.md), and
described BlackbirdBot posting a standalone ":mag: Opportunity Assessment"
(also retired — the hub is reply-only; the sidecar rides inside its
CONCLUDING reply). Rewrote both, and pointed at the deploy checklist's new
archive-and-diff step for the untracked rubric file.

Appended an addendum to the pr34-pitch-only-reconciliation-design.md doc
recording the removal cycle's four removals (private instructions,
reply-only hub / assessment relocation, human-PI interaction, phase-2
prompts), its ten locked decisions, and the ordinal fix as an
audit-discovered correction landed alongside them (dated) — superseding the
corresponding sections of that earlier design without erasing them.

Amended the deploy checklist:
- §4 (host-file hygiene) rewritten from "delete the stale rubric file" to
  "archive-and-diff it BEFORE deploy" — the old rationale (private_profile
  caches it once at startup; deleting without replacing leaves no rubric)
  no longer applies since nothing reads the file at runtime, but a new risk
  does: it may hold rubric content never migrated into the tracked prompt.
- §3a's purge rationale updated — the setting that used to make unreviewed
  proposals block Phase 5 is deleted, so purging them is tidiness now, not
  unblocking.
- §7 (verification): 7.2/7.3 tightened to check the hub's reply carries
  thread_ts NOT NULL and the assessment persists from that reply with the
  sidecar stripped (not a top-level post); 7.5's phase-2 line-number
  references replaced with "deleted outright, no code path left"; added 7.7
  (zero hub top-level posts, run-wide), 7.8 (a cap-reaching interview ends
  with a CONCLUDE reply, not a bare timeout — the ordinal-fix regression
  check), and 7.9 (zero PI flows: no acted-on PI DM, no new collab_private
  channel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…le (single reviewed diff for andrewsu)

Starting state: 10 failed, 57 passed — no collection errors (nothing in this
tier imports the deleted synthesize_private_profile by name), just stale
snapshots/fixture drift:

- test_agent_turn_gm.py (4 failed): pure prompt-content drift from Task 1
  (dc903ff, private-instructions/DM-rule/PI-flag Core Rules removal) and
  Task 2 (93702e6, hub reply-only + assessment-sidecar relocation into Phase
  4 CONCLUDE), plus the ordinal fix (55822a4).
- test_profile_pipeline_gm.py (6 failed): run_profile_pipeline no longer
  calls the deleted synthesize_private_profile, so it makes at most 2 LLM
  calls (public synthesis + one retry), never a 3rd "private seed" call. The
  shared FakeAnthropic fixture and five tests' scripts still scripted that
  extra response; on multi-call tests it got consumed as a bogus next public
  call (plain markdown failing JSON parsing), throwing off llm_calls_total /
  rejected_in_progress relative to the pinned snapshot.

Test-side fixes only, no test deleted (none of the brief's other-listed
casualties — phase-2/PostRef fixtures, a surviving hub phase-5 GM, a PI-flag
GM — exist in this tier):

- test_phase4_prompt_phase_progression_gm: build_phase4_prompt feeds
  phase4_guidance the ordinal (message_count + 1), so the fixture's prior
  counts 2/8/12 now produce ordinals 3/9/13 instead of the canonical
  EXPLORE/DECIDE/MUST_CONCLUDE examples (2/8/12) the test pins. Shifted the
  fixture's prior counts to 1/7/11.
- test_profile_pipeline_gm.py: dropped the dead _PRIVATE_SEED constant and
  every script referencing it (module docstring, _install_fakes, and 9 of 10
  tests); trimmed each multi-call test's script to the number of calls
  run_profile_pipeline actually makes now; adjusted the corresponding
  llm_calls_total assertions/snapshots; dropped the now-meaningless
  seed_set_after_first_run/seed_unchanged_on_rerun keys from the rerun test.
  private_profile_md/private_profile_seed stay pinned as None in the golden
  master (decision 5 kept the DB columns; nothing writes them anymore).

Regenerated once (--snapshot-update), verified green (67 passed, 16 snapshot
blocks), then reviewed the .ambr diff block-by-block (split by "# name:",
diffed independently): 0 blocks added/removed, 9 changed (4 in
test_agent_turn_gm.ambr, 5 in test_profile_pipeline_gm.ambr), 7 unchanged.
Every changed block traces to Task 1 (dc903ff), Task 2 (93702e6), the
ordinal fix (55822a4), or this task's own message_count fixture shift —
including one block (pubmed_outage_on_rerun_keeps_the_grounded_profile) that
came out byte-identical to its pre-cycle value once the fixture-consumption
bug was fixed, confirming that failure was a pure fixture artifact and not a
real behavior change. No unexplained hunk. No src/ change. No real_llm test
was run.

Full per-block accounting: .superpowers/sdd/2026-08-12-removal-cycle/task-9-report.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
… src lint to 231

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
Deletes the remaining human-PI-to-bot write surfaces the audit found still
live: the web dashboard's post_agent_message and connect_slack routes
(agent_page.py), reopen_proposal's Slack-post branch (DB inbox is now the
only, unconditional path), and pi_inbox.record_pi_dm/pi_may_post_to_channel
(orphaned once their only callers were gone). SimulationEngine's
_poll_slack_for_human_messages is renamed to _poll_slack_for_bot_messages and
loses its human branch outright — a human Slack message is no longer
ingested at all.

Closes the trigger loop those write paths used to feed: every GATED
MessageLog read (get_new_top_level_posts/get_replies_to_agent_posts/
get_tags_for_agent/has_new_reply_from_other) now filters out human-authored
(is_bot=False) entries unconditionally, independent of the cohort gate.
Before this, the surviving is_bot=False producer could still have set a
bot's has_pending_reply, granted reactive priority, or — via
_infer_agent_id's substring match (e.g. "Andrew Su (PI)" contains agent_id
"su") — fabricated a Phase-3 thread activation misattributed to a bot that
never posted anything. _entry_allowed's own human-bypass clause is left
untouched (it is a general cohort-gate primitive with its own permanent test
suite); the new invariant lives one layer up, in the four GATED methods
themselves, since it also has to hold when the cohort gate is off by
default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…pipeline (I1)

tests/integration/test_profile_pipeline_live.py asserted the pre-removal
shape: private_profile_seed non-empty, probe.private_calls == 1, and
"exactly two LLM calls" per happy-path run. synthesize_private_profile
(former step 9b) was deleted outright in the 2026-08-12 PI-interaction
removal cycle, and the golden masters already reflect that (private_profile_
seed/private_profile_md always None, one LLM call per run) — this file did
not follow.

Flips every private_profile_seed/private_calls assertion to match the
provable constant (None / 0), rewrites failure text to name the deletion
instead of a function that no longer exists, and removes the T4.2/T4.5
seed_set_after_first_run/seed_unchanged_on_rerun keys entirely — GM #4's
snapshot no longer carries either claim, so there was nothing live left to
reconcile them against. T4.2's seed comparison, a None == None tautology
with no surviving subject, is now a direct, honest pin instead.

File still collects cleanly (5 tests); never executed (real_llm + live_api,
skipif on ANTHROPIC_API_KEY which is unset here).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
…set dead code (M1-M5)

M1: config.py's daily_post_cap is deleted — pi_lab and scout_hub are the
only two roles that exist, scout_hub is hard-gated out of _phase5_new_post
before the cap check, so the else-branch of the settings.lab_daily_post_cap
/settings.daily_post_cap ternary was provably unreachable. Drops the
now-dead kwarg from 6 test stubs.

M2: state.py's message_count_offset (permanently 0 — its only writer was
deleted in an earlier removal-cycle commit) and its 12-line dead-code
comment are deleted; the no-op "- thread.message_count_offset" subtraction
in _reply_to_thread is removed. Characterization suite re-run and confirmed
unchanged (67 passed, 16/16 snapshots) — no golden master shifted.

M3: profile.py's private_profile_md column comment updated to say what is
actually true (retired, kept, no writers) instead of describing a write path
(agent via PI DM) that no longer exists.

M4: both prompt-set docs' §4 intro line corrected to "chosen by the ordinal
of the reply being written" — matching thread_guidance.phase4_guidance's
actual contract (Agent.build_phase4_prompt already feeds it the ordinal, not
the prior count). Doc-only prose outside the fenced blocks the doc-sync test
parses; that suite re-run and confirmed still green.

M5: thread_guidance.py's docstring no longer claims both roles' strings are
pinned by the golden master — only pi_lab's are (the GM never drives
scout_hub at all); scout_hub's doc-sync coverage is
test_doc_prompt_sync.py's own parametrized test, named explicitly now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
… decision 5

The prior commit's is_bot filter on the four GATED MessageLog reads was
over-broad: it also blocked get_new_top_level_posts/get_replies_to_agent_posts
/get_tags_for_agent, which decision 5 requires to keep returning human rows
for history/observability (pi_inbox views, ingestion-completeness reads).
Caught by tests/integration/test_cohort_engine_live.py::
test_db_ingestion_is_complete_and_reads_are_per_agent, which pins exactly
that contract and regressed at the prior commit.

Reverts the filter on those three methods (docstrings restated to explain
why: they are general-purpose per-agent reads, not activation-only, even
though their only real caller today is Phase 3). has_new_reply_from_other
keeps its filter unconditionally — it is the one GATED method whose entire
job is driving bot behavior (pending state, reactive priority), with no
other caller. The activation-inert half of decision 5 (a human row must
never activate a thread) moves to the actual point of activation instead:
SimulationEngine._phase3_activate_threads now filters is_bot directly in
each of its three loops (tag, reply, hub auto-activation).

Test fallout: reverted test_top_level_posts_filtered/test_tags_filtered/
test_partner_visible_in_pi_created_private_channel (test_cohort_isolation.py)
to their original human-inclusive assertions; deleted the three message-log-
level activation-exclusion tests added by the over-broad fix
(test_human_top_level_post_never_returned_even_with_the_gate_off,
test_human_reply_to_own_post_never_returned,
test_human_tag_never_activates_via_a_bot_name_substring) — the same
behavioral guarantee is already covered at the correct layer by
tests/unit/test_hub_auto_activation.py's engine-level tests (unchanged,
still passing, since they drive _phase3_activate_threads end to end and
that function still filters is_bot). has_new_reply_from_other's tests are
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oXRXEdWDcpitGYT3Y5hUP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant