Skip to content

Post-meeting execution + merge onto main - #3

Open
pgoel813 wants to merge 53 commits into
mainfrom
feat/doc04-112-workroom-dispatch
Open

Post-meeting execution + merge onto main#3
pgoel813 wants to merge 53 commits into
mainfrom
feat/doc04-112-workroom-dispatch

Conversation

@pgoel813

@pgoel813 pgoel813 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Post-meeting execution (Doc 07) + merge onto main

52 commits, 66 files, +12,272/−15 against main. Implements Doc 07's post-meeting execution
pipeline (B1→B8), lands Doc 04 §112's Workroom dispatch wrapper, and merges main's service
restructure back in.

Read the "Known gaps" section before approving. Several pieces are deliberately unwired,
and one architectural decision is left open for you rather than made here.


What this adds

The B1→B8 pipeline (services/control-plane/src/control_plane/post_meeting/):
extract → triage → clarify → plan → approval gate → dispatch → report → final gate.
Tiering is model judgment (Law 4); code owns only the §3.1 floor — if the model claims the
draft tier while reporting an unmet condition, code drops a tier. Nothing ever raises a tier
the model did not ask for.

Four migrations, 00100013: post_meeting_tasks, clarify_items, the
staged_drafts.status CHECK constraint, and post_meeting_tasks.planned_at.

Doc 04 §112: the dispatch_workroom tool wrapper, the completion callback, and the
post-meeting completion sink.

Three seams: close→intake (SEAM 1), plan approval (SEAM 2), accept/request-changes
(SEAM 3).

The merge

main dissolved the services/harness member and cut the live path over to
services/in-meeting, deleting the RunLoop spine and the old live brain. All 18 relocations
were taken from git's own rename detection rather than moved by hand — each came back as
CONFLICT (file location) with a suggested destination and no content conflict.

Four judgement calls, each recorded in the merge commit:

  • config/defaults.toml — both sections kept. The rejoin_* bare keys are placed
    before the [post_meeting] header; after it they would have been silently reparented
    into our section. Verified by parsing the result.
  • live_brain.py — resolved as a delete. Git's modify/delete default left our version
    in the tree; keeping it would have resurrected a module retired deliberately.
  • app.py — hand-merged last, after the package underneath was in place.
  • The live dispatch path is a dead end — see below.

Known gaps — please read

1. The live in-meeting dispatch path is dead, and this PR does not decide its replacement.

Our §112 work re-woke Proxy by putting the terminal Envelope on run_loop.queue. There is
no run loop. live_sink and the tool mount move, unwired and imported by nothing, to
control_plane/live_dispatch_deadend.py; their nine tests skip at module level.

in_meeting.trigger.EngagementTrigger.on_worker_done is the structural replacement seam — the
same concept, different payload. But choosing between our durable operation_runs claim
(recoverable, cross-process exclusion via the partial unique index) and the warm in-meeting
sandbox toolbelt
(faster, no run row, no reaper story) is a founder decision. Porting the
code would have made that choice silently. SessionDriver has no production caller on either
side.

2. The plan-approval route is still not mounted. The blocker changed: it used to be that
§112 did not exist, and every approval produced a permanently orphaned task. §112 now exists
and make_plan_dispatcher supplies a real dispatch=. What is missing is proof — the
end-to-end tests through the route against real Postgres were never written. The mount is one
line and goes in once they are green.

3. estimate_cost has no production supplier. run_dispatch(estimate_cost=None) skips
the §3.5 pre-dispatch cost gate entirely, so task_cost_ceiling is never consulted and
AC-PME-12 does not fire in production. Same shape as the SEAM 1 defect fixed in this PR.
Reported, not fixed — flagging rather than expanding scope.

4. The entire B7 report block has no production caller. build_report,
select_channel, build_draft_card, and deliver are reachable only from tests.

The defect class this PR is mostly about

Three separate wires in this branch looked connected, were exercised only by tests that
supplied the missing piece themselves, and did nothing in production. The last one is fixed
here:

SEAM 1 was dead. The call site had existed since B1 — scribe_runtime calls
_run_post_meeting_intake right after run_close_pass returns. Nothing ever set
CloseConfig.post_meeting_intake. So on every production close the hook was None, the seam
returned at its first line, and run_extract / run_triage / run_clarify / run_plan had
no production caller at all. Every action item in every meeting silently failed to become
a task. test_seam1_close_intake.py passed throughout — every one of its cases constructs the
hook itself.

Fixed three ways: a real supplier (make_intake_hook(db), wired at the only production
construction site, resolving tenant server-side from the meeting row); a loud ERROR on a
missing hook naming the consequence and the fix, replacing a silent return; and a test that
injects nothing and drives the real chain into real Postgres.

That test runs the production builder in a subprocess, which is load-bearing. settings
parses env once at import and caches it, so importing the real boot module in-process under
the fake env its gate demands poisons that cache. The first draft did exactly that and flipped
two genuinely failing doc04 boot tests to passing — doc07 + doc04 reported 529 passed while
doc04 alone reported 2 failed.

Migrations

Both branches independently added a 0009 whose down_revision was
0008_substrate_schema_gaps, giving 0008 two children and alembic two heads. Git cannot
see this
— the filenames differ, so the merge succeeds silently and the breakage surfaces
later as alembic upgrade head refusing to run. A double head is not a merge conflict; it is
a clean merge that produces an un-migratable tree.

Ours re-parents onto 0009_repo_maps and renumbers to 00100013. Verified in the merged
tree via the CLI: alembic heads → one; upgrade head clean on a fresh database, with both
branches' tables coexisting and each migration's effects present, not just the version stamp.

test_b8_final_gate now locates migrations by suffix glob rather than by number — pinning
the digits meant a pure re-parenting broke a test that has nothing to do with ordering.

Test results

Every suite, each side against its own schema. (An earlier sweep of mine ran main
against a merged-schema database and reported 31 phantom substrate failures; these numbers are
the corrected ones.)

suite main this branch drift
doc00 14F / 197P 14F / 197P none
doc01 3F / 13P 3F / 13P none
doc02 11F / 241P 11F / 241P none
doc03 1F / 412P 1F / 412P none
doc04 9F / 163P 9F / 188P same 9 ids, +25 passing
doc05 8F / 305P 8F / 305P none
doc07 absent 269 passed entirely new
doc08 4F / 247P 4F / 246P none (+2 flaky)
e2e 6F / 26P 6F / 26P none
eval 2F / 22P 2F / 22P none
code_intel 18F / 2P 18F / 2P none
hooks / reality 7P / 2P 7P / 2P none
scripts 4F / 23P 4F / 23P none
security 11P 11P none

Zero drift attributable to this branch. doc07 is 269/269 against real Postgres.

The 9 doc04 failures are inherited, not introduced — the same 9 test ids fail on
untouched main. All 9 fail on MeetingRuntime.run_loop, an attribute main's delete-wave
removed while leaving the tests referencing it. Worth fixing on main separately.

test_connect_page.py in doc08 is flaky: two consecutive runs of the identical tree gave 4F
and 6F. The stable set is 4, on both branches.

Not touched

docs/07* and acceptance/doc07/ are byte-identical to their pre-merge state. The four
deleted v2-* docs and the doc03 criteria edit come from main, verified absent there rather
than assumed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added post-meeting execution workflows for extracting, triaging, clarifying, planning, approving, and dispatching action items.
    • Added named-human approval gates, ownership checks, concurrency limits, cost controls, and plan expiry handling.
    • Added outcome reporting with channel selection and draft-card fallback.
    • Added staged draft validation that prevents repository pushes and pull requests.
    • Added structured AI output handling with validation and cost reporting.
  • Documentation

    • Added Doc 06 and Doc 07 specifications, acceptance criteria, dependency manifests, and implementation guidance.
  • Bug Fixes

    • Improved close-flow resilience so post-meeting processing failures do not block meeting completion.

pgoel813 and others added 30 commits July 27, 2026 10:34
Doc 06 (Proactive) and Doc 07 (Post-Meeting Execution) are frozen as written;
this places them on disk so they cannot be lost. AMENDMENTS-06-07.md lands
alongside as the landing record. No prose in either doc is altered.

Doc 06 is SPEC'D, not built — only the register/CANONICAL patches that point
at it follow (P1, P3, P4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…KROOM)

Doc 05 used `needs_review` for two different fields. Only the draft-side use
was wrong: the `staged_drafts` row enum is `proposed|accepted|rejected|applied`
(CANONICAL §4 line 125), and Doc 04 §3.16.1 already reads that row as
`status='proposed'`. Doc 05's prose contradicted both, independent of 06/07.

Fixed exactly three draft-side sites: lines 56, 309, 333.

Left untouched — the envelope `status=needs_review` is a valid EnvelopeStatus
(CANONICAL §1.2): lines 264 (§3.7 first), 293 (§3.7 third), 358 (§3.12), 404.

FLAGGED, NOT FIXED: line 368 is a FOURTH draft-side site carrying the same
defect (propose_change 'returns a draft_id with status=needs_review'). The
amendment pack names only 56/309/333 and this patch was scoped to those three.
Line 368 still contradicts CANONICAL §4 and needs a follow-up decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…M-ADOPTION)

PLATFORM-ADOPTION line 389 still told a reader to build a flags table for
`durable_meeting_sessions` and `proactive_enabled`. Doc 00 section 7 cut the
table for V0 and both named flags no longer exist.

This matters beyond tidiness: doc00's sealed criteria bundle asserts the
ABSENCE of `proactive_enabled` in libs/ and services/ (verified this pass --
grep returns zero). A builder following line 389 would break a sealed oracle.

PLATFORM-ADOPTION is a catalog, not a spec, so the bullet is struck through
and annotated rather than deleted, preserving provenance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oved)

Founder call F1 is APPROVED: reuse the vacated 07 slot rather than renumber.
The reuse is now explicit in the register, in one place, once -- silent reuse
was the only bad option.

Kept verbatim: the 2026-07-16 Close & Trace cut sentence. That cut stands and
is NOT reopened; Doc 07 says so itself.

Moved out of the deferred list into 07-POST-MEETING-EXECUTION.md:
  - staged-drafts approval bundle
  - post-meeting pings

Left deferred with no owner (deliberately NOT in Doc 07):
  - formatted show-your-work trace
  - decisions to index write-back (cross-meeting memory)

Build order for the slot: V1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng it

Two pointer-only patches, no founder call required. Together they remove the
two-live-copies drift that placing Doc 06 on disk would otherwise create.

P1 -- Doc 06 register row: status becomes "V1 -- SPEC'D in 06-PROACTIVE.md
(pure consumer of Docs 01-05; no re-pathing)". The 2026-07-16 cut date is
kept: that cut is precisely why the doc is V1. Marked SPEC'D, not built --
Doc 06 is not a current build target.

P3 -- the "DEFERRED DESIGN -- Proactive" section: the parallel design prose is
deleted and replaced with a pointer at the doc. The hooks list is KEPT
verbatim; all four V0 hooks were verified present this pass:
  - Doc 03 material-change emitter: libs/contracts/material_change.py,
    services/scribe/events.py
  - Doc 04 delivery priority rules: written generically, unchanged
  - Workroom read-tier entry: workroom/agent_config.py dispositions
    (quick/plan/critic/verifier/worker)
  - Doc 02 tile has-something signal: libs/contracts/capabilities.py

Two live descriptions of one design is the drift that produced four review
cycles. One wins; the other points.

No change needed at register line 74: it already reads off/semi/lead, which
matches Doc 06 section 3.4 exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New section 13, placed before the conformance rule so that rule still closes
the file. Seven scope decisions, none of which existed on the file before
(verified: zero D06/D07 hits pre-patch).

D07.1-D07.3 are the Doc 07 build boundary: one operation_runs row and no
second run table; no execution before a named human approves; staged drafts
only, never a push.

D06.1-D06.4 land as well even though Doc 06 is NOT being built. They are
scope records -- true whether or not the doc ships, cheap to keep, expensive
to re-derive -- and the section says so explicitly.

D07.1 is written to name the workroom_tasks prohibition (section 12.11) by
hand, because post_meeting_tasks is the exact shape a future reader would
mistake for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…seeds

Phase C step 1. Format template is acceptance/doc03/ -- the newest sealed
bundle and the only one carrying dependency_manifest.yaml (GENERATOR.md
section 8.4.3), so it is the current shape, not doc00/doc02 as first assumed.

  acceptance/doc07/requirements/requirements.yaml  16 requirements
  acceptance/doc07/criteria/criteria.yaml          16 criteria
  acceptance/doc07/dependency_manifest.yaml        2 classes + 4 golden paths

One criterion per seed. None merged, none split, none invented. Traceability
verified 1:1 in both directions.

BLOCKED (6 of 16), each naming its dependency and the grep that proves absence:
  clarify_items ABSENT (Doc 06 owns it; Doc 06 is spec'd, not built)
    -> AC-PME-03, AC-PME-04
  meeting_runtime worker with no media session ABSENT
    -> AC-PME-09, AC-PME-10
  libs/llm structured-output entrypoint on the sonnet seat ABSENT
    -> AC-PME-01, AC-PME-06

post_meeting_tasks and the [post_meeting] config are NOT counted as blockers:
they are Doc 07's own artifacts and criteria are authored before implementation
(GENERATOR.md section 1.1).

Contradictions carried onto the criteria they affect, flagged and NOT resolved:
  C-A on AC-PME-10 -- Doc 07 puts the task id in both scope_id and
    operation_type; the built code splits them the other way
    (dispatch.py:129-145, cost.py:323); CANONICAL 11.2 agrees with Doc 07 and
    not with the code. Three sources, two shapes.
  C-D on AC-PME-07 -- the pre-approval "no durable write" invariant cannot hold
    alongside the clarify_items write that AC-PME-03 requires in CLARIFYING.

Also recorded: staged_drafts.status has no CHECK constraint enforcing the enum
(0001_substrate.py:134-144), so AC-PME-15 asserts the value explicitly.

DECLARED GAP -- this bundle is CANDIDATE, not SEALED: GENERATOR.md section
8.4.1 requires a paired NEG criterion for each of the 14 non-null
dependency_class criteria. They are not emitted because the instruction was one
criterion per seed, invent none. ladder_schema_gate.py will fail the seal until
they exist. Recorded in dependency_manifest.yaml rather than passed over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hape (C-A)

Founder ruling on contradiction C-A: the code is right, the spec is wrong.

  scope_id       = meeting_id (cast to text at the one call site)
  operation_type = 'workroom:{task_id}'

Two sites amended, NO code changed:

  07-POST-MEETING-EXECUTION.md section 3.5 -- had scope_id = the task id AND
    operation_type = 'workroom:<taskId>', duplicating identity across both
    columns.
  CANONICAL-DECISIONS.md section 11.2 -- the parenthetical "it also holds
    workroom task_ids" was wrong about scope_id and was the reason two readers
    could both cite CANONICAL and disagree.

Ground truth both are now conformed to:
  services/harness/src/harness/dispatch.py:129-145 (_claim_workroom_row)
  libs/ops/src/ops/cost.py:323

This matters beyond tidiness: the partial unique index
operation_runs_one_running_per_scope is on (scope_id, operation_type), so which
column carries which id determines what "one running row per scope" actually
guarantees. With the ruling it means one running row per meeting per task.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ariant

Founder ruling on contradiction C-D. Doc 07 section 3.4 said no durable write
occurs outside the task's own record before APPROVED; section 3.3 requires a
clarify_items write while the task is in CLARIFYING, a state strictly before
APPROVED. Both could not hold.

Ruling: clarify_items is exempt. B3 writes it while in CLARIFYING.

The carve-out is written as CLOSED -- clarify_items is the only exempt table --
so it cannot be read later as a general licence to write before approval. The
rationale is on the line: asking a question is not a world-change, so Law 3 and
Invariant 6 are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ine 368)

P8 fixed lines 56, 309 and 333 -- the three the amendment pack named. The audit
found a fourth draft-side site the pack missed, at line 368, carrying the same
defect: propose_change described as returning a draft_id with
status=needs_review. Founder ruling: fix it.

05-WORKROOM.md now has zero draft-side needs_review sites. The remaining four
occurrences are all envelope-side and all correct, since needs_review IS a valid
EnvelopeStatus per CANONICAL section 1.2:

  264  section 3.7 first  -- critic fail-closed verdict
  293  section 3.7 third  -- the hard gate
  358  section 3.12       -- the envelope status enum itself
  404  failure behavior

The draft-side/envelope-side split that made this defect possible is now
consistent across Doc 04 section 3.16.1, Doc 05, and CANONICAL section 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…undle

Closes the generation gap declared when the bundle was first cut.

14 paired negative criteria per GENERATOR.md section 8.4.1 -- one for every
criterion whose dependency_class is non-null. AC-PME-13/14 are null and
correctly take no pair. Total 30 criteria.

Each NEG is derived from its positive, not templated: it names the specific
fault that positive is blind to. The recurring shape is fail-closed -- an
unreadable count is not zero (11-NEG), a missing estimate is not cheap
(12-NEG), a failed staging does not fall back to a push (15-NEG), an APPROVED
row with a null approver is not approved (07-NEG). NEG ladders are
{lint, unit, negative}: reality and integration drop off because the negative
rung IS that pair's real-fault exercise.

