Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
444 commits
Select commit Hold shift + click to select a range
8344f9d
test(slack): the two Retry-After contract tests follow parse_retry_af…
ahueb Sep 4, 2026
7eac6a0
fix(repo): ignore .mypy_cache and uv.lock in git and in the image (#2…
ahueb Sep 4, 2026
7647438
fix(agent): back off after repeated post failures instead of regenera…
ahueb Sep 4, 2026
e8c05b6
fix(ci): cap mypy and give the ceiling one finding of slack (#27 I7)
ahueb Sep 4, 2026
0a4e6d5
docs(runbook): name the snapshot check by position, not by a number t…
ahueb Sep 4, 2026
4c36fdf
docs(runbook): deploy note 21 — how to spot orphan private channels a…
ahueb Sep 4, 2026
d139a75
test(ci): the gate's two new ratchets actually fail when they should …
ahueb Sep 4, 2026
2b76219
fix(migrate): the snapshot path comes from MIGRATE_STATE_DIR, so --vi…
ahueb Sep 4, 2026
1a430c0
fix(deploy): per-vhost nginx rate-limit zones; drop the stale setupto…
ahueb Sep 4, 2026
4062eeb
fix(web): guard reopen_proposal's write block so a lost review race c…
ahueb Sep 4, 2026
f8c6f0e
ci: an opt-in smoke test that requirements.lock actually installs and…
ahueb Sep 4, 2026
e255c50
fix(web): a lost review race against the engine's implicit marker upg…
ahueb Sep 4, 2026
c851727
fix(deploy): dockerignore uv.lock too, not just gitignore it (#27 I5)
ahueb Sep 4, 2026
38cfe15
test(worker): the retry-ladder assertion compares the increment, not …
ahueb Sep 4, 2026
c049ec7
test(ci): the lock-smoke default test scrubs LOCK_SMOKE from its subp…
ahueb Sep 4, 2026
9a4078d
docs(pr): the PR body for the #20-#27 branch — closure claim, residua…
ahueb Sep 4, 2026
90bffd9
docs(plan): rulings appendix records the Phase 7 fix-wave decisions (…
ahueb Sep 4, 2026
dd7e06f
fix(migrate): postflight no longer fails a correct migration on 0025'…
ahueb Sep 4, 2026
663781f
fix(migrate): postflight verifies WHICH rows 0025 deleted, not how ma…
ahueb Sep 4, 2026
93a48e7
fix(web): a PI can actually clear their private instructions, and the…
ahueb Sep 4, 2026
34d3c15
fix(closure): the migration commits its own record; #21 V3 and #26 do…
ahueb Sep 4, 2026
22c3d78
test(badge): patch the health probe's own engine, so the gate stops d…
ahueb Sep 4, 2026
f9541ba
fix(ci): the lockfile gate checks the repo, not PyPI's release schedu…
ahueb Sep 4, 2026
8341b21
fix(profile): record raw_abstracts_hash only when the synthesis was a…
ahueb Sep 4, 2026
ac218fb
fix(agent): seed the reopen dedup set from reopened_at so a restart s…
ahueb Sep 4, 2026
c4de842
fix(agent): run the inbound handler before advancing the cursor, so a…
ahueb Sep 4, 2026
1230419
test(web): a genuinely concurrent first review and a concurrent waitl…
ahueb Sep 4, 2026
42f03f4
fix(profile): the publication insert tolerates a concurrent writer in…
ahueb Sep 4, 2026
2aaa371
test(inbound): a retried inbound e-mail does not create a second priv…
ahueb Sep 4, 2026
7cc2ea3
fix(web): tell an omitted private-profile field apart from an emptied…
ahueb Sep 4, 2026
f29e295
fix(agent): a refused Slack post keeps the text as a DB-only row inst…
ahueb Sep 4, 2026
a5666b4
test(web): pin review_proposal's non-constraint IntegrityError recove…
ahueb Sep 4, 2026
d1146a4
fix(inbound): a retried e-mail reads refined_in_channel, so it cannot…
ahueb Sep 4, 2026
5090322
fix: four over-implementations found by the sweep, including one that…
ahueb Sep 4, 2026
a47b6c5
test(conftest): the client fixture repoints the health probe's engine…
ahueb Sep 4, 2026
1a9176b
docs(plan): comprehensive remediation plan for the remaining #20-#27 …
ahueb Sep 4, 2026
7eaa705
docs(plan): note why the plan lives in docs/plans rather than the ski…
ahueb Sep 4, 2026
64e9981
docs(plan): v2 of the remaining-gaps plan, rebuilt after three advers…
ahueb Sep 4, 2026
ea6a1e1
fix(agent): a multi-tag root reserves the thread for every bot the PI…
ahueb Sep 4, 2026
554b139
fix(funding): the apostrophe class covers the remaining code points, …
ahueb Sep 4, 2026
a87dbdb
test(ci): the nested gate runs build their environment from an allow-…
ahueb Sep 4, 2026
63813dc
fix(profile): the seed guard keys on the cleared state, not the onboa…
ahueb Sep 4, 2026
edf84cf
fix(agent): post-failure strikes are keyed per agent, not shared acro…
ahueb Sep 4, 2026
4d2bb25
feat(test): tracked live-Slack isolation harness and a tracked decisi…
ahueb Sep 4, 2026
391e545
fix(private-channels): the Slack-off migration commits its own rows a…
ahueb Sep 4, 2026
79cee44
fix(scripts): a validating-but-incomplete synthesis no longer blanks …
ahueb Sep 5, 2026
d7ce1a5
fix(web): the reopen route reads refined_in_channel, so a retry canno…
ahueb Sep 5, 2026
962aa6c
fix(http): bound the retry budget with a deadline, pace NCBI at its c…
ahueb Sep 5, 2026
9cbfc00
fix(agent): a parked thread stops consuming a regular discussion slot…
ahueb Sep 5, 2026
bcef8be
fix(ci): the mypy ceiling records the measured count, its provenance …
ahueb Sep 5, 2026
1c75d70
fix(agent): a first-time activation keeps its channel backlog (#20 E6…
ahueb Sep 5, 2026
df5c45b
test(clients): pin the delegate-sync log, the ORCID/grants retry beha…
ahueb Sep 5, 2026
94792af
fix(agent): cap the LLM-log re-queue so a failing flush cannot grow w…
ahueb Sep 5, 2026
d9f0797
fix(web): the reopen handler stops 500ing on a lost race, keeps the P…
ahueb Sep 5, 2026
af25113
fix(ci): the gate reports the 103 gated tests it did not run (#27 I1)
ahueb Sep 5, 2026
0d1ebb2
fix(web): a lost review race tells the PI instead of showing a succes…
ahueb Sep 5, 2026
cd0ed78
fix(profile): account deletion cannot orphan an active agent, and pas…
ahueb Sep 5, 2026
f2870cf
docs(profile): the measured truncated-publication inventory and the p…
ahueb Sep 5, 2026
f0284f9
feat(migration): 0029 — thread_decisions.pi_engaged_at + agent_messag…
ahueb Sep 5, 2026
f4a2f80
fix(deploy): size nginx's memory limit against its actual zone arithm…
ahueb Sep 5, 2026
88c922b
docs: rule on the profile word-range gate/prompt divergence (#22 COR-16)
ahueb Sep 5, 2026
164592f
fix(scripts): build_cabo_sankey names the extra it needs instead of M…
ahueb Sep 5, 2026
efeb7d6
fix(deploy): keep docs/plans and docs/superpowers out of the image (#…
ahueb Sep 5, 2026
9505554
fix(web): a reopen marker is not a review and is not rendered as 0/4 …
ahueb Sep 5, 2026
e700dac
fix(inbound): a pre-Slack transient failure is retried, not consumed …
ahueb Sep 5, 2026
b913130
test(web): flip the pinned "Rating: 0/4" assertion now that the senti…
ahueb Sep 5, 2026
c8263e7
fix(agent): record the PI's inbound text before the handler runs (#20…
ahueb Sep 5, 2026
1aa5ac9
fix(deploy): set the agent memory limit from a measured turn, and rul…
ahueb Sep 5, 2026
f43e80e
fix(health): retry a reaped pooled connection instead of pre-pinging …
ahueb Sep 5, 2026
1aeaf0a
fix(email): only mark a notification expired when a replacement was a…
ahueb Sep 5, 2026
cb045b4
fix(email): the paused-notification send honours the recipient allowl…
ahueb Sep 5, 2026
141023f
fix(agent): the implicit review persists for an agent with no linked …
ahueb Sep 5, 2026
45d1198
fix(agent): a reopened thread's reply budget survives a restart (#20 …
ahueb Sep 5, 2026
b215244
fix(ci): re-record the mypy provenance at the final commit (#27 I1)
ahueb Sep 5, 2026
2737e08
docs: per-issue closure dispositions for #20-#27
ahueb Sep 5, 2026
71082fb
fix(migrate): derive the migration target and the supported-start ran…
ahueb Sep 5, 2026
4241094
docs(runbook): Part R carries the per-user pipeline re-run, 0029's me…
ahueb Sep 5, 2026
fbc3d2c
docs: post-merge issue-closure handoff for #20-#27
ahueb Sep 5, 2026
7ceda9c
fix(test): close two defects the post-implementation audits found (#2…
ahueb Sep 5, 2026
d5e013f
fix(email): a reopened proposal's rating arrives by e-mail reply, and…
ahueb Sep 5, 2026
b982d53
fix(http): stop the per-call SSL build blocking the loop, and pace NC…
ahueb Sep 5, 2026
bc03917
fix(profile): a clear is a clear whatever whitespace the PI left behi…
ahueb Sep 5, 2026
4f0e284
docs: root causes and fix plan for the 2026-09-08 audit findings
ahueb Sep 8, 2026
30fe99e
fix(agent): gate PI inbound side effects on sender ownership, not thr…
ahueb Sep 8, 2026
366e3ce
fix(agent): PI messages and DMs survive agent-run being down (audit 2…
ahueb Sep 8, 2026
121bb7d
fix(agent): evict long-parked threads and reconstruct post-failure ba…
ahueb Sep 8, 2026
5119d49
fix(slack): give the throttle retry a wait budget, not just an attemp…
ahueb Sep 8, 2026
3dcc91c
fix(agent): persist a PI's standing instruction to the DB first, and …
ahueb Sep 8, 2026
c3b454a
fix(agent): reload private/public profile caches independently so one…
ahueb Sep 8, 2026
7bd56f4
fix(agent): skip the private-profile disk write when the DB persist f…
ahueb Sep 8, 2026
d1af53b
fix(slack): run private_channels.py's Slack calls off the event loop …
ahueb Sep 8, 2026
fa52fa5
fix(agent): a revision-bookkeeping failure must not un-commit an alre…
ahueb Sep 8, 2026
d53f8cf
chore(agent): drop dead Agent.reload_profiles(); document RC-14's can…
ahueb Sep 8, 2026
029e9e1
fix(orcid): stop iterating a present-but-null container as empty (aud…
ahueb Sep 8, 2026
3b4ad13
fix(admin): admin user delete now refuses to orphan a live agent too …
ahueb Sep 8, 2026
ff91b52
fix(email): review reply tokens expire at the consumer, not just on r…
ahueb Sep 8, 2026
1a13ba4
fix(email): make the reply-token expiry invariant local (audit 2026-0…
ahueb Sep 8, 2026
808793a
fix(agent): a session-teardown failure after a committed profile row …
ahueb Sep 10, 2026
dfdcb92
fix(agent): pass sender_user_id to record_pi_message in the ts-collis…
ahueb Sep 10, 2026
45667a2
fix(email): a resend now mints a fresh reply_token instead of reusing…
ahueb Sep 10, 2026
65e234d
fix(agent): recover an interrupted PI inbound handler across restarts…
ahueb Sep 10, 2026
e254949
fix(admin): lock the agent row account-delete checks against a concur…
ahueb Sep 10, 2026
b3436e2
fix(pi-inbox): scope pi_may_reply_in_thread's participant check to th…
ahueb Sep 10, 2026
e56d068
docs(migration): record 0030's FK-validate and index-build lock cost,…
ahueb Sep 10, 2026
785de6f
fix(agent): post-failure backoff reconstruction requires a Slack-back…
ahueb Sep 10, 2026
fdab184
fix(agent): a pending PI row skipped for a tombstoned thread or unres…
ahueb Sep 10, 2026
dfd59ae
fix(agent): a PI DM for an agent not on the roster is now marked hand…
ahueb Sep 10, 2026
59cb254
fix(migrate): postflight/preflight now track 0030's sender_user_id FK…
ahueb Sep 10, 2026
3cd7177
fix(main): bound the health probe retry by what remains of the deadli…
ahueb Sep 8, 2026
1c27397
fix(nginx): enforce a safe CSP subset and collect Report-Only violati…
ahueb Sep 8, 2026
1aa85fe
fix(deploy): add scripts/redeploy.sh so migrate always reruns before …
ahueb Sep 8, 2026
e6850db
fix(config): make profiles/ writability explicit and preflight-checke…
ahueb Sep 8, 2026
feda36f
docs(closure): fix the PR-body Closes line and two stale figures in t…
ahueb Sep 8, 2026
06054a1
fix(nginx): drop form-action from the enforcing CSP header (audit 202…
ahueb Sep 8, 2026
c208dcc
fix(main): sanitise logged CSP fields and enforce the content-type ch…
ahueb Sep 8, 2026
2363700
fix(deploy): read migrate's exit code via docker wait, poll app healt…
ahueb Sep 8, 2026
02f23d3
docs(closure): add RC-5/RC-6 carve-outs and burst-9 to #27's decision…
ahueb Sep 8, 2026
8f1d54f
fix(cli): derive backfill-profile-revisions' profile paths from profi…
ahueb Sep 8, 2026
ca1fc1c
fix(config): stop populate_by_name from making PROFILES_DIR a second …
ahueb Sep 10, 2026
11bf5a5
docs(deploy): point production-migration.md Step 9 at redeploy.sh (au…
ahueb Sep 10, 2026
1527856
fix(preflight): refuse a nonexistent profiles_dir instead of creating…
ahueb Sep 10, 2026
b0b60d7
fix(tests): stop leaking a temp dir at import time; skip root-fragile…
ahueb Sep 10, 2026
e0ca2dd
docs(claude-md): match the rate-limit runbook line to RC-3's 8-attemp…
ahueb Sep 10, 2026
64819d1
test(health): widen the retry-bound timing margins so a loaded CI hos…
ahueb Sep 10, 2026
2931aa7
test(agent): the DM replay guard across a restart is handled_at, not …
ahueb Sep 10, 2026
9252213
docs(closure): record the RC-1/2/3/7/9 and RC-4/14 carve-outs for #20…
ahueb Sep 10, 2026
e0349b2
test(harness): the smoke test pins the current alembic head, 0030 (au…
ahueb Sep 10, 2026
a3f3547
fix(agent): cap in-process retries for a deterministically failing PI…
ahueb Sep 10, 2026
33021b5
fix(accounts): lock the account-delete guard's agent row by owner, no…
ahueb Sep 10, 2026
3f392f6
fix(main): strip every non-printable char from logged CSP fields, not…
ahueb Sep 10, 2026
7045532
fix(nginx,compose): rate-limit /api/csp-report and cap json-file log …
ahueb Sep 10, 2026
42eb9d5
fix(email): key inbound reply rate/help-email limiters by notificatio…
ahueb Sep 10, 2026
aae7c8c
fix(agent): only stamp a PI DM handled when its agent has no AgentReg…
ahueb Sep 10, 2026
38377e9
fix(agent): give a tombstoned inbound row bounded retries before stam…
ahueb Sep 10, 2026
4f11845
fix(email): bounce a reply to a superseded reminder token to a known …
ahueb Sep 10, 2026
ba53189
fix(main): read the CSP report body via stream so a chunked oversized…
ahueb Sep 10, 2026
8c50642
fix(deploy): resolve the single newest migrate container id before do…
ahueb Sep 10, 2026
d01e723
fix(profiles): resolve PROFILES_DIR lazily so a runtime COPI_PROFILES…
ahueb Sep 10, 2026
497be8d
refactor(agent): move PI_INBOUND_* constants to a dependency-free mod…
ahueb Sep 10, 2026
9e66199
fix(agent): a new post with a missing or unknown channel is refused, …
ahueb Sep 10, 2026
18665ff
fix(profiles): route onboarding and pipeline private-profile paths th…
ahueb Sep 10, 2026
01c8014
fix(deploy): select the migrate container by the oneoff label, not li…
ahueb Sep 10, 2026
d7f8646
fix(accounts): lock the users row too, not just the owned agent (audi…
ahueb Sep 10, 2026
0af19f2
fix(agent): RC-15's new_post channel refusal increments the skip stre…
ahueb Sep 10, 2026
b2722a9
fix(agent): PI-inbound attempt counter resets after the lookback wind…
ahueb Sep 10, 2026
c4ca474
fix(email): stale-token bounce budget is only spent on a send that ac…
ahueb Sep 10, 2026
2b992c8
fix(deploy): elect the migrate container only on an explicit oneoff=F…
ahueb Sep 10, 2026
2d15684
fix(agent): the PI-inbound attempt cap is a plain count pruned by bat…
ahueb Sep 10, 2026
a53f424
fix(email): charge the stale-token bounce budget for every send that …
ahueb Sep 10, 2026
0d410be
fix(deploy): fail closed when several migrate containers exist and no…
ahueb Sep 10, 2026
b614474
test(health): assert the retry's wait_for timeout instead of wall-clo…
ahueb Sep 10, 2026
9f1ca5a
docs(closure): PR body, plan and ledger record the 2026-09-08 audit f…
ahueb Sep 10, 2026
83cb084
docs(closure): Task 35 live-tier record and the final gate figures at…
ahueb Sep 10, 2026
38ca989
fix(slack): bound pagination wait time to one listing-level budget (a…
ahueb Sep 10, 2026
f0dc390
fix(slack): move Slack I/O off the shared to_thread pool onto a dedic…
ahueb Sep 10, 2026
e35f8f2
fix(email): distinguish suppressed/not-dispatched/failed/sent so the …
ahueb Sep 10, 2026
9714751
fix(deploy): fold grantbot into redeploy.sh's build/stop/start (audit…
ahueb Sep 10, 2026
a64f377
fix(preflight): quote profiles_dir in the chown remedy suggestion (au…
ahueb Sep 10, 2026
ba2a28e
docs(audit): record R-1..R-5 fixes, closing the 2026-09-08 "recorded,…
ahueb Sep 10, 2026
b0eb94c
fix(slack): cap each pagination page's wait budget at the per-call de…
ahueb Sep 10, 2026
4c809f0
fix(slack): shut the Slack I/O executor down on process exit (audit 2…
ahueb Sep 10, 2026
c82edaa
fix(slack): document the queuing bound and raise SLACK_IO_MAX_WORKERS…
ahueb Sep 10, 2026
ed9a3dc
fix(email): reserve the stale-token bounce budget slot before dispatc…
ahueb Sep 10, 2026
ca71d93
fix(email): split NOT_DISPATCHED into CLIENT_UNAVAILABLE vs MIME cons…
ahueb Sep 10, 2026
af8832a
docs(audit): record the opus review follow-up fixes to R-1..R-3 (audi…
ahueb Sep 10, 2026
015f328
fix(email): parse From via getaddresses, not first angle-bracket matc…
ahueb Sep 10, 2026
3722868
fix(email): require domain alignment for spf/dkim pass when dmarc!=pa…
ahueb Sep 10, 2026
0c31c29
fix(scripts): redact DSN via urlsplit, not a first-@ regex (audit 202…
ahueb Sep 10, 2026
4dfdc4e
fix(email): prune the unbounded in-memory rate-limit/dedup maps every…
ahueb Sep 10, 2026
2720f63
fix(pi-inbox): refuse posting into a channel with no agent_channels r…
ahueb Sep 10, 2026
f37a0ed
docs(audit): record the SEC3 opus review follow-up fixes (audit 2026-…
ahueb Sep 10, 2026
504aea6
fix(pi-inbox): also allow a channel with an existing public message r…
ahueb Sep 10, 2026
1c98506
fix(scripts): redact DSNs by hand, not via urlsplit (audit 2026-09-10…
ahueb Sep 10, 2026
689a46d
fix(email): make domain alignment one-directional; fix group-syntax d…
ahueb Sep 10, 2026
45c227b
fix(email): use a 30-day prune window for the three lifetime caps; pr…
ahueb Sep 10, 2026
5ce777d
fix(email): scope verdict/domain-tag extraction to their own quoted r…
ahueb Sep 10, 2026
ededc15
docs(audit): record the SEC3-1..SEC3-5 opus review follow-up fixes (a…
ahueb Sep 10, 2026
934811c
fix(agent): a transient ownership-lookup DB error must retry, not dis…
ahueb Sep 10, 2026
3ccbf9c
fix(slack): make an in-flight retry sleep interruptible on shutdown (…
ahueb Sep 10, 2026
a285aec
fix(deploy): reject a non-numeric migrate exit code instead of ignori…
ahueb Sep 10, 2026
57e77a8
fix(agent): update the ownership-lookup DB-failure test for K-1's con…
ahueb Sep 10, 2026
97a4e15
fix(agent): resync a stale on-disk private profile from the DB at sta…
ahueb Sep 10, 2026
bdf1f60
fix(email): decrement the stale-token bounce budget on refund, don't …
ahueb Sep 10, 2026
30c7f11
fix(agent): a new_post's known-channel check must not trust another p…
ahueb Sep 10, 2026
4cba561
fix(agent): only forget a PI-inbound attempt count after HANDLED actu…
ahueb Sep 10, 2026
e584f81
fix(agent): cap a persistently failing INGESTED-marker write (audit 2…
ahueb Sep 10, 2026
defe5b2
fix(slack): lazily re-create the Slack I/O pool after any shutdown (a…
ahueb Sep 10, 2026
438a54e
fix(email): update stale-token-bounce tests for send_html_email_outco…
ahueb Sep 10, 2026
96faf93
docs(audit): record the K-1..K-11 closure-audit fixes (audit 2026-09-…
ahueb Sep 10, 2026
a570521
fix(slack): give the shutdown-abort exception a real Slack-error-shap…
ahueb Sep 10, 2026
b46ac2b
fix(agent): only forget a PI-inbound attempt count once the HANDLED w…
ahueb Sep 10, 2026
1c18003
fix(slack): make the shutdown-abort signal per-pool, not one shared e…
ahueb Sep 10, 2026
b8bca06
fix(agent): restore subscribed_channels on a roster re-add (audit 202…
ahueb Sep 10, 2026
3bc8715
fix(agent): normalize whitespace and honor a cleared DB profile in th…
ahueb Sep 10, 2026
47fc012
fix(tests): shut down test-created Slack pools in fixture teardown (a…
ahueb Sep 10, 2026
e456acc
fix(slack): signal the fallback shutdown event too, not just the curr…
ahueb Sep 10, 2026
9d0165d
fix(agent): give the HANDLED write its own attempt budget, separate f…
ahueb Sep 10, 2026
4e8e237
fix(agent): a roster re-add now also resyncs the private profile from…
ahueb Sep 10, 2026
531290a
fix(agent): a missing ResearcherProfile row must not delete the on-di…
ahueb Sep 10, 2026
88bccdf
fix(agent): an unlink() failure on a genuine profile clear still inva…
ahueb Sep 10, 2026
38df5f7
fix(agent): a roster re-add must not subscribe an undiscovered privat…
ahueb Sep 10, 2026
8c00274
docs(audit): append the L section and a K follow-ups subsection (audi…
ahueb Sep 10, 2026
7c4b549
fix(tests): clear the fallback shutdown event after the real-lifespan…
ahueb Sep 10, 2026
d39aa70
fix(agent): delay the SIGTERM Slack-abort by a 20s grace period (audi…
ahueb Sep 10, 2026
56413f2
fix(agent): clear the fallback shutdown event when the Slack pool is …
ahueb Sep 10, 2026
f241e10
fix(agent): log a warning, not "resynced", when a profile resync's di…
ahueb Sep 10, 2026
3c2e595
fix(agent): the profile mtime watcher must not resurrect a force-clea…
ahueb Sep 10, 2026
2662cbc
fix(agent): cap and park a persistently failing PI-inbound fallback s…
ahueb Sep 10, 2026
37c66cb
fix(tests): make the L-1 fallback-event sleeper thread daemon and joi…
ahueb Sep 10, 2026
88f2f01
fix(agent): retry and queue a failed ThreadDecision write instead of …
ahueb Sep 10, 2026
428088d
fix(agent): route hot-path synchronous Slack calls off the event loop…
ahueb Sep 10, 2026
10b2961
docs(audit): append the M section covering M-1..M-8 (audit 2026-09-10)
ahueb Sep 10, 2026
700d5f2
fix(agent): make a force-cleared private profile's mtime watcher DB-a…
ahueb Sep 10, 2026
8a16257
fix(agent): wire the agent-run shutdown handler to the Slack pool, no…
ahueb Sep 10, 2026
96eb7a8
fix(agent): do not let a lazily-minted Slack pool clear a real shutdo…
ahueb Sep 10, 2026
eb4ac59
fix(agent): make the ThreadDecision write idempotent across retries (…
ahueb Sep 10, 2026
5dcd76b
fix(agent): replay a PI engagement review lost to a deferred ThreadDe…
ahueb Sep 10, 2026
73b8e6c
fix(agent): cap the pending ThreadDecision queue and drop the flush's…
ahueb Sep 10, 2026
2ac7b00
fix(agent): prune _pi_inbound_parked entries that fall out of the pol…
ahueb Sep 10, 2026
a4d5787
fix(agent): serialize concurrent _post_message calls per agent (audit…
ahueb Sep 10, 2026
3fdd205
docs(audit): append the N section covering N-1..N-8 (audit 2026-09-10)
ahueb Sep 10, 2026
8151591
fix(agent): replace per-pool shutdown machinery with one sticky proce…
ahueb Sep 10, 2026
824c3a3
fix(agent): only defer an implicit proposal review when a decision wr…
ahueb Sep 10, 2026
a39b2fc
fix(test): make the force-cleared-private-profile watcher test actual…
ahueb Sep 10, 2026
2966c8c
fix(agent): fix mtime-signature bookkeeping around the force-cleared …
ahueb Sep 10, 2026
aad4606
docs(audit): append the O section covering O-1..O-4 (audit 2026-09-10)
ahueb Sep 10, 2026
21e4d90
fix(slack): abort a pool sleeper before threading._shutdown() joins i…
ahueb Sep 10, 2026
ed889ec
fix(agent): signal shutdown unconditionally after the teardown flush …
ahueb Sep 10, 2026
deec46a
fix(slack): abort a call before attempt 0 when already shutting down …
ahueb Sep 10, 2026
7b2346b
fix(test): clear SHUTDOWN_REQUESTED in one autouse conftest fixture (…
ahueb Sep 10, 2026
cea1014
fix(agent): only purge a deferred review if no surviving payload shar…
ahueb Sep 10, 2026
79005dc
fix(agent): treat no-linked-user/no-profile-row as a definitive verdi…
ahueb Sep 10, 2026
6301f74
docs(audit): append the P section covering P-1..P-6 (audit 2026-09-10)
ahueb Sep 10, 2026
93d6916
fix(agent): pair-level survival for deferred implicit reviews; an unl…
ahueb Sep 10, 2026
3d51229
fix(shutdown): tolerate _register_atexit during interpreter shutdown;…
ahueb Sep 10, 2026
0dd503e
fix(agent): a missing registry row is definitive, one pair-level repl…
ahueb Sep 10, 2026
c7bdc7f
fix(agent): route roster-sync connect() through run_slack_call; insta…
ahueb Sep 10, 2026
ced03b1
fix(deploy): redeploy.sh's compose-file guard must not union $COMPOSE…
ahueb Sep 10, 2026
7a5d347
fix(agent): replay a deferred implicit review even for an agent off t…
ahueb Sep 10, 2026
56cd01a
fix(agent): give PiOwnershipLookupFailed its own retry budget, separa…
ahueb Sep 10, 2026
9f68872
fix(email): tolerate the RFC 8601 version token on the authserv-id, u…
ahueb Sep 10, 2026
48d7a92
fix(email): fall back to Cc/Delivered-To/X-Original-To when the reply…
ahueb Sep 10, 2026
4b7dc5e
fix(email): run outbound SES sends off the event loop through a dedic…
ahueb Sep 10, 2026
1ee8c7c
docs(audit): append the S section covering S-1..S-7; correct D8's sta…
ahueb Sep 10, 2026
159a4bc
fix(agent): signal handlers survive a closed loop, restore defaults a…
ahueb Sep 10, 2026
72caa26
fix(agent): restore signal defaults only after the run-status commit;…
ahueb Sep 10, 2026
f376d76
docs(closure): final gate and live-tier figures at 72caa26; Task 35 r…
ahueb Sep 10, 2026
75940dc
docs(comments): describe functionality instead of audit history acros…
ahueb Sep 11, 2026
cf3fc62
docs(plan): concise PR description draft for audit-issues-closure
ahueb Sep 11, 2026
26de01c
docs(plan): PR draft points at the Part R migration runbook with the …
ahueb Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
39 changes: 39 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
certbot
logs
data
profiles
*.log
__pycache__
**/__pycache__
Expand All @@ -15,3 +16,41 @@ venv
# Secrets — never in an image layer (compose injects these at runtime via env_file)
.env
.env.*

# Bloat + secrets that COPY . . would otherwise bake into every image layer.
# backups/ is the sharp one: it holds full production pg_dumps AND a
# plaintext prod .env (SLACK_BOT_TOKEN_* per agent, POSTGRES_PASSWORD,
# SLACK_CONFIG_TOKEN/_REFRESH_TOKEN) that the .env/.env.* patterns above do
# not catch — it has no leading dot and sits two directories down.
backups
.venv-test
.venv*
tests
.notes
mutants
build
.hypothesis
.ruff_cache
.coverage
.mutmut-cache
# Created by ci.sh's mypy stage — large relative to the built image's /app
# tree, and not something any runtime process reads.
.mypy_cache
# A stray uv.lock (a `uv` command run without --no-project writes one) is a second,
# competing lockfile; requirements.lock is the only one the image installs.
uv.lock
.playwright-mcp
copi.egg-info
.superpowers
**/.env*
docs/specs
# The one-off implementation plans and their evidence subtrees — a large
# fraction of /app's tree for text no process reads. Nothing under src/,
# scripts/, templates/ or alembic/ opens a path below docs/; every reference
# is a comment or a docstring (`grep -rn 'docs/' src/ scripts/ templates/
# alembic/`), which is the same reasoning that already excludes docs/specs
# above. docs/ itself is deliberately NOT excluded: docs/production-migration.md
# and docs/inbound-email.md are operator runbooks worth having next to the
# code in a shell inside the container.
docs/plans
docs/superpowers
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ profiles/**/*.md
# Sweep backups of agent memory (same prose as the .md files — never commit;
# this repo is public and a blanket `git add -A` must not pick them up).
profiles/**/*.pre-sweep
profiles/**/*.tmp
profiles/*.log

# Generated/scratch data and static asset bundles (not source)
Expand All @@ -63,6 +64,17 @@ certbot/
.coverage
htmlcov/

# mypy's incremental cache (scripts/ci.sh's mypy stage, #27 I5) — 58 MB of
# per-module analysis state, no reason to ever commit it.
.mypy_cache/

# Stray uv-native lockfile. requirements.lock (pip-compile, hash-pinned) is
# the one actually installed by the Dockerfile and checked by ci.sh's
# freshness gate; `uv lock`/`uv sync` commands run against this repo would
# otherwise leave a second, competing lockfile that is a public-repo secret
# risk if it ever picks up an internal index URL or credentials (#27 I5).
uv.lock

# Generated by scripts/export_agent_roster.py (roster snapshot for host provisioning)
data/agent_roster.json

Expand Down
29 changes: 20 additions & 9 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@

Python implementation of the CoPI researcher collaboration platform combined with the LabAgent multi-agent Slack system. ORCID OAuth, profile generation pipeline, profile editing UI, admin dashboard, and Slack-based AI agent simulation.

**GitHub:** https://github.com/andrewsu/coPI-python-opus
**GitHub:** https://github.com/SuLab/coPI.science
**Target domain:** copi.science
**Pilot:** 10 labs at Scripps Research
**Pilot:** Scripps Research labs — the live roster and count are at **/admin/agents** (see CLAUDE.md "Adding New PIs"); the number changes as PIs are provisioned.

## What's In Scope

- ORCID OAuth authentication
- Profile ingestion pipeline (ORCID → PubMed → PMC → Claude Opus synthesis)
- Profile review/editing web UI (FastAPI + Jinja2)
- Admin dashboard (users, profiles, jobs, agent activity)
- Slack agent system (8 bots, simulation engine)
- Slack agent system (bot count tracked live at /admin/agents; simulation engine)

## What's Out of Scope

- Matching engine (pairwise proposal generation)
- Swipe interface
- Notifications (email)
- Daily digest

## What Email Actually Does (in scope, built)

`src/services/email.py`, `email_inbound.py`, `email_notifications.py` —
proposal-review emails, reply intake, unsubscribe/settings, and a periodic
status-overview digest (`check_and_send_status_overviews`, called from the worker's
~300 s poll loop in `src/worker/main.py`; each PI's own cadence comes from
`email_notification_preferences.frequency`, which defaults to weekly —
`daily` is one option on that ladder, not the schedule).

## Key Specs

Expand Down Expand Up @@ -73,8 +80,8 @@ templates/ # Jinja2 HTML templates
Decisions made autonomously during implementation are recorded here for human review.

### 2026-03-20: Admin impersonation endpoint location
**Decision:** Admin impersonation routes placed at `/api/admin/impersonate` (POST) and `/api/admin/impersonate/stop` (POST) rather than inside the `/admin` router prefix.
**Reason:** The impersonate stop button posts from any page (including non-admin pages when impersonating), so a clean `/api/admin/` prefix was clearer. Both routes still require is_admin verification.
**Decision:** Admin impersonation routes are `POST /admin/impersonate` and `POST /admin/impersonate/stop` (`src/routers/admin.py`, mounted under the `/admin` prefix in `src/main.py`). *(Corrected 2026-09: this entry originally described an `/api/`-prefixed design that was never what shipped — see issue #26 DOC-3.)*
**Reason:** The impersonate-stop button posts from any page (including non-admin pages when impersonating). Both routes still require is_admin verification.

### 2026-03-20: Login page GET /login serves both redirect and HTML
**Decision:** `/login` GET route redirects directly to ORCID OAuth if not already logged in. The login.html page has its sign-in button also pointing to `/login` (which re-triggers the redirect).
Expand Down Expand Up @@ -102,7 +109,7 @@ Decisions made autonomously during implementation are recorded here for human re
**Reason:** Avoids Node.js build step in a Python project. Acceptable for pilot; switch to compiled Tailwind for production if performance matters.

### 2026-03-20: Profile markdown export
**Decision:** When a ResearcherProfile is saved/updated in the DB, automatically export it to `profiles/public/{lab}.md` if the user is one of the 8 pilot labs (matched by ORCID).
**Decision:** When a ResearcherProfile is saved/updated in the DB, automatically export it to `profiles/public/{lab}.md` (matched by ORCID). Written when there were 8 pilot labs; the roster is now whatever `AgentRegistry` holds — see `/admin/agents` for the live count, and `orcids.txt` (48 entries) for the seeding list. Do not treat any number in this file as the current roster size.
**Reason:** Keeps the DB (source of truth) and filesystem (agent input) in sync without a separate sync step.

## Implementation Status
Expand All @@ -115,11 +122,15 @@ Decisions made autonomously during implementation are recorded here for human re
- [x] Admin dashboard
- [x] Worker process
- [x] Agent system (Slack bots, simulation engine)
- [x] Agent profiles (8 pilot labs, auto-generated structure)
- [x] Agent profiles (auto-generated structure; current roster at /admin/agents)
- [x] Prompt files

## Pilot Lab ORCIDs

**Historical snapshot — the original pilot cohort, not the current roster.** The live roster is
`AgentRegistry` (see `/admin/agents`); the seeding list is `orcids.txt`, which now holds 48 ORCIDs.
This table is kept because the decisions above refer to it.

| PI | ORCID |
|---|---|
| Andrew Su | 0000-0002-9859-4104 |
Expand Down
58 changes: 51 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ docker compose exec -T -e TEST_DATABASE_URL=postgresql+asyncpg://copi:copi@postg
app python -m pytest tests/ -v
```

**Note (2026-09, #27 I3):** the command above works against the **dev** compose file, whose
`.:/app` bind mount supplies `tests/`. `tests/` is excluded from the built image by
`.dockerignore`, so the same command against a prod-built container reports
"file or directory not found: tests/". Run the suite on the host (`./scripts/ci.sh`) or with the
dev compose file.

The named database must already exist — the suite migrates it, it does not create
it. Add a fresh scratch DB with
`docker compose exec -T postgres createdb -U copi copi_xN`, and give concurrent
Expand Down Expand Up @@ -72,9 +78,11 @@ docker compose $C --profile agent run -d --name agent-run agent python -m src.ag

On resume the sim fetches Slack history for each bot in roster order before reaching
turn 1. Slack throttles this hard — expect ~10 minutes of
`[<first-agent>] Rate limited, retrying in 10s (attempt 1/3)` before the first
`=== Turn 1 ===`. Repeated `attempt 1/3` (never `2/3`) means each call 429s once then
succeeds on retry — that is forward progress, not a hang.
`[<first-agent>] Rate limited, retrying in 10s (attempt 1/8, 10.0s/180.0s of wait
budget used)` before the first `=== Turn 1: <agent> ===`. Repeated `attempt 1/8`
(never `2/8`) means each call 429s once then succeeds on retry — that is forward
progress, not a hang (RC-3 on this branch raised the ceiling from 3 attempts to 8 and
added the 180s cumulative wait-budget figure logged alongside it).

**Before restarting**, always save logs and rebuild containers:

Expand All @@ -88,12 +96,36 @@ ls -t logs/run_*.log | tail -n +11 | xargs rm -f
# 2. Stop the old container — GRACEFULLY. `docker rm -f` sends SIGKILL, which
# skips the shutdown flush and permanently loses the in-flight turn's
# messages (the DB, not Slack, is the durable store). `docker stop` sends
# SIGTERM; -t 30 leaves room for an in-flight LLM call to finish.
# SIGTERM; -t 30 leaves room for an in-flight LLM call to finish. One
# SIGTERM stops after the current turn and aborts Slack retry sleeps 20 s
# later; a SECOND signal aborts Slack immediately (the DB flush still
# runs); a THIRD terminates the process at once and can lose the flush --
# never send a third unless the process is wedged.
docker stop -t 30 agent-run
docker rm agent-run

# 3. Rebuild app + worker (picks up code changes)
docker compose $C up -d --build app worker
# 3. Redeploy app + worker + grantbot against the migrated schema — via
# scripts/redeploy.sh, NOT a bare `up -d --build`. `depends_on: migrate: condition:
# service_completed_successfully` only orders container CREATION: on an
# already-running stack, an existing exited `migrate` container from the last
# deploy can satisfy that condition without being re-run against the freshly
# built image, so old code can keep serving requests against a schema the new
# migration hasn't applied yet (audit 2026-09-08 RC-6, #27 I2; audit 2026-09-10
# R-4 added grantbot, which has the identical depends_on shape and was
# otherwise left running the old image). redeploy.sh builds
# migrate+app+worker+grantbot, STOPS app/worker/grantbot first, runs migrate
# and checks its exit code, only then starts the new app/worker/grantbot, then
# reloads nginx (the recreated app container gets a new IP — see the
# nginx-stale-upstream-ip memory note). `agent` is NOT part of this — it is a
# one-off with its own restart runbook above. It refuses to run unless both prod compose files are visible
# (via $COMPOSE_FILE or -f) and never passes an orphan-removal flag. The image
# runs as UID 10001, so profiles/ and data/ on the host must already be owned
# by 10001:10001 (never prompts/ — see docs/production-migration.md §10.8 and
# Part R.5 of docs/plans/2026-09-02-close-issues-20-27.md) or the services
# that mount them fail to write into their bind mounts — profiles/ and
# prompts/ are mounted on app/worker (and agent/grantbot); data/ is mounted
# only on agent/grantbot, not app/worker.
./scripts/redeploy.sh $C

# 4. Rebuild the agent image too — prod bakes code into the image, so skipping
# this silently runs whatever source was current at the last build.
Expand All @@ -115,6 +147,18 @@ they can decide whether to restart.** Roster changes — activating/inactivating
setting a new `slack_bot_token` in `AgentRegistry` — do NOT need a restart; they're
picked up live by `_sync_roster_from_db`.

**One-time Slack-ts repair (legacy rows).** A workspace that predates the DB-primary
conversation model may have `agent_messages` rows with `slack_ts IS NULL`. Replies to
threads rooted on those rows are silently kept off Slack — `_slack_parent_ts`
(`src/agent/simulation.py`) returns `None` for a legacy root and callers skip the
mirror rather than guess a timestamp Slack never issued. Run
`docker compose exec -e PYTHONPATH=/app app python scripts/backfill_slack_ts.py --apply` once, before your
next restart, to ask Slack which timestamps actually exist and repair them (safe to
re-run; read-only against Slack otherwise). `docs/production-migration.md` §8 Step 8
walks through this as an ordered step after the migration and before the app-code
deploy for a *fresh* migration; if your workspace is already at head and has never run
it, run it manually — nothing else will prompt you to.

## Adding New PIs

**The `AgentRegistry` table is the single source of truth for the agent roster.**
Expand All @@ -139,7 +183,7 @@ Each agent needs an `AgentRegistry` row with a unique `agent_id` (lowercase last
and `bot_name` (`{LastName}Bot`), created `status='pending'`. Self-service signups
(`src/routers/agent_page.py`) and the backfill scripts both create these automatically.

**Last-name collisions:** If a last name is already taken (e.g., Chunlei Wu = `wu`), prefix with the first initial (e.g., Peng Wu = `pwu` / `PWuBot`). The web UI applies this logic automatically.
**Last-name collisions:** If a last name is already taken (e.g., Chunlei Wu = `wu`), prefix with the first initial (e.g., Peng Wu = `pwu` / `PWuBot`). If that prefixed id is *also* taken (a third same-initial namesake), append a numeric suffix to the prefixed candidate (e.g. `pwu2` / `PWu2Bot`). The web UI applies this logic automatically.

### 3. Provision the Slack bot + activate (admin UI)

Expand Down
61 changes: 53 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,68 @@
FROM python:3.11-slim
FROM python:3.11-slim AS builder

WORKDIR /app

# Install build dependencies
# Build-time only: gcc/libpq-dev compile any dependency that ships as an sdist
# for this platform/Python combination. Not present in the runtime image
# below — asyncpg itself needs none of this, it bundles its own wire protocol
# implementation rather than linking libpq.
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY pyproject.toml .
COPY pyproject.toml requirements.lock ./
RUN pip install --no-cache-dir --require-hashes -r requirements.lock
COPY src/ src/
RUN pip install --no-cache-dir .
# --no-build-isolation: build isolation would otherwise fetch a fresh,
# unhashed setuptools/wheel from PyPI at build time just to satisfy
# pyproject.toml's [build-system] requires; the base image's preinstalled
# setuptools/wheel already satisfy it. This local `pip install .` therefore
# is not hash-verified the way the `-r requirements.lock` install above is
# — accepted, since it installs only this repo's own source, not a
# third-party artifact off the network.
RUN pip install --no-cache-dir --no-deps --no-build-isolation .

# Copy source
FROM python:3.11-slim AS runtime

WORKDIR /app

# libpq5 only: the runtime client library a compiled wheel may dlopen. Nothing
# currently links it — asyncpg is pure-protocol — this is insurance for a
# future psycopg dependency. No compiler, no -dev headers, no build toolchain
# of any kind in this stage. Deliberately avoids naming the builder-stage
# packages here — the structural test in tests/unit/test_dockerfile_build.py
# asserts their names are absent from this section.
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .

# Create directories for profiles and prompts
RUN mkdir -p profiles/public profiles/private prompts logs static
# Bake the bytecode cache while root still owns src/ — UID 10001 (set below)
# cannot write __pycache__ into root-owned src/, so without this every
# process start pays a first-import compile cost (~0.9s, measured). Must run
# AFTER src/ lands (COPY . . above) and BEFORE USER drops root.
RUN python -m compileall -q src

# Fixed UID so it matches whatever the prod host chowns the bind-mounted
# profiles/data trees to — a plain chown target on the host, not a real host
# account. Ownership is scoped to the directories the runtime user actually
# writes to (profiles/data/logs); src/, templates/, alembic/, scripts/ and
# static/ stay root-owned and read-only to this user, so a compromised
# process cannot rewrite its own code. static/ is deliberately excluded:
# StaticFiles only ever reads it, nothing under src/ writes to it, so a write
# grant there would be a needless stored-XSS surface on assets served
# straight to the browser.
RUN groupadd --gid 10001 copi \
&& useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin copi \
&& mkdir -p profiles/public profiles/private profiles/memory data logs \
&& chown -R 10001:10001 profiles data logs
ENV HOME=/app

USER 10001

EXPOSE 8000

Expand Down
Loading