GATES -- both run from the `main` copy of orchestrator/*.py, since commit
d9e60db deleted them from this branch as v1 process machinery. Not re-added,
so that deletion stands. Both need PYTHONUTF8=1 on Windows.

  ladder_schema_gate  PASS  30 criteria, 28 non-null, 14 NEG, 4 golden_path,
                            manifest bidirectionally consistent
  criteria_coverage   PASS  16/16 requirements covered (P0 4/4, P1 8/8, P2 4/4)

SEALED as ACCEPTANCE-DOC07 v1.0.0, bundle_hash 1ff8020b.

Blocker count fell 6 -> 4 on the rulings: clarify_items (C-C) and the libs/llm
structured-output seat both resolved. The remaining 4 are AC-PME-09/10 and
their NEG pairs, blocked on the no-media meeting_runtime worker.

The seal records that honestly: assurance_limits says a green run that omits
those four is NOT a green bundle. It also records that no test bodies and no
execution evidence exist yet -- sealing fixes the contract, not the evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… guards

Doc 07 section 4's one new durable store. Columns are section 4's list plus
tenant_id (Invariant 9) and meeting_id uuid (CANONICAL 11.2).

Deliberately NOT a second run table (CANONICAL 12.11 / 13.2 D07.1): the schema
carries no status, progress or last_heartbeat_at column. The run's record stays
the operation_runs row. operation_ref is a pointer to it, never a copy of its
state, and most rows -- informational, question, ticket -- never spawn a run.

Two guards in the substrate, not only in tests:

  CHECK post_meeting_tasks_approved_needs_approver
    state <> 'APPROVED' OR (approved_by IS NOT NULL AND approved_at IS NOT NULL)

  TRIGGER post_meeting_tasks_running_gate  BEFORE INSERT OR UPDATE
    RUNNING is entered only from APPROVED.

Two things about the trigger are worth review:

1. The UPDATE arm fires only on a real transition INTO running
   (OLD.state IS DISTINCT FROM 'RUNNING'). Without that clause every ordinary
   update to an already-RUNNING row -- writing cost, writing outcome -- would
   evaluate OLD.state='RUNNING' <> 'APPROVED' and raise. The invariant is about
   the transition, not about touching a running row.

2. It covers INSERT as well as UPDATE. This goes beyond the brief, which
   specified a BEFORE UPDATE trigger only. A BEFORE UPDATE trigger alone is
   bypassed by INSERT ... state='RUNNING', which enters RUNNING having never
   been APPROVED -- exactly what the invariant forbids. AC-PME-07-NEG asserts
   the database rejects this independently of application code, so the guard has
   to cover both write paths. Flagged, not applied silently.

Also flagged: Doc 07 section 4 names the column `cost`; it is created as
`cost_usd` to match the repo convention of naming the unit (meeting_cost carries
five *_usd columns, 0001_substrate.py:118-127).

NOT EXECUTED: no Postgres or Docker on this machine, so `alembic upgrade head`
has not been run. `alembic heads` parses the revision graph and resolves a
single head, and ruff check passes, but the SQL itself is unexecuted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The table is DEFINED in Doc 06 section 4, but Doc 06 is spec'd and not built, so
it had no builder. Founder ruling C-C: Doc 07 creates and owns it until Doc 06
lands. The migration ships with Doc 07 and says so at the top, because the next
person to read this file will be whoever builds Doc 06.

Column list is Doc 06 section 4's, unchanged -- question, kind, blocking_ref,
urgency, answer, answered_by -- specifically so Doc 06 can adopt the table
without a schema change and without creating a second one. Plus tenant_id
(Invariant 9) and meeting_id uuid (CANONICAL 11.2).

kind and urgency deliberately carry NO CHECK constraint. Doc 06 names the
columns but enumerates neither vocabulary; inventing an enum here would pin a
domain no authority has stated. Doc 06 owns those and can constrain them later.

The migration also records WHY this table is writable before approval: it is the
single carve-out from Doc 07 section 3.4's pre-approval durability invariant
(ruling C-D), and the carve-out is closed. AC-PME-07 pins the permitted
pre-approval write set to exactly {post_meeting_tasks, clarify_items}.

Partial index on (meeting_id) WHERE answer IS NULL -- the pending sweep only
ever reads unanswered rows.

NOT EXECUTED: no Postgres or Docker available; SQL is unexecuted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CANONICAL section 4 has always documented staged_drafts.status as
proposed|accepted|rejected|applied, but the enum lived only in a SQL comment.
The shipped column is plain `text NOT NULL DEFAULT 'proposed'` with no CHECK
(0001_substrate.py:141), so any string was accepted. operation_runs.status in
the SAME migration (line 88) does carry its CHECK -- this was an inconsistency
inside one file, not a policy.

It matters because of the defect P8/P8b just fixed: Doc 05 told builders in four
places that propose_change returns the draft at status=needs_review, a value
outside the enum, borrowed from the envelope status where it IS valid. The prose
is fixed, but nothing stopped that value being written and nothing stopped the
next one. AC-PME-15 and AC-PME-15-NEG both assert the database rejects an
out-of-enum status.

Forward reconciliation, same discipline as 0005 and 0008 -- 0001 is shipped and
never edited in place.

Existing rows: needs_review is mapped to proposed before the constraint is
added. That mapping is correct, not convenient -- Doc 04 section 3.16.1 already
reads a freshly staged draft as status='proposed', so a row written as
needs_review was a never-yet-accepted draft. Any OTHER out-of-enum value is left
alone on purpose and will make ADD CONSTRAINT fail loudly. An unknown status is
a real defect that deserves a human, not a silent coercion into the nearest enum
member.

NOT EXECUTED: no Postgres or Docker available; SQL is unexecuted. On a dev
database carrying rows with an unexpected status, this migration is expected to
fail loudly -- that is the intended behaviour, not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five keys, each with a comment giving the value, its unit and its range in the
established style of the file:

  max_concurrent_tasks  = 3      tasks,     range 1..10
  max_tasks_per_meeting = 10     tasks,     range 1..25
  task_cost_ceiling     = 1.00   USD/task,  range 0.25..5.00
  plan_expiry           = 48     hours,     range 4..168
  draft_tier_enabled    = true   boolean

Every key is a LIMIT or a SWITCH. None is a situation-to-action mapping, and
there is deliberately no key mapping item text to a tier -- tiering is model
judgment (Law 4).

Two choices worth review:

task_cost_ceiling sits below the ~$0.95-1.15 all-in cost of a meeting-hour, so
one unattended post-meeting task can never cost more than the meeting that
produced it.

draft_tier_enabled ships TRUE, unlike Doc 06's voice_enabled_classes which ships
empty. The asymmetry is deliberate and the reasoning is in the file: a spoken
proactive contribution reaches the room with no human in between, whereas
nothing here can surprise anyone -- no sandbox starts and no draft is written
until a named human approves the plan (section 3.4, D07.2), and the artifact is
then a staged draft behind a second human click (section 3.7). The approval gate
is the safety control, not this switch. It is also the one lever that stops
drafting without touching a code path if triage precision disappoints.

Validated: config/defaults.toml parses under tomllib and [post_meeting] reads
back with the expected types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-02, AC-PME-02-NEG, AC-PME-05, AC-PME-05-NEG.

Reads the close output into post_meeting_tasks rows at EXTRACTED with tier=None.
B1 extracts; B2 tiers.

Lives at services/harness/src/harness/post_meeting/ -- a subpackage of the
harness that already hosts meeting_runtime, NOT a new service. Sealed criterion
AC-REPO-006 pins services/* to exactly five directories and AC-REPO-007 pins
libs/* to six, so a sixth of either would break doc00. This also satisfies Doc 07
section 3.5's "no new deployable".

Two design points worth review:

run_extract is a TOTAL function -- it cannot raise. Doc 07 section 2 says that if
this component fails entirely the close sequence and the meeting record must be
unaffected, and the caller sits on the post-close path. The except clause is
deliberately broad (BLE001 noqa with the reason inline): a narrow catch would let
an unforeseen error class through into the close, which is the exact harm
AC-PME-02 exists to prevent. Partial progress is kept and returned alongside the
error rather than discarded.

resolve_owner takes ONE argument -- the owner the room stated. It has no access
to the roster, the transcript, or file authorship, so inference is impossible by
signature rather than merely discouraged. AC-PME-05 plants the three decoys the
criterion names (senior speaker, dominant speaker, last file author) and asserts
none is ever selected; one test asserts the signature itself.

VERIFICATION
  pytest tests/doc07          13 passed
  ruff check                  clean
  mypy --strict               clean (4 files)
  bandit                      No issues identified
  tests/doc00/test_m01_repo   9 passed (repo shape unchanged)

TWO ENVIRONMENT NOTES, neither caused by this change:

1. The venv was missing psycopg-binary, so ANY test importing harness failed at
   collection -- tests/doc04 included. tools/linux-verify-requirements.txt pins
   psycopg-binary==3.3.4 and CLAUDE.md prescribes installing that file after
   uv sync; I had skipped it. Installed into the venv only; no dependency,
   pyproject or uv.lock change.

2. tests/test_invariants.py has 2 failures (ac_inv_001, ac_inv_003). They shell
   out to `rg`, which is not installed on this Windows host. Verified
   pre-existing: both fail identically with this work stashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-01, AC-PME-01-NEG, AC-PME-06, AC-PME-06-NEG.

INFRA: libs/llm/src/llm/structured.py -- the structured-output surface the audit
found missing. libs/llm had only call_model(), which returns free text; Doc 07
section 3.1 needs "one structured call on the sonnet seat".

It is NOT a second model path. It is the general form of the realization the
close pass already ships (scribe/close.py): a forced-tool call whose input_schema
IS the output_schema, so the tool_use input is the structured payload. Every call
goes through libs.http.call_external, which the sealed vendor:anthropic
mock_boundary requires -- callers inject the seam rather than importing a client.

SEAT: TRIAGE_SEAT = ORCHESTRATOR. A new seat is not available -- sealed doc00
AC-CFG-002 (tests/doc00/test_m05_cfg.py:150) pins llm.routing.SEATS to exactly
eight. Of the three sonnet seats, ANSWER is answering a live human question and
WORKROOM is the sandboxed builder, so post-meeting triage is ORCHESTRATOR.
Rationale recorded at the constant.

LAW 4 is the shape of the module. The five draft-tier conditions are PROMPT text
(Doc 07 section 3.1: "they live in the prompt as the standard a task must
meet"), and the model returns both its chosen tier and whether each condition
held. Code owns only the floor: if the model claims the draft tier while
reporting an unmet condition, code drops one tier. That is a consistency check on
the model's own self-report, not a situation-to-action mapping -- there is no
rule table keyed on item text, and a test asserts two items with opposite text
and the same verdict land on the same tier.

Every failure path moves DOWN the tier order or assigns nothing. Nothing in this
module can raise a tier the model did not ask for:
  - vendor 5xx / timeout / no tool_use block -> no tier at all, items reported
    untiered so B4 never plans them. Not "informational", not a default.
  - out-of-enum tier -> REJECTED, never coerced to the nearest member. Coercion
    is how a hallucinated "ticket+draft" would silently become the draft tier.
  - unreadable draft_conditions_met -> treated as NOT met, so it drops.
  - duplicate verdict for one item -> first wins; a duplicate cannot upgrade.
  - verdict for an unknown item -> discarded, never applied to a neighbour.

A test caught a real prompt bug: with draft_tier_enabled=false the prompt removed
the tier from the list but still explained it, inviting the model to ask for a
tier it was not offered. Fixed in the product code -- the tier is now absent from
the prompt entirely in that mode.

VERIFICATION
  pytest tests/doc07            37 passed (13 B1 + 24 B2)
  ruff check                    clean
  mypy --strict                 clean (7 files)
  bandit                        No issues identified
  tests/doc00/test_m05_cfg.py   11 passed (seat table still exactly 8)

UNVERIFIED: the `reality` rung. AC-PME-01/-06 are vendor:anthropic and their
ladders require driving the real request through call_external against a
cassette. No cassette exists and no vendor call was made -- the tests use a
double that still invokes the op and the caller, so request construction really
executes, but the network does not. tests/cassettes/doc07_triage_*.yaml is
declared in the manifest and is not yet recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-03, AC-PME-03-NEG, AC-PME-04, AC-PME-04-NEG.

Ambiguity stops the line (Doc 07 section 3.3). An item missing an owner, a scope
or a done-condition never gets a plan; it becomes one clarify_items row and the
task is held at CLARIFYING.

Routing is mechanical, not judged -- no model call in this module. route_question
is a lookup over what the notes already attribute, and it returns None rather
than choosing a plausible recipient. Picking "someone senior" here would be the
ownership inference section 3.2 forbids, one layer down. AC-PME-04-NEG asserts no
fallback channel is invented and nothing is sent.

Ordering is the fail-closed guarantee: the task is moved to CLARIFYING BEFORE the
clarify_items insert is attempted. If the insert then fails, the item is already
held, so a database fault cannot leave an unscoped item plannable -- which is the
precise harm section 3.3 exists to prevent. The failure is surfaced and the
outcome marked pending (retryable), never recorded as a question successfully
asked. A test also covers the inverse: if the HOLD itself fails, no clarify row
is written either, so we never report having asked about an item we could not
hold.

Ownership is not re-derived here. assess() reads owner == UNRESOLVED, decided
once in B1. Only scope and done-condition are judged signals, and passing None
for either means "triage did not say", which is treated as MISSING -- an unknown
scope is not a scope, and the safe direction is to ask.

clarify_items is the one table writable before APPROVED (ruling C-D, closed
carve-out). A test asserts the union of tables written by this block is exactly
{post_meeting_tasks, clarify_items}, which is the set AC-PME-07 pins.

VERIFICATION
  pytest tests/doc07    54 passed (13 B1 + 24 B2 + 17 B3)
  ruff check            clean
  mypy --strict         clean (7 files)
  bandit                No issues identified

UNVERIFIED: the integration rung. AC-PME-03/04 are db:postgres and their
mock_boundary requires a real database; the fakes here re-implement migration
0009's two guards so the unit rung proves the application never ATTEMPTS an
illegal write, but no real Postgres was reachable on this host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-08, AC-PME-08-NEG.

Writes Doc 07 section 3.4's nine-field plan onto post_meeting_tasks.plan via one
structured call on the same ORCHESTRATOR seat as triage, and moves the task to
PLANNED. A test asserts the only table written is post_meeting_tasks -- the plan
text lives on the task's own record and nowhere else, which is what keeps a
pre-approval task inside the permitted write set.

A failed plan call leaves the task where it was. An item with no plan must not
look like an item awaiting approval.

EXPIRY IS QUIET. Section 3.4: "Proxy does not nag and never proceeds by
default." expire_stale_plans closes the task to DISCARDED with the reason on
`outcome` and sends nothing -- notifications_sent is 0 by construction, not by
configuration. There is no reminder path and no assume-yes path: the absence of
an answer is an answer.

The clock is INJECTED, not read. AC-PME-08-NEG drives a backward clock and a
crashed mid-pass sweep, and a module calling datetime.now() directly could not
be tested against either.

Re-runnability comes from the sweep only ever acting on rows still at PLANNED. A
task that already moved on -- approved, running, discarded -- is skipped, which
makes a restarted sweep a no-op on rows the first pass closed, and stops a
backward clock from reopening a terminal task. One failing row is recorded and
the sweep continues rather than aborting the rest.

mypy --strict caught a real narrowing hole: state.value behind a hasattr() check
is not narrowed for None. Replaced with an explicit _state_value() that returns
None for an unreadable state, so it falls through to "not PLANNED" -- the safe
direction, skipping rather than expiring a task whose state could not be read.

VERIFICATION
  pytest tests/doc07    68 passed (13 B1 + 24 B2 + 17 B3 + 14 B4)
  ruff check            clean
  mypy --strict         clean (8 files)
  bandit                No issues identified

UNVERIFIED: the reality rung -- the plan call is vendor:anthropic and no cassette
was recorded; and the integration rung, no Postgres on this host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-07, AC-PME-07-NEG. Doc 07's P0 safety boundary -- Law 3 and
Invariant 6 applied to the RUN. Invariant 6 covers the artifact at the end; this
covers the start.

Four properties, each enforced where it cannot be forgotten:

1. APPROVED requires a named human. approve() refuses an empty or placeholder
   approver; migration 0009's CHECK refuses it independently. AC-PME-07-NEG
   requires the database to reject it "independently of application code", so the
   application check is the fast path, not the guarantee.
2. RUNNING only from APPROVED. may_dispatch() is the application check; the
   BEFORE INSERT OR UPDATE trigger is the guarantee.
3. The pre-approval write set is a CLOSED constant --
   {post_meeting_tasks, clarify_items} -- and the test asserts the union of what
   B1..B3 actually wrote is a subset of it, plus explicit negatives for
   staged_drafts, operation_runs and meeting_cost. A future block that writes a
   third table fails this test rather than passing silently.
4. The gate FAILS CLOSED. An unreadable row shape, a null approver, an errored
   lookup -- all resolve to "not approved" and the reason is surfaced. A gate
   that fails open under substrate error is not a gate.

UNRESOLVED can never approve. It is the value meaning "nobody was named", so
letting it through would launder the exact ambiguity section 3.2 exists to hold.
SYSTEM, NONE, NULL and PROXY are refused for the same reason.

Two tests cover the trigger details flagged in migration 0009:
  - a row cannot be born RUNNING (the INSERT arm; a BEFORE UPDATE guard alone
    would miss it)
  - writing cost/outcome onto an already-RUNNING row is ALLOWED -- without the
    OLD.state IS DISTINCT FROM 'RUNNING' clause this would raise and deadlock
    every task on its first progress write.

The doubles are adversarial by construction: ForbiddenSandbox raises if reached
and FakeTaskStore re-implements both database guards. A permissive stub in either
place would make these criteria pass while the product violated them.

VERIFICATION
  pytest tests/doc07    84 passed (13+24+17+14+16)
  ruff check            clean
  mypy --strict         clean (9 files)
  bandit                No issues identified

UNVERIFIED: the integration rung. AC-PME-07/-NEG are db:postgres and the criteria
require the real CHECK and trigger to reject these writes; no Postgres on this
host, so only the application-side half is executed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Doc 07 section 3.5 demanded a meeting_runtime worker with no media session AND
asserted "No new deployable". MeetingRuntime was media-coupled by construction,
so the two could not both hold -- the contradiction the sealed bundle recorded in
acceptance/doc07/manifest.yaml assurance_limits, and the reason AC-PME-09/10 and
their NEG pairs were marked BLOCKED.

The clarification: "no media session" is a MODE on the existing MeetingRuntime
(media_session=False), not a second runtime type. In that mode nothing
media-bearing is constructed, and every media entry point refuses, so a no-media
worker cannot be turned into an observing one. That is how the worker requirement
and "no new deployable" are simultaneously satisfied.

SPLIT OUT of the B6 feature commit (was f88d70b). A build that amends the spec it
is judged against, inside its own feature commit, is not auditable: the reviewer
cannot see the amendment without reading the implementation that depends on it.
This commit lands the amendment first; the implementation follows in the next
commit. No wording changed in the split -- this is byte-identical to the hunk
that was in f88d70b.

The sealed bundle is NOT updated here. The four criteria it records as BLOCKED
stay recorded that way: changing acceptance artifacts mints a new bundle version
and invalidates prior evidence (GENERATOR.md section 1.1), and manifest.yaml sets
builder_writes: DENIED. Re-sealing is a founder action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-09/-NEG, AC-PME-10/-NEG, AC-PME-11/-NEG, AC-PME-12/-NEG.

INFRA -- the no-media mode, which resolves the contradiction the sealed bundle
recorded in assurance_limits. Doc 07 section 3.5 demanded a meeting_runtime
worker with no media session AND asserted "no new deployable"; MeetingRuntime was
media-coupled by construction, so those two could not both hold.

Resolved as a MODE on the existing class: media_session: bool = True. With it
False, nothing media-bearing is constructed -- no carrier subscription, no
HearingStage, no Scribe consumer, no STT-credential loop, and no ConsentGate (and
no transport import at all). start() and ingest_transcript() REFUSE in that mode,
so a no-media worker cannot be turned into an observing one.

That refusal is what preserves the existing consent invariant. The live path must
never have can_observe=None; here there IS no live path, and the refusals are what
guarantee one cannot appear later. Doc 07 section 3.5 amended with one
clarifying paragraph saying exactly this.

Verified non-regressive: tests/doc04 190 passed, 36 skipped -- unchanged.

DISPATCH is a decision, not an engine. It assembles nothing and runs nothing: it
calls harness.dispatch.assemble_bundle and the workroom dispatch that already
exist for the live path. Order is the safety property -- approval, then caps,
then cost, each a hard stop before the next, with the workroom reached last and
only on the fully-cleared path.

Every refusal is fail-CLOSED and named:
  - unreadable approval row      -> ERROR, no dispatch
  - unreadable cap count         -> WAITING, never treated as zero
  - unavailable cost estimate    -> COST_ASK, never treated as cheap
  - workroom dispatch failure    -> ERROR with "no fallback path exists"; the
                                    workroom is the only path ever attempted
A capped task WAITS and is not dropped; a test drives it through to dispatching
once a slot frees.

AC-PME-10 is asserted structurally: a test reads the persisted row and fails if
it ever grows a status, progress or heartbeat column -- that is the
workroom_tasks prohibition (CANONICAL 12.11 / D07.1) enforced as a test rather
than a comment. Two static tests pin that the partial unique index, not an
application lock, is the excluder, and that the run stays keyed
scope_id=meeting_id / operation_type='workroom:{task_id}' per P10.

VERIFICATION
  pytest tests/doc07    107 passed (13+24+17+14+16+23)
  pytest tests/doc04    190 passed, 36 skipped (no regression)
  ruff check            clean
  mypy --strict         clean (10 files)
  bandit                No issues identified

ENVIRONMENT: installed anthropic==0.116.0 from
tools/linux-verify-requirements.txt (venv only, no dependency change) -- without
it tests/doc03/scribe/test_reality_scribe.py failed collection.

PRE-EXISTING FAILURES, verified identical with this work stashed: 12 in
tests/doc02 + tests/doc03. Most are static scans that shell out to `rg`, which is
not installed on this host. One of them is
test_every_external_call_wrapped_with_call_external -- so that check did NOT
actually vet the new libs/llm structured seam. By construction it does route
through call_external and builds its client via the sanctioned
libs.http.external.anthropic_client, but that is design, not a passing test.

UNVERIFIED: AC-PME-09/-10 and their NEG pairs still have no integration or e2e
evidence -- no Postgres, so no real concurrent claim, no real worker kill, no
real reconcile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

NOTE: the Doc 07 section 3.5 clarification that was originally part of this
commit has been split into its own preceding spec() commit. A feature commit must
not carry the amendment to the spec it is judged against.
Criteria: AC-PME-13, AC-PME-14, AC-PME-16, AC-PME-16-NEG.

Three refusals define the module.

IT NEVER ROUNDS UP. CONFIDENCE_BY_STATUS maps each envelope status to a signal
and confidence_rank orders them, so the monotonicity is ASSERTED rather than
asserted-by-comment: for every status in the enum, the report's confidence rank
is <= that status's ceiling. Only `done` reads as confident. An unrecognised
status maps to FAILURE at the lowest rank -- an envelope we cannot read is not a
success. A failure carries its reason into the detail rather than having it
summarised away (Law 2).

NEEDS_CLARIFICATION IS A QUESTION. Distinct ReportKind, distinct confidence
(blocked-on-you), and a test asserts a question never borrows a failure reason
even when the envelope carries one. Being asked something is not the system
failing.

IT NEVER INVENTS A CHANNEL. select_channel only ever returns a member of Doc 02's
channel-report, or None. None means the report surfaces on the draft card rather
than vanishing -- including when a send FAILS, where the retry stays inside the
listed channels. Slack is not special-cased anywhere; a test greps the module to
prove it, so if P6 lands later Slack rides channel-report like any other channel
and nothing here changes.

Cadence is the four reportable events only. Silence means it is running.
Idempotency is keyed (task_id, kind), so a retry after a send that actually
succeeded does not duplicate, while a question and a completion for the same task
both still go out.

Two small correctness details worth noting: a bare string in `receipts` is
treated as not-an-iterable (otherwise it explodes into per-character receipts),
and recipients de-duplicate order-preservingly so an owner who is also a named
recipient is not messaged twice.

VERIFICATION
  pytest tests/doc07    130 passed (13+24+17+14+16+23+23)
  ruff check            clean (fixed one E501 by restructuring, not by widening)
  mypy --strict         clean (11 files)
  bandit                No issues identified

UNVERIFIED: AC-PME-16 is db:postgres and its integration rung needs a real
database for the multi-tenant channel-isolation arm; not run on this host.
AC-PME-13/-14 are dependency_class null and are fully exercised at the unit rung,
which is their whole ladder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-15, AC-PME-15-NEG. The last block: what lands in the world.

The module STAGES NOTHING. Doc 07 section 3.8 forbids writing staged_drafts
directly -- staging is propose_change through the Workroom, as Doc 05 defines it.
What lives here is the gate: given what the Workroom returned, decide whether the
task may be recorded as DRAFTED, and refuse anything that reached for the remote.
Two tests grep the whole package to prove no INSERT/UPDATE against staged_drafts
exists anywhere in it.

Three refusals:

1. The row must be at 'proposed'. Out-of-enum is rejected, and so is in-enum but
   later (accepted/rejected/applied) -- a freshly staged draft is 'proposed' and
   nothing else. needs_review is called out specifically: it is a valid ENVELOPE
   status (CANONICAL 1.2) and an invalid DRAFT-ROW status (CANONICAL 4), and
   accepting it would re-introduce at runtime exactly the confusion P8 and P8b
   removed from the spec. Migration 0011's CHECK is the database-side guarantee;
   a test asserts that constraint exists.

2. A draft_id without a retrievable bundle is an orphan, not a draft. Accepting
   one would show a human a draft they cannot open. On rejection the task is
   DISCARDED and no draft_id is recorded -- a test asserts the orphan id does not
   land on the row.

3. No push, ever. FORBIDDEN_REPO_WRITES is parametrised over in the tests so each
   forbidden operation is individually refused. The gate also refuses a token
   carrying contents:write AT ALL -- checking what was possible, not only what
   was done, because a token that can push is the precondition section 3.7
   promises not to have (D07.3, F4 declined).

The failure path is where this matters most: a test drives a failed GCS bundle
write and asserts the code does not fall back to a push, using a git double that
raises on any write attempt rather than a permissive stub.

VERIFICATION
  pytest tests/doc07    151 passed (13+24+17+14+16+23+23+21)
  ruff check            clean
  mypy --strict         clean (13 files)
  bandit                No issues identified

UNVERIFIED: AC-PME-15's integration and e2e rungs. Both need a real Postgres and
a real (or versioned-local) GCS bucket; neither is available on this host, so the
CHECK constraint and the real bundle round-trip are unexecuted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Self-audit after B8 found three assertions that could not fail. Each constructed
a ForbiddenSandbox and asserted call_count == 0, but never handed it to the code
under test -- so the counter read 0 whether or not the product misbehaved. Same
class of defect as the tautology fixed during B1.

  B2 test_ac_pme_06_neg_no_sandbox_is_started_during_triage
  B4 test_planning_starts_no_sandbox
  B4 test_ac_pme_08_no_sandbox_is_provisioned_by_expiry

The real property is that these modules have no sandbox to reach, which is
structural, so the first two now assert it structurally via a new
_support.assert_no_code_reference() helper. That helper is AST-based rather than
a substring grep: triage.py's seat comment legitimately says "WORKROOM is the
sandboxed builder", and a grep that fails on a comment is a test that punishes
documentation. It inspects imports, identifiers, attribute names and string
literals passed to calls, ignoring comments and docstrings.

The third is now behavioural: expiry writes only post_meeting_tasks and lands the
task terminal.

The ForbiddenSandbox / ForbiddenGitRemote doubles remain and are still meaningful
where they ARE wired in -- B5's ambiguous-approval test and B8's staging-failure
test both reach them through real code paths.

Also ran ruff over tests/ with --no-force-exclude and fixed 10 import-ordering
and unused-import findings. NOTE for the record: pyproject sets
extend-exclude = ["tests"] with force-exclude = true, so tests/ is deliberately
outside the repo's ruff gate. Earlier per-block reports in this branch said
"ruff clean" while passing tests/ paths; those runs only ever linted the
services/ and libs/ product surface. The product-code result was accurate; the
test-coverage claim was not.

  pytest tests/doc07   151 passed
  ruff (product)       clean
  ruff (tests, forced) clean
  mypy --strict        clean (13 files)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssion)

Caught by baselining doc00 against the pre-B1 commit rather than trusting the
failure list: doc00 showed 11 failures with B1-B8 applied and 10 without, and the
extra one was mine.

  AssertionError: undeclared_transitive_imports must be 0:
  ["libs/llm imports ['http'] not in declared deps ['contracts', 'pydantic']"]

B2 added libs/llm/src/llm/structured.py, whose lazy client construction imports
libs.http.external.anthropic_client -- the one sanctioned constructor. That made
libs/llm depend on http for the first time, and AC-REPO-004 requires every
cross-member import to be a declared dependency.

Declared `http` in libs/llm/pyproject.toml with the workspace source, matching
how libs/ops already declares its contracts/db/http deps, and relocked.

doc00 is now back to the 10-failure baseline exactly, with the same test ids.
doc07 unchanged at 151 passed.

Method note for the record: an earlier attempt to baseline with `git stash -u`
was worthless -- every block is committed, so there was nothing to stash and the
"baseline" run was the same tree. The real comparison needs a checkout of the
pre-B1 commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
post_meeting_tasks.operation_ref is `uuid REFERENCES operation_runs(id)`
(migration 0009). run_dispatch wrote `bundle.task_id` into it. Those are two
different uuids, so every dispatch would have raised a foreign-key violation the
moment a real database was attached. The unit suite never caught it because both
values were uuids and the fake store had no FK.

The correct value is WorkroomHandle.run_id -- the operation_runs primary key that
_claim_workroom_row returns (services/harness/src/harness/dispatch.py:196-197).
Under P10 the run is KEYED (scope_id=meeting_id, operation_type='workroom:{task_id}');
what we store is that row's id.

Two failure paths that were previously wrong:

  - A workroom return carrying no run_id (dispatch_workroom returns a
    DispatchDecision instead of a handle when its own cost gate runs) now yields
    ERROR. B6 gates cost itself and calls it ungated, so no run_id means no row
    was claimed and there is nothing to point at.

  - A failed operation_ref write is no longer swallowed. It cannot be: the run is
    claimed and executing, but the task record does not point at it, so nothing
    can report on it or reconcile it. Now ERROR, with the orphaned run id in the
    detail so it is recoverable by hand.

TEST FIX. test_ac_pme_10_operation_ref_points_at_one_run asserted
`out.operation_ref == tid` -- it encoded the bug. Renamed to
..._is_the_run_id_not_the_task_id and now asserts the run id AND that it differs
from the task id. The RecordingWorkroom double was returning a bare dict; it now
returns a WorkroomHandle-shaped object whose run_id is deliberately a distinct
uuid, so a double can no longer let the wrong-column bug pass.

Two new negative tests cover the two failure paths above.

  pytest tests/doc07/test_b6_dispatch.py   25 passed

Still unverified at this commit: the FK itself. That needs the real database,
which is being stood up next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects in the same check, one of which made the cap permanently wrong.

1. count_for_meeting counted EVERY row for the meeting -- no tier filter, no
   state filter. Only ticket+plan+draft ever reaches the Workroom (Doc 07
   section 3.1); informational, question, ticket and ticket+plan produce no run
   at all. So a meeting that generated eleven informational items -- items that
   by definition produce nothing -- permanently blocked every dispatch for that
   meeting. Untriaged rows (tier IS NULL) counted too.

   Replaced with count_dispatchable_for_meeting: dispatchable tier, non-terminal
   state. TERMINAL_STATES and DISPATCHABLE_TIERS are now named constants in
   models.py rather than inline literals. DRAFTED is deliberately NOT terminal --
   it is waiting on a human's accept click.

2. The two cap comparisons had inconsistent semantics: concurrency used >=
   against a count that EXCLUDES the candidate (it is not RUNNING yet), while the
   meeting cap used > against a count that INCLUDED it. That mixture is the
   off-by-one.

   Fixed by making both exclusive and both >=. count_dispatchable_for_meeting
   takes exclude_task_id, so each comparison now reads "are there already N
   others?". A cap of 3 admits exactly 3.

The fake store mirrors the real SQL rather than reimplementing a looser rule, so
the unit rung and the database agree on what counts.

Five new tests, each pinning one of the two defects:
  - 44 non-dispatchable items (11 each of the four non-draft tiers) do not block
  - 20 untriaged rows do not block
  - a cap of 3 admits exactly 3 and HOLDS the other 2 (not drops, not errors)
  - terminal tasks release their meeting slot
  - the candidate is excluded from its own count

One test needed isolating: max_concurrent_tasks bit before the meeting cap and
was measuring the wrong limit, so the meeting-cap test now runs with concurrency
set high.

  pytest tests/doc07   158 passed
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
B6 guarded start() and ingest_transcript() and stopped there. Two holes and
three unguarded siblings remained.

wire_orchestrator_pipe() was entirely unguarded and called
self.carrier.subscribe(). On a no-media worker that either attaches to a dead
carrier or -- worse -- to a live one it must not read. This was the actual hole:
the mode existed to make observation impossible and this path went straight
around it.

grant_consent() raised AttributeError on the None consent gate. It did fail, but
as a crash rather than a refusal, which is the wrong signal for a Law 3 control
and the wrong thing to see in a log. It now refuses explicitly.

build_run_loop(), run_orchestrator_loop() and run_until_meeting_end() are guarded
too. The last two would have inherited the refusal through
wire_orchestrator_pipe, but an explicit guard makes the error name the entry
point the caller actually used instead of a nested method.

The test is parametrised over all seven entry points and runs them against a
TripwireCarrier whose subscribe() raises. If any path ever reaches the carrier
again the test fails on the tripwire rather than on the absent RuntimeError, so
it cannot pass for the wrong reason. A second test asserts aclose() still works
-- a worker that cannot be torn down leaks.

  pytest tests/doc07   165 passed
  pytest tests/doc04   190 passed, 36 skipped (unchanged)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AC-PME-07-NEG says the database rejects an APPROVED row with a null approver
"independently of application code". AC-PME-15-NEG says the same of an
out-of-enum draft status. FakeTaskStore could not establish either: it
re-implements 0009's CHECK and trigger in Python, so it IS application code. It
proves the app never attempts the write -- a real property, but not the stated
one.

Every assertion in this module drives the REAL PostMeetingTaskStore SQL against a
REAL Postgres 15, and the expected outcome is a psycopg/asyncpg integrity error
raised by Postgres. Each is identified BY CONSTRAINT NAME, so a test cannot pass
because some other error happened to fire.

  post_meeting_tasks_approved_needs_approver  CheckViolation
    - state='APPROVED' with no approver at all
    - with approved_by but null approved_at, and the reverse
    - and the row is unchanged afterwards
  post_meeting_tasks_running_gate             RaiseError
    - UPDATE arm, from each of 5 non-APPROVED states
    - INSERT arm: a row cannot be born RUNNING
  staged_drafts_status_enum                   CheckViolation
    - needs_review, draft, verified, PROPOSED, ''

Happy paths are asserted too, because a guard that blocks everything proves
nothing: a real approval then RUNNING is accepted, all four CANONICAL section 4
enum values insert, the column defaults to 'proposed', and writes to an
ALREADY-running row still work (the trigger gates the transition, not every write
-- without that clause every task would deadlock on its first progress write).

Two further criteria gain real-DB evidence:

  AC-PME-10 -- operation_ref is proven to be a FOREIGN KEY to operation_runs(id).
    Writing the task id into it raises ForeignKeyViolationError. That is hard
    proof the defect fixed two commits ago was real and not theoretical; the unit
    rung could never have caught it because both values are uuids and the fake
    has no foreign keys. A real run_id is then accepted.

  AC-PME-11 -- the meeting-cap SQL is exercised against real rows: 12
    non-dispatchable items plus one untriaged row count 0, a real draft-tier task
    counts 1, excluding the candidate counts 0, and a terminal task releases its
    slot.

Marked `integration` and skips cleanly with no DSN (13 skipped, 0 errors), per
the sealed bundle's db:postgres mock_boundary. The _Pool adapter exposes the same
.acquire() surface as libs.db.Database so the store's real SQL -- placeholders,
casts, ANY()/ALL() arrays -- is what executes, not a paraphrase.

  with Postgres:    165 passed, 13 passed (integration)
  without Postgres: 165 passed, 13 skipped

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pgoel813 and others added 23 commits July 28, 2026 15:34
…_worker

Both were built on a premise that does not hold. Tracing what a dispatched task
actually needs showed Doc 05's SessionDriver takes (provider, sandbox_fs, store,
db, abort_registry, model, disposition, ...) and resolves the rest itself:

  notes reader     notes_reader takes db: Acquirer + meeting_id
  code_intel       _resolve_code_intel_server(meeting_id) builds it FRESH per
                   task from db; its docstring explicitly refuses a shared or
                   process-global server, so a runtime-held code_intel_ctx would
                   be rejected rather than used
  operation_runs   dispatch_workroom(db, bundle) -> _claim_workroom_row(db, ...)
  sandbox          sandbox_provider.provision(meeting_id=...), idempotent

Everything resolves from (db, meeting_id). services/workroom/ contains zero
references to MeetingRuntime. In no-media mode the runtime was a bag holding a db
the caller already has, and post_meeting_worker was a wrapper adding nothing.

Post-meeting dispatch is a DIRECT CALL and never a tool call: Doc 04 section 112
puts the registered tool wrappers inside the live harness, and that process is
torn down by the time post-meeting work runs. So the live tool-call path is not
available here even in principle -- which is a reason the worker was the wrong
shape, not a claim that no harness hosting exists.

REVERTED
  meeting_runtime.py  restored byte-identical to its pre-B1 state (03bd53d). My
                      only changes to it were the media_session field and the
                      seven guards -- 75 insertions, zero deletions -- so the
                      restore is exact, not an approximation.
  dispatch.py         post_meeting_worker removed; the "where it runs" paragraph
                      now states the real shape.
  test_b6_dispatch.py the 10 no-media tests removed with their imports.

Doc 07 section 3.5 still describes the mode at this commit; P11 (next commit)
corrects it. Split deliberately so the code revert and the spec correction are
each reviewable alone.

VERIFICATION
  pytest tests/doc07   168 passed (was 178; the 10 removed were no-media tests)
  pytest tests/doc04   2 failed, 224 passed -- back to the pre-B1 baseline
                       exactly, same two test ids
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A dispatched task is (db, meeting_id) handed to Doc 05's SessionDriver, which
resolves the notes reader, the code_intel server, the operation_runs claim and
the warm sandbox itself. No MeetingRuntime object is required.

WITHDRAWN: the "meeting_runtime worker with no media session" wording, and the
2026-07-27 clarification that made it a media_session=False mode. Both described
a hosting model the Workroom does not use. Evidence in the amendment:

  SessionDriver.__init__ takes provider, sandbox_fs, store, db, abort_registry,
  model, disposition, ... -- no runtime.
  _resolve_code_intel_server builds the server FRESH per task from db and its
  docstring explicitly refuses a shared/process-global one, so a runtime-held
  code_intel_ctx would be rejected rather than reused.
  sandbox_provider.provision(meeting_id=...) is idempotent per meeting.
  services/workroom/ has zero references to MeetingRuntime.

DELIBERATELY SILENT on where the LIVE in-meeting dispatch path is hosted. Doc 04
section 112 owns that -- the harness's registered tool functions and the
completion callback -- and is unchanged. Saying anything here about the live path
would create a fresh cross-doc conflict while fixing one.

The amendment also states plainly that it does NOT resolve the contradiction
recorded in the sealed bundle's assurance_limits. AC-PME-09/10 and their NEG
pairs stay blocked, correctly: the blocker is one layer below the worker, and the
amendment points at docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md (filed in a
later commit this pass).

Spec-only commit. No code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criteria: AC-PME-02, AC-PME-02-NEG.

run_meeting_close now calls post-meeting intake AFTER run_close_pass returns.
Doc 07 section 2: "This doc begins after the record is written, and only reads
it." Nothing here holds the bot, inserts a step into the ordered close, or
touches the notes object.

intake.py runs B1 to B4: extract, triage, clarify, plan. store.set_tier finally
has a caller -- triage's verdict is what moves a task off EXTRACTED, asserted by
a spy test.

ISOLATION IS STRUCTURAL, AT TWO LEVELS.

run_intake is total: it catches BaseException and reports on its result. The
close's own guard _run_post_meeting_intake is total too, and that redundancy is
deliberate -- the close's section 2 guarantee must not depend on intake staying
raise-free as it grows.

The broad catches are the point, not oversights. A narrow `except Exception`
would let KeyboardInterrupt and MemoryError through into the close, so the test
parametrises over RuntimeError, ConnectionRefusedError, KeyboardInterrupt and
MemoryError -- two Exception, two BaseException.

Three further isolation properties are asserted:
  - the close's return value is computed BEFORE intake and intake cannot
    substitute it (the guard returns None regardless of what the hook returns)
  - with no hook configured the close behaves exactly as it did before Doc 07
    existed -- the field defaults to None and the call is a no-op
  - intake never mutates the close record (deep-copy comparison)

Stage failures stop intake without stopping anything else: an extract failure
reports failed_stage='extract'; a triage failure leaves items at EXTRACTED with
tier NULL and no plan, so an untiered item can never be planned.

Ambiguity still stops the line: a question-tier item is held at CLARIFYING with a
clarify_items row and no plan. Informational items get no plan by construction.

VERIFICATION
  pytest tests/doc07   183 passed (15 new)
  pytest tests/doc03   1 failed, 390 passed, 85 skipped -- IDENTICAL to the
                       pre-B1 baseline, same test id
                       (test_ac_close_09_strict_order_render_gcs_chat_teardown,
                       pre-existing). Checked because this commit modifies the
                       close path AC-CLOSE-09 governs.
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… task

ACCEPTED and CHANGES_REQUESTED were unreachable states. Nothing wrote them, so a
task that produced a draft stayed at DRAFTED forever no matter what the human did
with it. Doc 04's accept route now hands the decision to Doc 07's task record.

The split of ownership is the load-bearing part:

  the route  owns the DRAFT's lifecycle -- staged_drafts.status to applied or
             rejected. That is unchanged.
  this seam  owns the TASK's lifecycle -- post_meeting_tasks.outcome plus the
             terminal state, found by draft_id.

Doc 07 section 3.8 forbids writing staged_drafts, and a test greps outcome.py to
prove it does not. It only reads a draft_id the route already resolved.

CHANGES_REQUESTED, never DISCARDED. A reviewer asking for another pass is not the
task being abandoned; section 3.9 spells the state out in full precisely so the
distinction is not lost, and a test pins it.

THE DRAFT ACTION ALWAYS WINS. The write-back runs after the apply and after the
audit line, and it is total at both levels -- the sink itself and the route's own
guard. By the time it runs the human's accept is already on durable storage, so
a bookkeeping fault must not turn a landed decision into an error response. The
test parametrises over RuntimeError, KeyboardInterrupt and MemoryError, two of
which a narrow `except Exception` would miss.

A draft with no post-meeting task returns None and is not an error: the live
in-meeting path stages drafts too, and those have no Doc 07 row.

post_meeting_outcome is an optional injected param on both handlers. A deployment
without post-meeting execution wired passes None and the route behaves exactly as
it did before Doc 07 existed.

One test asserts all three of section 3.9's terminal states are now reachable by
a real path: ACCEPTED and CHANGES_REQUESTED here, DISCARDED via plan expiry (B4)
and the final gate's refusal (B8).

VERIFICATION
  pytest tests/doc07              195 passed (12 new)
  pytest tests/doc04 tests/security  2 failed, 235 passed -- doc04 at its
                                  pre-B1 baseline, same two ids; security clean
  ruff / mypy --strict            clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed BLOCKED

Criteria: AC-PME-07, AC-PME-07-NEG.

POST /m/{meeting_id}/tasks/{task_id}/approve, mounted behind the same protected()
auth wall as the draft-accept surface. Doc 07 section 3.4 PLAN approval, which is
a different thing from accept_route.py's DRAFT acceptance:

  plan approval   happens BEFORE any work runs -- the gate that lets a task start
  draft accept    happens AFTER -- the click that lands the artifact

Both live in control_plane for the same reason: they must work long after the
meeting harness is gone (R1's confirm-at-build). A test asserts the module path
and that the two route paths are distinct.

APPROVED is written with state, approved_by and approved_at in ONE statement.
Migration 0009's CHECK rejects an APPROVED row missing either approver field, so
a two-step approval cannot exist even transiently.

Fail-closed order mirrors handle_accept: auth -> server-side tenant -> task
exists -> state is PLANNED -> write -> audit -> dispatch. Another tenant gets 404
rather than 403, matching the doc08 anti-leak rule -- never confirm a task exists
to a caller who should not know. UNRESOLVED, SYSTEM and Proxy are refused as
approvers, reusing approval.is_named_human so the route and B5 cannot drift.

THE HUMAN CLICK IS THE TRIGGER. No poller, scheduler or queue. A test parses the
module's imports and fails on sched/celery/apscheduler/queue/cron/asyncio,
because a background sweep that dispatched approved tasks would be "proceeding by
default", which section 3.4 forbids.

DISPATCH IS A DECLARED BLOCKED BOUNDARY, not a stub. WorkroomDispatchUnavailable
carries the reason and the three re-checkable pieces of evidence:

  1. harness.dispatch.dispatch_workroom has no production caller
  2. SessionDriver is constructed only in tests/doc05/*
  3. libs.agentkit.tools.TOOL_HANDLERS is {'echo','answer'} -- no
     dispatch_workroom handler; the behaviors mount only the NAME in an
     allow-list

and states that the live in-meeting path has the identical gap. Attribution is
Doc 04 section 112, which assigns the harness "the registered tool functions ...
thin wrappers over the other docs' APIs" and the create_task done-callback.

The route returns 202 with dispatch_blocked=true rather than pretending. Calling
dispatch_workroom would claim an operation_runs row nothing executes, leaving a
task RUNNING forever while the tests passed -- a green suite over a dead end.

The approval still LANDS when dispatch is blocked, and a test pins that: a human's
decision is not rolled back because machinery downstream of it is missing.

The boundary is the ABSENCE of a dispatcher, not a hard-coded refusal -- passing a
real dispatcher yields 200 and calls it, asserted.

VERIFICATION
  pytest tests/doc07     202 passed, 13 skipped (20 new)
  pytest tests/security  9 passed, 2 skipped -- the new route classifies as
                         tenant-scoped, not raw, with the route mounted live
  pytest tests/doc08     265 passed (ex the known-hanging connect_page file)
  pytest tests/doc04     2 failed, 224 passed -- pre-B1 baseline, same two ids
  ruff / mypy --strict   clean

mypy caught a real error mid-build: the first draft mounted via a nonexistent
app.protected. The mount now mirrors install_accept_route exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…docstrings

THREE DEFECTS, all "the code says something that is not true".

1. report.py modelled channel-report as Sequence[str] -- a parallel shape beside
   Doc 02's real contract. It now takes contracts.channels.ChannelReport, the
   same typed contract transport/chat.py gates DM delivery on, so a
   dm -> dm_available drift shows up on the real path instead of passing here.

   select_channel returns the draft card, and the reason is now stated: post-close
   the bot has left, so a platform DM is NOT reachable even when the MEETING
   reported one. A test pins exactly that -- ChannelReport(dm_available=True)
   still resolves to the card. Slack is not special-cased; if P6 lands it joins
   channels_in() and nothing else changes.

   Cards are now built by transport.chat.format_draft_card via build_draft_card,
   instead of a Doc 07 card shape. Doc 07 defines no card of its own, and the
   card render and the /m/ accept route must keep reading the SAME typed draft_id
   (CANONICAL 11.5) -- which only holds if there is one formatter. Two tests: the
   result is a real contracts.DraftCard linking /m/{meeting_id} and never a raw
   gs:// URI, and a draft_id-less report raises rather than rendering a click that
   points at nothing.

2. dispatch.py's docstring claimed it "calls harness.dispatch.assemble_bundle and
   harness.dispatch.dispatch_workroom". It imports neither -- both are INJECTED
   by the caller, which is what makes AC-PME-09's static no-second-engine
   assertion true by construction rather than by discipline. The docstring now
   says that, and adds the part that was missing: neither is wired in production,
   so a dispatched task would claim an operation_runs row nothing executes. It
   points at SEAM 2's refusal and the gap file.

3. clarify.py computed routed_to and stored it nowhere. Delivery is now
   explicitly DEFERRED with the reason in code, and a new delivery_deferred flag
   distinguishes the two states that were previously conflated:

     pending           nobody could be resolved to ask
     delivery_deferred a recipient WAS resolved, but nothing was sent

   Nothing is lost by not sending: Doc 07 section 3.6 says this doc "defines no
   channel of its own", and the question reaches its human by being a pending
   clarify_items row, which is what the draft card renders from. Persisting a
   recipient column would invent a routing table this doc does not own; sending
   would be the new messaging path section 3.8 forbids. When P6 lands, delivery
   becomes a send_chat at the B7 seam and routed_to becomes its addressee -- no
   schema change. A test asserts the deferred question is still reachable on the
   pending list.

VERIFICATION
  pytest tests/doc07     206 passed, 13 skipped (4 new)
  ruff / mypy --strict   clean; ruff on tests/ (forced) clean
  bandit                 No issues identified

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while wiring Doc 07's seams; recorded so it survives this conversation.

THE GAP. harness.dispatch.dispatch_workroom claims an operation_runs row and
returns a handle, and nothing in production consumes it. A dispatched task is
never executed. The LIVE in-meeting path has the identical gap -- this is not
specific to post-meeting execution.

OWNER: Doc 04, not Doc 05. 04-ORCHESTRATOR.md:112 assigns the harness "the
registered tool functions (speak/chat/screen/dispatch/... -- thin wrappers over
the other docs' APIs)" and the create_task done-callback. Both are missing.
Doc 05's SessionDriver is present, complete and waiting: it resolves the sandbox,
the code_intel server and the notes path itself from (db, meeting_id). There is
no missing capability on the Doc 05 side, only a missing caller on the Doc 04
side. My earlier report mis-attributed this and the file says so.

THREE RE-RUNNABLE CHECKS are in the file, not just prose, and all three were
re-verified at commit time:
  dispatch_workroom has no production caller (only docstrings, including the ones
    describing this gap)
  SessionDriver( appears nowhere in services/ or libs/ -- tests only
  TOOL_HANDLERS is {"echo", "answer"}; the behaviors mount only the NAME

WHY NO GATE FIRED: neither doc04 nor doc05 has an acceptance bundle -- acceptance/
holds doc00 doc01 doc02 doc03 doc07 and nothing else. A chain named in three
places in 04-ORCHESTRATOR.md went unbuilt because no sealed criterion could fail.
doc00's criteria DO mention dispatch_workroom, but that is a different function --
libs/ops/cost.py:292, the estimate gate -- which is wired. The name collision
makes the gap look covered. That is the mechanism, and it generalises: for a doc
without a bundle, "the spec says so" is enforced by nothing.

The file also records what depends on it today (SEAM 2's 202, post_meeting
dispatch's injected seams, and the four sealed criteria that stay BLOCKED
correctly), and a four-step close-out ending in "generate acceptance/doc04/".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The route's two halves fail asymmetrically, and that asymmetry manufactures
orphans.

The APPROVED write is DURABLE -- it lands in post_meeting_tasks with approved_by
and approved_at. The dispatch is BLOCKED. And Doc 07 section 3.4 forbids a poller
("Proxy does not nag and never proceeds by default"), so nothing sweeps for
approved-but-undispatched tasks. Every approval taken before section 112 lands is
therefore a PERMANENTLY ORPHANED TASK: approved, unrunnable, invisible to any
retry path. Returning 202 makes the block honest to the caller; it does not make
the row recoverable.

Unmounting is the mechanism, not a flag. doc00 section 7 pins V0 at zero active
runtime flags, and a flags table is machinery for nothing -- PLATFORM-ADOPTION's
flags bullet is annotated SUPERSEDED for the same reason (P9, earlier this
branch). Adding one here would contradict a sealed oracle to guard a route that
should simply not be reachable yet.

KEPT: the module, install_approve_route, and all 20 tests. Nothing about the
handler changes; only the one call in app.py is gone, replaced by a comment
explaining why and pointing at the gap file.

The gap file now records that the route is built and unmounted, why, and makes
mounting it step 4 of 5 in the close-out -- explicitly "not before", with the
one-line snippet.

VERIFIED
  create_app() exposes NO approve route; accept and reject still mounted
  pytest tests/doc07     219 passed
  pytest tests/security  11 passed -- enumeration unaffected
  pytest tests/doc08     265 passed, 1 skipped
  ruff                   clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in production imports libs.agentkit.tools.TOOL_HANDLERS. The only
references are tests/doc00/test_m12_con.py:168, which asserts merely that A
registry exists (the AC-CON-003 never-throw contract), and this repo docstrings
citing it as evidence. The model never reaches it.

Tools the wake turn can actually call are mounted as host-side SDK MCP servers
via create_sdk_mcp_server. Precedent: propose_change at
services/workroom/src/workroom/drafts.py:345, mounted into the wake turn
mcp_servers the way code_intel is (wake_turn.py:179,184).

The original evidence item stays in the list -- TOOL_HANDLERS genuinely has no
dispatch_workroom handler, and that is still true -- but it is not the thing to
fix. The never-throw contract still binds the new tool: it returns an is_error
content result rather than raising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Founder ruling: revert the P8 edit at the propose_change code sample. The code
was right and P8 over-reached on that one site.

P8 set out to fix one real defect -- Doc 05 used needs_review for the
staged_drafts ROW status, which collides with CANONICAL section 4's enum
(proposed|accepted|rejected|applied) and with Doc 04 section 3.16.1, which reads
the row as proposed. It changed four sites.

THREE WERE RIGHT AND STAND: lines 56, 309, 368. All three describe the row.

THE FOURTH WAS WRONG: the return-ok code sample. That return is an ENVELOPE, and
needs_review is a valid EnvelopeStatus per CANONICAL section 1.2. The shipped code
agrees and always did -- drafts.py writes the row proposed
(INSERT ... VALUES (..., proposed), lines 182-186 and 215) and returns
status=needs_review to the caller (lines 192, 221, 327). P8 briefly made the spec
describe behaviour the code does not have.

Section 3.8 now states the distinction explicitly rather than leaving it implicit:
the ROW is proposed, the tool RETURN carries the envelope status needs_review --
two different fields, both correct. A P8-correction note records what was reverted
and why, because the failure mode P8 named is real: three spellings for one
concept is how a fifth status gets invented. The fix is to keep the two fields
DISTINGUISHABLE, not to collapse them onto one word.

drafts.py is NOT touched, per the ruling.

Unaffected by construction: B8 validate_draft and the AC-PME-15 integration test
both validate the ROW, and migration 0011's CHECK still rejects needs_review on
the row. doc07 219 passed; doc05 at its 7-failure baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while scoping section 3.4 `edit`, in work I had already reported verified.

expire_stale_plans decides expiry from row["planned_at"]. THE COLUMN DID NOT
EXIST, and PostMeetingTaskStore never selected it. On the real substrate every row
fell into the `not isinstance(planned_at, datetime)` skip branch, so NOTHING EVER
EXPIRED. AC-PME-08 and AC-PME-08-NEG were unit-green and could not have run
against Postgres at all.

Same defect class as the operation_ref foreign key: a fake supplying a field the
real store cannot. The unit tests passed because they hand-built row dicts
carrying the key. That is the second time a fake has hidden a real-substrate gap
on this branch, and both were only found by asking what the REAL store returns.

FIXED
  migration 0012  planned_at timestamptz, nullable, plus a partial index on
                  (planned_at) WHERE state='PLANNED' -- the only rows the sweep
                  reads
  store.set_plan  stamps planned_at; it is the moment a plan is put in front of a
                  human
  store           new planned_tasks_for_sweep() returning exactly the three
                  fields expire_stale_plans needs

planned_at is deliberately NOT created_at. created_at starts ticking at extraction
(B1), so expiry measured from it would charge the human for the time Proxy spent
triaging and clarifying. planned_at makes the window mean "unanswered for N hours"
rather than "extracted N hours ago". NULL means no plan was ever presented, which
the sweep treats as not-expirable -- the same fail-safe direction as an unreadable
state.

The fake now mirrors the real store, so the unit rung and the database agree.

THREE NEW INTEGRATION TESTS on real Postgres 15.18, covering the path that could
not previously execute: set_plan stamps the clock (NULL before, non-NULL after);
the sweep reader returns (task_id, state, planned_at) with a real datetime; and
expire_stale_plans runs end to end over rows READ FROM POSTGRES -- not expired at
47h, closed quietly at 49h with notifications_sent == 0, and gone from the sweep
reader afterwards.

  pytest tests/doc07   222 passed (16 integration, was 13)
  alembic upgrade head clean, single head at 0012
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…it deferred)

Only `approve` existed. Section 3.4 lists five actions; four are now built and the
fifth is a recorded decision rather than an omission.

REJECT -> DISCARDED, nothing runs, rejector named. Deliberately distinct from
CHANGES_REQUESTED (SEAM 3): that is a reviewer asking for another pass on a draft
that exists; reject means the work should not happen at all.

DOWNGRADE TO A TICKET -> tier `ticket`, state back to TRIAGED, plan and planned_at
cleared. Implemented WITHOUT inventing a state: section 3.1 says the ticket tier
means "a human should do this", so the item stays alive and visible while leaving
the approval queue. Clearing planned_at removes it from the expiry sweep (no plan
awaits an answer); clearing plan avoids leaving a document on a ticket nobody will
action. Tests assert it is neither swept nor dispatchable afterwards, and that it
is NOT a discard.

EDIT -> plan rewritten, stays PLANNED, and two properties make it safe rather than
a way around the gate: no approval is granted or carried over (approved_by /
approved_at stay NULL, and a test proves RUNNING is still refused), and the expiry
clock RESTARTS because set_plan re-stamps planned_at. An edited plan is a new plan
awaiting a new decision; inheriting the old window would expire a plan the owner
had just engaged with. This is why the planned_at fix had to land first. An empty
edit is refused -- silently clearing a plan is not an edit.

SPLIT IS DEFERRED, per the founder call, with the cost on the record in both the
module and section 3.4 of the spec: a parent/child relation the table has no
column for (migration, plus a ruling on whether the parent stays non-terminal
while children run), max_tasks_per_meeting accounting across the split, and a rule
for children that disagree. ~1-1.5 days with the migration, for an action nobody
has requested pre-users. An owner wanting a split can reject, or edit the plan down
to the startable part.

All three share two preconditions, parametrised across every state and every
non-human actor: they apply ONLY to a PLANNED task, and they require a named human
(reusing approval.is_named_human so route and action cannot drift). 42 parametrised
cases assert the row is byte-identical after a refusal.

  pytest tests/doc07   257 passed (35 new)
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Standing rule, earned by two defects that shipped unit-green on this branch:

  post_meeting_tasks.operation_ref held the task id instead of the run id -- a
  foreign-key violation the in-memory fake had no foreign key to catch.

  planned_at was read by the expiry sweep but the column did not exist and the
  store never selected it, so nothing ever expired.

Both passed their unit tests because hand-built row dicts supplied fields the real
store could not. Neither was visible until the real store was asked what it
returns.

A fake gives you whatever you hand it, so a unit test over one proves the
application's intent and never the substrate's shape. For store-backed behaviour
the integration test is the one that can fail for the right reason, so it goes
first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/linux-verify-requirements.txt pinned 0.2.115 while uv.lock and the installed
venv are both 0.2.128. uv.lock is authoritative -- it is the single shared lockfile
CLAUDE.md names as the install source, and libs/agentkit requests >=0.2.128, so
0.2.115 could not satisfy the workspace at all. The flat pip list for the Linux
verify tier had simply drifted behind.

The pinned SDK contract holds on both versions: tool() gained no parameters between
them and create_sdk_mcp_server signature is unchanged. The only delta is
ToolAnnotations, which the dispatch tool deliberately does not use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written BEFORE the section 112 wrapper, per the standing rule just added to
CLAUDE.md. Both arms were previously "unit-verified" over a fake that has no
partial unique index and no notion of a crashed owner, so neither could have
failed for the right reason.

ARM 1 -- the atomic claim under REAL concurrency. Eight asyncio.gather'd claims on
eight real connections race the same INSERT ... ON CONFLICT; exactly one returns an
id and exactly one row exists. A second test drops to raw SQL with no application
coordination at all and additionally asserts the index DEFINITION contains
scope_id, operation_type and status='running' -- so if someone replaces the partial
index with an application lock the test fails on the missing index, not on a
behaviour that an app lock would also satisfy.

A third test pins the key's granularity: two DIFFERENT tasks in the same meeting
must both claim. Under amendment P10 the key is (meeting, task), so a second task
must not be blocked by the first -- which a scope_id-only key would have broken.

ARM 2 -- reclaim after the worker dies. A live owner blocks a replacement; the
owner's heartbeat then ages past stale_after_s (which is exactly what a killed
process looks like to the substrate, since a dead worker's only observable trait is
that it stopped heartbeating); the reaper flips the orphan to interrupted; a
replacement claims and gets a DIFFERENT run id; and there is still exactly one
running row, with the dead one left as interrupted rather than deleted.

Two guard tests either side of it: a LIVE run is never swept, and the sweep is
idempotent (second pass flips zero rows).

Marked integration; skips cleanly with no DSN.

  pytest tests/doc07/test_integration_claim_recycle.py   6 passed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… callback

The two pieces §112 assigns the harness and that were never built:
"the registered tool functions ... thin wrappers over the other docs' APIs" and
"every dispatched workroom is an asyncio.create_task(...) with a done-callback".

Built against the SDK contract pinned last session from claude_agent_sdk 0.2.128
AND the live reference at code.claude.com/docs/en/agent-sdk/custom-tools.

THE TOOL mirrors workroom.drafts.make_propose_change_tool: factory-per-query,
closing over the trusted host's db and this meeting's id.

  return shape   {"content":[{"type":"text","text":json.dumps(...)}]} — the Python
                 @tool decorator forwards only content and is_error, so a
                 machine-readable result must ride as JSON text. A test asserts the
                 returned keys are a subset of {content, is_error}.
  NO annotations readOnlyHint stays at its default false. The hint controls PARALLEL
                 batching, and this tool claims an operation_runs row and starts
                 real work — two batched dispatches would race the partial unique
                 index and one would silently lose. Commented at the factory and
                 asserted.
  description    NAMES every args.get() parameter it reads. Tool search is on by
                 default and DEFERS SDK MCP tools, so this string is what the model
                 sees first. A test greps args.get() out of the source and fails on
                 any name that is neither in the schema nor in the description —
                 the exact gap drafts.py has, where `files` is read but never named.

meeting_id is BOUND, never from args, and there is no parameter through which one
could be supplied. Two tests: a model emitting meeting_id/notes_ref/tenant_id is
ignored (the bundle still carries the bound meeting), and none of those names is in
the input schema at all.

Faults return a COMPOSED reason, not a raw exception. The SDK already converts an
uncaught exception into an error result, so "nothing throws" proves nothing — what
matters is the message Claude reads. Parametrised over db-down, sandbox-unavailable
and a raising clock, each asserting the reason states nothing was started and what
to do, and is not a bare exception string.

A declined cost gate is NOT an error: dispatch_workroom returns a DispatchDecision
with no run_id, the tool reads the absence of run_id, and returns accepted=false
with a budget reason.

RUN_AND_NOTIFY is §112's create_task + done-callback. It holds strong refs in a
module-level set because asyncio keeps only a WEAK reference — an unheld task can
be collected mid-run and vanish, the same bug provisioner.py:367 guards. Tests
drop every local reference, gc.collect(), and assert completion still arrives; a
second asserts the set releases afterwards so it is not a leak.

The done-callback is SYNC and never awaits — it runs on the event loop, so it may
only hand off. A test greps the callback body for `await`.

task.exception() non-None should be unreachable (Doc 05 Rule 6) but is never
swallowed: a synthesised failed Envelope naming the task is handed to the callback
and the violation is logged at ERROR. Empty receipts, because there is nothing to
cite when the run never returned. Cancellation takes the same path. A callback that
itself raises is logged and does not lose the task.

Status is forwarded unchanged across all five EnvelopeStatus values — the dispatch
layer never improves on what the Workroom reported (Law 2).

  pytest tests/doc04/test_dispatch_tool_wrapper.py   25 passed
  ruff / mypy --strict                               clean

Next commit wires the mount and the two sinks; app.py stays unmounted until then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… sinks

Completes the live wire. The tool wrapper landed last commit; this mounts it and
gives its completion somewhere to go.

THE MOUNT. live_brain._build_dispatch_server builds the SDK MCP server bound to
THIS meeting, and _build_servers merges it with the code_intel server so the wake
turn mounts both. Same shape as code_intel: factory-per-query, fail-closed. Without
a db handle or a run loop there is nothing to dispatch into, so the tool is simply
not mounted and the turn degrades to having no dispatch verb rather than mounting
one that cannot work.

meeting_id is bound from the runtime's own header, so the model cannot supply one.
run_task hands the claimed bundle to Doc 05's SessionDriver -- the driver that
existed, was complete, and had no production caller until now.

THE TWO SINKS, in a module of their own because the difference is not cosmetic:

  live_sink          the room is still there. put_nowait a MeetingEvent so Proxy
                     re-wakes and the wake turn delivers through the Emitter
                     (§3.2: the runtime delivers the done-moment, nothing polls).
  post_meeting_sink  the bot has left. No room, no Emitter, no wake turn -- so the
                     envelope goes to Doc 07's task record via run_final_gate, and
                     the draft card surfaces it. Post-meeting does NOT mount the
                     tool; there is no wake turn to call it.

Both are SYNCHRONOUS because they run inside an asyncio done-callback on the event
loop. The live sink only put_nowait's. The post-meeting sink schedules its write
with ensure_future and holds the handle in a module set -- an unheld task can be
collected mid-write, the same weak-reference trap run_and_notify guards.

Neither sink can lose the run. A full queue or a failed write is logged, not
raised: the run itself already completed and its envelope is durable on the
operation_runs row, so losing the NOTIFICATION must never look like losing the
WORK.

Cost stays where it was: the live path threads cost + estimate_usd through
dispatch_workroom so check_meeting_budget governs it, and post-meeting keeps its
own task_cost_ceiling check in run_dispatch. Neither double-gates.

  pytest tests/doc04   2 failed, 249 passed -- the SAME two baseline ids
                       (provisioner-boot, stt-refresh), pre-existing. 249 = the
                       baseline's 224 plus this work's 25.
  ruff / mypy --strict clean

Worth recording: those two baseline failures SKIP without a DSN and FAIL with one.
A run that forgets TEST_DATABASE_URL therefore looks greener than the truth.

app.py is still NOT mounting the approve route -- that stays last, after the
end-to-end tests prove the chain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…zily

The ordering risk was real, and it was total: dispatch_workroom did not mount on
any live meeting.

assemble_live_brain builds the wake turn at :501, which is what mounts the MCP
servers, and only calls build_run_loop at :515. It cannot do it the other way
round -- build_run_loop takes the wake adapter that building the wake turn
produces. So at mount time runtime.run_loop is None, always. _build_dispatch_server
read it eagerly, treated None as "nothing to dispatch into", and declined to
mount. Nothing raised. Nothing logged. Proxy simply had no dispatch verb, forever,
and every test I had written passed because each one handed the mount a runtime
with the loop already attached.

Reordering the assembly is not available, so the fix is to stop needing the loop
at mount time:

  * live_sink now takes resolve_run_loop: Callable[[], Any] instead of a run loop,
    and calls it at COMPLETION time. A task completion necessarily happens long
    after assembly, so a late read always sees the built loop.
  * the mount now requires only the db handle. No db really does mean nothing to
    dispatch into.
  * every not-mounted path is LOUD. Missing db logs at ERROR naming what will not
    work; a failed build logs the exception. Silence was the whole defect.
  * a missing or queue-less run loop at completion time raises RunLoopUnavailable
    into the log at ERROR, and a full queue logs too -- both say the envelope is
    still durable on the operation_runs row, so losing the NOTIFICATION never
    reads as losing the WORK.

TEST 11 (tests/doc04/test_dispatch_live_parity.py, 9 tests) asserts the mount in
the real assembly order rather than a hand-arranged one, which is the only reason
it catches this:

  - the tool mounts with run_loop=None                     <- the regression itself
  - assemble_live_brain's source still has build_wake_turn before build_run_loop,
    so if someone reorders it the lazy resolution gets revisited deliberately
  - _build_dispatch_server's body does not read run_loop before the sink
  - missing loop / queue-less loop / full queue each log at ERROR
  - end to end: tool call -> run -> completion -> MeetingEvent on the queue with
    ask_id == the task id, status and receipts unchanged, with the run loop built
    AFTER the dispatch

  pytest tests/doc04/test_dispatch_live_parity.py   9 passed
  ruff / mypy --strict clean

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Last commit's message recorded that doc04's two baseline failures skip without
TEST_DATABASE_URL and fail with it, so a run that forgets the DSN looks greener
than the truth. This closes that.

It is the same class of hazard as the two defects that shipped unit-green on this
branch. operation_ref held the wrong uuid; planned_at was read though the column
never existed. In all three cases the suite reported success while the substrate
was never asked anything -- twice because a fake answered instead of Postgres, and
once because nothing asked at all.

The gate lives in tests/conftest.py as a pytest_runtest_makereport wrapper: when
no DSN is resolvable, a skip whose own reason blames the database is rewritten to
a failure, with a message that says how to fix it. Doing it at the report layer
rather than by editing each gate matters twice over --

  * it catches EVERY form the gate takes. Module-level skipif marks (doc04/e2e,
    doc03/e2e, doc08), a fixture's pytest.skip (doc05, doc04's bundle-dispatch and
    reconcile-lifecycle), an in-body call. Seven files in doc04 alone, and the next
    one someone writes is covered for free.
  * it needs no guess about which tests are store-backed. The test already told us,
    in its own skip reason. No allowlist to drift.

It fires ONLY when there is no DSN anywhere, so with one set every existing gate
keeps its own semantics untouched -- doc03's suites additionally require
DOC03_STORE_SPEC_DB because their DB carries a divergent schema, and those skips
still skip.

The one escape hatch, PROXY_TESTS_ALLOW_NO_DB=1, turns the failures back into
skips for a contributor with no local Postgres who wants the unit tiers. It has to
be typed on purpose. It is a TEST-harness switch, not a product feature flag, so
doc00 section 7's zero-runtime-flags rule does not apply to it.

Every run now ends with one line stating whether the substrate was exercised,
because the trap is a human reading "N passed" and assuming the database was
involved.

The session-scoped dsn fixture moves to the root conftest (it FAILS rather than
skips, same rule) and tests/doc07/conftest.py plus the two local dsn fixtures in
tests/doc07 are deleted -- one mechanism, one place.

  doc04 + doc07, no DSN      24 failed, 465 passed, 34 errors   <- the honest state
  doc04 + doc07, waived      465 passed, 58 skipped
  doc04 + doc07, with DSN    2 failed, 521 passed   <- the SAME two baseline ids
                             (provisioner-boot, stt-refresh), unchanged
  ruff clean (tests run with --no-force-exclude; ruff excludes tests/ by default)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The approve route has always taken a dispatch callable and there was never anything
to pass, so dispatch=None raised WorkroomDispatchUnavailable and the route answered
202 dispatch_blocked. §112's wrapper exists now, so the refusal is gone and
harness.post_meeting.wire.make_plan_dispatcher is what goes in that parameter.

WHAT THE WIRE MODULE JOINS. Every piece already existed and was verified in
isolation; the defect was that no line of production code connected them.

  run_dispatch       B6's decision -- approval, then caps, then cost -- injected
                     with the real assemble_bundle and a real workroom dispatcher,
                     which is what its docstring always said it needed.
  dispatch_workroom  Doc 04's operation_runs claim (P10 keying).
  SessionDriver      Doc 05's driver. Complete, and constructed only in
                     tests/doc05/* until this commit. This is its first caller on
                     the post-meeting path.
  run_and_notify     §112's create_task + done-callback, with the strong reference.
  post_meeting_sink  where the terminal envelope lands when there is no meeting to
                     deliver it into: Doc 07's task record via B8's final gate.

THE CLAIM AND THE RUN ARE PAIRED. dispatch_workroom only CLAIMS; it does not
execute. That split is deliberate in Doc 04 -- the claim is the durable fact, the
execution is the work -- but it means a caller that claims and stops leaves a row at
'running' that nothing ever finishes. That dead end is precisely what the old
refusal existed to avoid creating, so the claim and SessionDriver are joined in one
place, and the handle comes back to B6 so operation_ref gets the run row's real id.

THE ROUTE'S CONNECTION IS IGNORED, deliberately and with a comment saying so.
handle_approve_plan runs inside the route's `async with db.acquire()`, released the
moment the route returns; the dispatched work outlives the request, so it takes the
db handle bound at install time and acquires its own. Using the route's connection
is a use-after-release that only shows up under load.

THE ASK IS THE APPROVED PLAN, not the raw extracted item. What the human read is
what may run. An APPROVED row with no plan is logged as a broken task record rather
than dispatched.

post_meeting_sink now resolves draft_row_for / bundle_exists INSIDE its scheduled
coroutine and awaits them if awaitable. Not a convenience: the draft row is a
staged_drafts read against Postgres and a sync callback on the event loop cannot do
one. Resolving eagerly would need the row before the run finished, which is
impossible -- the draft_id only exists once the task has proposed it. Its done
callback now also logs a failed write instead of discarding the handle silently.

THE ROUTE, after the write:
  * dispatch=None            202, dispatch_blocked, ERROR log naming the fix. A
                             missing dispatcher is now a wiring fault in whoever
                             mounted the route, not a property of the system.
  * dispatch raises          202, approved, "remains APPROVED". 202 not 500 on
                             purpose: a 500 invites a retry of the APPROVAL, which
                             would 409 on the PLANNED check and read as "your click
                             did not work" when it did.
  Neither path unwinds the approval. A human's decision is durable whatever happens
  downstream of it, and APPROVED is a re-dispatchable state.

WorkroomDispatchUnavailable is deleted along with the three tests that asserted the
refusal -- a class documenting an unbuilt dependency that IS built is a lie in the
code. Same for the stale docstrings in post_meeting/dispatch.py and live_brain's
"without a db handle or a run loop" (the run loop is explicitly not part of that
test any more).

  pytest tests/doc07/test_seam2_plan_approval.py     20 passed
  pytest tests/doc04 tests/doc07                     2 failed, 521 passed
        523 collected; the SAME two baseline ids (provisioner-boot, stt-refresh).
        Net zero test count: 4 refusal-path tests out, 4 dispatch tests in.
  ruff / mypy --strict clean

app.py still does NOT mount the route. That stays last, after tests 7/8/10.

docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md:94 still describes the refusal and is
now stale -- it gets rewritten when the route is mounted, not before, because until
then the gap is only half closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bic head

Daksh's origin/main and this branch each added a 0009 whose down_revision was
0008_substrate_schema_gaps. That gives 0008 two children and alembic two heads.

The dangerous part is not the collision, it is that GIT CANNOT SEE IT. The
filenames differ, so a merge succeeds silently, no conflict marker appears, and
the breakage surfaces later as `alembic upgrade head` refusing to run with
"Multiple head revisions are present". A double head is not a merge conflict; it
is a clean merge that produces an un-migratable tree.

Proven before the fix, using alembic's own RevisionMap over the merged 13-revision
set (the same computation `alembic heads` prints):

  HEADS -> ('0012_post_meeting_planned_at', '0009_repo_maps')
  children of 0008_substrate_schema_gaps -> ['0009_post_meeting_tasks', '0009_repo_maps']

THE FIX. Ours re-parents onto his and renumbers, so the chain is linear and his
migration keeps the 0009 slot it already holds on the branch that will be merged
INTO. Four git mvs, detected as pure renames:

  0009_post_meeting_tasks        -> 0010_post_meeting_tasks        (down: 0009_repo_maps)
  0010_clarify_items             -> 0011_clarify_items
  0011_staged_drafts_status_check-> 0012_staged_drafts_status_check
  0012_post_meeting_planned_at   -> 0013_post_meeting_planned_at

VERIFIED VIA THE CLI, against a scratch tree carrying his 0009_repo_maps beside
our renumbered chain (his file is NOT added to this branch -- nothing is merged
here, only made mergeable):

  alembic heads     0013_post_meeting_planned_at (head)      <- exactly one
  alembic history   linear, <base> -> 0001 ... 0009_repo_maps -> 0010 ... -> 0013

  alembic upgrade head, on a FRESH database (proxy_mergecheck, 0 tables before):
    alembic_version   0013_post_meeting_planned_at, 1 row
    17 tables, and BOTH branches' tables coexist --
      repo_maps (his) + post_meeting_tasks, clarify_items (ours)
    the last two migrations' EFFECTS present, not just the version stamp:
      post_meeting_tasks.planned_at + post_meeting_tasks_planned_sweep_idx (0013)
      staged_drafts_status_enum CHECK constraint (0012)

  pytest tests/doc07 against that fresh database   263 passed
  ruff clean

ALSO: test_b8_final_gate now locates the status-check migration by SUFFIX glob
instead of by hard-coded number, asserting exactly one match. The numeric prefix
is not stable -- pinning the digits meant a pure re-parenting broke a test that
has nothing to do with ordering, and it would break again on the next renumber.
It was the only reference to our revision filenames anywhere outside migrations/.

This is the first step of the integration order and the only one that fails
silently, which is why it goes first. Nothing else is merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…production

The seam site has existed since B1: scribe_runtime calls _run_post_meeting_intake
immediately after run_close_pass returns. What never existed was anything to put in
CloseConfig.post_meeting_intake. So on every production close the hook was None,
the seam returned at its first line, and run_extract / run_triage / run_clarify /
run_plan had NO PRODUCTION CALLER AT ALL. Every action item in every meeting
silently failed to become a task.

It stayed invisible because it had the same shape as the defects before it. A wire
that looks connected, is exercised only by tests that supply the missing piece
themselves, and says nothing when it no-ops. tests/doc07/test_seam1_close_intake.py
constructs CloseConfig(..., post_meeting_intake=<something>) in every single case.

THREE PARTS.

1. THE SUPPLIER. post_meeting.wire.make_intake_hook(db) returns hook(final_notes, *,
   meeting_id) -- the exact shape the seam invokes. The tenant is resolved
   SERVER-SIDE from the meetings row; the hook signature has no tenant parameter, so
   a caller cannot supply one. caller/call_external default to the real Anthropic
   structured caller and the one libs.http funnel, so production passes only db.
   Wired at server._build_close_config, the ONLY production construction site.

2. THE SILENCE. A missing hook now logs at ERROR naming the meeting, the
   consequence ("no action item from this meeting will become a task"), and the fix
   ("pass post_meeting_intake=make_intake_hook(db)"). Still not raised: Doc 07 §2
   requires the close and the record to be identical whether or not post-meeting
   execution exists. The cost of being wrong is one ERROR line; the cost of being
   quiet was the entire feature.

3. THE TEST THAT WOULD HAVE CAUGHT IT. tests/doc07/test_seam1_production_wiring.py
   injects no hook. It asks the real _build_close_config whether the field is set,
   and drives the real hook through real Postgres to assert task rows land with the
   tenant from the meeting row.

   It runs the builder in a SUBPROCESS, and that is the load-bearing part. settings
   parses env once at import and caches it; importing the real boot module in-process
   under the fake env its gate demands poisons that cache for the rest of the run.
   The first draft did exactly that and FLIPPED TWO GENUINELY FAILING doc04 boot
   tests TO PASSING -- doc07+doc04 reported 529 passed while doc04 alone reported 2
   failed. Evicting sys.modules afterwards did not fix it either. A test that hides
   two real failures to prove one wire is a bad trade, so the probe is isolated by a
   process boundary instead.

  doc04 alone                   2 failed, 258 passed
  this file + doc04             2 failed, 264 passed   <- same 2; no masking
  doc07 + doc04                 2 failed, 527 passed   <- same 2
  the two are the known ids (provisioner-boot, stt-refresh), unchanged
  ruff / mypy --strict clean

Two of my own bugs found writing this, both worth naming: the fake call_external
raised instead of running the op (failing triage for the wrong reason and hiding
whether the wiring worked), and the fake triage caller returned a bare tier instead
of the verdicts array the schema requires. The fake now reads item_refs back OUT of
the prompt rather than reconstructing them, so it cannot drift from extract's real
ref format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
origin/main dissolved the `services/harness` member (47712ca) and cut the live
meeting path over to the new `services/in-meeting` engine (6a6f4a8), deleting the
RunLoop spine and the old live brain on the way (d00c158). This merge moves Doc 07
onto that ground.

RENAMES ACCEPTED FROM GIT, NOT HAND-MOVED. All 18 relocations came back as
"CONFLICT (file location)" with git's own suggested destination; every one was a
pure relocation with no content conflict, so each was accepted as-is:

  services/harness/src/harness/post_meeting/*.py  (16 files)
  services/harness/src/harness/dispatch_sinks.py
  services/harness/src/control_plane/plan_approval_route.py
      -> services/control-plane/src/control_plane/...

scribe_runtime.py and server.py auto-merged through git's rename detection, so
SEAM 1's call site and its brand-new supplier landed at the moved path untouched.

THE FOUR JUDGEMENT CALLS.

* config/defaults.toml -- adjacency only, both sections kept. His bare rejoin_*
  keys are placed BEFORE our [post_meeting] header, because a bare key after a
  section header belongs to that section: appending ours first would have silently
  reparented his two keys into [post_meeting]. Verified by parsing the result.

* app.py -- his install_meetings_route kept; our block hand-merged last, after the
  package underneath was in place. Its text is REWRITTEN rather than carried over,
  because the reason it stated is now false: the approve route was blocked on §112's
  wrapper, and §112 exists. It stays unmounted on a different and narrower ground --
  the end-to-end tests through the route were never written.

* live_brain.py -- resolved as a DELETE. Git's modify/delete default left our
  version in the tree; keeping it would have resurrected a module Daksh retired
  deliberately.

* THE LIVE DISPATCH PATH IS A DEAD END, and is recorded as one rather than repaired.
  Our §112 work re-woke Proxy by putting the terminal Envelope on run_loop.queue.
  There is no run_loop. live_sink and the tool mount move, UNWIRED and imported by
  nothing, to control_plane/live_dispatch_deadend.py; their nine tests move to
  tests/doc04/test_live_dispatch_deadend.py and skip at module level.

  That module records what this merge must not decide: in_meeting.trigger's
  on_worker_done is the structural replacement seam (same concept -- "a pure tap: a
  finished background worker wakes the loop to deliver its result"), but choosing
  between our durable operation_runs claim and his warm in-meeting sandbox toolbelt
  is a founder decision nobody has made. Note SessionDriver has no production caller
  on EITHER side. Porting the code would have silently made that choice.

  The POST-MEETING dispatch path is untouched and still live: it never used the run
  loop, because after a meeting there is no room to deliver into.

IMPORTS. Tests and sources rewritten harness.* -> control_plane.*, including the
hardcoded source paths in the AC-PME-15 "this module never writes staged_drafts"
assertions. The SEAM 1 subprocess probe now inherits the parent's sys.path instead
of assuming cwd, since the member it imports has moved once already.

MIGRATIONS. The renumber holds with his 0009_repo_maps genuinely in the tree:
`alembic heads` -> 0013_post_meeting_planned_at, exactly one; `upgrade head` clean
on a fresh database (proxy_postmerge).

SEALED BUNDLE UNTOUCHED. docs/07*, acceptance/doc07/ are byte-identical to
pre-merge. The four deleted v2-* docs and the doc03 criteria edit are his, verified
absent on origin/main rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Doc 07 adds post-meeting execution from close-record intake through triage, clarification, planning, approval, Workroom dispatch, reporting, and staged-draft handling. It adds PostgreSQL persistence, production wiring, acceptance artifacts, and unit and integration tests.

Changes

Post-meeting execution

Layer / File(s) Summary
Contracts, acceptance, and persistence
acceptance/doc07/*, product/v0-spec/*, migrations/versions/*, config/defaults.toml, libs/llm/*
Defines Doc 07 requirements and criteria. Adds task, clarification, staged-draft, and planning persistence. Adds structured Anthropic output support and post-meeting limits.
Intake, triage, clarification, and planning
services/control-plane/src/control_plane/post_meeting/{models,extract,triage,clarify,plan,intake,store,config}.py
Extracts action items, assigns tiers, records unresolved ownership, persists clarification items, generates plans, expires stale plans, and returns guarded intake results.
Approval and Workroom dispatch
services/control-plane/src/control_plane/{plan_approval_route,dispatch,dispatch_sinks,live_dispatch_deadend}.py, services/control-plane/src/control_plane/post_meeting/{approval,dispatch,wire}.py
Adds named-human approval, concurrency and cost gates, canonical Workroom dispatch, asynchronous completion handling, and production close wiring.
Reporting, drafts, and outcomes
services/control-plane/src/control_plane/post_meeting/{report,final_gate,outcome,owner_actions}.py, services/control-plane/src/control_plane/accept_route.py
Classifies and delivers reportable outcomes. Validates proposed drafts without repository writes. Records accept, change-request, reject, downgrade, and plan-edit outcomes.
Integration validation
tests/doc07/*, tests/doc04/*, tests/conftest.py
Adds broad unit, seam, dispatch-wrapper, and real-Postgres coverage. The test configuration requires a database or an explicit waiver for store-backed tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CloseRuntime
  participant IntakeHook
  participant PostMeetingPipeline
  participant PostgreSQL
  participant PlanApprovalRoute
  participant Workroom
  participant ReportDelivery

  CloseRuntime->>IntakeHook: invoke with finalized notes and meeting ID
  IntakeHook->>PostMeetingPipeline: extract, triage, clarify, and plan
  PostMeetingPipeline->>PostgreSQL: persist task lifecycle records
  PlanApprovalRoute->>PostgreSQL: record named-human approval
  PlanApprovalRoute->>Workroom: dispatch approved task
  Workroom-->>ReportDelivery: return completion envelope
  ReportDelivery->>PostgreSQL: record draft and task outcome
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary post-meeting execution work and the accompanying merge onto main.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/doc04-112-workroom-dispatch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 65

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@acceptance/doc07/criteria/criteria.yaml`:
- Around line 763-771: Update the schema_gap note in the criteria configuration
to acknowledge that migration 0012_staged_drafts_status_check.py adds the
staged_drafts_status_enum CHECK constraint. Remove the stale recommendation to
add a CHECK constraint, and preserve the distinction between AC-PME-15-NEG and
AC-PME-07-NEG.
- Around line 451-465: Replace obsolete services/harness citations with the
current control-plane locations and update the runtime/entrypoint scan while
preserving the existing blocker: in acceptance/doc07/criteria/criteria.yaml
lines 451-465 and 511-520, acceptance/doc07/dependency_manifest.yaml lines
28-41, and acceptance/doc07/manifest.yaml lines 71-82, reference
services/control-plane/src/control_plane/meeting_runtime.py:53 and
dispatch.py:51 for MeetingRuntime and assemble_bundle, and ensure the evidence
reflects the current absence of a no-media worker entrypoint.

In `@acceptance/doc07/dependency_manifest.yaml`:
- Around line 28-41: Update the blocked_dependencies entry for the missing
meeting_runtime worker to reflect the current tree: replace the stale
services/harness ownership and
services/harness/src/harness/meeting_runtime.py:54 evidence with a valid
services/in-meeting path and resolved file:line citation, or state “not found by
this method” when no current citation can be established. Apply the same
correction to the corresponding entry in criteria.yaml.

In `@acceptance/doc07/manifest.yaml`:
- Around line 28-38: Update the manifest’s mandatory_faults count from 58 to 60,
matching the 60 unique fault_model_refs declared by criteria.yaml while leaving
the other counts unchanged.

In `@docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md`:
- Line 56: Update the fenced code block around the ASCII chain diagram in
DOC04-WORKROOM-DISPATCH-UNWIRED.md to specify the text language, using a text
fence while preserving the diagram content.
- Around line 94-96: Update DOC04-WORKROOM-DISPATCH-UNWIRED.md to state that the
approval route records the approval and returns 202 dispatch_blocked with “no
dispatcher is configured” when no dispatcher exists, rather than raising
WorkroomDispatchUnavailable. Replace the dispatch module reference with
services/control-plane/src/control_plane/post_meeting/dispatch.py, and revise
closure steps 1–2 to reference the existing make_dispatch_workroom_server and
make_plan_dispatcher implementations, focusing remaining work on mounting the
server, wiring the dispatcher into the application, and adding end-to-end proof.

In `@libs/llm/src/llm/structured.py`:
- Line 25: Optionally update the import in structured.py to source Awaitable and
Callable from collections.abc instead of typing, while retaining Any and
Protocol from typing; no other changes are required.

In `@migrations/versions/0010_post_meeting_tasks.py`:
- Around line 111-112: Update migrations/versions/0010_post_meeting_tasks.py
lines 111-112 to add a BEFORE UPDATE FOR EACH ROW trigger and function that
assign now() to NEW.updated_at for post_meeting_tasks, and drop both in
downgrade(). Apply the same trigger/function maintenance to
migrations/versions/0011_clarify_items.py lines 63-64 for clarify_items,
including matching cleanup in downgrade().

In `@migrations/versions/0012_staged_drafts_status_check.py`:
- Around line 44-60: Update upgrade() to remove the _ENUM module constant and
replace the f-string in the staged_drafts_status_enum ALTER TABLE statement with
the literal status values directly in the CHECK clause. Preserve the existing
normalization statement and constraint behavior.

In `@migrations/versions/0013_post_meeting_planned_at.py`:
- Around line 51-54: Update the index definition in the migration to match the
sweep query filters: use (tenant_id, planned_at) for tenant-scoped lookups while
retaining the state = 'PLANNED' predicate. Do not include meeting_id, since the
reader does not filter by it.

In `@product/v0-spec/05-WORKROOM.md`:
- Line 311: The amendment notes use stale hard-coded line numbers; replace them
with durable section anchors. In product/v0-spec/05-WORKROOM.md at lines
311-311, update the P8 note to cite §2.5.1, §3.8, and §3.13.6 instead of lines
56, 309, and 368. In product/v0-spec/07-POST-MEETING-EXECUTION.md at lines
172-172, remove line numbers while retaining the existing Doc 05 §2 and §3.8
anchors and the code-sample reference as a section anchor.
- Line 56: Update the staged-draft gate wording around propose_change, including
the matching phrasing at the later occurrence, to distinguish the persisted
staged_drafts row status from the returned envelope status: the row remains
status='proposed', while the return carries status='needs_review' alongside
draft_id. Preserve the existing no-landing and human-approval behavior.

In `@product/v0-spec/06-PROACTIVE.md`:
- Around line 47-53: Add the `text` language tag to the fenced verdict-schema
block in the proactive specification, and apply the same fenced-block language
annotation to the corresponding schema in Doc 07. Preserve the schema contents
unchanged.
- Around line 143-145: Update the clarify_items ownership wording in Doc 06,
including the schema note and the repeated ownership statement, to state that
both Doc 06 and Doc 07 write rows, with Doc 07 also completing them. Preserve
the existing question, kind, blocking_ref, urgency, answer, and answered_by
field details.

In `@product/v0-spec/07-POST-MEETING-EXECUTION.md`:
- Line 134: Update both §4 references, including the D07.1 CANONICAL-DECISIONS
text, to remove the withdrawn “meeting_runtime worker without a media session”
hosting model. State instead that SessionDriver resolves the run from (db,
meeting_id) and that no new deployable is introduced, while preserving the
surrounding execution and durability requirements.

In `@product/v0-spec/AMENDMENTS-06-07.md`:
- Line 5: Update the patch-count references in the document introduction and
checklist summary: change all three occurrences claiming eight patches—including
the line 5 and line 22 statements and the “five of the eight patches” wording
near line 133—to reflect the nine patches defined by P1 through P9. Do not
modify any other content.

In `@product/v0-spec/CANONICAL-DECISIONS.md`:
- Line 224: Update the §2 DDL comment referenced near the `meeting_id` schema
definition to remove the stale `task_id` association and state that `scope_id`
contains the `meeting_id` only. Keep the existing P10 ruling and UUID/type
requirements unchanged, including the documented `meeting_id::text` cast at the
atomic claim call site.

In `@services/control-plane/src/control_plane/accept_route.py`:
- Around line 271-286: Update the docstring in
services/control-plane/src/control_plane/accept_route.py:271-286 to reference
control_plane.post_meeting.outcome instead of harness.post_meeting.outcome. In
migrations/versions/0010_post_meeting_tasks.py:18-20, replace the obsolete
services/harness/src/harness/dispatch.py:129-145 citation with the current
file:line location of the workroom:{task_id} dispatch wrapper, or state not
found by this method if it cannot be located.
- Line 115: Update install_accept_route and install_reject_route to accept
post_meeting_outcome and pass it into their route handlers, then update app.py
to mount both routes with a sink adapter that invokes the configured
meeting-outcome service. Add tests covering both mounted HTTP paths and verify
accepted or rejected tasks are no longer left in DRAFTED.
- Around line 178-184: Update the outcome handling around _post_meeting_outcome
in the accept and decline flows to skip task closure when
applied.already_applied or declined.already_applied is true. Wire the mounted
routes with a synchronous adapter that invokes the appropriate asynchronous
record_accept or record_changes_requested operation and waits for its
completion, rather than passing the coroutine-returning methods directly to
dispatch_sinks.post_meeting_sink.

In `@services/control-plane/src/control_plane/dispatch.py`:
- Around line 227-235: Update the _DISPATCH_TOOL_DESCRIPTION text to describe
dispatching work to run in the background rather than naming the internal
“Workroom” component. Preserve the existing guidance about when to use the tool,
its arguments, asynchronous task_id response, and repository behavior, while
removing all user-visible internal component names.
- Around line 316-317: Update the exception handler around the completion
callback in the dispatch callback flow to catch Exception instead of
BaseException, preserving logging for callback failures such as RuntimeError
while allowing KeyboardInterrupt and SystemExit to propagate.
- Around line 287-291: Move the asyncio and logging imports, along with the log
logger initialization, from run_and_notify to module scope beside the existing
imports. Remove the per-call imports and logger creation from run_and_notify,
while preserving its existing task-scheduling behavior.
- Around line 391-411: The successful cost-gated path in the dispatch flow must
expose the claimed run identifier. Update dispatch_workroom and the surrounding
handling so a successful call returns or resolves a WorkroomHandle containing
run_id before the existing run_id check; preserve the None path for budget
refusals, then continue to run_task and run_and_notify with the resolved
identifier.

In `@services/control-plane/src/control_plane/live_dispatch_deadend.py`:
- Around line 130-135: Update the preserved import in the revival path around
live dispatch to reference make_dispatch_workroom_server from
control_plane.dispatch instead of harness.dispatch, and extend the restoration
checklist near the existing live_sink and mount-point items to record this
required import correction.

In `@services/control-plane/src/control_plane/plan_approval_route.py`:
- Around line 19-23: Update the docstring references near the dispatch wiring
and the corresponding references at the later noted section to use
control_plane.post_meeting.wire.make_plan_dispatcher instead of
harness.post_meeting.wire.make_plan_dispatcher. Keep the described dispatch
behavior unchanged.
- Around line 253-260: Update _write_approved to return the cursor’s
affected-row count after the guarded UPDATE, rather than always returning None.
In handle_approve_plan, treat a zero count as a lost race and return HTTP 409;
only report approved=True and 200 when the update affects a row.
- Around line 229-260: Update _load_task_row and _write_approved to use asyncpg
asynchronously: await fetchrow and execute directly on the pool/connection with
$1-style parameters, and make both helpers async. Update their callers in the
approval route to await them, preserving the existing tenant/state checks and
single-statement approval update.

In `@services/control-plane/src/control_plane/post_meeting/__init__.py`:
- Around line 3-6: Update the module docstring in the post_meeting package to
replace the stale services/harness and harness-hosting references with
services/control-plane and its control-plane host service. Preserve the existing
meeting_runtime, no-new-deployable, and AC-REPO-006 justification while naming
the relocated package’s actual service.

In `@services/control-plane/src/control_plane/post_meeting/clarify.py`:
- Around line 182-189: Update run_clarify and PostMeetingTaskStore.set_state to
accept and use tenant_id, constraining the task-state update WHERE clause to
both tenant_id and task_id. Validate task_id before the existing try block, and
make set_state fail when no row is updated so clarification insertion cannot
proceed for an unmatched task.
- Around line 166-173: Normalize nullable owner and text values in the
item-processing loop of run_clarify before converting them to strings. Map a
None owner to UNRESOLVED and a None text value to an empty string, while
preserving existing non-null values and downstream assess behavior.

In `@services/control-plane/src/control_plane/post_meeting/config.py`:
- Around line 16-38: Update load_post_meeting_config to validate each resolved
limit after type coercion: max_concurrent_tasks, max_tasks_per_meeting,
task_cost_ceiling, and plan_expiry must be positive. For any non-positive value,
use that key’s value from _FALLBACK while preserving valid configured values and
the existing draft_tier_enabled behavior.
- Around line 41-49: Replace the deep libs.db.src.* and libs.llm.src.* imports
across the post-meeting modules with the repository’s canonical db and llm
workspace import seams; in config.py, use db.config.load_defaults, and apply the
corresponding canonical imports in plan.py, triage.py, and wire.py. In wire.py,
import the HTTP facade from libs.http.external, preserving existing behavior and
exposing fallback observability through db.config.load_defaults if required.

In `@services/control-plane/src/control_plane/post_meeting/dispatch.py`:
- Around line 176-208: Prevent failed Workroom dispatches from leaving tasks
stuck in RUNNING: update the flow around assemble_bundle, store.set_state, and
workroom_dispatch so RUNNING is written only after a successful handle with a
non-null run_id, or explicitly revert the task to APPROVED before returning
ERROR from both the dispatch exception and missing-run_id branches. Preserve the
existing error outcomes and ensure no failed claim retains a concurrency slot.

In `@services/control-plane/src/control_plane/post_meeting/extract.py`:
- Around line 176-180: Replace BaseException with Exception in run_extract at
services/control-plane/src/control_plane/post_meeting/extract.py:176-180,
_record at
services/control-plane/src/control_plane/post_meeting/outcome.py:77-83, and
_dispatch at
services/control-plane/src/control_plane/post_meeting/wire.py:158-163. Preserve
the existing error-result and logging behavior while allowing cancellation and
process-shutdown exceptions to propagate.

In `@services/control-plane/src/control_plane/post_meeting/final_gate.py`:
- Around line 17-19: Update migration references in
services/control-plane/src/control_plane/post_meeting/final_gate.py lines 17-19
and its comment at line 36 from 0011 to 0012_staged_drafts_status_check; in
tests/doc07/_support.py lines 26-29, 41, and 150 from 0009 to
0010_post_meeting_tasks; and in tests/doc07/test_b5_approval.py lines 3-6 from
0009's CHECK and trigger to 0010_post_meeting_tasks. Use suffix-based migration
names in all prose.
- Around line 107-124: Add a validated no-push audit record at the
Workroom/provider boundary, capturing the actual operations and token scopes
associated with each dispatch. Update dispatch_sinks.py’s production path and
_observe_chunk/Envelope flow to preserve this evidence, then pass the record
explicitly to run_final_gate and assert_no_repo_writes instead of relying on
empty defaults.

In `@services/control-plane/src/control_plane/post_meeting/intake.py`:
- Around line 116-131: Update the triage-to-run_clarify mapping in the list
comprehension so has_scope and has_done_condition use independent evidence from
extraction or triage rather than the shared Tier.QUESTION check. Preserve
separate owner, scope, and done-condition signals so run_clarify blocks any item
missing either condition and prevents underspecified items from reaching
planning.
- Around line 147-166: The exception handlers in run_intake and
run_intake_guarded must re-raise asyncio.CancelledError before handling other
BaseException values, preserving cancellation propagation to the close task.
Update both “total”/“never raises” docstrings and related comments to explicitly
exclude cancellation while retaining existing handling for non-cancellation
failures.

In `@services/control-plane/src/control_plane/post_meeting/plan.py`:
- Around line 154-175: Update run_plan’s generate_structured error handling to
catch any Exception, not only StructuredOutputError, record the exception in
result.error, log the failed item, and return the result so one task’s failure
does not stop processing remaining items in the batch.
- Around line 207-251: Update expire_stale_plans to require a tenant_id and
validate that every task row belongs to that tenant before calling
store.set_outcome; reject or raise on mismatches so cross-tenant rows cannot be
expired. Preserve the existing state, expiry, skip, and error handling for rows
that pass the tenant check, and use the row’s established tenant identifier
field.

In `@services/control-plane/src/control_plane/post_meeting/report.py`:
- Around line 84-85: Define _STATUS_CEILING independently from
CONFIDENCE_BY_STATUS using explicit per-status maximum values, rather than
copying the mapping. Keep status_rank and build_report unchanged so the
AC-PME-14 assertion compares the computed confidence against an independently
declared ceiling and can detect upward rounding.
- Around line 285-295: Update the send loop around report delivery so failed
sends do not add their key to the caller-provided already_delivered/seen set;
only add the key after a successful send. Preserve card surfacing and error
recording for failures, using a separate tracking set if needed to prevent
duplicate card entries without suppressing future delivery retries.

In `@services/control-plane/src/control_plane/post_meeting/store.py`:
- Around line 116-135: Require tenant scoping for all task reads, including
planned_tasks_for_sweep, get, and task_id_for_draft. Do not allow None to
produce cross-tenant results; require a tenant_id predicate in each query or
fail closed with an explicit runtime guard when the fetched row’s tenant_id
differs from the caller’s tenant. Preserve the existing task and draft lookup
behavior for matching tenants.

In `@services/control-plane/src/control_plane/post_meeting/triage.py`:
- Around line 190-208: Make run_triage honor its “never raises” contract by
moving load_post_meeting_config into the protected error-handling path and
broadening handling around build_prompt to catch malformed configuration or item
data, including TypeError and ValueError. On either failure, return the existing
empty TriageResult so items remain untiered, while preserving the current
successful structured-generation behavior.

In `@services/control-plane/src/control_plane/post_meeting/wire.py`:
- Around line 245-269: Update _dispatch around run_and_notify so task-scheduling
failures after dispatch_workroom claims the run are caught, recorded through the
existing failure mechanism, and the unsubmitted coroutine is closed to avoid an
unawaited-coroutine warning. Preserve normal run_and_notify behavior and return
the handle on successful scheduling.

In `@services/control-plane/src/control_plane/scribe_runtime.py`:
- Line 872: Update the hook assignment near the meeting-intake handling to use
direct access via close_config.post_meeting_intake instead of getattr with a
fallback, preserving the existing hook behavior.
- Around line 861-900: Update both run_intake and run_intake_guarded to re-raise
asyncio.CancelledError before their broad BaseException handlers, preserving
cancellation instead of swallowing it. In _run_post_meeting_intake, wrap the
complete await hook(final_notes, meeting_id=meeting_id) operation in an
asyncio.timeout using the project’s configured intake timeout, so database
acquisition and downstream LLM work are bounded; retain the existing failure
logging for timeout and other non-cancellation errors.

In `@tests/doc04/test_dispatch_tool_wrapper.py`:
- Around line 216-240: The sandbox failure path in
test_faults_return_a_composed_actionable_reason expects “Nothing was started”
even though dispatch_workroom has already claimed an operation_runs row. Update
the production flow around dispatch_workroom and its outer error handling to
release the claim when task startup fails, or accurately distinguish post-claim
failures in the composed reason; then revise this test’s assertion to match the
persisted-state behavior while retaining actionable guidance.
- Around line 101-109: Remove the vacuous getattr assertion in
test_no_annotations_so_readonlyhint_stays_false, or replace it with an assertion
against the tool’s serialized shape that directly verifies readOnlyHint is
false. Keep the existing annotations is None structural check if no serialized
representation is available.
- Around line 254-288: Update
test_the_cost_gate_declining_is_not_an_error_but_is_not_accepted to remove the
unused GatedDB class and use the monkeypatch fixture to replace
mod.dispatch_workroom, eliminating manual restoration. Also update the dispatch
handling to distinguish accepted and declined DispatchDecision results using
their dispatched status rather than inferring from run_id or handle presence.

In `@tests/doc07/_support.py`:
- Around line 295-313: Update the AST scanning logic in the surrounding helper
to use a single ast.walk(tree) pass: retain the existing import, name, and
attribute collection, remove the no-op ast.Constant branch, and handle string
constants from ast.Call arguments inline during that same traversal. Preserve
the current behavior of collecting only call-argument strings.

In `@tests/doc07/test_b2_triage.py`:
- Around line 173-185: Update
test_ac_pme_06_each_violated_condition_drops_a_tier so its parametrization
reflects actual triage behavior: since run_triage uses draft_conditions_met and
ignores failed_conditions, reduce the test to one representative met=False case
and explicitly assert failed_conditions is advisory only, or modify the
triage/verdict flow to record and use failed_conditions before retaining the
per-condition parametrization.

In `@tests/doc07/test_b3_clarify.py`:
- Around line 227-240: Fix
test_ac_pme_04_neg_malformed_channel_set_sends_nothing so it asserts an
observable delivery outcome instead of the never-written sent list: remove the
ineffective sent assertion and verify res.outcomes[0].delivery_deferred is False
and routed_to is None. Keep the existing pending-state and task-state
assertions, and align the test with run_clarify’s current interface since it
accepts no sender.

In `@tests/doc07/test_b6_dispatch.py`:
- Around line 127-129: Update the banned-token check in the test around lowered
source so the “class .*queue” pattern is evaluated as a regular expression via
re.search, or replace it with the intended literal token. Preserve the existing
checks for “sandboxprovider(”, “e2b.”, and “scheduler(”.

In `@tests/doc07/test_b7_report.py`:
- Around line 233-235: Replace the hard-coded source path in
tests/doc07/test_b7_report.py:233-235 by importing
control_plane.post_meeting.report and resolving pathlib.Path(report.__file__).
Apply the same change in tests/doc07/test_seam2_plan_approval.py:227-229, using
the existing control_plane.plan_approval_route module import (m) and
pathlib.Path(m.__file__).

In `@tests/doc07/test_integration_claim_recycle.py`:
- Line 157: Replace suffix-based sweep count assertions around the integration
test’s sweep results with exact status-string comparisons: compare swept to
"UPDATE 1", the zero-row result at the later assertion to "UPDATE 0", and
first/second to "UPDATE 1"/"UPDATE 0" respectively. Preserve the existing
assertion messages and test flow.

In `@tests/doc07/test_integration_db.py`:
- Around line 14-17: Update the test documentation to use asyncpg exception
terminology, including references to asyncpg.exceptions.* and non-integrity
RaiseError. Revise the migration references from 0011 to
0012_staged_drafts_status_check.py and from 0012 to
0013_post_meeting_planned_at.py, while preserving the staged_drafts_status_enum
constraint name.

In `@tests/doc07/test_owner_actions.py`:
- Around line 151-157: Replace the dynamic __import__ expression in the
expire_stale_plans call with a module-level import of PostMeetingConfig,
matching the existing pattern in test_b4_plan.py. Use the imported
PostMeetingConfig directly when constructing the config argument, leaving the
test behavior unchanged.

In `@tests/doc07/test_seam1_close_intake.py`:
- Around line 139-159: Update
test_ac_pme_02_a_raising_intake_never_reaches_the_close and the
_run_post_meeting_intake contract to distinguish cancellation from other
BaseException failures: add asyncio.CancelledError coverage that asserts
cancellation propagates, while retaining the existing expectation that
RuntimeError, ConnectionRefusedError, KeyboardInterrupt, and MemoryError are
swallowed. Ensure the implementation does not absorb task cancellation.
- Around line 219-235: Replace the tautological assertions in
test_ac_pme_02_neg_run_intake_itself_never_raises and
test_ac_pme_02_neg_guarded_wrapper_returns_none_on_total_failure with checks
that garbage or invalid dependencies produce a failed result rather than silent
success, and verify the result identifies the failed intake stage. Preserve the
guarded wrapper’s documented None outcome if applicable, but when it returns a
result assert failure and the corresponding stage name.

In `@tests/doc07/test_seam3_outcome.py`:
- Around line 151-156: Add an asynchronous test case alongside
test_route_guard_passes_the_action_and_actor that injects an async sink into
_post_meeting_outcome, awaits the route helper as required, and asserts the
recorded action, draft_id, and who values are observed. Keep the existing
synchronous-shape coverage intact while ensuring coroutine sinks are awaited.
- Around line 70-92: Update test_all_three_terminal_states_are_now_reachable so
the DISCARDED case is produced through an actual plan-expiry function or B8
final-gate refusal, rather than calling store.set_outcome directly; retain the
assertion that all three terminal states are reachable. If no real path can be
exercised here, instead rename the test and narrow its docstring and expected
states to the two outcomes written by record_accept and
record_changes_requested.
- Line 21: Replace the dynamic __import__("datetime") expression used to define
NOW with a normal typed datetime import, while preserving the existing UTC
datetime value and strict mypy type checking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ae2dc963-ff79-4342-a31f-a7d42af51025

📥 Commits

Reviewing files that changed from the base of the PR and between 4b26e8b and dc4298c.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (65)
  • CLAUDE.md
  • acceptance/doc07/criteria/criteria.yaml
  • acceptance/doc07/dependency_manifest.yaml
  • acceptance/doc07/manifest.yaml
  • acceptance/doc07/requirements/requirements.yaml
  • config/defaults.toml
  • docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md
  • libs/llm/pyproject.toml
  • libs/llm/src/llm/structured.py
  • migrations/versions/0010_post_meeting_tasks.py
  • migrations/versions/0011_clarify_items.py
  • migrations/versions/0012_staged_drafts_status_check.py
  • migrations/versions/0013_post_meeting_planned_at.py
  • product/v0-spec/05-WORKROOM.md
  • product/v0-spec/06-PROACTIVE.md
  • product/v0-spec/07-POST-MEETING-EXECUTION.md
  • product/v0-spec/AMENDMENTS-06-07.md
  • product/v0-spec/CANONICAL-DECISIONS.md
  • product/v0-spec/PLATFORM-ADOPTION.md
  • product/v0-spec/SPINE-REGISTER.md
  • services/control-plane/src/control_plane/accept_route.py
  • services/control-plane/src/control_plane/app.py
  • services/control-plane/src/control_plane/dispatch.py
  • services/control-plane/src/control_plane/dispatch_sinks.py
  • services/control-plane/src/control_plane/live_dispatch_deadend.py
  • services/control-plane/src/control_plane/plan_approval_route.py
  • services/control-plane/src/control_plane/post_meeting/__init__.py
  • services/control-plane/src/control_plane/post_meeting/approval.py
  • services/control-plane/src/control_plane/post_meeting/clarify.py
  • services/control-plane/src/control_plane/post_meeting/config.py
  • services/control-plane/src/control_plane/post_meeting/dispatch.py
  • services/control-plane/src/control_plane/post_meeting/extract.py
  • services/control-plane/src/control_plane/post_meeting/final_gate.py
  • services/control-plane/src/control_plane/post_meeting/intake.py
  • services/control-plane/src/control_plane/post_meeting/models.py
  • services/control-plane/src/control_plane/post_meeting/outcome.py
  • services/control-plane/src/control_plane/post_meeting/owner_actions.py
  • services/control-plane/src/control_plane/post_meeting/plan.py
  • services/control-plane/src/control_plane/post_meeting/report.py
  • services/control-plane/src/control_plane/post_meeting/store.py
  • services/control-plane/src/control_plane/post_meeting/triage.py
  • services/control-plane/src/control_plane/post_meeting/wire.py
  • services/control-plane/src/control_plane/scribe_runtime.py
  • services/control-plane/src/control_plane/server.py
  • tests/conftest.py
  • tests/doc02/test_m1_invite_launches_real_bot.py
  • tests/doc04/test_dispatch_tool_wrapper.py
  • tests/doc04/test_live_dispatch_deadend.py
  • tests/doc07/_support.py
  • tests/doc07/test_b1_extract.py
  • tests/doc07/test_b2_triage.py
  • tests/doc07/test_b3_clarify.py
  • tests/doc07/test_b4_plan.py
  • tests/doc07/test_b5_approval.py
  • tests/doc07/test_b6_dispatch.py
  • tests/doc07/test_b7_report.py
  • tests/doc07/test_b8_final_gate.py
  • tests/doc07/test_integration_claim_recycle.py
  • tests/doc07/test_integration_db.py
  • tests/doc07/test_owner_actions.py
  • tests/doc07/test_seam1_close_intake.py
  • tests/doc07/test_seam1_production_wiring.py
  • tests/doc07/test_seam2_plan_approval.py
  • tests/doc07/test_seam3_outcome.py
  • tools/linux-verify-requirements.txt

Comment on lines +451 to +465
blocked:
status: BLOCKED
blocking_dependency: "meeting_runtime worker runnable with no media session"
dependency_owner: services/harness
evidence: >-
services/harness/src/harness/meeting_runtime.py:54 MeetingRuntime is media-coupled by
construction: it owns `carrier`, a live HearingStage (`_hearing`), the Scribe consumer,
a consent_gate, and an STT credential-refresh loop. No no-media entrypoint and no
worker `main()` exist — grep for '__main__|def main(' across services/harness/src/harness
returns zero hits (audit 2026-07-27).
consequence: >-
Doc 07 §3.5 requires the Workroom package to run "in a meeting_runtime worker with no
media session — no transport, no Scribe, no tile" and asserts "No new deployable".
Today there is no way to start that worker without media. Either a no-media mode is
added to MeetingRuntime, or Doc 07 §3.5 is amended.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether services/harness still exists on this branch.
fd -t d 'harness' services || echo 'no services/harness directory'
fd -t f 'meeting_runtime.py' services
# Find the relocated dispatch/assemble_bundle definition.
rg -n --type=py 'def assemble_bundle'
# List every stale citation in the acceptance bundle.
rg -n 'services/harness|harness\.dispatch|harness/dispatch' acceptance product

Repository: dakshparikh12/proxy

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- relevant files ---'
git ls-files \
  acceptance/doc07/criteria/criteria.yaml \
  acceptance/doc07/dependency_manifest.yaml \
  acceptance/doc07/manifest.yaml \
  services/control-plane/src/control_plane/meeting_runtime.py \
  services/in-meeting \
  services/harness

printf '%s\n' '--- current service directories ---'
fd -t d -d 3 . services | sort | head -200

printf '%s\n' '--- runtime and dispatch symbols ---'
rg -n --glob '*.py' 'class MeetingRuntime|def main\(|if __name__|def assemble_bundle|assemble_bundle|harness\.dispatch|services/harness|services/in-meeting|services/control-plane' services acceptance/doc07

printf '%s\n' '--- acceptance references with context ---'
rg -n -C 3 'services/harness|harness\.dispatch|harness/dispatch|assemble_bundle|meeting_runtime' acceptance/doc07

Repository: dakshparikh12/proxy

Length of output: 16840


🏁 Script executed:

#!/bin/bash
set -u
for f in \
  acceptance/doc07/criteria/criteria.yaml \
  acceptance/doc07/dependency_manifest.yaml \
  acceptance/doc07/manifest.yaml \
  services/control-plane/src/control_plane/meeting_runtime.py
do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    wc -l "$f"
    case "$f" in
      acceptance/doc07/criteria/criteria.yaml) sed -n '440,565p' "$f" ;;
      acceptance/doc07/dependency_manifest.yaml) sed -n '20,78p' "$f" ;;
      acceptance/doc07/manifest.yaml) sed -n '63,88p' "$f" ;;
      services/control-plane/src/control_plane/meeting_runtime.py) sed -n '1,130p' "$f" ;;
    esac
  else
    printf 'not found: %s\n' "$f"
  fi
done

Repository: dakshparikh12/proxy

Length of output: 18891


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- MeetingRuntime lifecycle and worker entrypoints ---'
ast-grep outline services/control-plane/src/control_plane/meeting_runtime.py
rg -n -C 4 'def __post_init__|def start\(|def aclose\(|carrier|_hearing|start_meeting_scribe|consent_gate|stt_refresh' services/control-plane/src/control_plane/meeting_runtime.py
printf '%s\n' '--- main entrypoints in current service source ---'
rg -n --glob '*.py' '(^|[[:space:]])def main\(|if __name__\s*==\s*["'\'']__main__["'\'']' services
printf '%s\n' '--- current dispatch definitions and surrounding references ---'
sed -n '40,80p' services/control-plane/src/control_plane/dispatch.py
sed -n '118,155p' services/control-plane/src/control_plane/dispatch.py
printf '%s\n' '--- all stale acceptance citations, including inherited negative criteria ---'
rg -n -C 1 'services/harness|harness\.dispatch|harness/dispatch' acceptance/doc07

Repository: dakshparikh12/proxy

Length of output: 29003


Replace obsolete Doc 07 path citations. The current clone has no services/harness directory. MeetingRuntime is at services/control-plane/src/control_plane/meeting_runtime.py:53, and assemble_bundle is at services/control-plane/src/control_plane/dispatch.py:51.

Update all affected references in acceptance/doc07/criteria/criteria.yaml, acceptance/doc07/dependency_manifest.yaml, and acceptance/doc07/manifest.yaml. Retain the blocker, but ground it in the current runtime and entrypoint scan.

📍 Affects 3 files
  • acceptance/doc07/criteria/criteria.yaml#L451-L465 (this comment)
  • acceptance/doc07/criteria/criteria.yaml#L511-L520
  • acceptance/doc07/dependency_manifest.yaml#L28-L41
  • acceptance/doc07/manifest.yaml#L71-L82
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acceptance/doc07/criteria/criteria.yaml` around lines 451 - 465, Replace
obsolete services/harness citations with the current control-plane locations and
update the runtime/entrypoint scan while preserving the existing blocker: in
acceptance/doc07/criteria/criteria.yaml lines 451-465 and 511-520,
acceptance/doc07/dependency_manifest.yaml lines 28-41, and
acceptance/doc07/manifest.yaml lines 71-82, reference
services/control-plane/src/control_plane/meeting_runtime.py:53 and
dispatch.py:51 for MeetingRuntime and assemble_bundle, and ensure the evidence
reflects the current absence of a no-media worker entrypoint.

Source: Coding guidelines

Comment on lines +763 to +771
notes:
schema_gap: >-
The staged_drafts.status column is plain text with DEFAULT 'proposed' and NO CHECK
constraint enumerating proposed|accepted|rejected|applied
(migrations/versions/0001_substrate.py:134-144, audit 2026-07-27). CANONICAL §4
line 125 documents the enum in a comment only. This criterion therefore asserts the
value explicitly rather than relying on the database to reject a wrong one, and a
CHECK constraint is recommended as a follow-up so the enum is enforced where it is
declared.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the staged_drafts status CHECK constraint landed in migration 0012.
fd -t f '0012_staged_drafts_status_check.py' migrations | xargs -r cat -n
# Show the original staged_drafts definition the note cites.
fd -t f '0001_substrate.py' migrations --exec rg -n -C4 'staged_drafts|status'

Repository: dakshparikh12/proxy

Length of output: 6459


🏁 Script executed:

#!/bin/bash
set -eu
criteria="$(fd -t f 'criteria.yaml' acceptance | head -n 1)"
printf '%s\n' "== $criteria: schema_gap note =="
sed -n '755,775p' "$criteria"
printf '%s\n' "== $criteria: AC-PME-07-NEG =="
sed -n '1098,1115p' "$criteria"
printf '%s\n' "== $criteria: AC-PME-15-NEG =="
sed -n '1362,1380p' "$criteria"
printf '%s\n' "== references to staged_drafts_status_enum and schema_gap =="
rg -n -C2 'staged_drafts_status_enum|schema_gap|AC-PME-07-NEG|AC-PME-15-NEG' "$criteria"

Repository: dakshparikh12/proxy

Length of output: 3364


🏁 Script executed:

#!/bin/bash
set -eu
criteria='acceptance/doc07/criteria/criteria.yaml'
printf '%s\n' "== $criteria: schema_gap note =="
sed -n '755,775p' "$criteria"
printf '%s\n' "== $criteria: AC-PME-07-NEG =="
sed -n '1098,1115p' "$criteria"
printf '%s\n' "== $criteria: AC-PME-15-NEG =="
sed -n '1362,1380p' "$criteria"
printf '%s\n' "== relevant references =="
rg -n -C2 'staged_drafts_status_enum|schema_gap|AC-PME-07-NEG|AC-PME-15-NEG' "$criteria"

Repository: dakshparikh12/proxy

Length of output: 3696


Update the schema_gap note. Migration 0012_staged_drafts_status_check.py adds the staged_drafts_status_enum CHECK constraint. AC-PME-15-NEG asserts this behavior; AC-PME-07-NEG concerns a different database constraint. Remove the stale follow-up recommendation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acceptance/doc07/criteria/criteria.yaml` around lines 763 - 771, Update the
schema_gap note in the criteria configuration to acknowledge that migration
0012_staged_drafts_status_check.py adds the staged_drafts_status_enum CHECK
constraint. Remove the stale recommendation to add a CHECK constraint, and
preserve the distinction between AC-PME-15-NEG and AC-PME-07-NEG.

Comment on lines +28 to +41
# ── Dependencies the audit found ABSENT (build blockers, not verification deps) ──
blocked_dependencies:
- missing: meeting_runtime worker runnable with no media session
state: ABSENT
owner: services/harness
evidence: >-
services/harness/src/harness/meeting_runtime.py:54 — MeetingRuntime is media-coupled
by construction (carrier, HearingStage, Scribe consumer, consent_gate, STT refresh
loop). No no-media entrypoint; no worker main() in services/harness/src/harness.
blocks: [AC-PME-09, AC-PME-09-NEG, AC-PME-10, AC-PME-10-NEG]
note: >-
Doc 07 §3.5 requires this worker and simultaneously asserts "No new deployable".
Either MeetingRuntime gains a no-media mode or §3.5 is amended. This is the only
remaining external blocker on the bundle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Re-ground the blocked-dependency evidence on the current tree.

This entry cites services/harness/src/harness/meeting_runtime.py:54 and dependency_owner: services/harness. This PR removes the harness member and relocates the service to services/in-meeting. A file:line citation that no longer resolves violates the grounded-or-silent rule. Re-cite the current path, or state not found by this method. The same stale citations appear in acceptance/doc07/criteria/criteria.yaml; see the consolidated comment.

As per coding guidelines: "Ground claims in the current clone with file:line, or state not found by this method."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acceptance/doc07/dependency_manifest.yaml` around lines 28 - 41, Update the
blocked_dependencies entry for the missing meeting_runtime worker to reflect the
current tree: replace the stale services/harness ownership and
services/harness/src/harness/meeting_runtime.py:54 evidence with a valid
services/in-meeting path and resolved file:line citation, or state “not found by
this method” when no current citation can be established. Apply the same
correction to the corresponding entry in criteria.yaml.

Source: Coding guidelines

Comment on lines +28 to +38
counts:
authority_clauses: 16 # Doc 07 Appendix C seeds
requirements: 16
derived_obligations: 0 # none derived; every criterion traces to an Appendix C seed
criteria_total: 30
positive_criteria: 16
negative_criteria: 14
blocking_criteria: 30
golden_path_criteria: 4
mandatory_faults: 58
tests_and_evaluations: 30 # test_ids declared; not yet authored

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Count fault_model_refs entries and criteria in the Doc 07 bundle.
python3 - <<'PY'
import yaml, pathlib
p = pathlib.Path("acceptance/doc07/criteria/criteria.yaml")
docs = yaml.safe_load(p.read_text(encoding="utf-8"))
total = sum(len(c.get("fault_model_refs") or []) for c in docs)
print("criteria:", len(docs))
print("fault_model_refs total:", total)
print("positives:", sum(1 for c in docs if not c["criterion_id"].endswith("-NEG")))
print("negatives:", sum(1 for c in docs if c["criterion_id"].endswith("-NEG")))
PY

Repository: dakshparikh12/proxy

Length of output: 274


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- criteria file ---'
nl -ba acceptance/doc07/criteria/criteria.yaml | sed -n '1,240p'
printf '%s\n' '--- manifest counts ---'
nl -ba acceptance/doc07/manifest.yaml | sed -n '20,45p'

Repository: dakshparikh12/proxy

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- criteria file ---'
sed -n '1,260p' acceptance/doc07/criteria/criteria.yaml
printf '%s\n' '--- manifest counts ---'
sed -n '20,45p' acceptance/doc07/manifest.yaml

Repository: dakshparikh12/proxy

Length of output: 13172


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

path = Path("acceptance/doc07/criteria/criteria.yaml")
criterion = None
counts = {}
total = 0

for line in path.read_text(encoding="utf-8").splitlines():
    if line.startswith("- criterion_id: "):
        criterion = line.split(": ", 1)[1].strip()
        counts[criterion] = 0
    elif line.startswith("  - F-"):
        if criterion is None:
            raise SystemExit("fault_model_refs entry has no criterion")
        counts[criterion] += 1
        total += 1

positive = [key for key in counts if not key.endswith("-NEG")]
negative = [key for key in counts if key.endswith("-NEG")]

print("criteria:", len(counts))
print("positive criteria:", len(positive))
print("negative criteria:", len(negative))
print("fault_model_refs total:", total)
print("positive fault_model_refs:", sum(counts[key] for key in positive))
print("negative fault_model_refs:", sum(counts[key] for key in negative))
print("criteria with counts other than 2:", [
    (key, counts[key]) for key in counts if counts[key] != 2
])
PY

Repository: dakshparikh12/proxy

Length of output: 373


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

path = Path("acceptance/doc07/criteria/criteria.yaml")
criterion = None
refs = {}
for line in path.read_text(encoding="utf-8").splitlines():
    if line.startswith("- criterion_id: "):
        criterion = line.split(": ", 1)[1].strip()
        refs[criterion] = []
    elif line.startswith("  - F-"):
        refs[criterion].append(line.strip()[2:])

all_refs = [ref for values in refs.values() for ref in values]
unique_refs = sorted(set(all_refs))
duplicates = sorted({ref for ref in all_refs if all_refs.count(ref) > 1})

print("reference entries:", len(all_refs))
print("unique fault IDs:", len(unique_refs))
print("duplicate fault IDs:", duplicates)
for criterion_id in ("AC-PME-07", "AC-PME-13"):
    print(criterion_id, len(refs[criterion_id]), refs[criterion_id])
PY

printf '%s\n' '--- AC-PME-07 and AC-PME-13 context ---'
grep -n -A85 -B5 -E '^[-] criterion_id: AC-PME-07$|^[-] criterion_id: AC-PME-13$' acceptance/doc07/criteria/criteria.yaml

Repository: dakshparikh12/proxy

Length of output: 8245


Set mandatory_faults to 60. criteria.yaml contains 60 unique fault_model_refs: 32 positive and 28 negative criteria. The manifest value 58 is incorrect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acceptance/doc07/manifest.yaml` around lines 28 - 38, Update the manifest’s
mandatory_faults count from 58 to 60, matching the 60 unique fault_model_refs
declared by criteria.yaml while leaving the other counts unchanged.

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Awaitable, Callable, Protocol

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🌐 Web query:

Does ruff rule UP035 flag importing Callable and Awaitable from typing instead of collections.abc?

💡 Result:

Yes, Ruff rule UP035 (deprecated-import) flags imports of Callable and Awaitable from the typing module when they should be imported from collections.abc [1][2]. This rule is designed to enforce the use of modern, standard-library locations for various classes and types, as dictated by Python's evolution (such as PEP 585) [1][3]. Because these changes can have implications for code running on older Python versions, Ruff's behavior is version-aware: 1. Version Sensitivity: The rule respects the --target-version configuration [4][1]. For example, the rewrite for typing.Callable to collections.abc.Callable was historically excluded for Python 3.9.0 and 3.9.1 due to bugs in those specific versions [2][5]. 2. Scope: UP035 specifically flags the imports themselves (e.g., from typing import Callable) [6][7]. It does not necessarily flag usages of the fully qualified name (e.g., typing.Callable) directly in the code, as the rule is currently implemented to target import statements [6][8]. 3. Relationship with Other Rules: While UP035 handles these imports, other rules like UP006 (non-pep585-annotation) handle the replacement of these types in type annotations (e.g., changing List[int] to list[int]) [1][8]. Due to ongoing development, there can occasionally be overlaps or inconsistencies in how these rules are applied across different Python versions [8]. In summary, if you are targeting a modern Python version, Ruff will suggest moving Callable and Awaitable from typing to collections.abc via UP035 [1][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -t f 'structured\.py|pyproject\.toml|ruff\.toml|\.ruff\.toml' . | head -80
printf '%s\n' '--- import and package metadata ---'
rg -n -C 3 'from typing import Any, Awaitable, Callable, Protocol|requires-python|target-version|select|extend-select|UP035' \
  --glob 'structured.py' --glob 'pyproject.toml' --glob 'ruff.toml' --glob '.ruff.toml' .
printf '%s\n' '--- ruff availability ---'
if command -v ruff >/dev/null 2>&1; then
  ruff --version
else
  echo 'ruff: not installed'
fi

Repository: dakshparikh12/proxy

Length of output: 8090


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- configured Ruff result ---'
ruff check libs/llm/src/llm/structured.py
printf '%s\n' '--- explicit UP035 result ---'
ruff check --select UP035 libs/llm/src/llm/structured.py
printf '%s\n' '--- rule explanation ---'
ruff rule UP035 | sed -n '1,100p'

Repository: dakshparikh12/proxy

Length of output: 1383


Use collections.abc for Awaitable and Callable as an optional cleanup.

UP035 is not enabled in the configured Ruff rule set, so this change is not required for the current lint configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/llm/src/llm/structured.py` at line 25, Optionally update the import in
structured.py to source Awaitable and Callable from collections.abc instead of
typing, while retaining Any and Protocol from typing; no other changes are
required.

Source: Coding guidelines

Comment on lines +139 to +159
@pytest.mark.parametrize(
"boom",
[RuntimeError("intake exploded"), ConnectionRefusedError("db gone"),
KeyboardInterrupt(), MemoryError()],
ids=["runtime", "conn", "baseexc", "memory"],
)
async def test_ac_pme_02_a_raising_intake_never_reaches_the_close(boom):
"""The close's own guard: whatever class intake fails with, the close proceeds.

KeyboardInterrupt and MemoryError are included deliberately — they are BaseException,
not Exception, and a narrow `except Exception` would let them through into the close.
"""
async def exploding(final_notes, *, meeting_id):
raise boom

cfg = CloseConfig(
bucket=object(), bucket_name="b", post_chat_link=None,
post_meeting_intake=exploding,
)
# Must not raise.
await _run_post_meeting_intake(cfg, MEETING, notes())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add asyncio.CancelledError to the parametrization, or assert that it propagates.

The test pins that _run_post_meeting_intake swallows any BaseException. asyncio.CancelledError is a BaseException. If the close guard catches it too, a cancelled close task absorbs its own cancellation, the task keeps running past shutdown, and the event loop cannot stop it cleanly. The current cases (KeyboardInterrupt, MemoryError) do not distinguish the two behaviors.

Decide the contract and pin it. Cancellation should normally propagate while other BaseException classes are contained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_seam1_close_intake.py` around lines 139 - 159, Update
test_ac_pme_02_a_raising_intake_never_reaches_the_close and the
_run_post_meeting_intake contract to distinguish cancellation from other
BaseException failures: add asyncio.CancelledError coverage that asserts
cancellation propagates, while retaining the existing expectation that
RuntimeError, ConnectionRefusedError, KeyboardInterrupt, and MemoryError are
swallowed. Ensure the implementation does not absorb task cancellation.

Comment on lines +219 to +235
@pytest.mark.negative
async def test_ac_pme_02_neg_run_intake_itself_never_raises():
"""Even handed garbage, intake returns a result rather than propagating."""
res = await run_intake(
object(), meeting_id=MEETING, tenant_id=TENANT,
task_store=None, clarify_store=None, caller=None, call_external=None,
)
assert isinstance(res.ok, bool)


@pytest.mark.negative
async def test_ac_pme_02_neg_guarded_wrapper_returns_none_on_total_failure():
out = await run_intake_guarded(
notes(), meeting_id=MEETING, tenant_id=TENANT,
task_store=object(), clarify_store=object(), caller=None, call_external=None,
)
assert out is None or out.ok in (True, False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Both assertions are tautological and cannot fail.

TriageResult.ok-style properties return self.error is None, so isinstance(res.ok, bool) at line 226 is true for every possible outcome, including a silent success on garbage input. Line 235 is worse: out is None or out.ok in (True, False) holds for any value run_intake_guarded can return. Neither test would catch a regression.

Assert the intended contract: the call returns a failed result, and it names the stage that failed.

Proposed assertions
     res = await run_intake(
         object(), meeting_id=MEETING, tenant_id=TENANT,
         task_store=None, clarify_store=None, caller=None, call_external=None,
     )
-    assert isinstance(res.ok, bool)
+    assert res.ok is False, "garbage input produced a successful intake"
+    assert res.error is not None
+    assert res.failed_stage == "extract"
     out = await run_intake_guarded(
         notes(), meeting_id=MEETING, tenant_id=TENANT,
         task_store=object(), clarify_store=object(), caller=None, call_external=None,
     )
-    assert out is None or out.ok in (True, False)
+    assert out is None or out.ok is False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.mark.negative
async def test_ac_pme_02_neg_run_intake_itself_never_raises():
"""Even handed garbage, intake returns a result rather than propagating."""
res = await run_intake(
object(), meeting_id=MEETING, tenant_id=TENANT,
task_store=None, clarify_store=None, caller=None, call_external=None,
)
assert isinstance(res.ok, bool)
@pytest.mark.negative
async def test_ac_pme_02_neg_guarded_wrapper_returns_none_on_total_failure():
out = await run_intake_guarded(
notes(), meeting_id=MEETING, tenant_id=TENANT,
task_store=object(), clarify_store=object(), caller=None, call_external=None,
)
assert out is None or out.ok in (True, False)
`@pytest.mark.negative`
async def test_ac_pme_02_neg_run_intake_itself_never_raises():
"""Even handed garbage, intake returns a result rather than propagating."""
res = await run_intake(
object(), meeting_id=MEETING, tenant_id=TENANT,
task_store=None, clarify_store=None, caller=None, call_external=None,
)
assert res.ok is False, "garbage input produced a successful intake"
assert res.error is not None
assert res.failed_stage == "extract"
`@pytest.mark.negative`
async def test_ac_pme_02_neg_guarded_wrapper_returns_none_on_total_failure():
out = await run_intake_guarded(
notes(), meeting_id=MEETING, tenant_id=TENANT,
task_store=object(), clarify_store=object(), caller=None, call_external=None,
)
assert out is None or out.ok is False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_seam1_close_intake.py` around lines 219 - 235, Replace the
tautological assertions in test_ac_pme_02_neg_run_intake_itself_never_raises and
test_ac_pme_02_neg_guarded_wrapper_returns_none_on_total_failure with checks
that garbage or invalid dependencies produce a failed result rather than silent
success, and verify the result identifies the failed intake stage. Preserve the
guarded wrapper’s documented None outcome if applicable, but when it returns a
result assert failure and the corresponding stage name.


TENANT = uuid.uuid4()
MEETING = uuid.uuid4()
NOW = __import__("datetime").datetime(2026, 7, 28, tzinfo=__import__("datetime").timezone.utc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a normal import for datetime.

__import__("datetime") returns Any, which removes type checking from NOW under mypy --strict, and it is harder to read than a module import.

Proposed change
+from datetime import datetime, timezone
+
...
-NOW = __import__("datetime").datetime(2026, 7, 28, tzinfo=__import__("datetime").timezone.utc)
+NOW = datetime(2026, 7, 28, tzinfo=timezone.utc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NOW = __import__("datetime").datetime(2026, 7, 28, tzinfo=__import__("datetime").timezone.utc)
from datetime import datetime, timezone
NOW = datetime(2026, 7, 28, tzinfo=timezone.utc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_seam3_outcome.py` at line 21, Replace the dynamic
__import__("datetime") expression used to define NOW with a normal typed
datetime import, while preserving the existing UTC datetime value and strict
mypy type checking.

Comment on lines +70 to +92
async def test_all_three_terminal_states_are_now_reachable():
"""§3.9's full terminal set, each written by a real path."""
reachable = set()
for writer, expected in (
(record_accept, TaskState.ACCEPTED),
(record_changes_requested, TaskState.CHANGES_REQUESTED),
):
store = FakeTaskStore()
draft = uuid.uuid4()
tid = await _drafted(store, draft)
await writer(draft_id=draft, store=store)
reachable.add(store.rows[tid]["state"])
assert store.rows[tid]["state"] == expected.value
# DISCARDED is reached by plan expiry (B4) and by the final gate's refusal (B8).
store = FakeTaskStore()
tid = await _drafted(store, uuid.uuid4())
await store.set_outcome(tid, state=TaskState.DISCARDED, outcome="expired")
reachable.add(store.rows[tid]["state"])
assert reachable == {
TaskState.ACCEPTED.value,
TaskState.CHANGES_REQUESTED.value,
TaskState.DISCARDED.value,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The test does not show that DISCARDED is reachable by a real path.

The docstring says "each written by a real path". Lines 84-87 write DISCARDED by calling store.set_outcome directly from the test. That proves the store accepts the value, not that plan expiry or the final gate ever produces it. The comment at line 83 names the two real writers, so the assertion at lines 88-92 reads as coverage that does not exist.

Either drive DISCARDED through the plan-expiry function or the B8 gate, or narrow the docstring and the test name to the two states this test actually exercises.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_seam3_outcome.py` around lines 70 - 92, Update
test_all_three_terminal_states_are_now_reachable so the DISCARDED case is
produced through an actual plan-expiry function or B8 final-gate refusal, rather
than calling store.set_outcome directly; retain the assertion that all three
terminal states are reachable. If no real path can be exercised here, instead
rename the test and narrow its docstring and expected states to the two outcomes
written by record_accept and record_changes_requested.

Comment on lines +151 to +156
async def test_route_guard_passes_the_action_and_actor():
from control_plane.accept_route import _post_meeting_outcome

seen: list = []
_post_meeting_outcome(lambda **kw: seen.append(kw), "reject", "d7", "Priya")
assert seen == [{"action": "reject", "draft_id": "d7", "who": "Priya"}]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

This test locks in a synchronous sink shape.

The double is lambda **kw: seen.append(kw), which returns immediately. The real recorders record_accept and record_changes_requested are coroutine functions; lines 44 and 55 await them. This test therefore cannot detect the case where the injected sink is a coroutine function and _post_meeting_outcome never awaits it. See the comment on services/control-plane/src/control_plane/accept_route.py lines 178-184 for the root cause.

Add a case that passes a coroutine function as the sink and asserts the write-back is observed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_seam3_outcome.py` around lines 151 - 156, Add an
asynchronous test case alongside test_route_guard_passes_the_action_and_actor
that injects an async sink into _post_meeting_outcome, awaits the route helper
as required, and asserts the recorded action, draft_id, and who values are
observed. Keep the existing synchronous-shape coverage intact while ensuring
coroutine sinks are awaited.

@coderabbitai coderabbitai Bot 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.

Review continued from previous batch...


So the intended chain is fully specified:

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to this fenced block.

markdownlint reports MD040 for line 56. The block is an ASCII chain diagram, so text is enough.

📝 Proposed fix
-```
+```text
 wake turn (model emits the tool)
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md` at line 56, Update the fenced
code block around the ASCII chain diagram in DOC04-WORKROOM-DISPATCH-UNWIRED.md
to specify the text language, using a text fence while preserving the diagram
content.

Source: Linters/SAST tools

Comment on lines +94 to +96
| `control_plane/plan_approval_route.py` | **Built, tested, and deliberately NOT MOUNTED** — see below. If mounted it raises `WorkroomDispatchUnavailable` and returns **202 `dispatch_blocked`**, with the approval still landing. |
| `harness/post_meeting/dispatch.py` | Injected `assemble_bundle` / `workroom_dispatch`; never imports them. Callable once a real dispatcher exists. |
| `acceptance/doc07/` | **AC-PME-09, AC-PME-09-NEG, AC-PME-10, AC-PME-10-NEG stay BLOCKED**, and that is correct. The sealed `assurance_limits` records the blocker as the no-media worker; the real blocker is this, one layer down. Re-sealing is a founder action (`builder_writes: DENIED`). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does WorkroomDispatchUnavailable still exist? Is the approve route mounted? Where does post_meeting/dispatch.py live?
rg -n 'WorkroomDispatchUnavailable' -g '*.py'
rg -n 'install_approve_route' -g '*.py'
fd -t f 'dispatch.py' --full-path -x echo {}
rg -n 'make_dispatch_workroom_server|dispatch_workroom_tool' -g '*.py'

Repository: dakshparikh12/proxy

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(plan_approval_route\.py|dispatch\.py|live_dispatch_deadend\.py|test_seam2_plan_approval\.py|DOC04-WORKROOM-DISPATCH-UNWIRED\.md|dispatch\.py)$'
printf '%s\n' '--- target symbols and route wiring ---'
rg -n -C 3 'WorkroomDispatchUnavailable|install_approve_route|dispatch_workroom_tool|make_dispatch_workroom_server|no dispatcher is configured|Closing it|Closing It' \
  services control_plane harness tests docs 2>/dev/null || true

Repository: dakshparikh12/proxy

Length of output: 20086


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(plan_approval_route\.py|dispatch\.py|live_dispatch_deadend\.py|test_seam2_plan_approval\.py|DOC04-WORKROOM-DISPATCH-UNWIRED\.md)$'
printf '%s\n' '--- target symbols and route wiring ---'
rg -n -C 3 'WorkroomDispatchUnavailable|install_approve_route|dispatch_workroom_tool|make_dispatch_workroom_server|no dispatcher is configured|Closing it|Closing It' \
  services control_plane harness tests docs 2>/dev/null || true

Repository: dakshparikh12/proxy

Length of output: 20086


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- gap document ---'
sed -n '88,152p' docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md
printf '%s\n' '--- dispatcher implementation ---'
sed -n '1,28p;120,270p' services/control-plane/src/control_plane/post_meeting/wire.py
printf '%s\n' '--- dispatch wrapper ---'
sed -n '330,452p' services/control-plane/src/control_plane/dispatch.py
printf '%s\n' '--- route and application wiring ---'
sed -n '1,30p;112,145p;155,205p' services/control-plane/src/control_plane/plan_approval_route.py
sed -n '185,215p' services/control-plane/src/control_plane/app.py
printf '%s\n' '--- path/import references ---'
rg -n 'harness\.dispatch|harness\.post_meeting\.dispatch|control_plane\.post_meeting\.dispatch|post_meeting\.wire|make_plan_dispatcher' \
  services tests docs 2>/dev/null || true

Repository: dakshparikh12/proxy

Length of output: 27841


Update the DOC04 gap statement and closure steps.

  • Line 94: The route no longer raises WorkroomDispatchUnavailable. Without a dispatcher, it records the approval and returns 202 dispatch_blocked with "no dispatcher is configured" in detail.
  • Line 95: Replace harness/post_meeting/dispatch.py with services/control-plane/src/control_plane/post_meeting/dispatch.py.
  • Steps 1–2: make_dispatch_workroom_server already exists in services/control-plane/src/control_plane/dispatch.py, and make_plan_dispatcher already composes dispatch_workroom, SessionDriver, and run_and_notify in services/control-plane/src/control_plane/post_meeting/wire.py. Describe the remaining work as mounting the existing server, wiring the dispatcher into the application, and adding the required end-to-end proof.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/gaps/DOC04-WORKROOM-DISPATCH-UNWIRED.md` around lines 94 - 96, Update
DOC04-WORKROOM-DISPATCH-UNWIRED.md to state that the approval route records the
approval and returns 202 dispatch_blocked with “no dispatcher is configured”
when no dispatcher exists, rather than raising WorkroomDispatchUnavailable.
Replace the dispatch module reference with
services/control-plane/src/control_plane/post_meeting/dispatch.py, and revise
closure steps 1–2 to reference the existing make_dispatch_workroom_server and
make_plan_dispatcher implementations, focusing remaining work on mounting the
server, wiring the dispatcher into the application, and adding end-to-end proof.

Source: Linters/SAST tools


*Everything upstream that must change for Doc 06 (Proactive) and Doc 07 (Post-Meeting Execution) to be buildable, with exact anchors. Nothing here is architecture: 06 and 07 are frozen as written. This is the landing checklist.*

**Read this first.** Four decisions need a founder call (Part 1). Eight patches then apply mechanically (Part 2). Two things earlier reviews asked for turned out to be **already satisfied or wrong**, and are retracted with evidence (Part 4) — do not spend time on them. Nothing in Part 3 may be touched at all.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the patch count: the pack contains nine patches, not eight.

Line 5 and line 22 state "Eight patches". The body defines P1 through P9. Line 133 repeats "five of the eight patches". Update all three places so the checklist count matches the content.

📝 Proposed fix
-**Read this first.** Four decisions need a founder call (Part 1). Eight patches then apply mechanically (Part 2).
+**Read this first.** Four decisions need a founder call (Part 1). Nine patches then apply mechanically (Part 2).
-## Part 2 · The eight patches
+## Part 2 · The nine patches

Also applies to: 22-22

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@product/v0-spec/AMENDMENTS-06-07.md` at line 5, Update the patch-count
references in the document introduction and checklist summary: change all three
occurrences claiming eight patches—including the line 5 and line 22 statements
and the “five of the eight patches” wording near line 133—to reflect the nine
patches defined by P1 through P9. Do not modify any other content.

- The dispatch funnel's `meeting_id`-isolation, the WS gateway's `resolve_session`, and Tier-1 durability all read these tables.

### 11.2 · `meeting_id` type pinned = **UUID** everywhere in app tables (`meetings.id`, `meeting_cost.meeting_id`, `staged_drafts.meeting_id`, `transcript_segments.meeting_id`, `note_deltas.meeting_id`). **Only `operation_runs.scope_id` stays `text`** (it also holds workroom `task_id`s) — the atomic claim casts `meeting_id::text` at the call site. Update the §2/§3/§4 tables' `meeting_id` columns to `uuid`; document the one cast.
### 11.2 · `meeting_id` type pinned = **UUID** everywhere in app tables (`meetings.id`, `meeting_cost.meeting_id`, `staged_drafts.meeting_id`, `transcript_segments.meeting_id`, `note_deltas.meeting_id`). **Only `operation_runs.scope_id` stays `text`** — the atomic claim casts `meeting_id::text` at the call site. **(Amendment P10, 2026-07-27, ruling on C-A: `scope_id` holds the `meeting_id`, NOT the workroom `task_id`. The task id lives in `operation_type` as `workroom:{task_id}` and nowhere else. The parenthetical that used to read "it also holds workroom `task_id`s" was wrong and disagreed with the built path — `services/harness/src/harness/dispatch.py:129-145`, `libs/ops/src/ops/cost.py:323`. Doc 07 §3.5 is conformed to the same shape.)** Update the §2/§3/§4 tables' `meeting_id` columns to `uuid`; document the one cast.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Conform the §2 DDL comment to the P10 ruling.

Line 224 rules that scope_id holds the meeting_id and never the workroom task_id. The §2 DDL comment at line 72 still reads -- meeting_id | task_id. This file wins over every other doc, so the stale comment now contradicts the ruling one section above it. Extend the P10 instruction to also correct that comment.

📝 Proposed fix
-  scope_id          text NOT NULL,          -- meeting_id | task_id
+  scope_id          text NOT NULL,          -- meeting_id ONLY (P10); the task id lives in operation_type
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@product/v0-spec/CANONICAL-DECISIONS.md` at line 224, Update the §2 DDL
comment referenced near the `meeting_id` schema definition to remove the stale
`task_id` association and state that `scope_id` contains the `meeting_id` only.
Keep the existing P10 ruling and UUID/type requirements unchanged, including the
documented `meeting_id::text` cast at the atomic claim call site.

Comment on lines +391 to +411
outcome = await dispatch_workroom(
db, bundle, cost=cost, estimate_usd=estimate_usd
)
run_id = getattr(outcome, "run_id", None)
if run_id is None:
# The cost gate declined: no row was claimed, so there is nothing running.
return _tool_ok(
{
"accepted": False,
"reason": (
"The estimated cost exceeds this meeting's remaining task "
"budget, so nothing was started. Tell the room and ask "
"whether to spend it."
),
}
)
run_and_notify(
run_task(bundle, run_id=run_id),
task_id=task_id,
on_complete=on_complete,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every caller of dispatch_workroom and check how each detects the gate outcome.
set -uo pipefail

rg -nP --type=py -C 10 '\bdispatch_workroom\s*\(' services tests

# The DispatchDecision shape: does it carry a run_id at all?
rg -nP --type=py -C 12 'class DispatchDecision' libs services

Repository: dakshparikh12/proxy

Length of output: 28412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dispatch_workroom contract and implementation ---'
sed -n '130,225p' services/control-plane/src/control_plane/dispatch.py

printf '%s\n' '--- affected control-plane caller ---'
sed -n '340,430p' services/control-plane/src/control_plane/dispatch.py

printf '%s\n' '--- result types and cost gate ---'
sed -n '1,115p' libs/ops/src/ops/cost.py
rg -n -C 12 'estimate_usd|DispatchDecision|dispatched|WorkroomHandle|return decision|return handle' \
  services/control-plane/src/control_plane/dispatch.py libs/ops/src/ops/cost.py

printf '%s\n' '--- tests for gated dispatch ---'
sed -n '235,325p' tests/doc04/test_bundle_dispatch.py

Repository: dakshparikh12/proxy

Length of output: 42993


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

dispatch_path = Path("services/control-plane/src/control_plane/dispatch.py")
cost_path = Path("libs/ops/src/ops/cost.py")

dispatch = ast.parse(dispatch_path.read_text())
cost = ast.parse(cost_path.read_text())

def find_function(tree, name):
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
            return node
    raise SystemExit(f"not found by this method: {name}")

fn = find_function(dispatch, "dispatch_workroom")
tool = find_function(dispatch, "dispatch_workroom_tool")
gate = find_function(cost, "dispatch_workroom")

# Confirm the successful gated path returns the decision after claiming a row.
returns = [
    (node.lineno, ast.unparse(node.value))
    for node in ast.walk(fn)
    if isinstance(node, ast.Return)
]
assert any("decision" == value and line >= 199 for line, value in returns), returns
assert any("handle" == value and line >= 196 for line, value in returns), returns

# Confirm the tool reads run_id directly and treats absence as a budget refusal.
tool_source = ast.get_source_segment(dispatch_path.read_text(), tool)
assert tool_source is not None
assert "getattr(outcome, 'run_id', None)" in tool_source
assert "if run_id is None" in tool_source

# Confirm DispatchDecision has no run_id field and the cost gate's success result
# contains dispatched=True but no run_id.
decision = next(
    node for node in ast.walk(cost)
    if isinstance(node, ast.ClassDef) and node.name == "DispatchDecision"
)
fields = {
    node.targets[0].id
    for node in decision.body
    if isinstance(node, ast.AnnAssign)
    and isinstance(node.target, ast.Name)
}
assert "dispatched" in fields
assert "run_id" not in fields

gate_source = ast.get_source_segment(cost_path.read_text(), gate)
assert gate_source is not None
assert "dispatched=True" in gate_source
assert "run_id" not in gate_source

print("resolved: successful gated dispatch returns DispatchDecision(dispatched=True) without run_id")
print("resolved: the tool interprets missing run_id as a budget refusal")
print("resolved: _claim_workroom runs before the successful gated return")
PY

Repository: dakshparikh12/proxy

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

dispatch_file = Path("services/control-plane/src/control_plane/dispatch.py")
cost_file = Path("libs/ops/src/ops/cost.py")
dispatch_text = dispatch_file.read_text()
cost_text = cost_file.read_text()
dispatch_tree = ast.parse(dispatch_text)
cost_tree = ast.parse(cost_text)

def functions(tree, name):
    return [
        node for node in ast.walk(tree)
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
        and node.name == name
    ]

dispatch_fn = functions(dispatch_tree, "dispatch_workroom")[0]
tool_fn = functions(dispatch_tree, "dispatch_workroom_tool")[0]
gate_fn = functions(cost_tree, "dispatch_workroom")[0]

returns = [
    (node.lineno, ast.dump(node.value, include_attributes=False))
    for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
]
assert any(
    isinstance(node.value, ast.Name) and node.value.id == "decision" and node.lineno >= 199
    for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
), returns
assert any(
    isinstance(node.value, ast.Name) and node.value.id == "handle" and node.lineno >= 196
    for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
), returns

# Locate getattr(outcome, "run_id", None) structurally.
run_id_getattr = [
    node for node in ast.walk(tool_fn)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Name)
    and node.func.id == "getattr"
    and len(node.args) >= 2
    and isinstance(node.args[0], ast.Name)
    and node.args[0].id == "outcome"
    and isinstance(node.args[1], ast.Constant)
    and node.args[1].value == "run_id"
]
assert run_id_getattr, "tool does not read outcome.run_id"
assert any(
    isinstance(node, ast.If)
    and isinstance(node.test, ast.Compare)
    and any(
        isinstance(op, ast.Is) for op in node.test.ops
    )
    and isinstance(node.test.comparators[0], ast.Constant)
    and node.test.comparators[0].value is None
    for node in ast.walk(tool_fn)
), "tool has no run_id-is-None branch"

decision_cls = next(
    node for node in ast.walk(cost_tree)
    if isinstance(node, ast.ClassDef) and node.name == "DispatchDecision"
)
fields = {
    node.target.id
    for node in decision_cls.body
    if isinstance(node, ast.AnnAssign)
    and isinstance(node.target, ast.Name)
}
assert "dispatched" in fields
assert "run_id" not in fields

# The gate's successful return sets dispatched=True, while the async dispatcher
# returns decision after the row claim.
assert any(
    isinstance(node, ast.keyword)
    and node.arg == "dispatched"
    and isinstance(node.value, ast.Constant)
    and node.value.value is True
    for node in ast.walk(gate_fn)
), "gate has no dispatched=True result"

claim_calls = [
    node for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Name)
    and node.func.id == "_claim_workroom_row"
]
decision_returns = [
    node for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
    and isinstance(node.value, ast.Name)
    and node.value.id == "decision"
]
assert claim_calls and decision_returns
assert claim_calls[0].lineno < decision_returns[-1].lineno

print("resolved: successful gated dispatch returns DispatchDecision(dispatched=True) without run_id")
print("resolved: the tool treats missing outcome.run_id as refusal")
print("resolved: the row claim occurs before the successful gated return")
PY

Repository: dakshparikh12/proxy

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

dispatch_file = Path("services/control-plane/src/control_plane/dispatch.py")
cost_file = Path("libs/ops/src/ops/cost.py")
dispatch_tree = ast.parse(dispatch_file.read_text())
cost_tree = ast.parse(cost_file.read_text())

def fn(tree, name):
    return next(
        node for node in ast.walk(tree)
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
        and node.name == name
    )

dispatch_fn = fn(dispatch_tree, "dispatch_workroom")
tool_fn = fn(dispatch_tree, "dispatch_workroom_tool")
gate_fn = fn(cost_tree, "dispatch_workroom")

claim_lines = sorted(
    node.lineno for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Name)
    and node.func.id == "_claim_workroom_row"
)
decision_return_lines = sorted(
    node.lineno for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
    and isinstance(node.value, ast.Name)
    and node.value.id == "decision"
)
handle_return_lines = sorted(
    node.lineno for node in ast.walk(dispatch_fn)
    if isinstance(node, ast.Return)
    and isinstance(node.value, ast.Name)
    and node.value.id == "handle"
)

decision_cls = next(
    node for node in ast.walk(cost_tree)
    if isinstance(node, ast.ClassDef) and node.name == "DispatchDecision"
)
fields = sorted(
    node.target.id for node in decision_cls.body
    if isinstance(node, ast.AnnAssign)
    and isinstance(node.target, ast.Name)
)

run_id_getattr_lines = sorted(
    node.lineno for node in ast.walk(tool_fn)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Name)
    and node.func.id == "getattr"
    and len(node.args) >= 2
    and isinstance(node.args[0], ast.Name)
    and node.args[0].id == "outcome"
    and isinstance(node.args[1], ast.Constant)
    and node.args[1].value == "run_id"
)

dispatch_true_lines = sorted(
    node.lineno for node in ast.walk(gate_fn)
    if isinstance(node, ast.keyword)
    and node.arg == "dispatched"
    and isinstance(node.value, ast.Constant)
    and node.value.value is True
)

print("claim_lines:", claim_lines)
print("decision_return_lines:", decision_return_lines)
print("handle_return_lines:", handle_return_lines)
print("DispatchDecision fields:", fields)
print("tool outcome.run_id getattr lines:", run_id_getattr_lines)
print("gate dispatched=True keyword lines:", dispatch_true_lines)

print("resolved: DispatchDecision has no run_id:", "run_id" not in fields)
print("resolved: tool reads outcome.run_id:", bool(run_id_getattr_lines))
print("resolved: gate can return dispatched=True:", bool(dispatch_true_lines))
print(
    "resolved: successful gated return follows claim:",
    bool(claim_lines and decision_return_lines and min(claim_lines) < max(decision_return_lines)),
)
PY

Repository: dakshparikh12/proxy

Length of output: 594


Return the WorkroomHandle after a successful cost gate.

A successful gated call claims the operation_runs row, then returns DispatchDecision(dispatched=True) without run_id. The tool treats the missing run_id as a budget refusal, skips run_task, and leaves the row running.

Return the handle on the successful path, or resolve it before the tool checks run_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/control-plane/src/control_plane/dispatch.py` around lines 391 - 411,
The successful cost-gated path in the dispatch flow must expose the claimed run
identifier. Update dispatch_workroom and the surrounding handling so a
successful call returns or resolves a WorkroomHandle containing run_id before
the existing run_id check; preserve the None path for budget refusals, then
continue to run_task and run_and_notify with the resolved identifier.

Comment on lines +127 to +129
lowered = src.lower()
for token in ("class .*queue", "sandboxprovider(", "e2b.", "scheduler("):
assert token not in lowered

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

One banned token is a regex used in a substring test, so it never matches.

"class .*queue" is a regular expression. Line 129 performs a substring test against lowered source, so this element can never match and asserts nothing. Use re.search for the pattern, or replace it with a literal.

🐛 Proposed fix
+    import re
+
     lowered = src.lower()
-    for token in ("class .*queue", "sandboxprovider(", "e2b.", "scheduler("):
+    assert not re.search(r"class \w*queue", lowered), "a local queue class was defined"
+    for token in ("sandboxprovider(", "e2b.", "scheduler("):
         assert token not in lowered
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lowered = src.lower()
for token in ("class .*queue", "sandboxprovider(", "e2b.", "scheduler("):
assert token not in lowered
import re
lowered = src.lower()
assert not re.search(r"class \w*queue", lowered), "a local queue class was defined"
for token in ("sandboxprovider(", "e2b.", "scheduler("):
assert token not in lowered
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_b6_dispatch.py` around lines 127 - 129, Update the
banned-token check in the test around lowered source so the “class .*queue”
pattern is evaluated as a regular expression via re.search, or replace it with
the intended literal token. Preserve the existing checks for “sandboxprovider(”,
“e2b.”, and “scheduler(”.

Comment on lines +233 to +235
src = pathlib.Path(
"services/control-plane/src/control_plane/post_meeting/report.py"
).read_text(encoding="utf-8").lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both tests read source files through a hard-coded repo-root-relative path. The assertions pass only when pytest runs from the repository root, and they break silently at the next directory move. Resolve the path from the imported module instead, so the check follows the module wherever it lives.

  • tests/doc07/test_b7_report.py#L233-L235: import control_plane.post_meeting.report and read pathlib.Path(report.__file__) instead of the literal services/control-plane/src/... path.
  • tests/doc07/test_seam2_plan_approval.py#L227-L229: import control_plane.plan_approval_route and read pathlib.Path(m.__file__); the module is already imported at Line 77 in a sibling test.
📍 Affects 2 files
  • tests/doc07/test_b7_report.py#L233-L235 (this comment)
  • tests/doc07/test_seam2_plan_approval.py#L227-L229
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_b7_report.py` around lines 233 - 235, Replace the hard-coded
source path in tests/doc07/test_b7_report.py:233-235 by importing
control_plane.post_meeting.report and resolving pathlib.Path(report.__file__).
Apply the same change in tests/doc07/test_seam2_plan_approval.py:227-229, using
the existing control_plane.plan_approval_route module import (m) and
pathlib.Path(m.__file__).

" AND last_heartbeat_at < now() - make_interval(secs => $2)",
meeting_id, float(stale_after_s()),
)
assert swept.endswith("1"), f"the stale run was not swept: {swept!r}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Compare the sweep row count exactly instead of using endswith.

conn.execute returns a status string such as "UPDATE 1". swept.endswith("1") also matches "UPDATE 11" and "UPDATE 21", and endswith("0") also matches "UPDATE 10". A sweep that touched more rows than intended still passes.

♻️ Proposed change
-    assert swept.endswith("1"), f"the stale run was not swept: {swept!r}"
+    assert swept == "UPDATE 1", f"the stale run was not swept exactly once: {swept!r}"

Apply the same exact comparison at Line 195 ("UPDATE 0") and Line 223 (first == "UPDATE 1", second == "UPDATE 0").

Also applies to: 195-196, 223-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_integration_claim_recycle.py` at line 157, Replace
suffix-based sweep count assertions around the integration test’s sweep results
with exact status-string comparisons: compare swept to "UPDATE 1", the zero-row
result at the later assertion to "UPDATE 0", and first/second to "UPDATE
1"/"UPDATE 0" respectively. Preserve the existing assertion messages and test
flow.

Comment on lines +14 to +17
So every assertion here runs the REAL ``PostMeetingTaskStore`` SQL against a REAL
Postgres, and the expected outcome is a ``psycopg`` integrity error raised by Postgres
itself — identified by constraint name, so a test cannot pass because some *other* error
happened to fire.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Which migration declares each object, and what are the constraint names?
rg -n 'staged_drafts_status_enum|status_check|planned_at|approved_needs_approver' migrations/versions

Repository: dakshparikh12/proxy

Length of output: 2597


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- test module imports and assertions ---'
sed -n '1,145p' tests/doc07/test_integration_db.py
printf '%s\n' '--- migration-related docstring sections ---'
sed -n '195,220p' tests/doc07/test_integration_db.py
sed -n '312,338p' tests/doc07/test_integration_db.py
printf '%s\n' '--- exception references in the module ---'
rg -n 'asyncpg|psycopg|IntegrityError|ForeignKeyViolation|CheckViolation|UniqueViolation|constraint' tests/doc07/test_integration_db.py

Repository: dakshparikh12/proxy

Length of output: 10081


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact stale docstrings ---'
sed -n '320,332p' tests/doc07/test_integration_db.py
printf '%s\n' '--- migration revision chain and declarations ---'
sed -n '1,75p' migrations/versions/0011_clarify_items.py
sed -n '1,75p' migrations/versions/0012_staged_drafts_status_check.py
sed -n '1,65p' migrations/versions/0013_post_meeting_planned_at.py

Repository: dakshparikh12/proxy

Length of output: 9394


Update the stale test documentation.

  • Replace psycopg with asyncpg exception terminology. The module uses asyncpg.exceptions.*, including non-integrity RaiseError.
  • Change migration 0011 to 0012_staged_drafts_status_check.py.
  • Change migration 0012 to 0013_post_meeting_planned_at.py.
  • The constraint name remains staged_drafts_status_enum.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_integration_db.py` around lines 14 - 17, Update the test
documentation to use asyncpg exception terminology, including references to
asyncpg.exceptions.* and non-integrity RaiseError. Revise the migration
references from 0011 to 0012_staged_drafts_status_check.py and from 0012 to
0013_post_meeting_planned_at.py, while preserving the staged_drafts_status_enum
constraint name.

Comment on lines +151 to +157
res = await expire_stale_plans(
rows, store=store, now=second + timedelta(hours=1),
config=__import__(
"control_plane.post_meeting.config", fromlist=["PostMeetingConfig"]
).PostMeetingConfig(plan_expiry_hours=48),
)
assert res.expired == []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace __import__ with a normal import.

test_b4_plan.py already imports PostMeetingConfig at module level. Use the same import here so the config type is visible to readers and to mypy.

♻️ Proposed refactor

Add the import at the top of the file:

+from control_plane.post_meeting.config import PostMeetingConfig
 from control_plane.post_meeting.plan import expire_stale_plans

Then simplify the call:

     res = await expire_stale_plans(
         rows, store=store, now=second + timedelta(hours=1),
-        config=__import__(
-            "control_plane.post_meeting.config", fromlist=["PostMeetingConfig"]
-        ).PostMeetingConfig(plan_expiry_hours=48),
+        config=PostMeetingConfig(plan_expiry_hours=48),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res = await expire_stale_plans(
rows, store=store, now=second + timedelta(hours=1),
config=__import__(
"control_plane.post_meeting.config", fromlist=["PostMeetingConfig"]
).PostMeetingConfig(plan_expiry_hours=48),
)
assert res.expired == []
from control_plane.post_meeting.config import PostMeetingConfig
from control_plane.post_meeting.plan import expire_stale_plans
res = await expire_stale_plans(
rows, store=store, now=second + timedelta(hours=1),
config=PostMeetingConfig(plan_expiry_hours=48),
)
assert res.expired == []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doc07/test_owner_actions.py` around lines 151 - 157, Replace the
dynamic __import__ expression in the expire_stale_plans call with a module-level
import of PostMeetingConfig, matching the existing pattern in test_b4_plan.py.
Use the imported PostMeetingConfig directly when constructing the config
argument, leaving the test behavior unchanged.

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