Compare commits

...

307 commits

Author SHA1 Message Date
twentyOne2x
a4462b1fbe
Merge pull request #75 from living-ip/codex/working-leo-live-clone-20260710
Some checks are pending
CI / lint-and-test (push) Waiting to run
Prove guarded Leo source composition on VPS clone
2026-07-11 08:46:10 +02:00
twentyOne2x
e426db87ee Prove guarded Leo source composition on VPS clone 2026-07-11 08:43:51 +02:00
twentyOne2x
9fa8e686f1
Merge pull request #73 from living-ip/codex/teleo-skill-pack-live-truth-20260710
Some checks are pending
CI / lint-and-test (push) Waiting to run
Teach Teleo skills current VPS and GCP runtime truth
2026-07-10 18:15:08 +02:00
twentyOne2x
15f9133920 Teach Teleo skills current VPS and GCP runtime truth
- Distinguish source synchronization from service, migration, worker, and database state changes.
- Bound GCP claims to current VM, Cloud SQL inventory, logs, and serial evidence.
- Keep the operating pack reliable with 14 skill validations and 809 passing tests.

`.agents/skills/teleo-gcp-parity-ops/SKILL.md`
`.agents/skills/teleo-infra-provenance/SKILL.md`
`.agents/skills/teleo-kb-db-change-workflow/SKILL.md`
`.agents/skills/teleo-leo-onboarding/SKILL.md`
`.agents/skills/teleo-vps-runtime-ops/SKILL.md`
`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.json`
`docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.md`
`docs/reports/leo-working-state-20260709/pr72-vps-auto-deploy-runtime-nonchange-current.json`
`docs/reports/leo-working-state-20260709/pr72-vps-auto-deploy-runtime-nonchange-current.md`
`docs/reports/leo-working-state-20260709/skill-pack-manifest.json`
`docs/reports/leo-working-state-20260709/skill-pack-readme.md`
`tests/test_repo_skill_pack.py`
2026-07-10 18:13:39 +02:00
twentyOne2x
771679061f
Merge pull request #72 from living-ip/codex/working-leo-db-path-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add guarded canonical claim application
2026-07-10 17:51:20 +02:00
twentyOne2x
fed5cb0805 Add guarded canonical claim application
- Normalize rich proposals into atomic reviewed canonical graph bundles with exact row verification.
- Harden reviewer/apply roles, upgrade ACLs, and prove generic plus Helmer lifecycles in isolated PostgreSQL.
- Publish repo-native VPS/GCP/Cory skills and live-readonly GCP inventory without changing the live VPS runtime.

`.agents/skills/crabbox/SKILL.md`
`.agents/skills/teleo-gcp-parity-ops/SKILL.md`
`.agents/skills/teleo-kb-db-change-workflow/SKILL.md`
`.agents/skills/teleo-leo-onboarding/SKILL.md`
`.agents/skills/teleo-vps-runtime-ops/SKILL.md`
`.agents/skills/working-leo-cory-outcomes/SKILL.md`
`docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json`
`docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
`docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
`docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
`docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
`docs/reports/leo-working-state-20260709/gcp-parallel-delegation-current.json`
`docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json`
`docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json`
`docs/reports/leo-working-state-20260709/skill-pack-manifest.json`
`docs/reports/leo-working-state-20260709/skill-pack-readme.md`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`scripts/apply_proposal.py`
`scripts/apply_worker.py`
`scripts/approve_proposal.py`
`scripts/kb_apply_prereqs.sql`
`scripts/kb_proposal_normalize.py`
`scripts/probe_gcp_db_parity.py`
`scripts/run_approve_claim_clone_canary.py`
`scripts/run_approve_claim_isolated_container_canary.sh`
`tests/test_apply_proposal.py`
`tests/test_apply_worker.py`
`tests/test_approve_proposal.py`
`tests/test_kb_apply_prereqs.py`
`tests/test_kb_proposal_normalize.py`
`tests/test_kb_proposal_routes.py`
`tests/test_probe_gcp_db_parity.py`
`tests/test_repo_skill_pack.py`
2026-07-10 17:49:37 +02:00
twentyOne2x
339565585d Refresh Leo production apply authorization gate
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 06:29:14 +02:00
twentyOne2x
a717965b45 Refresh Leo Telegram-visible preflight
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 06:26:22 +02:00
twentyOne2x
63278b6354 Record Leo post-repair direct-claim proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 06:22:11 +02:00
twentyOne2x
090becae16 Clarify Leo proposal state semantics
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:46:38 +02:00
twentyOne2x
1ffb3ae099 Tighten Leo direct-claim overclaim guardrails
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:39:48 +02:00
twentyOne2x
c88b54d573 Prove Leo restart survival and tighten identity proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:32:21 +02:00
twentyOne2x
741f1d6782 Add Leo direct-claim remote runner
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:17:52 +02:00
twentyOne2x
9f892c5e34 Harden Leo Telegram-visible preflight readback
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:11:14 +02:00
twentyOne2x
7de4c38953 Add Leo Telegram-visible capture receipt harness
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 05:03:18 +02:00
twentyOne2x
97755068d3 Add Leo remote preflight readback command
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:56:09 +02:00
twentyOne2x
34b45365ec Add Leo direct-claim preflight probes
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:50:04 +02:00
twentyOne2x
24cd49f16a Avoid stale production authorization SHA
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:36:13 +02:00
twentyOne2x
48c3b26599 Refresh production apply authorization SHA
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:33:49 +02:00
twentyOne2x
3a0c47c83d Add Telegram visible direct-claim authorization packet
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:31:38 +02:00
twentyOne2x
262ddb9e0d Refresh Leo direct-claim post-deploy proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:26:31 +02:00
twentyOne2x
7ab563e1ae Add GCP parity readiness probe
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:21:49 +02:00
twentyOne2x
dc42ff45ae Add Leo production receipt assembler
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:14:17 +02:00
twentyOne2x
db0c5382c2 Add Leo production apply authorization verifier
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:09:55 +02:00
twentyOne2x
427a9f1a1e Add Leo production apply authorization packet
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 04:05:28 +02:00
twentyOne2x
45bec0ed84 Refresh Leo direct-claim proof after deploy
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:58:51 +02:00
twentyOne2x
4662254899 Add Leo authorized apply session plan
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:52:26 +02:00
twentyOne2x
17d0f1f71a Add Leo production receipt template
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:47:14 +02:00
twentyOne2x
57bd705079 Record Leo live production preflight baseline
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:43:44 +02:00
twentyOne2x
0d70c09f6f Support VPS local preflight collection
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:41:29 +02:00
twentyOne2x
637d285ecb Add Leo production preflight collector
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:39:44 +02:00
twentyOne2x
1a19d84538 Refresh Leo direct-claim benchmark proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:34:13 +02:00
twentyOne2x
0e2c37e8fa Validate Leo production apply receipts
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:26:18 +02:00
twentyOne2x
3351d2d886 Harden Leo apply runbook regression gates
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:22:17 +02:00
twentyOne2x
ad343933bf Record Leo direct-claim handler pass
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:11:50 +02:00
twentyOne2x
08b3d79e0f Tighten Leo direct-claim answer contract
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 03:03:58 +02:00
twentyOne2x
79cfe2dc4a Require Leo direct-claim follow-up line
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:49:58 +02:00
twentyOne2x
ddb916ab73 Align Leo direct-claim benchmark semantics
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:45:01 +02:00
twentyOne2x
ee777c89a9 Guide Leo direct claims to all proposal statuses
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:38:21 +02:00
twentyOne2x
6fa86d5e01 Add Leo direct-claim handler harness
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:34:18 +02:00
twentyOne2x
cc9ce1c0f3 Show proposal apply state in Leo KB bridge
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:23:00 +02:00
twentyOne2x
5e9b493c01 Deploy leoclean bridge binaries
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:21:45 +02:00
twentyOne2x
9f6961947b Fix leoclean auto-deploy restart sudoers
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:19:30 +02:00
twentyOne2x
dbb79b22bc Fix Leo direct-claim KB proposal readback
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:16:39 +02:00
twentyOne2x
25057e29c2 Record Working Leo direct-claim handler regression
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-10 02:08:46 +02:00
twentyOne2x
23b1c87ac0 Add Working Leo direct-claim benchmark and apply runbook
Some checks are pending
CI / lint-and-test (push) Waiting to run
Generate ready-not-executed operator runbook for the integrated apply packet set.

Extend Cory-style benchmark with no-context direct-claim prompts and expected follow-ups.

Refresh retained reports and tests for the 20-prompt mixed-evidence score.
2026-07-10 01:14:39 +02:00
twentyOne2x
a1e656dda3 Score Cory-style Working Leo outcome sandbox
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Build mixed live-retained and sandbox fixture benchmark results for OE plus CS prompts
- Retain full 14/14 Cory-style outcome score without claiming live CS Telegram proof
- Update handoff docs and tests for the sandbox-first claim ceiling

Files:
- docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md
- docs/reports/leo-working-state-20260709/current-truth-index.md
- docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-results-current.json
- docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-results-current.md
- docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.json
- docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.md
- docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md
- docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md
- scripts/build_working_leo_cory_outcome_sandbox.py
- scripts/working_leo_open_ended_benchmark.py
- tests/test_build_working_leo_cory_outcome_sandbox.py
- tests/test_working_leo_open_ended_benchmark.py
2026-07-10 01:05:29 +02:00
twentyOne2x
fd7ab67b44 Verify Working Leo apply readiness gate
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Add retained-artifact verifier for integrated apply packet readiness
- Record ready=true while preserving production_apply_executed=false
- Cover success and authorization-boundary failure paths with focused tests

Files:
- docs/reports/leo-working-state-20260709/current-truth-index.md
- docs/reports/leo-working-state-20260709/working-leo-apply-readiness-current.json
- docs/reports/leo-working-state-20260709/working-leo-apply-readiness-current.md
- docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md
- docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md
- scripts/verify_working_leo_apply_readiness.py
- tests/test_verify_working_leo_apply_readiness.py
2026-07-10 00:58:38 +02:00
twentyOne2x
2a15bdef06 Prove full live-safe Working Leo Telegram benchmark
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Run OE-01 through OE-05 live Telegram read-only benchmark coverage
- Retain full-suite scoring artifacts and no-mutation safety readback
- Broaden scorer tests for live Leo wording without weakening claim ceilings

Files:
2026-07-10 00:52:30 +02:00
twentyOne2x
01a65b1b9b Score retained Working Leo Telegram benchmark evidence
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Rescore retained live Telegram replies with partial versus full coverage labels
- Retain OE-01 benchmark score artifacts without mutating Leo or production DB
- Update Working Leo truth docs and tests so partial proof is not overclaimed

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/telegram-live-open-ended-benchmark-score-current.json`
`docs/reports/leo-working-state-20260709/telegram-live-open-ended-benchmark-score-current.md`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`scripts/working_leo_open_ended_benchmark.py`
`tests/test_working_leo_open_ended_benchmark.py`
2026-07-10 00:30:37 +02:00
twentyOne2x
dd2d03a3ea Add integrated Working Leo packet rehearsal
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Compose prepared KB packets in production dependency order with rollback order

- Prove full disposable clone apply/rollback while production stays unchanged
- Retain missing packet SQL, integrated manifest, state image, and tests

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-packet-summary.json`
`docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-rollback-rehearsal.sql`
`docs/reports/leo-working-state-20260709/helmer-7powers-production-postflight.sql`
`docs/reports/leo-working-state-20260709/helmer-7powers-production-preflight.sql`
`docs/reports/leo-working-state-20260709/leo-db-state-25-integrated-apply-rehearsal.svg`
`docs/reports/leo-working-state-20260709/production-apply-authorized-commit.sql`
`docs/reports/leo-working-state-20260709/production-apply-packet-summary.json`
`docs/reports/leo-working-state-20260709/production-apply-packet.json`
`docs/reports/leo-working-state-20260709/production-apply-rollback-rehearsal.sql`
`docs/reports/leo-working-state-20260709/production-delete-rollback.sql`
`docs/reports/leo-working-state-20260709/production-postflight.sql`
`docs/reports/leo-working-state-20260709/production-preflight.sql`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`docs/reports/leo-working-state-20260709/working-leo-integrated-clone-rehearsal-current.log`
`docs/reports/leo-working-state-20260709/working-leo-integrated-packet-current.md`
`docs/reports/leo-working-state-20260709/working-leo-integrated-packet.json`
`docs/reports/leo-working-state-20260709/working-leo-integrated-packet.md`
`scripts/working_leo_integrated_packet.py`
`tests/test_working_leo_integrated_packet.py`
2026-07-10 00:27:00 +02:00
twentyOne2x
394a14660d Add governance concept schema packet
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Create guarded schema and links for governance gates and concept maps
- Prove clone apply and rollback while production schema stays unchanged
- Retain packet docs, state image, and focused tests for apply review

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/governance-concept-apply-authorized-commit.sql`
`docs/reports/leo-working-state-20260709/governance-concept-apply-rollback-rehearsal.sql`
`docs/reports/leo-working-state-20260709/governance-concept-clone-rehearsal-current.log`
`docs/reports/leo-working-state-20260709/governance-concept-current.md`
`docs/reports/leo-working-state-20260709/governance-concept-delete-rollback.sql`
`docs/reports/leo-working-state-20260709/governance-concept-packet.json`
`docs/reports/leo-working-state-20260709/governance-concept-postflight.sql`
`docs/reports/leo-working-state-20260709/governance-concept-preflight.sql`
`docs/reports/leo-working-state-20260709/leo-db-state-24-governance-concept-schema.svg`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`scripts/kb_governance_concept_packet.py`
`tests/test_kb_governance_concept_packet.py`
2026-07-10 00:17:38 +02:00
twentyOne2x
b508a1106b Add Rio strategy context packet
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Create guarded Rio strategy nodes and anchors after the mapped base packet
- Prove clone apply and rollback while production rows and Leo runtime stay unchanged
- Retain packet docs, state image, and focused tests for future apply review

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/leo-db-state-23-rio-strategy-context.svg`
`docs/reports/leo-working-state-20260709/rio-strategy-apply-authorized-commit.sql`
`docs/reports/leo-working-state-20260709/rio-strategy-apply-rollback-rehearsal.sql`
`docs/reports/leo-working-state-20260709/rio-strategy-clone-rehearsal-current.log`
`docs/reports/leo-working-state-20260709/rio-strategy-context-current.md`
`docs/reports/leo-working-state-20260709/rio-strategy-delete-rollback.sql`
`docs/reports/leo-working-state-20260709/rio-strategy-packet.json`
`docs/reports/leo-working-state-20260709/rio-strategy-postflight.sql`
`docs/reports/leo-working-state-20260709/rio-strategy-preflight.sql`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`scripts/kb_rio_strategy_context_packet.py`
`tests/test_kb_rio_strategy_context_packet.py`
2026-07-10 00:11:23 +02:00
twentyOne2x
ae00beeb3b Document Working Leo DB identity provenance
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Map VPS Hermes, Forgejo, SOUL rendering, and document-artifact boundaries.

- Expand Cory-style benchmark for identity render, decision matrix, and source refs.

- Retain live-readback diagrams/reports and focused test coverage.

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/leo-db-loading-diagram-20260709T214741Z.mmd`
`docs/reports/leo-working-state-20260709/leo-db-loading-diagram-20260709T214741Z.svg`
`docs/reports/leo-working-state-20260709/leo-db-provenance-report-20260709T214741Z.md`
`docs/reports/leo-working-state-20260709/leo-db-state-21-forgejo-load-agent-loop.svg`
`docs/reports/leo-working-state-20260709/leo-db-state-22-identity-render-matrix-artifacts.svg`
`docs/reports/leo-working-state-20260709/vps-hermes-db-mapping-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json`
`scripts/working_leo_open_ended_benchmark.py`
`tests/test_working_leo_open_ended_benchmark.py`
2026-07-09 23:59:42 +02:00
twentyOne2x
1826206053 Fix Working Leo skill manifest paths
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Align the skill-pack manifest with the committed .agents/skills layout so worker onboarding points at the real files.

- Preserve the existing skill pack and evidence references without changing runtime behavior or production DB state.

- Verify the Hermes leoclean skill surface test passes after the path correction.

`docs/reports/leo-working-state-20260709/skill-pack-manifest.json`
2026-07-09 23:43:11 +02:00
twentyOne2x
f34feb0ffb Add Working Leo cross-surface KB proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Add broad Cory-style outcome scenarios to the open-ended benchmark so regular-use ambiguity is tested, not only exact ID canaries.

- Add a clone-proven cross-surface strategy anchor packet that updates the mapped rich proposal anchor only after Claim D exists.

- Retain VPS Docker DB, claim/body metadata, and cross-surface proof reports with ruff plus 37 focused tests passing.

`docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md`

`docs/reports/leo-working-state-20260709/cross-surface-apply-authorized-commit.sql`

`docs/reports/leo-working-state-20260709/cross-surface-clone-rehearsal-current.log`

`docs/reports/leo-working-state-20260709/cross-surface-clone-rehearsal-failed-source-id-constraint.log`

`docs/reports/leo-working-state-20260709/cross-surface-delete-rollback.sql`

`docs/reports/leo-working-state-20260709/cross-surface-packet.json`

`docs/reports/leo-working-state-20260709/cross-surface-postflight.sql`

`docs/reports/leo-working-state-20260709/cross-surface-preflight.sql`

`docs/reports/leo-working-state-20260709/cross-surface-resolution-current.md`

`docs/reports/leo-working-state-20260709/current-truth-index.md`

`docs/reports/leo-working-state-20260709/leo-db-state-17-cory-outcome-benchmark.svg`

`docs/reports/leo-working-state-20260709/leo-db-state-18-db-vs-workspace.svg`

`docs/reports/leo-working-state-20260709/leo-db-state-19-claims-body-metadata.svg`

`docs/reports/leo-working-state-20260709/leo-db-state-20-cross-surface-anchor.svg`

`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`

`docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`

`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`

`docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json`

`scripts/kb_cross_surface_resolution_packet.py`

`scripts/working_leo_open_ended_benchmark.py`

`tests/test_kb_cross_surface_resolution_packet.py`

`tests/test_working_leo_open_ended_benchmark.py`
2026-07-09 23:39:51 +02:00
twentyOne2x
d9c794bebc Add Helmer ledger apply rehearsal
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 23:17:28 +02:00
twentyOne2x
9dde83c602 Add open-ended Working Leo canary proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 23:09:44 +02:00
twentyOne2x
2759397a12 Retain Helmer 7 Powers packet proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Record clone-proven Helmer 7 Powers packet evidence for team handoff.
- Update Working Leo state and execution plan with the current non-production claim ceiling.
- Add live rollback and clone rollback logs so reviewers can inspect the row-level proof.

`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/helmer-7powers-clone-commit-delete-rollback-current.log`
`docs/reports/leo-working-state-20260709/helmer-7powers-live-preflight-rollback-current.log`
`docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-packet-current.md`
`docs/reports/leo-working-state-20260709/leo-db-state-14-helmer-packet.svg`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
2026-07-09 23:01:06 +02:00
twentyOne2x
699a9e7917 Add Working Leo operator skill pack
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Add reusable Teleo/Leo skills for VPS, GCP, Telegram, KB changes, provenance, and handoff onboarding.
- Add guarded KB proposal tooling for approved rich proposals, apply packets, and rollback proof.
- Add Cory-style open-ended benchmark prompts and tests alongside the precise regression canaries.

`.agents/skills/leo-telegram-canary-ops/SKILL.md`
`.agents/skills/teleo-gcp-parity-ops/SKILL.md`
`.agents/skills/teleo-infra-provenance/SKILL.md`
`.agents/skills/teleo-kb-db-change-workflow/SKILL.md`
`.agents/skills/teleo-leo-onboarding/SKILL.md`
`.agents/skills/teleo-proof-handoff/SKILL.md`
`.agents/skills/teleo-vps-runtime-ops/SKILL.md`
`.agents/skills/working-leo-cory-outcomes/SKILL.md`
`docs/reports/leo-working-state-20260709/claim-source-contract-preview-current.md`
`docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md`
`docs/reports/leo-working-state-20260709/current-truth-index.md`
`docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md`
`docs/reports/leo-working-state-20260709/gcp-db-parity-current-blocker-20260709.json`
`docs/reports/leo-working-state-20260709/helmer-7powers-creation-plan.json`
`docs/reports/leo-working-state-20260709/kb-apply-canary-execute-current.md`
`docs/reports/leo-working-state-20260709/kb-apply-canary-plan-current.md`
`docs/reports/leo-working-state-20260709/leo-db-state-13-skill-pack.svg`
`docs/reports/leo-working-state-20260709/operator-surface-map.md`
`docs/reports/leo-working-state-20260709/production-apply-packet-current.md`
`docs/reports/leo-working-state-20260709/rich-proposal-creation-plan-mapped-current.md`
`docs/reports/leo-working-state-20260709/skill-pack-manifest.json`
`docs/reports/leo-working-state-20260709/skill-pack-readme.md`
`docs/reports/leo-working-state-20260709/telegram-gateway-handler-canary-current.json`
`docs/reports/leo-working-state-20260709/telegram-gateway-handler-canary-current.md`
`docs/reports/leo-working-state-20260709/telegram-live-canary-current.json`
`docs/reports/leo-working-state-20260709/telegram-live-canary-current.md`
`docs/reports/leo-working-state-20260709/telegram-live-db-write-canary-20260709.json`
`docs/reports/leo-working-state-20260709/telegram-live-db-write-canary-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`
`docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
`docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json`
`scripts/approve_proposal.py`
`scripts/kb_claim_source_contract_preview.py`
`scripts/kb_rich_proposal_apply_packet.py`
`scripts/kb_rich_proposal_creation_plan.py`
`scripts/working_leo_open_ended_benchmark.py`
`tests/test_approve_proposal.py`
`tests/test_kb_claim_source_contract_preview.py`
`tests/test_kb_rich_proposal_apply_packet.py`
`tests/test_kb_rich_proposal_creation_plan.py`
`tests/test_working_leo_open_ended_benchmark.py`
2026-07-09 22:56:48 +02:00
twentyOne2x
2e2dcff4d3
Stage linked KB edge proposals from leoclean (#71)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 09:52:47 +02:00
twentyOne2x
a1814671e1
Point leoclean claim links at Leo host
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 09:41:00 +02:00
twentyOne2x
1e0d747ae4
Add KB proposal apply preview
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 09:11:07 +02:00
twentyOne2x
5e1d7a5b8c
Render KB claim links in leoclean replies (#68)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Render KB claim links in leoclean replies.

Local evidence:
- 13 focused tests passed.
- py_compile passed for Telegram bot, both leoclean KB bridges, and KB claim routes.
- ruff F,E9 passed for touched files.

GitHub checks were queued at merge time, not failing.
2026-07-09 08:58:02 +02:00
twentyOne2x
4a035d8727
Teach leoclean external doctrine framing (#67)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 08:43:01 +02:00
twentyOne2x
93c480ea3c
Add GCP leoclean skill sync helper (#66)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 08:21:02 +02:00
twentyOne2x
3e6ac9b8e0
Merge pull request #65 from living-ip/codex/sync-leoclean-skills-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Sync leoclean skills during deploy
2026-07-09 08:02:51 +02:00
twentyOne2x
a7152b0a67 Sync leoclean skills during deploy 2026-07-09 08:01:43 +02:00
twentyOne2x
59525bf0bc
Merge pull request #64 from living-ip/codex/claim-links-telegram-rendering-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add canonical KB claim links
2026-07-09 07:57:05 +02:00
twentyOne2x
817d2a4592 Add canonical KB claim links 2026-07-09 07:55:47 +02:00
twentyOne2x
f2a0c0b02f
Merge pull request #63 from living-ip/codex/kb-proposal-normalization-preview-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add KB proposal normalization preview
2026-07-09 03:03:12 +02:00
twentyOne2x
c34096f605 Add KB proposal normalization preview 2026-07-09 03:01:40 +02:00
twentyOne2x
2bcf01fc8e
Merge pull request #62 from living-ip/codex/fix-kb-proposal-dashboard-runtime-import-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Fix KB proposal dashboard runtime import path
2026-07-09 02:51:39 +02:00
twentyOne2x
dfcc10e304 Fix KB proposal dashboard runtime import path 2026-07-09 02:50:25 +02:00
twentyOne2x
0e812a74d7
Merge pull request #61 from living-ip/codex/kb-proposal-admin-review-page-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add KB proposal review dashboard
2026-07-09 02:47:36 +02:00
twentyOne2x
5ec375cd6e Add KB proposal review dashboard 2026-07-09 02:46:14 +02:00
twentyOne2x
1703232f24
Add leoclean KB proposal review packets (#60)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 02:24:07 +02:00
twentyOne2x
8e3d1877c6
Source-control leoclean KB bridge (#59)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 01:09:53 +02:00
twentyOne2x
4f00f1335d
Avoid scanning backups during auto deploy chmod (#58)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-09 00:19:49 +02:00
twentyOne2x
b9acc4855c
Make GCP artifact publish idempotent (#57)
Some checks failed
CI / lint-and-test (push) Has been cancelled
2026-07-07 14:40:23 +02:00
twentyOne2x
464d1b3fd9
Add GCP infra execute canary runner (#56)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 14:34:11 +02:00
twentyOne2x
8a935b4625
Add portable restore canary capsule (#55)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 14:22:03 +02:00
twentyOne2x
fc451ec94f
Harden GCP readiness checker commands (#54)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 14:10:50 +02:00
twentyOne2x
50a744656f
Use dedicated GCP readiness service account (#53)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 13:58:37 +02:00
twentyOne2x
dba08ad63f
Add GCP service communication contract (#52)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 13:51:24 +02:00
twentyOne2x
c8bb649f95
Add GCP runtime baseline apply runner
Some checks are pending
CI / lint-and-test (push) Waiting to run
Adds a dry-run-by-default GCP runtime baseline runner, readiness contract coverage, docs, and tests for the resources required by GCP readiness.
2026-07-07 13:38:42 +02:00
twentyOne2x
c9a2573e86
Harden GCP artifact and readiness receipts
Some checks are pending
CI / lint-and-test (push) Waiting to run
Retains immutable Artifact Registry image digests and makes GCP readiness workflows fail after uploading evidence when readiness checks fail.
2026-07-07 13:24:14 +02:00
twentyOne2x
38ca357f81
Add Cloud SQL restore readback verifier (#49)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 13:16:24 +02:00
twentyOne2x
607baefc52
Add idempotent GCP IAM apply runner
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 13:05:08 +02:00
twentyOne2x
8f96b44539
Add GCP IAM split plan
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:54:53 +02:00
twentyOne2x
04e586c23e
Retain GCP readiness workflow artifacts
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:47:29 +02:00
twentyOne2x
0c9dde3f36
Add GitHub WIF GCP readiness probe (#45)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:41:09 +02:00
twentyOne2x
6dd7f9fe8c
Add GCP Cloud SQL restore drill (#44)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:34:21 +02:00
twentyOne2x
2b4423e3d1
Add SQLite to Postgres restore canary (#43)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:21:54 +02:00
twentyOne2x
ce261a8ee6
Add VPS SQLite KB backup canary (#42)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 12:05:46 +02:00
twentyOne2x
de2bfc150d
Add GCP Cloud SQL standby readiness (#41)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 11:52:08 +02:00
twentyOne2x
d878a61d8d
Check GCP communication posture (#40)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 11:27:03 +02:00
twentyOne2x
d89ee7565a
Add GitHub Artifact Registry publishing (#39)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 11:20:09 +02:00
twentyOne2x
630a1f6ea7
Harden GCP Docker build and infra readiness (#38)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-07 11:13:41 +02:00
twentyOne2x
d1512d13a5
Merge pull request #37 from living-ip/codex/gcp-staging-container-20260706
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add GCP staging container smoke
2026-07-06 20:38:58 +02:00
twentyOne2x
90f232fb8f Add GCP staging container smoke 2026-07-06 20:35:56 +02:00
Fawaz
4de98b29fd
feat(kb): apply-worker — auto-land approved proposals (stage 2 automation) (#36)
Some checks are pending
CI / lint-and-test (push) Waiting to run
* feat(kb): apply-worker to auto-land approved proposals (stage 2 automation)

Event-driven worker that turns a HUMAN-approved kb_stage proposal into canonical
state, so an approval in Telegram surfaces in Leo's identity without Leo applying
its own work.

- Fires only on status='approved' (never pending_review) -> proposer != applier
  holds; the human approval stays the trigger. No auto-approve anywhere.
- Reuses scripts/apply_proposal.py verbatim as the sole apply path (same txn,
  rowcount=1 guard, FK stamp). Connects as the narrow kb_apply role, never
  superuser, never inside the hermes harness.
- Render hook (--render-cmd / KB_APPLY_RENDER_CMD) is inert until the SOUL
  renderer (PR2) is deployed; applying still works, rendered SOUL just lags.
- Ships INERT: report-only unless --enable / KB_APPLY_WORKER_ENABLED=1. systemd
  oneshot service + 5min timer, both shipped disabled.
- 10 unit tests; candidate query validated read-only vs prod (0 applyable today).

* fix(kb): apply-worker --max-per-tick cap + poison-pill retry ceiling

Fixer draft-exit items:
- --max-per-tick=1 (default): an enabled worker lands applies one-at-a-time
  and observably instead of draining the whole approved queue in one tick.
- --max-attempts=3 ceiling with a persisted failure-count state file: a
  deterministically-failing approved proposal is treated as a poison pill and
  skipped after N consecutive failures, instead of retrying every tick forever.
  State persists on disk because the worker runs oneshot per timer tick.

Both are inert until the worker is enabled; it still ships disabled.
2026-07-05 18:23:56 -04:00
Fawaz
7bb6fc417b
feat(kb): apply_proposal engine (stage 2 of KB apply pipeline) (#35)
Some checks are pending
CI / lint-and-test (push) Waiting to run
* feat(kb): apply_proposal engine to land approved proposals into canonical

Stage 2 of the KB apply pipeline (approve -> APPLY -> render -> surface).
Turns an approved kb_stage.kb_proposals row into canonical public.* rows and
flips the ledger to 'applied' in one verified transaction.

- Connects as the narrow kb_apply role (never superuser): writes only
  strategies, strategy_nodes, claim_evidence, claim_edges + kb_proposals ledger.
  Enforces "agents propose, do not self-apply" at the DB boundary.
- Per-type handlers: revise_strategy (versioned strategy + node replace),
  add_edge, attach_evidence (requires existing source_id; source minting is
  intentionally out of scope for kb_apply's grants).
- Strict apply_payload contract (v1); freeform eval packets are normalized
  upstream, not applied directly.
- --dry-run prints exact SQL; idempotent (refuses non-approved / already-applied);
  transactional with an in-txn DO-block invariant check that rolls back on failure.
- Unit tests cover SQL builders, validation, dispatch, and status guards.

* fix(kb): rowcount=1 apply guard + real applied_by FK stamp

Closes the three draft-exit review items on the apply engine:

- Ledger flip now runs in a DO block asserting exactly one 'approved'
  row moved to 'applied' (GET DIAGNOSTICS row_count). Closes the
  concurrent double-apply race — load_proposal (read) and the flip
  (write) are separate statements, so a row lock cannot span them; only
  one concurrent apply can match status='approved', so rowcount=1 is the
  authoritative guard. A loser RAISEs and the whole txn rolls back.
- applied_by_agent_id is stamped as a real FK resolved from public.agents
  by handle, defaulting to the kb-apply service agent — no more NULL FK,
  no backfill needed.
- scripts/kb_apply_prereqs.sql: one-time superuser bootstrap — inserts the
  kb-apply service-agent row (kb_apply never gets INSERT on agents), grants
  kb_apply SELECT on public.agents, and ensures the one-active-strategy
  unique index (idempotent; already present on prod).

18/18 unit tests pass.

* fix(kb): hard-resolve applied_by handle, RAISE on NULL FK

Resolve applied_by into a variable and assert NOT NULL before the ledger
flip, instead of an inline subselect that silently stamps a NULL
applied_by_agent_id on an unresolved handle. Since the FK is ON DELETE SET
NULL, a bad handle (typo/unseeded agent) was a legal silent NULL -- the
perpetually-NULL FK we eliminated. Unresolved handle now hard-fails ->
rollback. Non-default --applied-by (operator, future drafters) is the path
that goes through the lookup and could strand NULL.
2026-07-04 19:57:49 -04:00
twentyOne2x
66ecbf316e
Merge pull request #34 from living-ip/codex/teleo-agent-autorecover-20260702
Some checks failed
CI / lint-and-test (push) Has been cancelled
Add Telegram agent autorecovery timer
2026-07-02 23:38:30 +02:00
twentyOne2x
29e7e27658 add teleo agent autorecovery timer 2026-07-02 23:37:22 +02:00
twentyOne2x
8ba76ec128
Merge pull request #33 from living-ip/codex/teleo-agent-transcript-readwrite-20260702
Some checks are pending
CI / lint-and-test (push) Waiting to run
Allow Telegram agents to write transcript dumps
2026-07-02 23:29:56 +02:00
twentyOne2x
8c6874f8e4 Allow Telegram agents to write transcripts 2026-07-02 23:28:45 +02:00
twentyOne2x
fd6132e6ce
Allow Leo paid research gate by chat title (#32)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-02 01:19:17 +02:00
twentyOne2x
c1eb4f5718
Merge pull request #31 from living-ip/codex/leo-paid-research-standalone-ack-20260701
Some checks are pending
CI / lint-and-test (push) Waiting to run
Split Telegram paid research acknowledgement from answer
2026-07-01 21:37:00 +02:00
twentyOne2x
a9a90f0faf Split paid research acknowledgement from answer 2026-07-01 21:35:42 +02:00
twentyOne2x
1872e57e26
Merge pull request #30 from living-ip/codex/leo-wallet-first-checkout-20260701
Some checks are pending
CI / lint-and-test (push) Waiting to run
Stop exposing Leo checkout URL buttons by default
2026-07-01 13:07:14 +02:00
twentyOne2x
6114750555 Stop exposing Leo checkout URL buttons by default 2026-07-01 13:05:31 +02:00
twentyOne2x
5bdd62d312
Merge pull request #29 from living-ip/codex/leo-realistic-payment-card-20260701
Some checks are pending
CI / lint-and-test (push) Waiting to run
Render Leo x402 checkout as Telegram button card
2026-07-01 01:05:51 +02:00
twentyOne2x
c6d4376a51 Render Leo x402 checkout as Telegram button card 2026-07-01 01:04:18 +02:00
twentyOne2x
ac64fd4cf4
Tag Leo auto-resume poll requests (#28)
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-07-01 00:47:36 +02:00
twentyOne2x
a27b9b3440
Merge pull request #27 from living-ip/codex/leo-telegram-payment-card-20260701
Some checks are pending
CI / lint-and-test (push) Waiting to run
Render Leo paid research as Telegram payment card
2026-07-01 00:27:40 +02:00
twentyOne2x
56df23aba0 Render Leo paid research as Telegram payment card 2026-07-01 00:26:00 +02:00
twentyOne2x
096adcb3b9
Merge pull request #26 from living-ip/codex/leo-telegram-checkout-qr-20260630
Some checks are pending
CI / lint-and-test (push) Waiting to run
Send Leo checkout QR photo in Telegram
2026-06-30 21:57:08 +02:00
twentyOne2x
af41a3d4ca Send Leo checkout QR photo in Telegram 2026-06-30 21:55:20 +02:00
twentyOne2x
29429d6385
Merge pull request #25 from living-ip/codex/leo-telegram-auto-resume-20260630
Some checks are pending
CI / lint-and-test (push) Waiting to run
Auto-resume Leo paid research in Telegram
2026-06-30 20:07:31 +02:00
twentyOne2x
85e1f78b97 Auto-resume Leo paid research in Telegram 2026-06-30 20:05:28 +02:00
twentyOne2x
06a8b547ab
Merge pull request #24 from living-ip/codex/leo-telegram-x402-quote-resume-20260630
Some checks are pending
CI / lint-and-test (push) Waiting to run
Enable quote-first Leo Telegram research
2026-06-30 19:13:33 +02:00
twentyOne2x
9b7feb4278 Enable quote-first Leo Telegram research 2026-06-30 19:12:12 +02:00
twentyOne2x
19f5346118
Merge pull request #23 from living-ip/codex/avoid-starting-legacy-teleo-leo-20260626
Some checks failed
CI / lint-and-test (push) Has been cancelled
Avoid starting inactive legacy Leo bot on deploy
2026-06-26 13:43:39 +02:00
twentyOne2x
a55f0a4500 Avoid starting inactive legacy Leo bot on deploy 2026-06-26 13:40:50 +02:00
twentyOne2x
b119255d0e
Merge pull request #22 from living-ip/codex/auto-deploy-github-remote-20260626
Use GitHub remote for VPS auto deploy
2026-06-26 13:06:39 +02:00
twentyOne2x
400b7200ba Use GitHub remote for VPS auto deploy 2026-06-26 01:19:16 +02:00
twentyOne2x
19f1221c22
Merge pull request #21 from living-ip/codex/restart-wallet-test-on-telegram-deploy-20260625
Restart wallet-test agent after Telegram deploys
2026-06-26 01:09:50 +02:00
twentyOne2x
bac4222eeb Restart wallet-test agent after Telegram deploys 2026-06-26 01:07:32 +02:00
twentyOne2x
e16d87dd57
Merge pull request #20 from living-ip/phase1b-agent-routing-local
Merge phase 1b routing and Leo x402 Telegram research
2026-06-25 18:08:44 +02:00
twentyOne2x
cb702fadf0 Merge remote-tracking branch 'origin/main' into codex/phase1b-pr20-resolve-20260625
# Conflicts:
#	telegram/agent_config.py
#	telegram/bot.py
#	telegram/http_chat_proxy.py
#	tests/test_telegram_leo_x402_bridge.py
2026-06-25 18:07:36 +02:00
twentyOne2x
1a71efcde2
Add Teleo research eval schema
Adds graph schema prerequisite plus research-eval schema/docs/tests for Leo tool-use benchmarks and x402 research telemetry. Validated by full local pytest and green CI.
2026-06-24 14:21:03 +02:00
twentyOne2x
533295d38c
Gate Telegram market context for Leo research (#18) 2026-06-23 19:16:24 +02:00
twentyOne2x
bfc28e084b
Wire Leo Telegram x402 smart research (#17)
* Wire Leo Telegram x402 smart research

* Suppress token-bearing Telegram HTTP logs

* Keep Telegram typing visible during Leo proxy calls

* Allow Leo Telegram social research spend cap

* Route contextual Leo research prompts to smart research

* Generalize Leo smart research intent routing

* Resume Leo smart research from paid work orders
2026-06-23 18:37:33 +02:00
twentyOne2x
30544dce05
Route Telegram smart research commands (#16) 2026-06-23 11:56:06 +02:00
twentyOne2x
d4f2530284
Merge pull request #15 from living-ip/codex/leo-wallet-test-existing-token-20260622
Use existing Leo wallet-test Telegram bot
2026-06-23 11:18:19 +02:00
twentyOne2x
b593dda1cf Use existing Leo wallet-test Telegram bot 2026-06-22 21:53:37 +02:00
twentyOne2x
754f5aeee7
Add Leo wallet-test Telegram runtime verifier (#14) 2026-06-22 21:41:17 +02:00
twentyOne2x
595f977a94
Add Telegram smart research gate installer (#13) 2026-06-22 21:33:56 +02:00
twentyOne2x
dba8a21e74
Allow per-agent Telegram env files (#12) 2026-06-22 21:27:32 +02:00
twentyOne2x
d0a4f518d5
Add Leo Telegram smart research bridge (#11) 2026-06-22 21:24:00 +02:00
twentyOne2x
2433ed2d8a
Merge pull request #10 from living-ip/codex/leo-wallet-test-token-installer-20260622
Add safe Telegram agent token installer
2026-06-22 20:49:06 +02:00
twentyOne2x
84e1269900 Add safe Telegram agent token installer 2026-06-22 20:47:44 +02:00
twentyOne2x
988e8581d6
Merge pull request #9 from living-ip/codex/teleo-ci-pyyaml-20260622
Declare PyYAML dependency for CI
2026-06-22 20:34:16 +02:00
twentyOne2x
9c29322972 Fix optional Telegram approval imports in tests 2026-06-22 20:33:02 +02:00
twentyOne2x
e4c0621538 Declare PyYAML dependency for CI 2026-06-22 20:31:39 +02:00
twentyOne2x
e1f2834c23
Merge pull request #8 from living-ip/codex/leo-wallet-test-telegram-20260622
Add Leo wallet-test Telegram agent config
2026-06-22 20:30:25 +02:00
twentyOne2x
adbdf4dbba Add Leo wallet test Telegram agent config 2026-06-22 20:29:52 +02:00
twentyOne2x
50b4d2345b
Merge pull request #7 from living-ip/leo-test-readiness-ancestor-check
Stabilize leo-test readiness main check
2026-06-20 15:51:24 +02:00
twentyOne2x
3c20f08d8d Stabilize leo-test readiness main check 2026-06-20 15:50:55 +02:00
twentyOne2x
513960651d
Merge pull request #6 from living-ip/leo-test-deploy-readiness-check
Add Leo test deploy readiness check
2026-06-20 15:48:59 +02:00
twentyOne2x
fa2895c45b Add Leo test deploy readiness check 2026-06-20 13:03:10 +02:00
twentyOne2x
4cc6a5d06e
Merge pull request #5 from living-ip/leo-disposable-test-agent
Add disposable Leo Telegram test agent
2026-06-20 01:48:53 +02:00
twentyOne2x
a84289a9fc
Merge pull request #4 from living-ip/leo-auto-deploy-restart-scope
Restart Leo agent after Telegram deploy changes
2026-06-20 01:48:40 +02:00
twentyOne2x
5c0222f0c4 Add disposable Leo Telegram test agent 2026-06-20 01:10:21 +02:00
twentyOne2x
1e62a94d3a Restart Leo agent after Telegram deploy changes 2026-06-20 01:03:54 +02:00
twentyOne2x
f3c63e2f8d Restart Leo agent after Telegram deploy changes 2026-06-19 23:39:23 +02:00
twentyOne2x
20f85f94dc
Merge pull request #3 from living-ip/leo-telegram-x402-bridge
Add Leo Telegram x402 bridge
2026-06-19 19:36:04 +02:00
twentyOne2x
b769eb68b6 Add Leo Telegram x402 bridge 2026-06-19 19:28:04 +02:00
twentyOne2x
2e7d4e7450 Add Leo Telegram x402 bridge 2026-06-19 19:27:12 +02:00
twentyOne2x
71ea7a625c Add decision engine replay harness
- Add source-linked model discovery registry for bakeoff candidates
- Add Rio, Theseus, and KB interop fixtures with deterministic replay proof
- Gate CI on replay output; verify with 424-test suite

`.crabbox.yaml`
`.github/workflows/ci.yml`
`docs/llm-refinement-decision-engine.md`
`docs/model-discovery-registry.md`
`fixtures/decision-engine-eval/kb_interop_propose_only.json`
`fixtures/decision-engine-eval/rio_meteora_lp_incentives.json`
`fixtures/decision-engine-eval/theseus_live_model_switch_reject.json`
`scripts/check_llm_refinement_contract.py`
`scripts/replay_decision_engine_eval.py`
`tests/test_decision_engine_replay.py`
2026-06-01 17:37:38 +02:00
twentyOne2x
27e48f3e16 Add KB interop from transcript
- Encode transcript requirements for model discovery and Pentagon boundary
- Add KB read/propose skill for Hermes, OpenClaw, and Claude-style agents
- Extend LLM contract checks; verify with 422-test suite

`.agents/skills/living-ip-kb-interop/SKILL.md`
`.agents/skills/nousresearch-hermes-agent/SKILL.md`
`.agents/skills/openclaw-agent/SKILL.md`
`docs/llm-refinement-decision-engine.md`
`scripts/check_llm_refinement_contract.py`
2026-06-01 17:16:46 +02:00
twentyOne2x
aee534e686 Add decision engine refinement contracts
- Define Rio and Theseus as economics and model-integrity evaluators
- Add DB, Hermes, and OpenClaw skills with no-secret defaults
- Gate CI on LLM refinement contracts; verify with 422-test suite

`.agents/skills/decision-engine-refinement/SKILL.md`
`.agents/skills/nousresearch-hermes-agent/SKILL.md`
`.agents/skills/openclaw-agent/SKILL.md`
`.agents/skills/teleo-db-operator/SKILL.md`
`.crabbox.yaml`
`.github/workflows/ci.yml`
`docs/llm-refinement-decision-engine.md`
`scripts/check_llm_refinement_contract.py`
2026-06-01 15:50:48 +02:00
twentyOne2x
a2620c1f19 Add Crabbox CI contract gate 2026-06-01 15:36:03 +02:00
twentyOne2x
69b4987415 Add Crabbox remote proof layer 2026-05-30 01:53:10 +02:00
twentyOne2x
59951346b2 Prove phase 1b local e2e 2026-05-29 15:08:09 +02:00
twentyOne2x
cdb0b1498d Add phase 1b local review guide 2026-05-29 14:17:28 +02:00
twentyOne2x
ca96f5f8e3 Harden local phase 1b review path 2026-05-29 14:16:12 +02:00
twentyOne2x
b9cb965591 Record phase 1b staging blocker 2026-05-29 14:01:21 +02:00
twentyOne2x
7390e1e843 Implement phase 1b agent routing 2026-05-29 14:00:13 +02:00
Fawaz
377924dabe
feat(phase1-step3): rewire critical scripts Forgejo -> GitHub (decision-engine)
Phase 1 Step 3 — migrate research-session.sh and pipeline-health-check.py off Forgejo onto GitHub living-ip/decision-engine. eval-dispatcher.sh / eval-worker.sh documented as dead code (replaced by daemon).
2026-05-22 21:43:08 -04:00
aaab659900 Merge pull request 'fix(activity-feed): emit Forgejo pr_url fallback so every event has a clickthrough' (#12) from fix/forgejo-pr-url-fallback into main
Some checks failed
CI / lint-and-test (push) Has been cancelled
Reviewed-on: #12
2026-05-13 04:40:38 +00:00
Teleo Agents
e78308862a fix(activity-feed): emit Forgejo pr_url fallback so every event has a clickthrough
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Previously _github_pr_url() only returned a URL when prs.github_pr was
populated. That field is set on only 3 of 4094 merged PRs (the rare cases
mirrored to the public GitHub repo), so pr_url was null for ~100% of the
feed. The frontend whole-row PR overlay (livingip-web PR #30) renders
only when pr_url is non-null, so until now no rows had the overlay.

Pipeline-attributed events (reweave/*, ingestion/*) are the most visible
victim: their /contributors/pipeline link lands on a sparse stub, with
no way to reach the actual commit/PR they refer to.

Fix: rename _github_pr_url -> _pr_url and fall back to the canonical
Forgejo URL (git.livingip.xyz/teleo/teleo-codex/pulls/{number}) when no
GitHub mirror exists. Verified 200 OK against a sample (#10568). GitHub
URL still wins when available.

Result: 1972/1972 events in _build_events now carry a pr_url. Whole-row
overlay starts working for everything including pipeline events.
2026-05-13 04:29:54 +00:00
a54f52234a Merge pull request 'fix(attribution): classify submitted_by by branch prefix at PR discovery' (#11) from fix/reattribute-by-branch-prefix into main
Some checks are pending
CI / lint-and-test (push) Waiting to run
Reviewed-on: #11
2026-05-13 03:57:04 +00:00
Teleo Agents
c9515c770a fix(attribution): classify submitted_by by branch prefix at PR discovery
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
reweave.py and ingestion run as the operator Forgejo token, so the prior
opener-based classifier set submitted_by=m3taversal for every system
maintenance PR. backfill_submitted_by.py never overrides non-NULL rows,
so this misattribution accumulated: ~2,748 reweave/ingestion PRs and
~3,706 <agent>/ research/entity PRs were credited to the operator on
the leaderboard and contribution_events table.

Two parts:

1. lib/merge.py: at PR discovery, classify by branch prefix first.
     reweave/, ingestion/             -> submitted_by = 'pipeline'
     <agent>/ (per _AGENT_NAMES)      -> submitted_by = '<agent>'
     otherwise human                  -> submitted_by = author.lower()
     otherwise pipeline               -> submitted_by = None
                                         (extract.py sets from proposed_by)
   Origin flag updated so domain detection and priority still fire for
   branch-classified pipeline PRs. Human PRs lowercased to maintain the
   canonical-handle contract enforced in PR #9.

2. scripts/reattribute-by-branch-prefix.py: historical cleanup.
   Per affected PR (atomic):
     - UPDATE prs.submitted_by  -> target
     - UPDATE sources.submitted_by where source_path matches
     - UPDATE contribution_events handle ('m3taversal',role='author')
       -> target, kind='agent'. Collision (target already has author
       event for PR) deletes the m3ta row; target wins.

   Scope is deliberately conservative: extract/ branches stay attributed
   to m3taversal because proposed_by-missing legitimately defaults to the
   operator (telegram drops). Only reweave/, ingestion/, and <agent>/.

   Dry-run shows 6,454 PRs + 284 events to move. Pre-flight collision
   query returns 0; pre-flight kind check confirms m3ta has only role=author
   events on this set (no challenger/synthesizer/evaluator).

   Idempotent. Dry-run by default. Run with --apply after deploy + DB
   snapshot.
2026-05-13 03:49:10 +00:00
3dca3aab5f Merge pull request 'docs: rewrite public README' (#8) from ship/readme-public-rewrite into main
Some checks are pending
CI / lint-and-test (push) Waiting to run
Reviewed-on: #8
2026-05-13 03:20:02 +00:00
2ee9dd5150 Merge pull request 'fix(activity-feed): canonicalize contributor handle so profile links resolve' (#9) from fix/activity-feed-canonical-handle into main
Some checks are pending
CI / lint-and-test (push) Waiting to run
Reviewed-on: #9
2026-05-13 03:19:41 +00:00
b29ec95dd8 Merge pull request 'fix(attribution): canonicalize submitted_by at write time + historical normalizer' (#10) from fix/canonicalize-submitted-by into main
Some checks are pending
CI / lint-and-test (push) Waiting to run
Reviewed-on: #10
2026-05-13 03:19:27 +00:00
Teleo Agents
74bf0461e8 fix(attribution): canonicalize submitted_by at write time + historical normalizer
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Companion / write-side fix to fix/activity-feed-canonical-handle.

The activity-feed canonicalization was a read-side guard. The bug at the
source is that extract.py and two backfill scripts write decorated
strings (Vida (self-directed), pipeline (reweave), @m3taversal) into
prs.submitted_by and sources.submitted_by. Downstream readers
(lib.contributor.insert_contribution_event, scripts/scoring_digest,
diagnostics/activity_feed_api) all strip the decorator on read — but
anything that reads the column verbatim (like /api/activity-feed before
the read-side fix) 404s on /contributors/{decorated-handle}.

Stop writing the decorator. The self-directed signal is already carried
by intake_tier == research-task plus the prs.agent column; the suffix
is redundant string noise that costs us correctness at every consumer
that forgets to strip.

Changes:

- lib/extract.py:690 — write canonical handle via attribution.normalize_handle.
  Direct elif for intake_tier == research-task now stores just agent_name.
  @m3taversal -> m3taversal.

- diagnostics/backfill_submitted_by.py — same fix in two branches plus
  the reweave branch (pipeline (reweave) -> pipeline).

- scripts/backfill-research-session-attribution.py — UPDATE prs sets
  agent handle alone, no suffix. Docstring + log line updated.

- scripts/normalize-submitted-by.py (new) — one-time backfill that
  canonicalizes existing prs.submitted_by and sources.submitted_by rows.
  Strips trailing parenthetical decorators, lowercases, drops @. Defaults
  to dry-run; --apply to commit. Skips rows that would normalize to
  invalid handles (no garbage falls through silently).

Dry-run against live pipeline.db:
  prs:     3008 rows need normalization (clean mappings, 0 invalid)
  sources: 730 rows need normalization (clean mappings, 0 invalid)
  Total:   3738 rows. All map to existing handle column values.

After this lands + auto-deploys, the operator should run
  python3 scripts/normalize-submitted-by.py --apply
once to clean historical rows. The read-side canonicalization in
diagnostics/activity_feed_api.py (fix/activity-feed-canonical-handle)
becomes redundant defense-in-depth instead of load-bearing.

No KB writes.
2026-05-13 02:56:50 +00:00
Teleo Agents
01097da22c fix(activity-feed): canonicalize contributor handle so profile links resolve
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
The activity feed was returning decorated strings like "Vida (self-directed)"
and "@m3taversal" in the contributor field. The frontend uses that field as
both display label and routing handle, so /contributors/Vida%20(self-directed)
404s — Next fires notFound() in [handle]/page.tsx.

Root cause: _normalize_contributor only stripped @ and whitespace; it did not
lowercase or strip the " (self-directed)" suffix that extract.py and the
older backfill_submitted_by.py wrote into prs.submitted_by. Mixed-case
agent names (Vida vs vida) and pipeline decorators ("pipeline (reweave)")
both fell through.

Fix: lowercase + strip any trailing parenthetical decorator. Valid handles
match ^[a-z0-9][a-z0-9_-]{0,38}$ per attribution._HANDLE_RE and cannot
contain parens, so the strip is lossless.

DB simulation against 3612 merged-PR events: 0 orphan handles after
normalization (was 12 orphan label-variants before).

No KB writes — pure read-side normalization in the API layer.
2026-05-13 02:39:18 +00:00
6c66da33e4 feat(activity-feed): add pr_url field for GitHub PR clickthrough
Some checks failed
CI / lint-and-test (push) Has been cancelled
2026-05-11 20:58:36 -04:00
c3f2010a42 feat(activity-feed): add kind + target_url, fix research-session pseudo-slugs
Some checks are pending
CI / lint-and-test (push) Waiting to run
The /api/activity-feed event shape didn't give the frontend a reliable
clickability signal. Two failure modes:

1. Source-archive events (extract/* PRs that filed a paper into
   inbox/archive/ but didn't extract a claim) returned claim_slug="".
   Frontend rendered <Link href="/claims/"> which Next normalized to
   /claims and redirected to /knowledge-base. Wrong page.

2. Research/entity session commits (e.g. astra/research-2026-05-11)
   with empty descriptions fell through to "create" classification with
   a pseudo-slug like research-2026-05-11. Frontend rendered
   /claims/research-2026-05-11 -> 404.

Fix:

- Add `kind` enum (canonical): claim_merged | claim_enriched |
  claim_challenged | source_archived | session_digest. Replaces the
  internal `type` for downstream consumers; `type` kept populated for
  in-flight callers during migration.

- Add `target_url`: explicit clickability signal. Frontend renders
  <Link> when non-null, <span> when null. No special-casing needed.
    * claim_* events -> /claims/{slug}
    * source_archived -> Forgejo blob URL at inbox/archive/{domain}/{slug}.md
    * session_digest -> null (no clickthrough surface yet)

- Detect research/entity commits with empty descriptions as
  session_digest in _classify_event, instead of synthesizing a phantom
  create event with a date-shaped pseudo-slug.

- type filter accepts both legacy `type` and new `kind` values so
  callers migrate at their own pace.

Verified live: source events resolve to inbox/archive/{domain}/...
Forgejo URLs, session-digest rows return target_url=null,
claim_merged events keep /claims/{slug} unchanged.
2026-05-11 12:36:25 +01:00
ed4893e837 fix(claims): unwrap ```markdown code fences + 404 for fragments
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two issues Ship hit on the Montreal Protocol claim:

1. 500 on canonical stem lookup. File starts with ```markdown wrapper
   instead of bare --- frontmatter delimiter. _split_frontmatter checked
   startswith("---") and bailed, returning "frontmatter parse failed".
   Same wrapper exists on 6 other claim files (audit grep). Now strip
   the wrapper before frontmatter detection.

2. 404 on long activity-feed slug. Same root cause — _build_indexes
   couldn't read the file's title from frontmatter, so by_title never
   indexed it, so title-fallback resolution had nothing to match against.
   Both bugs collapse once we unwrap.

Also: switched "file exists but has no frontmatter" from 500 to 404 with
reason=file_no_frontmatter. These are stray enrichment fragments living
in domains/ that never got merged into a parent claim. From the API
caller's perspective there's no claim at that slug — 500 implied
"server bug, retry later" which isn't actionable.

Verified: 3/3 wrapped claims (montreal, medicare, dod) now return 200
warm-cache ~13ms. Long-slug repro (montreal) resolves via title fallback
to canonical stem. Negative test (nonsense slug) still 404.
2026-05-11 12:02:54 +01:00
73880e138d fix(claims): resolve long activity-feed slugs to canonical file stems
Some checks are pending
CI / lint-and-test (push) Waiting to run
Activity feed emits slugs derived from PR description (the slugified claim
title), which can be longer than the on-disk file stem (agents pick shorter
hand-chosen filenames). Pure exact-stem lookup 404s on those.

Three-tier resolution in handle_claim_detail:
1. Exact stem match (existing behavior)
2. Title fallback: normalize requested slug, look up via by_title index
   (already populated from frontmatter title during _build_indexes)
3. Prefix fallback: longest common prefix among stems, anchored at 32 chars
   to prevent spurious hits

Response slug returns the canonical on-disk stem so frontend share-links
and caches converge to one form.

Repro: GET /api/claims/spacex-and-amazon-kuiper-non-endorsement-of-wef-debris-
guidelines-demonstrates-systemic-voluntary-governance-failure-at-the-scale-
where-it-matters-most was 404; now 200, returns shorter on-disk slug
'...-governance-failure'. Negative case (nonsense slug) still 404s.

Reported by Ship — Cory-facing demo path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:51:41 +01:00
1bc541ac93 fix(reaper): tighten research-session pattern to literal YYYY-MM-DD shape
Some checks are pending
CI / lint-and-test (push) Waiting to run
Apply Ganymede review of 50b888a:

MUST-FIX — pattern %/research-2% was broader than the comment claimed.
Matched anything/research-2[anything] including agent-named branches like
theseus/research-2nd-attempt-on-X or vida/research-2024-revisited. The
documented invariant said "date suffix only" but the SQL didn't enforce
it. Defense-in-depth was the framing; pattern needed to match the
framing.

Fix uses SQLite `_` single-char wildcards: research-20__-__-__ requires
exactly research-20[2-char][-][2-char][-][2-char], i.e. literal
YYYY-MM-DD shape. Threads the needle:
  - theseus/research-2026-04-30  ✓ (catches all 15 currently stuck)
  - rio/research-2099-12-31      ✓ (good through 2099)
  - theseus/research-2nd-attempt ✗ (correctly excluded)
  - vida/research-2024-revisited ✗ (correctly excluded — no -MM-DD shape)
  - rio/research-batch-agents-... ✗ (no date prefix at all)

NIT — comment said "Three classes qualify" then listed four. Off-by-one
fixed; comment now correctly says "Four classes."

Pre-deploy verified: tighter pattern catches all 15 currently-stuck
research PRs (clay/leo/astra/theseus/vida/rio research-2026-{04-28
through 05-02}). Zero false-positive risk on current branch namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:10:49 +01:00
50b888a751 fix(reaper): extend allowlist to */research-2* daily-cron sessions
Apply Step 1 of stuck-PR triage. The May 7 reaper allowlist (extract/,
reweave/, fix/) deliberately excluded all agent-prefix branches per
Ganymede's review nit #3 — the rationale being that agent branches are
WIP feature work owned by the agent and shouldn't be auto-closed.

That decision was correct for theseus/feature-foo style branches.
It's wrong for {agent}/research-{YYYY-MM-DD} branches: those are daily
cron output, categorically disposable, regenerated by tomorrow's session.
Same shape as extract/ — content the pipeline-cron created and can
recreate, not feature work owned by the agent.

Production impact: 15 of 16 currently-stuck PRs are research-session
verdict-deadlocks aged 8-12 days. Without this change they sit forever
because the substantive_fixer can't classify (eval_issues=[] or
mechanical-only) and the reaper allowlist excludes them. Once live, next
hourly reaper cycle picks them up under the standard 24h-deadlock gate.

Pattern choice: %/research-2* (date-suffix) over %/research-% (loose).
Verified 15/15 stuck PRs match the tight pattern; sanity-check found
rio/research-batch-agents-memory-harnesses (manually-named, not date-
suffixed) which the loose pattern would catch and the tight pattern
correctly excludes. Closed-status today, but a future hand-named research
thesis branch sitting in request_changes for 24h would have been at risk.
The date prefix '2' threads the needle until 2030 and ages naturally.

Documented as an allowlist invariant ("disposable pipeline-generated
branches") rather than a list, per Step 3 of the plan — future additions
should match the invariant or update it explicitly.

Verified live before pushing:
- 15/15 currently stuck research PRs match the new pattern
- Zero false positives on existing branch namespace (closed branches
  excluded by status='open' guard regardless)
- Existing extract/ reweave/ fix/ allowlist members unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:00:48 +01:00
0eb26327fc feat(claims): /api/claims/{slug} canonical detail endpoint
Some checks are pending
CI / lint-and-test (push) Waiting to run
Implements Ship's claim detail contract — one round-trip, all data
resolved server-side. Replaces thin domain-only stub with full tree walk
(domains/ + foundations/ + core/), DB joins for PRs and reviews, and
server-side wikilink resolution to eliminate frontend N+1 cascades.

Response shape (Ship brief 2026-04-29):
  slug, title, domain, secondary_domains, confidence, description,
  created, last_review, body (raw markdown), sourced_from, reviews,
  prs, edges {supports,challenges,related,depends_on}, wikilinks

Wikilink resolution:
- Builds title→stem index from frontmatter title field, fallback to
  filename stem normalized via _normalize_for_match
- Returns flat {link_text: slug_or_null} map; unresolved → null so
  frontend can render plain text
- Inline normalization (lowercase, hyphen↔space, collapse whitespace,
  strip punctuation). Note: lib/attribution.py exposes only
  normalize_handle today, not the title normalizer Ship referenced.
  If a canonical helper lands later, point at it.

Caches:
- title→slug index: 60s TTL (warm cache <20ms p50 verified)
- list endpoint: 5min TTL (preserved from prior)
- Cold: ~3.3s for tree walk of 1,866 files; warm: 13-17ms

Bug fixed in second pass:
- _resolve_sourced_from defaulted title="" which leaked LIKE '%%'
  matching every PR. Now requires non-empty title+stem; handler falls
  back to slug.replace("-"," ") when frontmatter title is missing.

Verified live on VPS:
- AI diagnostic triage claim (no fm.title): sourced_from=1, prs=0
  (correct — Feb claim, pre-description-tracking)
- Recent extract PR claim: sourced_from=1 with URL, prs=1, reviews=1,
  last_review populated, edges 3 supports + 7 related, wikilinks 0
- 404 on missing slug: correct
- Claim with [[maps/...]] wikilink: 5/6 resolved (correct null on map)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:37:26 +01:00
fc002354d4 fix(substantive_fixer): json_valid guard in front of json_each
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ganymede review of 5db6a02 (msg 2 of 3): json_each(invalid_json) throws
'malformed JSON' and propagates up through EXISTS, failing the SELECT.
The fix-cycle call site at teleo-pipeline.py:104 isn't try/except wrapped
(the reaper at line 109-116 is, the substantive cycle isn't), so a single
corrupt eval_issues row would trip the fix-stage breaker after 5 occurrences.

Fix is one line — AND json_valid(eval_issues) before the EXISTS clause.
json_valid(NULL) returns NULL (false in WHERE), json_valid(invalid) returns 0,
json_valid(valid) returns 1. SQLite 3.9+, predates VPS 3.45.1.

WARN-on-corrupt-JSON path kept per Ganymede's Q3 — json_valid and json.loads
use technically distinct parsers, cost is ~3 rows × parse-empty-string per
cycle, journal entry names the failure mode if SQLite ever surfaces a row
that passes both SQL guards but fails json.loads.

Comment updated to reflect new guard ordering.
2026-05-08 13:12:25 -04:00
5db6a0248c fix(substantive_fixer): SQL-side actionable-tag filter, eliminate head-of-line
Step 4 of the stuck-PR triage. Push the FIXABLE/CONVERTIBLE/UNFIXABLE_TAGS
intersection from a post-fetch Python loop into the SELECT WHERE clause via
json_each + EXISTS. LIMIT 3 now always returns 3 actionable rows (or fewer if
that's all there are), eliminating the head-of-line block where 3 oldest
empty-eval_issues PRs occupied the slots forever.

Background: 11 hours of post-deploy logs showed substantive_fix_cycle stuck
emitting "0 actionable from 3 candidate(s) — head-of-line: [(3922, []), (3926,
[]), (3940, [])]" every cycle. Reaper closed those three on schedule, then a
new triple of empty-eval_issues PRs took their place. Reaper-as-primary-clearance
worked but is defense-in-depth, not the right architecture. Source of the block
is upstream in this SELECT.

Implementation choice: json_each + EXISTS over LIKE. Robust against tag-name
substring overlap, future-proof against tag renames, and SQLite 3.45.1 on VPS
fully supports it. Verified live: returns 13 of 28 currently-stuck PRs as
actionable, 15 fall through to reaper as before.

Tag list builds from the routing constants at runtime so adding a new tag
auto-updates the SELECT filter — no two-place edit footgun.

WARN-on-corrupt-JSON path retained as defense-in-depth (json_each and
json.loads use different parsers; technically possible for a row to pass one
but not the other).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:52:12 -04:00
4b2b59b184 fix(reaper): branch allowlist for disposable pipeline-managed branches
Some checks are pending
CI / lint-and-test (push) Waiting to run
Apply Ganymede review nit #3 from f97dd15 review (the deferred close_on_forgejo
fix already landed in e14b5f2 — Ganymede was reviewing the older commit).

SQL gate previously had no branch filter — empirically all 92 candidates were
extract/* but structurally any agent branch in the deadlock shape was a
candidate. Positive allowlist for extract/, reweave/, fix/ scopes the reaper
to disposable pipeline-managed branches that the pipeline created and can
recreate. Agent branches (theseus/, vida/, epimetheus/, etc.) are WIP feature
work and must not be reaped — owners review their own PRs on their own cadence.

Cheap target-class lock complementing the LIMIT 50 blast-radius cap.
Same scoping principle as PIPELINE_OWNED_PREFIXES, but tighter — epimetheus/
review branches are pipeline-owned for merge purposes but NOT disposable.

Items 2-4 from this review:
- WARNING #2 (audit_log idx_audit_event_ts): defer to followup branch alongside
  sync-mirror migration cleanup, as Ganymede suggested.
- NIT #3 (this commit): branch allowlist applied.
- NIT #4 (token asymmetry comment=admin/close=leo): confirmed established
  codebase pattern. merge.py:946-948 does the same — comment system-toned,
  close attributed to Leo for verdict-source UI clarity. Not accidental.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:43:53 -04:00
ba234ec4b3 fix(reaper): apply Ganymede review — dual-PATCH drift, breaker isolation, env config
Followup to f97dd15. Four fixes from review:

MUST-FIX #1 — Forgejo double-PATCH drift
  reaper closes PR via forgejo_api PATCH at line 689, then close_pr() at
  line 700 issued a second PATCH (default close_on_forgejo=True). On
  transient failure of the second PATCH, close_pr returns False without
  updating the DB → status='open' even though Forgejo is closed. Pass
  close_on_forgejo=False so DB close is unconditional after the explicit
  Forgejo PATCH succeeds.

MUST-FIX #2 — reaper exception trips fix breaker
  Unhandled exception in verdict_deadlock_reaper_cycle propagated to
  stage_loop, recording fix-stage failures. After 5 reaper failures the
  fix breaker would open and block mechanical+substantive for 15 min.
  Wrap reaper call in try/except in fix_cycle (same exception-isolation
  pattern as ingest_cycle's extract_cycle wrapper). Defense-in-depth
  must never block primary paths.

WARNING #1 — throttle SQL full-scan
  audit_log only has idx_audit_stage. Filtering on event alone caused
  full-table scans every 60s. Added stage='reaper' so the planner uses
  the existing index — reaper writes audit rows under stage='reaper'
  already so the filter is correct.

WARNING #2 — REAPER_DRY_RUN as code constant
  Flipping dry-run → live required edit + commit + push + deploy +
  restart. Moved REAPER_DRY_RUN, REAPER_DEADLOCK_AGE_HOURS,
  REAPER_INTERVAL_SECONDS, REAPER_MAX_PER_RUN to lib/config.py with
  os.environ.get() overrides. Operator now flips via systemctl edit
  teleo-pipeline.service (Environment=REAPER_DRY_RUN=false) + restart.
  Defaults remain safe: dry-run, 24h age, hourly throttle, 50/run cap.

NIT — dry-run counter naming
  Renamed local `closed` counter in dry-run path to `would_close` so the
  heartbeat audit ("X closed, Y would-close") and journal log are
  unambiguous. Function still returns closed + would_close so callers
  see total work done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:43:53 -04:00
e63d27d259 fix(reaper): verdict-deadlock reaper — close stuck PRs after 24h
Defense-in-depth for PRs that substantive_fixer can't make progress on.
Targets two stuck-verdict shapes empirically observed in production:

  1. leo:request_changes + domain:approve
     Leo asked for substantive fix; fixer either failed silently
     (no_claim_files / no_review_comments / etc.) or the issue tag isn't
     in FIXABLE | CONVERTIBLE | UNFIXABLE.

  2. leo:skipped + domain:request_changes
     Eval bypassed Leo (eval_attempts >= MAX). Domain rejected with no
     structured eval_issues. fixer can't classify the issue.

92 PRs match this gate today, oldest at 2026-04-24 (13d stuck).

Behavior:
  - Hourly throttle via audit_log sentinel ('verdict_deadlock_reaper_run').
  - REAPER_DRY_RUN=True default — first deploy emits 'would_close' audit
    events only. No DB writes. No Forgejo writes. (Ship Apr 24 directive.)
  - 24h cooldown, oldest-first, capped at 50 per run.
  - Heartbeat audit fires whether dry-run or live, so throttle works.
  - Live mode: posts comment + closes Forgejo PR + close_pr() in DB.
    Audits 'verdict_deadlock_closed' per PR.
  - Forgejo PATCH None → skip DB close (avoid drift).

Wired into fix_cycle() in teleo-pipeline.py. Runs after mechanical
and substantive fixes, never blocks them.

Followup (post first-run audit verification):
  - Operator inspects 'verdict_deadlock_would_close' audit rows
  - Flips REAPER_DRY_RUN to False, redeploys
  - Reaper actually closes on next hourly tick
2026-05-07 23:43:53 -04:00
517e9884cc fix(substantive_fixer): WARN on corrupt eval_issues JSON
Some checks are pending
CI / lint-and-test (push) Waiting to run
Third silent return path in substantive_fix_cycle — JSON-decode except
at the eval_issues parse drops rows that don't reach skipped_no_tags
or substantive_rows. If all 3 LIMIT-3 candidates have corrupt JSON,
cycle returns 0,0 with no log entry.

WARN level (not INFO): corrupt JSON is abnormal (post-merge column
drift, hand-edited DB row, partial write during crash). If this fires,
ops want to chase the upstream column-write path. If it never fires,
baseline noise stays at zero.

Closes the visibility gap on ALL silent returns in this function, not
just the two patched in 3f8666e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:33:08 -04:00
3f8666ee0c fix(substantive_fixer): surface silent-skip reasons at INFO
Two silent paths in substantive_fix_cycle masked a 13-day stall:

1. Filter strips all candidates → return 0,0 with no log. With LIMIT 3
   ordered created_at ASC, if the oldest 3 have no fixer-actionable tags
   (e.g. eval_issues=[] from leo:skipped+domain:request_changes), the
   cycle silently picks the same head-of-line every tick.

2. _fix_pr early-returns logged at DEBUG only — invisible without
   fleet-wide DEBUG. Skip reasons (no_claim_files, no_review_comments,
   not_open lock, worktree_failed, etc.) never surfaced in journalctl.

Patch: log skipped candidate eval_issues when no actionable rows
found (path 1); promote DEBUG→INFO for per-PR skip reasons (path 2).
Zero behavior change — observability only.

Diagnosis context: 98 PRs stuck >3d, last successful substantive_fixer
event 2026-04-24. Need journal evidence to choose between (a) one-line
fix to the cycle, (b) larger _fix_pr regression. (Ship Step 2 directive.)
2026-05-07 11:58:22 -04:00
87f97eb4fa sync-mirror: surface tracker SELECT/INSERT failures to ops log
Some checks failed
CI / lint-and-test (push) Has been cancelled
Per Ganymede review: silent fall-through with no log entry is the
failure mode that bites. SELECT redirects stderr to $LOG, falls back
to empty string on failure. INSERT wrapped in if-not branch with WARN
log naming the (branch, sha, pr_number) so duplicate auto-create
possibility is visible.

Matches the Step 0/0b/4.5 observability pattern from prior reviews.
Behavior unchanged on the success path; failures now greppable.
2026-05-01 15:48:28 +01:00
ad1d82f5ee fix(sync-mirror): tracker gate to break empty auto-create loop
Diagnosis (per Ganymede pushback): the original mechanism story was wrong.
Vida and Leo show 100+ PRs at 0 merge failures — luck doesn't produce
that. Real cause is sync-mirror's auto-create loop, not session spawning.

Verified data:
- vida/research-2026-04-30: 1 commit on branch, 303 PRs in DB
- reweave/2026-04-29: 1 commit on branch, 840 PRs in DB
- Cron fires once/day per agent; reweave fires once/day at 01:00 UTC
- Forgejo currently has 0 PRs for vida (all merged/closed); 3 distinct
  SHAs total across reweave's history (PRs replay same SHA repeatedly)

Mechanism (confirmed in /opt/teleo-eval/logs/sync.log):
1. Pipeline merges PR → calls _delete_remote_branch on Forgejo
2. Next sync cycle: git fetch forgejo --prune drops the local Forgejo
   ref; refs/remotes/origin still has it (GitHub copy untouched)
3. comm sees branch GitHub-only → re-pushes to Forgejo at original SHA
4. HAS_PR check uses ?state=closed&limit=50 — closed PR for this branch
   scrolled out of pagination window long ago → returns "no"
5. Auto-create POST → fresh Forgejo PR (e.g. #7295 created at 21:46 for
   branch SHA from 04:12)
6. Pipeline merges (cherry-pick is empty no-op since content's on main;
   reweave union produces "already up to date" via the empty-diff guard
   shipped in 923454c) → _delete_remote_branch → loop

Fix (per Ganymede design point #2: "right place is discovery, not
_claim_next_pr"): SHA-based tracker in pipeline.db. Records (branch, sha)
after every successful auto-create. Subsequent cycles see the same
(branch, sha) → skip the entire push+create sequence. Cheap O(1) sqlite
lookup per branch per cycle.

Why SHA, not branch: research-session.sh and nightly-reweave.sh both use
--force push, so a branch can legitimately get new commits over time.
Tracker keys on SHA so genuine new commits produce a tracker miss → PR
creation proceeds normally. No regression on legitimate branch reuse.

Why pipeline.db, not flat file: shared with discover_external_prs +
audit_log + the agent's own tooling; survives sync-mirror restarts;
ACID-safe under the cron's 2-min cadence. CREATE IF NOT EXISTS is
inline (no migration needed) because this table is private to
sync-mirror — pipeline daemon doesn't read it.

Validated against /tmp/pipeline-test.db copy: gate fires on known
(branch, sha), misses on new SHA (correctly allows new content).

Defense-in-depth — leaves existing HAS_PR check in place. Tracker is
the durable signal; HAS_PR is best-effort and may catch cases the
tracker hasn't seen yet (e.g. PR created out-of-band).

Reweave numbers (Ganymede point #3): same shape, same fix. Both research
and reweave loops killed by the same gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:42:47 +01:00
923454c9ea extract: document basename-uniqueness invariant + skip _-prefixed archive files
Some checks failed
CI / lint-and-test (push) Has been cancelled
Two nits from Ganymede review of ed4af4d:

1. Archive-basename filter depends on basename-uniqueness across queue+archive.
   Current naming (date-prefix + topic-slug) makes collisions rare, but if
   short generic names like "notes.md" enter the queue, the filter silently
   false-positives. Comment block names the assumption.

2. Archive walk now skips _-prefixed files, matching the standing convention
   everywhere else (search.py STRUCTURAL_FILES, reweave wiki-link skip, Layer
   0 entity exclusion). Defensive — no _*.md exists under inbox/archive/
   today, but consistent with codebase convention if a future operator drops
   _README.md to document the directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:09:19 +01:00
ed4af4d72e fix(extract): dedup queue sources whose basename is already in archive
Daemon re-extracted same source every ~4h cycle when research-session
commits on agent branches re-introduced already-archived queue files.
Existing daemon filters (DB-status, open-PR, 4h cooldown) all missed
this pattern because the queue file gets a fresh sources row at
status='unprocessed' on each re-add, the cooldown lapses exactly at
the cycle interval, and the open-PR filter only catches in-flight
extractions.

Add an archive-basename filter immediately after the queue scan: if
a file with this basename exists anywhere under inbox/archive/, skip.
Archive copy is the source of truth — once extracted, the queue copy
is stale by definition.

Validation against pipeline.db (last 7d):
  78 sources had multiple extract PRs (32% duplicate rate)
  73/78 (94%) carry an archive copy and would have been caught.
  Current queue: 35/99 sources (35%) have archive duplicates today.

Pentagon-Agent: Epimetheus <0144398e-4ed3-4fe2-95a3-3d72e1abf887>
2026-04-30 11:05:39 +01:00
ed5f7ef6cc fix(merge): correct audit-ref comment + add sentinel-drift warning
Some checks failed
CI / lint-and-test (push) Has been cancelled
Two nits from Ganymede line-level review of 7741c1e:

1. Comment at lines 562-565 said --force-with-lease but code is plain
   --force. Comment now describes the actual behavior: bot-owned per-PR
   audit ref, intentional overwrite on stale refs from prior aborted
   attempts, no concurrent writer to lease against.

2. Sentinel-regex extraction in _merge_domain_queue dispatch had no
   graceful-failure log. If the _merge_no_ff_external success-message
   contract drifts and any of the three regexes (M, audit_ref, external
   PR #) miss, dispatch silently builds a comment with None values and
   writes audit_log JSON with null fields. Added a warning log when any
   regex misses — signal-only, doesn't gate the close path since the
   merge already succeeded.

Branch: epimetheus/external-merge-flow-bug1
Parent: 7741c1e (Ship Msg 3 architecture review close)
Diff:   +11/-3, single file lib/merge.py

Ganymede: 3-message protocol Msg 3 (nits applied, ball returned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:19:08 +01:00
7741c1e6de fix(merge): synthetic _merged/* ref + function-owned ff-push (Ship Msg 3)
Phase 2 review fix #1 (architectural pushback): replace force-push of
contributor's gh-pr-N/* branch with a three-step synthetic-branch flow:

  1. Worktree on local branch _merged-{slug} from origin/main
  2. git merge --no-ff origin/{branch} into the local branch
  3. Push merge commit to origin/_merged/{branch} (synthetic audit ref)
  4. Function ff-pushes merge_sha → origin/main directly

Contributor's gh-pr-N/* branch on Forgejo is now never touched.
Force-pushing it would have rewritten the tip with a merge commit the
contributor didn't author — confusing bot force-push in Forgejo PR UI.
Mirrors the _clean/* synthetic branch pattern in cherry-pick.

Function now owns the push to main (was dispatch's job for cherry-pick
and reweave). Returns sentinel "merged --no-ff (external PR #N, M=<sha>,
audit_ref=...)" that dispatch detects to skip its ff-push and route
directly to PR-close + mark_merged + audit. Audit detail JSON now
includes merge_commit_sha + audit_ref + github_pr (Ship review #5).

Smoke-tested in scratch repo end-to-end:
  - contributor branch tip unchanged ✓
  - audit ref _merged/gh-pr-90/... carries merge SHA ✓
  - main tip equals merge SHA (ff-push, no force) ✓
  - contributor SHA ancestor of main → GitHub badge fires ✓

Sentinel return parsed via 3 regexes in dispatch (full 40-char SHA in
return string for durability). Branch comment in dispatch explicitly
notes contributor branch is left in place — sync-mirror keeps the
GitHub PR <-> Forgejo PR link observable through it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:32:52 +01:00
992b4ee36f feat(merge): _merge_no_ff_external for gh-pr-* branches (Phase 2)
External GitHub fork PRs need their contributor commit SHA in main's history
for GitHub's "merged" badge to fire. Cherry-pick rewrites the SHA, breaking
that detection. New _merge_no_ff_external function preserves the SHA via a
true merge commit.

Mechanics (mirrors _cherry_pick_onto_main shape):
1. Fetch origin/main + origin/{branch}
2. Detached worktree at origin/main, git merge --no-ff origin/{branch}
   with verbose message: "Merge external GitHub PR #{N}: {branch_slug}"
3. Force-push merge commit M as origin/{branch}, replacing branch tip
4. Dispatch's existing ff-push origin/{branch} → main propagates M to main

M has parents [main_sha, contributor_sha]. M is a fast-forward descendant
of main_sha (first-parent chain), so the ff-push to main is valid without
--force. Contributor SHA reachable from main → GitHub recognizes merged.

Conflict handling: same auto-resolve as cherry-pick — entity-only conflicts
take main's version (--ours = current worktree HEAD = main), other conflicts
abort with detail.

Backout: config.EXTERNAL_PR_NO_FF_MERGE = True (default). Set False to fall
back to cherry-pick if no-ff destabilizes throughput one week pre-Accelerate.

Branch dispatch in _merge_domain_queue:
- reweave/* → _merge_reweave_pr (existing)
- gh-pr-N/* AND config.EXTERNAL_PR_NO_FF_MERGE → _merge_no_ff_external (new)
- everything else → _cherry_pick_onto_main (existing default)

Verified end-to-end in scratch repo:
- merge commit M has [main_sha, contributor_sha] as parents
- contributor SHA is ancestor of M
- after ff-push, contributor SHA is in main's history (GitHub badge fires)
- regex parses 8 cases correctly (real fork PR + edge cases reject cleanly)

Architecture per Ship Msg 3 / doc v3 (537cfd5 on epimetheus/external-merge-flow-design).
Phase 1 (sync-mirror self-heal) deployed yesterday. Phase 3 (FwazB PR #90 cleanup)
queued behind this deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:18:37 +01:00
de204db539 fix(sync-mirror): tighten gh-pr-* regex + document SQL-integer-safety
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ganymede review nit on commit 1eb259d:

- Regex changed from [0-9]* (zero-or-more) to [0-9][0-9]* (one-or-more,
  portable BRE form of [0-9]+ that works on both GNU and BSD sed).
- Empty/non-numeric branches now fail at parse, not just at the empty-guard
  below — SQL-integer-safety load-bearing on the regex alone.
- Comment above the UPDATE notes the integer-validation invariants
  (INTEGER `number` column + regex-validated gh_pr_num) since bash sqlite3
  has no parametric binding.

Smoke tested: gh-pr-/foo, gh-pr-abc/foo no longer parse to non-empty.
gh-pr-90/main, gh-pr-4066/contrib/x, gh-pr-1/x all parse correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:07:50 +01:00
1eb259de8a fix(sync-mirror): self-heal sweep for orphaned gh-pr-* github_pr links
Step 0 (new): runs once per cron tick before per-repo work. Selects PR rows
where branch matches gh-pr-% but github_pr IS NULL, parses the PR number
from the branch name, and updates github_pr + source_channel='github'.

Recovers from races and transient failures in the existing Step 4.5 link
UPDATE — no retry path before. The sweep IS the backfill: same SELECT/UPDATE
heals historical orphans (FwazB PR 4066 picked up on first cron tick) AND
future races on subsequent ticks. No separate one-shot script needed.

Properties:
- Idempotent: SELECT empty when clean, zero work
- No API calls: branch name encodes the GitHub PR number deterministically
- Bounded log volume: one line per actually-healed row
- Runs before any sync_repo work, ahead of branch-mirror loop and the
  auto-create-PR block in Step 4 — same-cycle convergence on fresh races

Closes the Bug #2 path that left FwazB's PR 4066 with github_pr=NULL,
preventing on_merged() from posting comment + closing the GitHub PR.

Verified end-to-end on live DB snapshot:
- before: 4066 had github_pr=NULL
- after sweep: 4066 has github_pr=90, source_channel='github'
- second run: zero output (idempotent)

Phase 1 of docs/external-contributor-merge-flow.md (v2, sweep-only).
Ship architecturally approved Msg 2/2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:02:37 +01:00
b8504c1b60 docs: rewrite public README
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Replaces the directory-listing format with one that explains what the
pipeline does and shows production scale. Verified all numbers against
production (1,546 claims, 13 domains, 1,975 merged PRs, 508 last-7d
throughput, 94% approval, ~\$0.10/merged claim incl. all stages).

Removes the VPS layout section (IP + paths + username) per Epimetheus
review — that detail moves to the private teleo-ops repo. Generalizes
deploy targets without naming the host.

Adds two Mermaid diagrams (pipeline flow + review tier matrix), both
syntactically safe across GitHub and Forgejo 9 / Gitea 1.22.

Drops the per-directory ownership table — CODEOWNERS is the single
source of truth on review authority. Keeps the high-level role map
for orientation only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 10:19:18 +01:00
33f6ca9e3f fix(mirror): setup script pushes main+tags only (consistency with sync-mirror)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Initial setup-infra-mirror.sh did `git push origin --all`, which contradicted
the main_only mode protection landed in b9c4947 — agent review branches
(epimetheus/*, ganymede/*) ended up publicly visible on the new GitHub
teleo-infrastructure mirror until I deleted them.

Initial push now mirrors the recurring sync's main_only path: refs/heads/main
+ tags only. Re-running the setup script is now idempotent at branch level —
won't redo the agent-branch leak.

Cleanup applied to live GitHub teleo-infrastructure: 18 stale agent review
branches deleted via single batched push (epimetheus/* x14, ganymede/* x3,
ship/metadao-scraper). Only main remains. Codex bidirectional mirror unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:09:25 +01:00
b9c4947637 fix(mirror): restrict main_only mode to main+tags (Ganymede review)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Finding #1 (recommendation, applied): infra-mode now pushes only main + tags
to GitHub. Agent review branches (epimetheus/*, ganymede/*) stay Forgejo-only.
Public GitHub history reflects merged work, not pre-review WIP with internal
agent context.

Bidirectional mode unchanged — codex still mirrors all branches so external
contributors can fork from any branch.

Nit #4: setup script m3taversal username has a comment explaining it's a
placeholder for fine-grained PAT auth, mirrors the existing teleo-codex remote.

Two pre-existing nits filed for follow-up branch:
- hardcoded `living-ip:` in GH_PR_NUM head filter (line 273)
- spurious CRITICAL log on GH→forgejo→GH cycles (re-fetch forgejo after Step 2.5)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:54:18 +01:00
bf647b7abb feat(mirror): refactor sync-mirror.sh for multi-repo, add infra setup script
Wraps the per-repo body in sync_repo() and loops over MIRROR_REPOS at the
bottom. teleo-codex stays bidirectional (full PR roundtrip + pipeline.db
linking). teleo-infrastructure runs main_only: branch+tag sync Forgejo→
GitHub, ff-only GitHub→Forgejo on main, divergence alerting per-repo.
Steps 2.1 (fork PR refs) and 4 (Forgejo PR auto-create + DB link) gated
on MODE=bidirectional.

Setup script (deploy/setup-infra-mirror.sh) initializes the bare repo at
/opt/teleo-eval/mirror/teleo-infrastructure.git, configures remotes,
performs initial Forgejo→GitHub push. Idempotent. Pre-flight checks both
GitHub repo (must be created manually first — fine-grained PAT can't
create repos in the org) and Forgejo repo are accessible.

Per-repo divergence state file (.divergence-count.<repo>) so each repo
has independent counter + alert state. Also pulls in the source_channel
update from Apr 6 that lived only on VPS (line 215 added 'github').

Not deployed yet — pending Ganymede review and GitHub repo creation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:22:33 +01:00
1351db70a9 fix(tests): apply Ganymede review nits + add m3taversal reset script
Some checks are pending
CI / lint-and-test (push) Waiting to run
3 nits from review of d60b6f8 + Q4 ask:

1. test_window_24h_only_today: replace always-true assertion with
   concrete `assert handles == ["carol"]`. Push alice's most-recent
   event from -1 days to -2 days to eliminate fixture-vs-query
   microsecond drift on the 24h boundary.
2. _call helper: asyncio.get_event_loop().run_until_complete →
   asyncio.run (deprecation in 3.12, raises in some 3.14 contexts).
3. test_invalid_limit_falls_to_default: dead first call removed,
   misleading "7 entries" comment now matches assertion.

Q4: scripts/reset-m3taversal-sourcer.py captures the surgical
UPDATE we ran on VPS as a reviewable artifact. Idempotent (no-op
on already-reset rows), audit_log entry per run. Ganymede's point:
DB mutations should leave a code paper trail, not just an audit
row whose origin lives only in the executor's memory.

30/30 tests pass on VPS hermes venv (aiohttp 3.13.5, py 3.11.15).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:35:18 +01:00
d60b6f8bf2 test(leaderboard): cover all four slicings + AND-prefix regression
Adds tests/test_leaderboard.py — 30 cases against
diagnostics/leaderboard_routes.py. Two reasons:

(1) Zero coverage on an endpoint Argus + Oberon are about to consume
    for the May 5 hackathon UI. Two bugs slipped through this morning
    (404 wiring missing in app.py; AND-prefix SQL syntax error on
    rolling-window). Tests prevent regression.

(2) Tests serve as living documentation for Oberon's frontend
    integration — each test names a contract guarantee
    (test_left_join_handles_missing_contributors_row,
    test_composed_window_kind_domain, test_role_breakdown_present).

Coverage:
  - _parse_window unit tests (10): all_time, Nd, Nh, caps, garbage,
    case-normalization, and explicit no-AND-prefix assertion
  - handle_leaderboard integration (18): every kind value, every
    window family, domain filter, composed filters, limit + has_more,
    invalid-input fallback, role breakdown shape, empty-window shape,
    LEFT JOIN COALESCE for handles missing from contributors
  - 2 contract assertions: LEADERBOARD_PUBLIC_PATHS membership +
    KIND_VALUES set

Run: 30/30 pass on VPS hermes venv (aiohttp 3.13.5, pytest 9.0.2).
Skips clean locally without aiohttp via pytest.importorskip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:46:47 +01:00
cd5aac5cc6 fix(activity-feed): remove [:120] slug truncation
Some checks are pending
CI / lint-and-test (push) Waiting to run
Claim slugs were being cut at 120 chars in _extract_claim_slugs, causing
Timeline event clicks to 404 when the on-disk filename exceeded that
length (frontend builds /api/claims/<slug> from the truncated value).

This fix landed Apr 26 but regressed when the file was redeployed —
committing the unmangled version to repo so deploy.sh re-shipping
doesn't reintroduce the cap.

Verified live: max slug now 265 chars, 16 of 30 over the old 120 cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:27:31 +01:00
7c6417d6be test(diagnostics): activity_endpoint classify_pr_operation suite
Move tests from /tmp into the proper test suite. 22 cases covering:

- Leo gotcha: extract/* + commit_type=enrich/challenge classifies by
  commit_type, not branch prefix (same pattern as the contributor-role
  wiring fix)
- Reweave priority: branch.startswith('reweave/') wins over
  _MAINTENANCE_COMMIT_TYPES — nightly reweave PRs classify as enrich,
  not infra. Locks in the bifurcation against future priority refactors
- Full NON_MERGED_STATUS_TO_OPERATION coverage: open, approved, closed,
  conflict, validating, reviewing, merging, zombie
- Knowledge-producing commit_types (research, entity) → new
- Maintenance commit_types (fix, pipeline) → infra
- Defensive: null inputs, unknown status

aiohttp imported at module load — file uses pytest.importorskip so it
runs cleanly in any environment with aiohttp installed and skips gracefully
otherwise. sys.path inject for diagnostics/ since it isn't packaged.

Reviewed-by: Ganymede

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:39:44 +01:00
42d35d4e15 fix(diagnostics): wire /api/leaderboard into app.py + fix rolling-window SQL
Some checks are pending
CI / lint-and-test (push) Waiting to run
de7e5ec landed leaderboard_routes.py + the route file's register fn but
the import + register_leaderboard_routes(app) call + auth-middleware
allowlist were never added to app.py — endpoint returned 404 in production.

Three minimal edits to app.py mirror the existing register_*_routes pattern
(import at line 28, allowlist OR-clause at line 512, register call at 2365).

Plus a SQL bug in _parse_window: rolling-window clauses prefixed "AND "
but the WHERE composition uses " AND ".join(...), producing
"WHERE 1=1 AND AND ce.timestamp..." → sqlite3.OperationalError on every
window=Nd / window=Nh request. Stripped the prefix and added a comment so
the asymmetry doesn't bite again.

Verified on VPS:
  GET /api/leaderboard?window=all_time&kind=person → 200, 11 rows
  GET /api/leaderboard?window=7d&kind=person → 200, 2 rows
  GET /api/leaderboard?window=30d&kind=person → 200, 9 rows
  GET /api/leaderboard?domain=internet-finance → 200, 3 rows
  GET /api/leaderboard?kind=agent → 200, leo/rio/clay/astra/vida

Unblocks: Argus dashboard cutover, Oberon column reorder, Leo's CI
taxonomy broadcast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:30:26 +01:00
de7e5ec709 feat(diagnostics): /api/leaderboard reads contribution_events directly
Some checks are pending
CI / lint-and-test (push) Waiting to run
New endpoint replaces the legacy /api/contributors *_count read path with
event-sourced reads from the Phase A contribution_events ledger.

- Params: window (all_time | Nd | Nh), kind (person | agent | org | all),
  domain (filter), limit (default 100, max 500)
- Returns per-handle CI, full role breakdown (author/challenger/synthesizer/
  originator/evaluator), events_count, pr_count, first/last contribution
- ORDER BY ci DESC, last_contribution DESC — recent contributors break ties
- Read-only sqlite URI; total/has_more computed for paginated UIs

Wiring (import + register + _PUBLIC_PATHS entry) currently applied to live
app.py on VPS only — repo app.py has drift from Ship's uncommitted /api/search
POST contract. Next deploy.sh round-trip needs both to land together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:16:41 +01:00
369f6c96da fix(attribution): credit research-session sources to agents, not m3taversal (#7)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Forward fix: research-session.sh writes intake_tier: research-task (no proposed_by — extract.py infers agent from branch).

Backfill: 304 PRs reattributed across 30 days (rio 74, clay 70, astra 53, vida 48, theseus 30, leo 29). Already applied to production.

Format reconciliation: normalize_handle strips (self-directed) suffix so both halves canonicalize to the same agent handle.

5 idempotency tests passing. Production replay self-extinguishes (delta 3839→3839).
2026-04-27 11:59:54 +00:00
6aff03ff56 fix(attribution): unify research-session format on "(self-directed)" suffix
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Resolves the format inconsistency between the forward fix and the 304-row
backfill. Both halves now produce prs.submitted_by = "rio (self-directed)":

- research-session.sh: drop proposed_by from the frontmatter template.
  extract.py path 1 (proposed_by-driven) no longer fires; path 2 fires
  instead and constructs f"{agent} (self-directed)" — matches backfill.

- attribution.py: normalize_handle now strips "(self-directed)" suffix
  immediately after lowercase+@-strip, before alias lookup. Closes the
  phantom-person-event class on any future replay through
  record_contributor_attribution. Round-trips through alias rules keyed
  on bare agent names.

Test (5 cases) still passes; suffix-strip behavior verified against
hostile inputs (whitespace, casing, mid-string occurrences must NOT
match — only trailing pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:53:52 +01:00
319e03e2c6 test(attribution): prove research-backfill replay is idempotent
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Five tests against the real contribution_events schema (lib/db.py:181-209):
- pr-level dedup with NULL claim_path via idx_ce_unique_pr partial index
- per-claim dedup with non-NULL claim_path via idx_ce_unique_claim partial index
- pr-level and per-claim events coexist on the same pr_number
- backfill (INSERT correct + DELETE wrong) is a true no-op on replay
- replay against already-backfilled state preserves unrelated events

Schema case identified: case 2 with partial-index split solution already in
place. Two partial UNIQUE indexes target disjoint row sets (claim_path IS NULL
vs IS NOT NULL), bypassing SQLite's NULL-not-equal-NULL UNIQUE quirk.

Production replay verified: re-running backfill --apply against the live DB
returns "misattributed PRs found: 0" because the first-run UPDATE flipped the
WHERE predicate. Total contribution_events count: 3839 → 3839.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:50:17 +01:00
2d332c66d4 fix(attribution): credit research-session sources to agents, not m3taversal
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Two-part fix for a bug where every claim extracted from agent overnight
research sessions was being credited to m3taversal in contribution_events
(visible in the activity feed as "@m3taversal" on agent-derived claims).

Forward fix (research/research-session.sh):
The frontmatter template the agent prompt instructs Claude to use now
includes `proposed_by: ${AGENT}` and `intake_tier: research-task`. With
those fields present, extract.py path 1 (line 687) takes precedence and
sets prs.submitted_by to the agent handle, which then propagates into
contribution_events as a kind='agent' author event for the agent.

Without the fields, extract.py fell through to the default branch on
line 695 and set submitted_by='@m3taversal'.

Backfill (scripts/backfill-research-session-attribution.py):
Identifies research-session-derived PRs by finding teleo-codex commits
matching `^<agent>: research session YYYY-MM-DD —`, listing the
inbox/queue/*.md files added in each commit's diff, and matching those
filename basenames against prs.source_path. Only PRs currently
submitted_by='@m3taversal' AND merged within the configurable window
are touched. Default --dry-run; --apply to commit.

For each match the script:
  1. UPDATE prs SET submitted_by = '<agent> (self-directed)'
  2. INSERT OR IGNORE the agent author event (kind='agent', weight=0.30)
     with the original PR's domain, channel, merged_at preserved
  3. DELETE the misattributed m3taversal author event

Applied 30-day backfill on VPS:
  - 304 PRs re-attributed (rio 74, clay 70, astra 53, vida 48,
    theseus 30, leo 29)
  - 297 m3taversal author events deleted, 304 agent author events
    inserted (delta of 7 = pre-v24 PRs that never had m3ta events
    in the first place; we still create the new agent event)
  - m3taversal author count: 1368 → 1071 (−22%)
  - Pre-backfill DB snapshot: pipeline.db.bak-pre-research-attribution

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:38:53 +01:00
dea1b02aa6 fix(attribution): narrow exception + document gate asymmetry (Ganymede review)
Two follow-up fixes from Ganymede's review of d0fb4c9:

1. is_publisher_handle: narrow `except Exception` to sqlite3.OperationalError.
   Pre-v26 DB fallback only needs to catch the "table doesn't exist" case;
   broader exceptions (programming errors, locks, corruption) should propagate.

2. upsert_contributor gate: add comment documenting the alias-resolution
   asymmetry between insert_contribution_event (alias-resolved via
   normalize_handle) and upsert_contributor (bare lower+lstrip-@). Today this
   is fine because the v26 classifier produced one publisher row per canonical
   handle. Branch 3 will normalize alias→canonical at writer entry points,
   tightening this gate transparently.

Unit tests for the gates (positive + negative + alias resolution) deferred to
Branch 3 alongside the auto-create flow tests.

Smoke-tested:
  - pre-v26 fallback (no publishers table) → None (correct)
  - case-insensitive match (CNBC → id=1) → correct
  - @ prefix strip (@cnbc → id=1) → correct
  - non-publisher handle (alexastrum) → None (correct)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:25:24 +01:00
d0fb4c96e3 fix(attribution): gate writer on publishers table (regression prevention)
Schema v26 (commit 3fe524d) split orgs/citations from contributors into
the publishers table. Without a writer-side gate, every merged PR with
`sourcer: cnbc` (or similar) re-creates CNBC as a contributor and
undoes the v26 classifier cleanup. Once normal pipeline traffic resumes,
the contributors table re-pollutes within hours.

Fix: belt-and-suspenders gate at both writer surfaces.

1. `lib/attribution.py::is_publisher_handle(handle, conn)` — returns
   publisher.id if handle exists in publishers.name, else None. Falls
   back gracefully on pre-v26 DBs (no publishers table → returns None →
   writer behaves like before, no regression).

2. `lib/contributor.py::insert_contribution_event` — checks
   is_publisher_handle on canonical handle before INSERT. If it's a
   publisher, debug-log + return False. Prevents originator events for
   CNBC/SpaceNews/etc.

3. `lib/contributor.py::upsert_contributor` — same gate at top. Prevents
   the contributors table from re-acquiring publisher rows.

Verified end-to-end against live VPS DB snapshot:
  - CNBC originator event: blocked (insert returns False)
  - CNBC contributors row: blocked (no row created)
  - alexastrum, thesensatore, newhandle_xyz: pass through unchanged
  - is_publisher_handle handles case-insensitive lookup correctly
    (CNBC and cnbc both match publisher_id=3)

Pre-deploy event count was 3705. Post-classifier cleanup: 3623 (82 org
events purged). Going forward, no new org events accumulate.

Branch 2 of the schema-v26 rollout. Branch 3 (auto-create at tier='cited',
extract.py sources.publisher_id wiring) is separate scope and not required
for regression prevention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:21:10 +01:00
926a397839 fix(activity): re-apply source classifier + add date-prefix slug fallback
Some checks are pending
CI / lint-and-test (push) Waiting to run
Regression: aeae712's source/create distinction was lost — VPS reverted to
pre-aeae712 behavior where every extract/* knowledge PR returned type=create
regardless of whether a claim was written. Source archives surfaced as
"New claim" chips with date-prefix slugs that 404 on click.

Root cause: aeae712 was deployed via local file copy and never pushed to
origin; a subsequent rsync from origin/main overwrote it with the older
classifier. This branch ships from origin so deploy.sh's repo-first gate
makes recurrence impossible.

- Restore aeae712: extract/* + empty description -> source, with
  empty claim_slug + source_slug field, ci_earned 0.15
- Add Leo's regex fallback: candidate_slug matching
  ^\d{4}-\d{2}-\d{2}-.+-[a-f0-9]{4}$ -> source regardless of branch
  /commit_type/description state. Catches edge cases where description
  leaks but is just a source title (slugified into the inbox filename
  pattern), not a claim insight.
- Add 'challenge' to _FEED_COMMIT_TYPES (latent bug — challenge PRs
  would be filtered out before classification because the filter
  list omitted them; memory says 0 challenges exist so it never
  triggered, but schema support belongs in the filter)
- _build_events: compute candidate slug before classify so the regex
  fallback has a slug to inspect

Verified locally on Leo's example PRs (#4014, #4016) — both classify
as source. VPS smoke pending deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:47:00 +01:00
3fe524dd14 fix(classify): Ganymede review fixes — alias cleanup + counter accuracy + handle alignment
Some checks failed
CI / lint-and-test (push) Has been cancelled
1. WARNING — orphan contributor_aliases after publisher/garbage delete:
   Added alias cleanup to the transaction (gated on --delete-events, same
   audit rationale as events). Both garbage and publisher deletion loops
   now DELETE matching contributor_aliases rows. Dry-run adds an orphan
   count diagnostic so the --delete-events decision is informed.

2. NIT — inserted_publishers counter over-reports on replay:
   INSERT OR IGNORE silently skips name collisions, but the counter
   incremented unconditionally. Now uses cur.rowcount so a second apply
   reports 0 inserts instead of falsely claiming 100. moved_to_publisher
   set remains unconditional — publisher rows already present still need
   the matching contributors row deleted.

3. NIT — handle-length gate diverged from writer path:
   Widened from {0,19} (20 chars) to {0,38} (39 chars) to match GitHub's
   handle limit and contributor.py::_HANDLE_RE. Prevents future long-handle
   real contributors from falling through to review_needed and blocking
   --apply. Current data has 0 review_needed either way.

Bonus (Q5): Added audit_log entry inside the transaction. One row in
audit_log.stage='schema_v26', event='classify_contributors' with counter
detail JSON on every --apply run. Cheap audit trail for the destructive op.

Verified end-to-end on VPS DB snapshot:
- First apply: 100/9/9/100/0 (matches pre-fix)
- Second apply: 0/9/0/0/0 (counter fix working)
- With injected aliases + --delete-events: 2 aliases deleted, 1 pre-existing
  orphan correctly left alone (outside script scope), audit_log entry
  written with accurate counters.

Ganymede msg-3. Protocol closed.
2026-04-24 20:47:21 +01:00
45b2f6de20 feat(schema): v26 — publishers + contributor_identities + sources provenance
Separates three concerns currently conflated in contributors table:
  contributors — people + agents we credit (kind in 'person','agent')
  publishers   — news orgs / academic venues / platforms (not credited)
  sources      — gains publisher_id + content_type + original_author columns

Rationale (Cory directive Apr 24): livingip.xyz leaderboard was showing CNBC,
SpaceNews, TechCrunch etc. at the top because the attribution pipeline credited
news org names as if they were contributors. The mechanism-level fix is a
schema split — orgs live in publishers, individuals in contributors, each
table has one semantics.

Migration v26:
  - CREATE TABLE publishers (id PK, name UNIQUE, kind CHECK IN
    news|academic|social_platform|podcast|self|internal|legal|government|
    research_org|commercial|other, url_pattern, created_at)
  - CREATE TABLE contributor_identities (contributor_handle, platform CHECK IN
    x|telegram|github|email|web|internal, platform_handle, verified, created_at)
    Composite PK on (platform, platform_handle) + index on contributor_handle.
    Enables one contributor to unify X + TG + GitHub handles.
  - ALTER TABLE sources ADD COLUMN publisher_id REFERENCES publishers(id)
  - ALTER TABLE sources ADD COLUMN content_type
    (article|paper|tweet|conversation|self_authored|webpage|podcast)
  - ALTER TABLE sources ADD COLUMN original_author TEXT
    (free-text fallback, e.g., "Kim et al." — not credit-bearing)
  - ALTER TABLE sources ADD COLUMN original_author_handle REFERENCES contributors(handle)
    (set only when the author is in our contributor network)
  - ALTER wrapped in try/except on "duplicate column" for replay safety
  - Both SCHEMA_SQL (fresh installs) + migration block (upgrades) updated
  - SCHEMA_VERSION bumped 25 -> 26

Migration is non-breaking. No data moves yet. Existing publishers-polluting-
contributors row state is preserved until the classifier runs. Writer routing
to these tables lands in a separate branch (Phase B writer changes).

Classifier (scripts/classify-contributors.py):
  Analyzes existing contributors rows, buckets into:
    keep_agent   — 9 Pentagon agents
    keep_person  — 21 real humans + reachable pseudonymous X/TG handles
    publisher    — 100 news orgs, academic venues, formal-citation names,
                   brand/platform names
    garbage      — 9 parse artifacts (containing /, parens, 3+ hyphens)
    review_needed — 0 (fully covered by current allowlists)

  Hand-curated allowlists for news/academic/social/internal publisher kinds.
  Garbage detection via regex on special chars and length > 50.
  Named pseudonyms without @ prefix (karpathy, simonw, swyx, metaproph3t,
  sjdedic, ceterispar1bus, etc.) classified as keep_person — they're real
  X/TG contributors missing an @ prefix because extraction frontmatter
  didn't normalize. Cory's auto-create rule catches these on first reference.

  Formal-citation names (Firstname-Lastname form — Clayton Christensen, Hayek,
  Ostrom, Friston, Bostrom, Bak, etc.) classified as academic publishers —
  these are cited, not reachable via @ handle. Get promoted to contributors
  if/when they sign up with an @ handle.

  Apply path is transactional (BEGIN / COMMIT / ROLLBACK on error). Publisher
  insert happens before contributor delete, and contributor delete is gated
  on successful insert so we never lose a row by moving it to a failed
  publisher insert.

  --apply path flags:
    --delete-events  : also DELETE contribution_events rows for moved handles
                       (default: keep events for audit trail)
  --show <handle>   : inspect a single row's classification

Smoke-tested end-to-end via local copy of VPS DB:
  Before: 139 contributors total (polluted with orgs)
  After:  30 contributors (9 agent + 21 person), 100 publishers, 9 deleted
  contribution_events: 3,705 preserved
  contributors <-> publishers overlap: 0

Named contributors verified present after --apply:
  alexastrum (claims=6)  thesensatore (5)  cameron-s1 (1)  m3taversal (1011)

Pentagon agent 'pipeline' (claims_merged=771) intentionally retained — it's
the process name from old extract.py fallback path, not a real contributor.
Classified as agent (kind='agent') so doesn't appear in person leaderboard.

Deploy sequence after Ganymede review:
  1. Branch ff-merge to main
  2. scp lib/db.py + scripts/classify-contributors.py to VPS
  3. Pipeline already at v26 (migration ran during earlier v26 restart)
  4. Run dry-run: python3 ops/classify-contributors.py
  5. Apply: python3 ops/classify-contributors.py --apply
  6. Verify: livingip.xyz leaderboard stops showing CNBC/SpaceNews
  7. Argus /api/contributors unaffected (reads contributors directly, now clean)

Follow-up branch (not in this commit):
  - Writer routing in lib/contributor.py + extract.py:
    org handles -> publishers table + sources.publisher_id
    person handles with @ prefix -> auto-create contributor, tier='cited'
    formal-citation names -> sources.original_author (free text)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:47:21 +01:00
f0f9388c1f feat(diagnostics): add POST /api/search for chat API contract
Some checks are pending
CI / lint-and-test (push) Waiting to run
Wire the search endpoint to accept POST bodies matching the embedded
chat contract (query/limit/min_score/domain/confidence/exclude →
slug/path/title/domain/confidence/score/body_excerpt). GET path retained
for legacy callers and adds a min_score override for hackathon debug.

- _qdrant_hits_to_results() shapes raw hits into chat response format
- handle_api_search() dispatches POST vs GET
- /api/search added to _PUBLIC_PATHS (chat is unauthenticated)
- POST route registered alongside existing GET

Resolves VPS↔repo drift flagged by Argus before next deploy.sh run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:58:30 +01:00
0f2b153c92 fix(backfill): Ganymede review — fix tautological guard + origin='human'
Some checks are pending
CI / lint-and-test (push) Waiting to run
Addresses two findings in commit 762fd42 review:

1. BUG: guard query was tautological. `SELECT MAX(number) FROM prs WHERE
   number < 900000` filters out exactly what the `>= 900000` check tests.
   Replaced with a direct check for unexpected rows in the synthetic range
   (excluding our known 900068/900088).

2. WARNING: origin defaults to 'pipeline' via schema default. lib/merge.py
   convention is origin='human' for external contributors. Synthetic rows
   now set origin='human', priority='high' — matches discover_external_prs
   for real GitHub PRs. Prevents Phase B origin-based filtering from
   misclassifying Alex/Cameron as machine-authored.

Also flagged in review: credit projection was optimistic. Author events are
PR-level (not per-claim), so Alex gets 1×0.30 author credit, not 6. Same
for Cameron. Per-claim originator credit goes to the 7 frontmatter sourcers
where applicable. Not a code change — expectation reset for Cory.
2026-04-24 16:49:12 +01:00
762fd4233e feat(backfill): synthetic PR rows for pre-mirror GitHub PRs #68 (Alex) + #88 (Cameron)
Two historical GitHub PRs merged before our sync-mirror.sh tracked github_pr:
  - GitHub PR #68: alexastrum, 6 claims, merged Mar 9 2026 via squash merge
  - GitHub PR #88: Cameron-S1, 1 claim, merged early April

Their claim files were lost during a Forgejo→GitHub mirror overwrite and later
recovered via direct-to-main commits (dba00a79, da64f805). Because the
recovery commits bypassed the pipeline, our 'prs' table has no row to attach
originator events to — all 4 backfill-events.py strategies returned None,
leaving Alex + Cameron at 0 originator credits despite real historical work.

This reconstructs synthetic 'prs' rows so the existing github_pr strategy in
backfill-events.py attaches 7 originator events on re-run:
  - Numbers 900068 / 900088 live in a clearly-synthetic range that cannot
    collide with real Forgejo PRs (current max: 3941)
  - github_pr=68/88 wires up the existing lookup strategy
  - submitted_by=alexastrum / cameron-s1 establishes author attribution
  - merged_at from the recovery commit messages (not recovery-commit time)
  - last_error tags the rows as synthetic for future audits

Idempotent: INSERT OR IGNORE via check on number OR github_pr. Safe to replay.
Reversible: DELETE FROM prs WHERE number IN (900068, 900088).

After applying this script:
  python3 ops/backfill-events.py
will credit Alex with 6 author + 6 originator events (author=1.80, originator=0.90)
and Cameron with 1 author + 1 originator (0.30 + 0.15), all dated to the
historical merge dates — so 7d/30d leaderboard windows show them correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:33:37 +01:00
10d5c275da fix(backfill): normalize commit_date via datetime() in time-proximity query
Some checks are pending
CI / lint-and-test (push) Waiting to run
SQLite datetime comparison fails lexicographically across ISO-T and
space-separator formats: '2026-03-27 18:00:14' < '2026-03-27T17:43:04+00:00'
because space (0x20) < T (0x54). PRs merged same-day but earlier than the
commit hour were silently excluded from the time-proximity cascade.

Shaga's 3 stigmergic-coordination claims resolved to PR #2032 (later, wrong)
instead of #2025 (earlier, correct). Fixed by wrapping both sides in
datetime(), which normalizes to space-separator before comparison.

Verified: all 3 Shaga claims now resolve to #2025 via git_time_proximity.
No change to totals (126 originator events, 5 proximity hits) — the fix
corrects WHICH PR each proximity-matched claim resolves to, not whether.

Caught by Ganymede review of 1d6b515.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:16:03 +01:00
1d6b51527a feat(backfill): 4-strategy PR recovery for originator events
Rewrite claim-level pass in backfill-events.py to recover the Forgejo PR
that introduced each claim via a cascade of 4 strategies (reliability
order), replacing the single title→description match that missed PRs
with NULL description (Cameron #3377) and bare-subject extracts (Shaga's
Leo research PR).

## Strategies
  1. sourced_from frontmatter → prs.source_path stem match
  2. git log first-add commit → subject pattern → prs.branch
     - "<agent>: extract claims from <slug>"  → extract/<slug>
     - "<agent>: research session YYYY-MM-DD" → <agent>/research-<date>
     - "<agent>: (challenge|contrib|entity|synthesize)" → <agent>/*
     - "Recover X from GitHub PR #N"           → prs.github_pr=N
     - "Extract N claims from X" (no prefix)   → time-proximity on
       agent-owned branches within 24h
  3. Current title_desc fallback for anything the above miss

## Dry-run projection (1,662 merged PRs)

Before:
  Claims processed: 33
  Originator events: 6
  Breakdown: {no_pr_match: 1608, no_sourcer: 26, invalid_handle: 21, skip_self: 6}

After:
  Claims processed: 505 (+472)
  Originator events: 126 (+120)
  Strategy hits: git_subject=412, sourced_from=88, git_time_proximity=5
  Breakdown: {no_pr_match: 1095, no_sourcer: 67, invalid_handle: 359, skip_self: 20}

## Verified on real VPS data
- @thesensatore claims: 3/5 resolve via git_time_proximity to leo/ PRs
- Cameron-S1, alexastrum: remain None — their recovery commits
  (dba00a79, da64f805) bypassed the pipeline entirely, no Forgejo PR
  record exists. Requires synthetic prs rows — deferred to separate
  commit with its own Ganymede review (write operation, larger blast
  radius than this pure-read backfill change).

## Implementation
- New find_pr_for_claim(conn, repo, md) helper returns (pr_number, strategy)
- Claim-level pass uses it first, falls back to title_desc map
- Strategy counter surfaced in summary output for operator visibility

Idempotent — backfill re-runs skip duplicate events via the partial
UNIQUE index on contribution_events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:06:52 +01:00
540ba97b9d fix(attribution): Phase A followup — bug #1 + 4 nits + refactor (Ganymede review)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Addresses Apr 24 review of 58fa8c52. All 6 findings landed.

Bug #1 — git log -1 returns latest commit, not first (semantic mismatch
with "original author" comment):
  Drop -1 flag, take last line of default-ordered log output (= oldest).
  Fixes mis-credit on multi-commit PRs where a reviewer rebased/force-pushed.

Nit #2 — forward writer didn't pass merged_at:
  Fetch merged_at in the prs SELECT, thread pr_merged_at through all 5
  insert_contribution_event call sites. Keeps forward-emitted and backfilled
  event timestamps on the same timeline after merge retries.

Nit #3 — legacy-counts fallback paths emit no events (parity gap):
  git-author and prs.agent fallback paths now emit challenger/synthesizer
  events via the TRAILER_EVENT_ROLE map when refined_type matches. Closes
  the gap where external-contributor challenge/enrich PRs would accumulate
  legacy counts but disappear from event-sourced leaderboards.

Nit #4 — migration v24 agent seed missing 'pipeline':
  Added "pipeline" to the seed list. Plus new migration v25 with idempotent
  corrective UPDATE so existing envs (where v24 already ran) pick up the
  fix on restart without requiring manual SQL. Verified on VPS state:
  pipeline row was kind='person', will flip to 'agent' on redeploy.

Nit #5 — backfill summary prints originator attempted=0 in wrong pass:
  Split the "=== Summary ===" header into "=== PR-level events ===" and
  "=== Claim-level originator pass ===" with originator counts in the
  right block. Operator-facing cosmetic.

Refactor #6 — AGENT_BRANCH_PREFIXES duplicated in 2 sites:
  Extracted to lib/attribution.py as single source of truth. contributor.py
  imports it. backfill-events.py keeps its local copy (runs standalone
  without pipeline package import) with a sync-reference comment.

No behavioral drift for the common case. Backfill re-runs cleanly against
existing forward-written events (UNIQUE-index idempotency).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:13:54 +01:00
58fa8c5276 feat(attribution): Phase A — event-sourced contribution ledger (schema v24)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Introduces contribution_events table + non-breaking double-write. Schema
lands today, forward traffic writes events alongside existing count upserts,
backfill script replays history. Phase B will add leaderboard API reading
from events; Phase C switches Argus dashboard over.

## Schema v24 (lib/db.py)

- contribution_events: one row per credit-earning event
  (id, handle, kind, role, weight, pr_number, claim_path, domain, channel, timestamp)
  Partial UNIQUE indexes handle SQLite's NULL != NULL semantics:
    idx_ce_unique_claim on (handle, role, pr_number, claim_path) WHERE claim_path NOT NULL
    idx_ce_unique_pr    on (handle, role, pr_number)             WHERE claim_path IS NULL
  PR-level events (evaluator, author, challenger, synthesizer) dedup on 3-tuple.
  Per-claim events (originator) dedup on 4-tuple. Idempotent on replay.
- contributor_aliases: canonical handle mapping
  Seeded: @thesensatore → thesensatore, cameron → cameron-s1
- contributors.kind TEXT DEFAULT 'person'
  Migration seeds 'agent' for known Pentagon agent handles.

## Role model (confirmed by Cory Apr 24)

Weights: author 0.30, challenger 0.25, synthesizer 0.20, originator 0.15, evaluator 0.05
- author:     human who submitted the PR (curation + submission work)
- originator: person who authored the underlying content (rewards external creators)
- challenger: agent/person who brought a productive disagreement
- synthesizer: cross-domain work (enrichments, research sessions)
- evaluator:  reviewer who approved (Leo + domain agent)

Humans-are-always-author: agents credit is capped at evaluator/synthesizer/
challenger. Pentagon agents classify as kind='agent' and surface in the
agent-view leaderboard, not the default person view.

## Writer (lib/contributor.py)

- New insert_contribution_event(): idempotent INSERT OR IGNORE with alias
  normalization + kind classification. Falls back silently on pre-v24 DBs.
- record_contributor_attribution double-writes alongside existing
  upsert_contributor calls. Zero risk to current dashboard.
- Author event: emitted once per PR from prs.submitted_by → git author →
  agent-branch-prefix.
- Originator events: emitted per claim from frontmatter sourcer, skipping
  when sourcer == author (avoids self-credit double-count).
- Evaluator events: Leo (always when leo_verdict='approve') + domain_agent
  (when domain_verdict='approve' and not Leo).
- Challenger/Synthesizer: emitted from Pentagon-Agent trailer on
  agent-owned branches (theseus/*, rio/*, etc.) based on commit_type.
  Pipeline-owned branches (extract/*, reweave/*) get no trailer-based event —
  infrastructure work isn't contribution credit.

## Helpers (lib/attribution.py)

- normalize_handle(raw, conn=None): lowercase + strip @ + alias lookup
- classify_kind(handle): returns 'agent' for PENTAGON_AGENTS, else 'person'
  Intentionally narrow. Orgs get classified by operator review, not heuristics.

## Backfill (scripts/backfill-events.py)

Replays all merged PRs into events. Idempotent (safe to re-run). Emits:
- PR-level: author, evaluator, challenger, synthesizer
- Per-claim: originator (walks knowledge tree, matches via description titles)

Known limitation: post-merge PR branches are deleted from Forgejo, so we
can't diff them for granular per-claim events. Claim→PR mapping uses
prs.description (pipe-separated titles). Misses some edge cases but
recovers the bulk of historical originator credit. Forward traffic gets
clean per-claim events via the normal record_contributor_attribution path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:59:22 +01:00
93917f9fc2 fix(attribution): --diff-filter=A + handle sanity filter + remove legacy fallback
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ganymede review findings on epimetheus/contributor-attribution-fix branch:

1. BUG: record_contributor_attribution used `git diff --name-only` (all modified
   files), not just added. Enrich/challenge PRs re-credited the sourcer on every
   subsequent modification. Fixed: --diff-filter=A restricts to new files only.
   The synthesizer/challenger/reviewer roles for enrich PRs are still credited
   via the Pentagon-Agent trailer path, so this doesn't lose any correct credit.

2. WARNING: Legacy `source`-field heuristic fabricated garbage handles from
   descriptive strings ("sec-interpretive-release-s7-2026-09-(march-17",
   "governance---meritocratic-voting-+-futarchy"). Removed outright + added
   regex handle sanity filter (`^[a-z0-9][a-z0-9_-]{0,38}$`). Applied before
   every return path in parse_attribution (the nested-block early return was
   previously bypassing the filter).

   Dry-run impact: unique handles 83→70 (13 garbage filtered), NEW contributors
   49→48, EXISTING drift rows 34→22. The filter drops rows where the literal
   garbage string lives in frontmatter (Slotkin case: attribution.sourcer.handle
   was written as "senator-elissa-slotkin-/-the-hill" by the buggy legacy path).

3. NIT: Aligned knowledge_prefixes in the file walker to match is_knowledge_pr
   (removed entities/, convictions/). Widening those requires Cory sign-off
   since is_knowledge_pr currently gates entity-only PRs out of CI.

Tests: 17 pass (added test_bad_handles_filtered, test_valid_handle_with_hyphen_passes,
updated test_legacy_source_fallback → test_legacy_source_fallback_removed).

Ganymede review — 3-message protocol msg 3 pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:58:55 +01:00
3fe0f4b744 fix(attribution): credit sourcer/extractor from claim frontmatter
Three layers of contributor-attribution bug surfaced by Apr 24 leaderboard
investigation. alexastrum, thesensatore, cameron-s1 all had real merged
contributions but zero credit in the contributors table.

1. lib/attribution.py: parse_attribution() only read `attribution_sourcer:`
   prefix-keyed flat fields. ~42% of claim files (535/1280) use the bare-key
   form `sourcer: alexastrum` written by extract.py. Added bare-key handling
   between the prefixed-flat path and the legacy-source-field fallback.
   Block format (`attribution: { sourcer: [...] }`) still wins when present.

2. lib/contributor.py: record_contributor_attribution() parsed the diff text
   with regex looking for `+- handle: "X"` lines. This matched neither the
   bare-key flat format nor the `attribution: { sourcer: [...] }` block
   format Leo uses for manual extractions. Replaced the regex parser with
   a file walker that calls attribution.parse_attribution_from_file() on
   each changed knowledge file — single source of truth for both formats.

3. scripts/backfill-sourcer-attribution.py: walks all merged knowledge files,
   re-attributes via the canonical parser, upserts contributors. Default
   additive mode preserves existing high counts (e.g. m3taversal.sourcer=1011
   reflects Telegram-curator credit accumulated via a different code path
   that this fix does not touch). --reset flag for the destructive case.

Dry-run preview (additive mode):
  - 670 NEW contributors to insert (mostly source-citation handles)
  - 77 EXISTING contributors with under-counted role columns
  - alexastrum: 0 → 6, thesensatore: 0 → 5, cameron-s1: 0 → 2
  - astra.sourcer: 0 → 96, leo.sourcer: 0 → 44, theseus.sourcer: 0 → 18
  - m3taversal.sourcer: 1011 (preserved, not 22 from file walk)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:48:41 +01:00
05d15cea56 feat(activity): Timeline data gaps — type filter + commit_type classifier + source_channel reshape
Three hackathon-critical fixes for Timeline page rendering (Accelerate Solana, May 5):

Gap 1 — /api/activity respects ?type= now:
  - accepts single or comma-separated operation types
    (extract|new|enrich|challenge|infra)
  - over-fetches 5× limit (capped 2000) so post-build filtering still
    fills the requested page size
  - unknown types filter out cleanly

Gap 2 — classify_pr_operation() replaces STATUS_TO_OPERATION for merged PRs:
  - commit_type wins over branch prefix for merged PRs so extract/* branches
    with commit_type='enrich' or 'challenge' surface correctly (same gotcha
    as the contributor-role wiring fix)
  - priority: challenge → enrich (incl. reweave/) → maintenance (infra) → new
  - challenged_by detection carried over from activity_feed_api._classify_event
  - non-merged statuses unchanged (extract/new/infra/challenge as before)
  - SQL now selects commit_type + description alongside existing columns
  - 14 unit tests covering the gotcha matrix

Gap 3 — _CHANNEL_MAP reshape:
  - extract/, ingestion/ default → 'unknown' (was 'telegram'; telegram-origin
    classification now requires explicit tagging at ingestion time)
  - agent/maintenance mappings unchanged
  - github_pr override and gh-pr-* branches continue to return 'github'
  - 'web' registered as the canonical in-app submission channel (matches
    the platform-named pattern established by telegram/github/agent)
  - module docstring enumerates all six valid channels

Deployed to VPS; diagnostics + pipeline restarted clean.
Smoke: type=enrich returns 22 events (was 0), type=challenge returns 0
(matches DB — zero challenge commit_types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:51:58 +01:00
cfcb06a6dc fix(diagnostics): commit claims_api + register routes that were VPS-only
Some checks are pending
CI / lint-and-test (push) Waiting to run
Root cause (per Epi audit):
- /api/claims, /api/contributors/list, /api/contributors/{handle} returned
  404 in prod. The route registrations and claims_api.py module existed only
  on VPS — never committed. Today's auto-deploy of an unrelated app.py change
  rsync'd the repo (registration-less) version over the VPS edits, wiping
  endpoints Vercel depended on.
- Recurrence of the deploy-without-commit pattern (blindspot #2).

Brings repo to parity with the live, working VPS state:
- Add diagnostics/claims_api.py (161 lines, was VPS-only)
- Wire register_claims_routes + register_contributor_routes in app.py
  alongside the existing register_activity_feed call

beliefs_routes.py is also VPS-only and currently unregistered (orphaned by
the same Apr 21 manual edit that dropped its registration). Left out of this
commit pending a decision on whether to revive or delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:51:17 +01:00
2f6424617b feat: wire Timeline activity endpoint + surface source_channel
/api/activity and /api/activity-feed were never registered in app.py —
both files existed but neither route was reachable (confirmed 404 on VPS).
Register both so Timeline and gamification feeds can consume them.

Adds source_channel to /api/activity payload (both PR rows and audit
events — audit rows return null since they aren't tied to a specific PR).
Migration v22 already populated prs.source_channel on VPS with enum:
telegram=2340, agent=698, maintenance=102, unknown=11, github=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:22:12 +01:00
9a943e8460 feat: expose source_channel on activity feed
Adds p.source_channel to the SELECT and surfaces it on each event.
Migration v22 populated the column with enum values: telegram, agent,
maintenance, unknown, github. Timeline UI needs this to show per-event
provenance (2340 telegram, 698 agent, 102 maintenance, 11 unknown, 1 github).

Nulls fall back to "unknown" — only 0 rows currently null, but the
fallback is defensive for future inserts before backfill runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:20:26 +01:00
84f6d3682c fix(eval): treat empty diff as conservative fallback in auto-close gate
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ganymede review nit: if get_pr_diff returns an empty string (edge case —
Forgejo quirk, empty PR), the old `if diff is None` branch would miss it,
the `elif diff and ...` would evaluate False (empty string is falsy), and
control would fall to `else` — triggering auto-close on zero diff content.

Change `if diff is None` → `if not diff` so empty string ALSO falls through
to the conservative path. Matches the stated posture: skip auto-close when
in doubt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:24:16 +01:00
33c17f87a8 feat(eval): auto-close near-duplicate PRs when merged sibling exists
Prevents Apr 22 runaway-damage pattern (44 open PRs manually bulk-closed)
where a source extracted 20+ times before the cooldown gate landed, each
leaving an orphan 'open' PR after eval correctly rejected as near-duplicate.

Gate fires in dispose_rejected_pr before attempt-count branches:
  all_issues == ["near_duplicate"]  (exact match — compound carries signal)
  AND sibling PR exists with same source_path in status='merged'
  AND diff contains "new file mode"  (not enrichment-only)
  → close on Forgejo + DB with audit, post explanation comment.

Ganymede review — 5 must-fix/warnings applied + 1 must-add:
- Exact match on single-issue near_duplicate (compound rejections preserved)
- Enrichment guard via diff scan (eval_parse regex can flag enrichment prose)
- 10s timeout on get_pr_diff — conservative fallback on Forgejo wedge
- Forgejo comment with canned explanation (best-effort, try/except)
- Partial index idx_prs_source_path + migration v23
- Explicit p1.source_path IS NOT NULL in WHERE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:17:29 +01:00
a053a8ebf9 fix(backfill): don't regress terminal source statuses to unprocessed
Some checks are pending
CI / lint-and-test (push) Waiting to run
backfill-sources.py runs every 15 minutes and derives sources.status
purely from directory location. If a source file is in inbox/queue/,
it blindly overwrites the DB status to 'unprocessed' — even when the
DB already had 'extracted' or 'null_result'.

This is why the 43 zombies kept coming back after manual backfill:
cron re-reset them every 15 minutes, then each 4h cooldown expiry
re-triggered runaway extraction on the same source.

Fix: never regress from a terminal status (extracted, null_result,
error, ghost_no_file) to 'unprocessed'. File location is ambiguous
(legitimately new vs. zombie from failed archive); DB is authoritative.
Legitimate re-extraction still works — it goes through the needs_reextraction
path which is unaffected by this gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:29:33 +01:00
97b590acd6 fix: close cooldown-dependence gaps in extract.py (Ganymede review)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Three targeted fixes from Ganymede's review of commit 469cb7f:

BUG #1 — Success path now updates sources.status='extracting' before PR
creation, so queue scan's DB-authoritative filter catches sources between
PR creation and merge. Previously the cooldown gate was load-bearing for
this window, not belt-and-suspenders as claimed.

BUG #2 — Second null-result path (line 573, triggered when enrichments
existed but all targets were missing in worktree) now updates DB. Without
this, that path created no PR, no DB mark, and would have re-entered the
runaway loop 4h later when the cooldown window expired.

NIT #6 — 4h cooldown moved to config.EXTRACTION_COOLDOWN_HOURS. Tunable
without code change. Log format now shows the configured hours.

Also backfilled 59 pre-existing zombie queue-path rows where the file
was already archived but DB status said 'unprocessed' — these would have
leaked past the DB filter once the 4h cooldown expired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:33:10 +01:00
469cb7f2da fix: stop runaway re-extraction loop in extract.py
Some checks are pending
CI / lint-and-test (push) Waiting to run
Three changes reduce extraction cost and duplicate PR flood:

1. 4-hour cooldown gate — skip sources with ANY PR (merged/closed/open)
   created in the last 4h. Prevents same source re-extracting every 60s
   while archive step lags behind merge.

2. DB-authoritative status — sources.status is now updated in the pipeline DB
   at each extraction terminal point (null_result, success). Queue scan checks
   DB first so sources with failed archives (e.g., root-owned worktree files
   blocking git pull --rebase) don't get re-extracted forever. Also moves
   archival into the extraction branch so it goes through PR merge instead
   of a fragile separate main-worktree push.

3. source_channel wiring — extract.py PR INSERT now sets source_channel from
   classify_source_channel(branch). Previously daemon-created PRs had NULL
   source_channel, breaking Argus dashboard filters. Combined with Ship's
   in-branch archive refactor.

Root incident: blockworks-metadao-strategic-reset.md extracted 31 times in
12 hours. Nine other sources hit 10-22 extractions each. Near-duplicate
rejection rate jumped to 94%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:19:30 +01:00
8de28d6ee0 feat: bidirectional source↔claim linking
Some checks are pending
CI / lint-and-test (push) Waiting to run
Forward link: claims get `sourced_from: {domain}/{filename}` at extraction time.
Reverse link: after merge, backlink_source_claims() updates source files with
`claims_extracted:` list. All disk writes happen under async_main_worktree_lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 13:00:59 +01:00
05f375d775 feat: filter system accounts from leaderboard, add primary_ci field
- SYSTEM_ACCOUNTS set excludes pipeline/unknown/teleo-agents from /api/contributors/list
- primary_ci field: action_ci.total when available, else role-based ci_score
- action_ci included in list endpoint for each contributor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:33:47 +01:00
4101048cd0 feat: wire action-type CI into contributor profiles
- contribution_scores table stores per-PR CI with action type
- Profile endpoint returns action_ci alongside role-based ci_score
- Branch-name attribution: contrib/NAME/ PRs attributed to NAME
- Cameron now shows 0.32 CI + BELIEF MOVER badge from challenge
- Handle variant matching (cameron-s1 → cameron) for cross-system lookup
- Full historical backfill: 985 scores across 9 contributors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:29:01 +01:00
af027d3ced feat: add contributor profile API endpoint
GET /api/contributors/{handle} — returns CI score, badges, domain
breakdown, role percentages, contribution timeline, review stats.
GET /api/contributors/list — leaderboard with min_claims filter.

Git-log fallback for contributors not in pipeline.db (Cameron, Alex).
Badge system: FOUNDING CONTRIBUTOR, BELIEF MOVER, KNOWLEDGE SOURCER,
DOMAIN SPECIALIST, VETERAN, FIRST BLOOD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:22:13 +01:00
1b27a2de31 feat: add /api/activity-feed endpoint with hot/recent/important sort
Serves contribution events from pipeline.db. Classifies PRs as
create/enrich/challenge, normalizes contributors, derives summaries
from branch names when descriptions are empty. Hot sort uses
challenge*3 + enrich*2 + signal / hours^1.5 decay from event time.
Domain and contributor filters, pagination (limit/offset).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:16:46 +01:00
11e026448a sync: dashboard_routes.py from VPS — digest + contributor-graph endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:01:15 +01:00
c3d0b1f5a4 feat: contributor graph PNG generator + API endpoint
matplotlib chart with dual axes — cumulative claims (#00d4aa) and
contributors (#7c3aed) on dark background. 1200x630 for Twitter.
Auto-regenerates hourly via /api/contributor-graph endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:01:02 +01:00
88e8e15c6d feat: add /api/digest/latest endpoint for scoring digest data
Serves the latest scoring-digest-latest.json from cron output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:55:24 +01:00
5463ca0b56 feat: add daily scoring digest with CREATE/ENRICH/CHALLENGE classification
Classifies merged PRs by action type, scores with importance multiplier
(confidence, domain maturity, connectivity bonus), updates contributor
records, posts summary to Telegram, serves via /api/digest/latest.

Cron: 7:07 UTC daily (8:07 AM London).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:55:13 +01:00
e043cf98dc feat: add wiki-link audit script for codex graph integrity
Crawls domains/foundations/core/decisions for [[wiki-links]], resolves
against claim files, entities, maps, and agents. Reports dead links,
orphans, and connectivity stats. Prerequisite for CI scoring connectivity
bonus — broken links would inflate scores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:46:55 +01:00
9c0be78620 fix: align CI role weights with contribution-architecture.md
config.py had extractor-heavy weights (0.40) from initial bootstrap.
Correct weights per approved architecture: challenger 0.35, synthesizer
0.25, reviewer 0.20, sourcer 0.15, extractor 0.05. backfill-ci.py
already had correct weights; this fixes the live computation in health.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:37:47 +01:00
c29049924e fix: wire commit_type into contributor role assignment
The contributor attribution always recorded "extractor" regardless of
the PR's refined commit_type. Added COMMIT_TYPE_TO_ROLE mapping and
applied it in all three attribution paths (Pentagon-Agent trailer,
git author fallback, PR agent fallback).

Backfill script resets and re-derives role counts from prs.commit_type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 10:27:36 +01:00
f463f49b46 fix: prevent false 'already up to date' on fork PRs with merge commits
When a contributor merges main into their fork branch (standard GitHub
workflow), merge-base equals main SHA, triggering the 'already up to
date' early return. This closes the PR without cherry-picking the new
content. Cameron's PR #3377 hit this exact bug.

Fix: add a diff check before returning 'already up to date'. If the
branch has actual content changes vs main, proceed to cherry-pick
instead of short-circuiting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 22:41:14 +01:00
9505e5b40a feat: add /api/contributor-growth endpoint + cumulative growth script
Adds async git-log-based endpoint for cumulative contributor and claim
tracking. 5-minute cache, excludes bot accounts, tags founding contributors.
Standalone CLI script also included for ad-hoc data generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 22:19:42 +01:00
f0cf772182 Merge remote-tracking branch 'origin/epimetheus/reduce-rejections'
Some checks are pending
CI / lint-and-test (push) Waiting to run
2026-04-20 19:03:26 +01:00
4fc541c656 Skip liquidated entities in portfolio fetcher
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ranger was liquidated — no point fetching empty data every cron run.
Also purged 1,647 pre-Apr-20 snapshot rows (incomplete NAV data from
data collection ramp-up, not actual market movement).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 18:55:04 +01:00
b7242d2206 Wire rejection_reason into review records + fix ingestion domain routing
Some checks are pending
CI / lint-and-test (push) Waiting to run
rejection_reason was always NULL in review_records — now populated with
comma-joined issue tags (near_duplicate, frontmatter_schema, etc.) at both
rejection call sites. Also fixes stale reviewer_model="gpt-4o" hardcoding
to use config.EVAL_DOMAIN_MODEL (currently Gemini Flash).

Ingestion branches (ingestion/futardio-*, ingestion/metadao-*) now resolve
to internet-finance domain instead of falling through to "general".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 18:03:34 +01:00
12078c8707 Reduce near-duplicate and frontmatter schema rejections
Near-duplicate (159+ rejections):
- Add extract-time dedup gate: SequenceMatcher check before file write ($0)
- Strengthen extraction prompt: high-similarity matches (>=0.75) get explicit
  "DO NOT extract, use enrichment instead" warning
- Strip [[wiki link]] brackets from related_claims field

Frontmatter schema (129+ rejections):
- Normalize LLM confidence aliases (high→likely, medium→experimental, etc.)
  in both _build_claim_content and validate_schema
- Strip code fences (```markdown/```yaml) from entity content in extract.py
  and from diff content in validate.py tier0.5 check
- Code fences were root cause of "no_frontmatter" failures: parser sees
  ```markdown as first line, not ---

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 18:03:26 +01:00
7a753da68b fix: auto-deploy.sh rsync excludes broken + add tests/ sync
Some checks are pending
CI / lint-and-test (push) Waiting to run
- Switch RSYNC_FLAGS string to RSYNC_OPTS bash array (same fix as deploy.sh
  in 368b579 — string passed literal quotes to rsync, matching nothing)
- Add tests/ to rsync targets and syntax check glob for parity with deploy.sh
- All 8 rsync calls now use "${RSYNC_OPTS[@]}" expansion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:22:11 +01:00
febbc7da30 add rio and theseus telegram bot agent configs
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two YAML files on VPS but not in repo. Agent identity, KB scope, and
voice configs for the Telegram bots. No secrets (tokens reference file
paths, not inline values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:20:21 +01:00
368b5793d3 fix: deploy.sh rsync excludes were broken — quotes passed literally
Some checks are pending
CI / lint-and-test (push) Waiting to run
RSYNC_FLAGS as a string meant --exclude='__pycache__' passed literal
quotes to rsync, matching nothing. Switched to bash array (RSYNC_OPTS)
so excludes work correctly. __pycache__ and .pyc files no longer sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:17:31 +01:00
670c50f384 fix: add telegram/ and tests/ to deploy pipeline, remove hardcoded API key
Some checks are pending
CI / lint-and-test (push) Waiting to run
deploy.sh was missing telegram/ and tests/ directories — code existed in
repo but never synced to VPS. Also removes hardcoded twitterapi.io key
from x-ingest.py (reads from secrets file like all other modules).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:15:55 +01:00
a479ab533b fix: add fetch_coins.py to auto-deploy loop + legacy migration comment
Some checks are pending
CI / lint-and-test (push) Waiting to run
- auto-deploy.sh: fetch_coins.py was missing from the root-level .py deploy
  loop (line 72). Only manual deploy.sh had it. Next cycle syncs it to VPS.
- fetch_coins.py: document the ALTER TABLE loop as legacy migration for older
  DBs that predate the CREATE TABLE columns.

Reviewed-by: Ganymede
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:06:35 +01:00
eac5d2f0d3 fix: add fetch_coins.py to deploy.sh deployment list
Some checks are pending
CI / lint-and-test (push) Waiting to run
fetch_coins.py was committed to repo root but deploy.sh only deployed
teleo-pipeline.py and reweave.py. This meant bug fixes to fetch_coins
would silently fail to reach VPS on deploy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:01:31 +01:00
5071ecef16 fix: apply Ganymede review fixes to portfolio code
Some checks are pending
CI / lint-and-test (push) Waiting to run
dashboard_portfolio.py:
- datetime.utcnow() → datetime.now(timezone.utc) (deprecation fix)
- days parameter validation with try/except + min(..., 365) on 2 endpoints

fetch_coins.py:
- isinstance(chain, str) guard prevents AttributeError on string chain values
- Log when adjusted market cap differs from DexScreener value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:00:02 +01:00
ddf3c25e88 sync VPS state: portfolio dashboard + fetch_coins.py
Some checks are pending
CI / lint-and-test (push) Waiting to run
Pull live app.py from VPS to close 243-line drift. Add portfolio
dashboard (renamed from v2), portfolio nav link, and fetch_coins.py
(daily cron script for ownership coin data). Delete stale lib/ copy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 16:55:36 +01:00
cde92d3db1 fix: wrap breaker calls in stage_loop to prevent permanent task death
Some checks are pending
CI / lint-and-test (push) Waiting to run
A transient DB lock in breaker.record_failure() inside an except handler
killed the asyncio coroutine permanently — snapshot_cycle died Apr 18 and
never recovered. All three breaker call sites now have their own try/except.

Also includes HTML injection fix for github_feedback review_text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 12:37:28 +01:00
83526bc90e fix: quote YAML edge values containing colons, skip unparseable files in reweave merge
Root cause of 84% reweave PR rejection rate: claim titles with colons
(e.g., "COAL: Meta-PoW: The ORE Treasury Protocol") written as bare
YAML list items, causing yaml.safe_load to fail during merge.

Three changes:
1. frontmatter.py: _yaml_quote() wraps colon-containing values in double quotes
2. reweave.py: _write_edge_regex uses _yaml_quote for new edges
3. merge.py: skip individual files with parse failures instead of aborting entire PR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:07:28 +01:00
ae860a1d06 fix: set execute bit on research-session.sh and install-hermes.sh
Mode 100644 → 100755. Previous commit added the safety net but missed
the actual git mode change due to staging order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:54:39 +01:00
878f6e06e3 fix: restore execute bits on .sh files, add chmod safety net to auto-deploy
research-session.sh and install-hermes.sh were committed with mode 100644
during repo reorganization (d2aec7fe). rsync -az preserved the non-executable
mode, breaking all research agent cron jobs since Apr 15. Safety net in
auto-deploy.sh ensures any future permission loss is auto-corrected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:54:13 +01:00
ac794f5c68 Fix source_channel migration: add to SCHEMA_SQL, default 'unknown' not 'telegram'
Ganymede review findings:
1. source_channel was missing from CREATE TABLE (fresh installs wouldn't have it)
2. Default fallback changed from 'telegram' to 'unknown' — unknown prefixes
   are genuinely unknown, not telegram
3. Cross-reference comments added between BRANCH_PREFIX_MAP and _CHANNEL_MAP

Also wires classify_source_channel into merge.py PR discovery INSERT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:27:15 +01:00
25a537d2e1 fix: divergence alerting — alert suppression bug + stale ref detection
Bug: echo "alerted" ran regardless of curl success, permanently suppressing
alerts on delivery failure. Fix: if/then/else wraps the state write.

Warning: stale tracking refs after push steps caused false divergence.
Fix: re-fetch both remotes before comparing.

Both findings from Ganymede review of Step 6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:10:32 +01:00
0f868aefab Add GitHub PR feedback module and fix attribution for mirrored PRs
Some checks failed
CI / lint-and-test (push) Has been cancelled
github_feedback.py posts pipeline status to GitHub PRs at three touchpoints:
discovery ack, eval review result, and merge/close outcome. Only fires for
PRs with a github_pr link (set by sync-mirror.sh). All calls non-fatal.

contributor.py: expanded git author fallback to scan all non-merge commits
(was only checking last commit), added teleo-bot and github-actions[bot]
to bot filter list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:16:28 +01:00
13f21f7732 feat: external contributor pipeline — fork PR handling, attribution, prefix recognition
- Mirror: fetch GitHub fork PR refs (refs/pull/*/head), push to Forgejo as gh-pr-N/branch
- Mirror: fork PRs auto-create Forgejo PR with GitHub PR title, link github_pr in DB
- db.py: add contrib + gh-pr-* to classify_branch for external contributor branches
- contributor.py: git commit author as attribution fallback (before branch agent)
- contributor.py: skip bot/generic authors (m3taversal, teleo, pipeline)
- Tests: fix fallback test for new git author path, add external contributor test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:14:01 +01:00
0b28c71e11 Wire github_pr storage into sync-mirror.sh (Step 3)
Some checks are pending
CI / lint-and-test (push) Waiting to run
When mirror auto-creates a Forgejo PR from a GitHub branch, look up the
GitHub PR number via API and store it in pipeline.db (github_pr column
from migration v21). Enables reverse mapping for feedback and back-sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:10:06 +01:00
fb121e4010 Add github_pr column to prs table (migration v21)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Enables GitHub↔Forgejo PR linking for the contributor pipeline.
Mirror script will store GitHub PR number when creating Forgejo PRs,
allowing back-sync of eval feedback and merge/close status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:07:04 +01:00
26a8b15f56 fix: skip merge commits in cherry-pick to prevent fork workflow content loss
Some checks are pending
CI / lint-and-test (push) Waiting to run
External contributors who run `git merge main` create merge commits that
cherry-pick can't handle without -m flag. --no-merges filters these out.
Added detection for branches with only merge commits but real content diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:04:45 +01:00
687f3d3151 fix: prevent broken wiki links in extraction (226 rejections)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two changes to address the #1 rejection reason:

1. extraction_prompt.py: Explicitly tell LLM NOT to use [[wiki links]]
   in body text — use connections/related_claims JSON fields instead.
   Remove misleading "post-processor handles wiki links" language.

2. extract.py _get_kb_index(): Expand KB index to include entity stems
   from entities/{domain}/ so the LLM knows what entities exist when
   building connections. Previously only showed domain claims.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:28:58 +01:00
22b6ebb6f6 fix: lower reweave threshold 0.70→0.55, increase batch 50→200
Some checks are pending
CI / lint-and-test (push) Waiting to run
Orphan ratio at 39.6% (443/1118 claims) vs <15% target. Root cause:
reweave threshold 0.70 too strict for text-embedding-3-small — 56% of
orphans found "no neighbors." At 0.55, dry-run shows 0% no-neighbor
skips. Batch size 200 clears backlog in ~3-4 nights at ~$0.20/run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:18:50 +01:00
0ce7412396 fix: check Forgejo close return value in 2 merge.py paths to prevent ghost PRs
Both the "already merged" path and _handle_permanent_conflicts closed PRs on
Forgejo without checking the return value. On API failure, the DB update would
proceed anyway, creating ghost PRs (DB=closed/merged, Forgejo=open). Now both
paths check for None return and skip DB updates on failure — same pattern as
close_pr in pr_state.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:18:50 +01:00
28b25329b3 fix: remove FIRST early return that also blocked re-extraction
Some checks are pending
CI / lint-and-test (push) Waiting to run
There were TWO `if not unprocessed: return 0, 0` gates. The previous
fix (c763c99) only addressed the second one. The first at line 746
fires before the re-extraction query even runs. Replace with a comment
explaining why we don't early-return there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:17:20 +01:00
c763c99910 fix: re-extraction loop runs even when queue is empty
Some checks are pending
CI / lint-and-test (push) Waiting to run
The re-extraction check was below an early return that fires when
unprocessed queue is empty. Sources in needs_reextraction state were
never picked up unless new sources happened to arrive simultaneously.
Move re-extraction query above the gate so both paths run independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:04:49 +01:00
4c3ce265e4 fix: sanitize enrichment target_file path traversal
Some checks are pending
CI / lint-and-test (push) Waiting to run
Path(target).name strips directory components from LLM-generated
target filenames, preventing path traversal via ../. Same pattern
already applied to claim filenames (line 404) and entity filenames
(line 416). Ganymede-approved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:40:37 +01:00
46ad508de7 Phase 6b: extract post_merge.py from merge.py — post-merge effects
Some checks are pending
CI / lint-and-test (push) Waiting to run
7 functions extracted to lib/post_merge.py:
- embed_merged_claims, reciprocal_edges, find_claim_file, add_edge_to_file,
  archive_source_for_pr, commit_source_moves, update_source_frontmatter_status

git_fn injection pattern (same as contributor.py) for 3 async functions
that need git operations. Unused async_main_worktree_lock import removed
from merge.py.

merge.py: 1562 → 1200 lines (−362). Total reduction from 1912: −712 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:20:59 +01:00
ed1edd6466 Phase 6a: extract frontmatter.py from merge.py — pure YAML helpers
4 functions + 2 constants extracted to lib/frontmatter.py:
- parse_yaml_frontmatter, union_edge_lists, serialize_edge_fields,
  serialize_frontmatter, REWEAVE_EDGE_FIELDS, RECIPROCAL_EDGE_MAP

merge.py: 1678 → 1562 lines (−116).
test_reweave_merge.py: replaced local function copies with imports from
frontmatter.py — fixes missing challenged_by in test's REWEAVE_EDGE_FIELDS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:16:38 +01:00
53dc18afd5 Phase 5: Extract contributor.py from merge.py (−234 lines)
Some checks are pending
CI / lint-and-test (push) Waiting to run
5 functions extracted: is_knowledge_pr, refine_commit_type,
record_contributor_attribution, upsert_contributor, recalculate_tier.

git_fn parameter injection avoids circular import (merge→contributor,
contributor needs _git from merge). Single call site passes _git.

merge.py: 1912 → 1678 lines. 23 new tests, zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:26 +01:00
f46e14dfae refactor: Phase 4 — extract eval_actions.py, drop underscore prefixes in eval_parse
Some checks are pending
CI / lint-and-test (push) Waiting to run
Three changes:

1. Drop underscore prefixes in eval_parse.py — functions are now the public
   API of the module (filter_diff, parse_verdict, classify_issues, etc.).
   All 12 functions renamed, imports updated in evaluate.py and tests.

2. Extract eval_actions.py from evaluate.py — 3 async PR disposition functions:
   - post_formal_approvals: submit Forgejo reviews from 2 agents
   - terminate_pr: close PR, post rejection comment, requeue source
   - dispose_rejected_pr: disposition logic for rejected PRs on attempt 2+
   evaluate.py drops from ~1140 to 911 lines.

3. 14 new tests in test_eval_actions.py covering all three functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:57:51 +01:00
376b77999f refactor: Phase 3 — fix close_pr ghost bug, wire stale_pr, extract eval_parse
Some checks are pending
CI / lint-and-test (push) Waiting to run
Critical bug fix: close_pr now checks forgejo_api return value and
skips DB update on Forgejo failure, preventing ghost PRs (DB closed,
Forgejo open). Returns bool so callers can handle failures.

_terminate_pr checks return value — skips source requeue on failure.
stale_pr.py migrated from raw Forgejo+DB to close_pr (last raw close
transition eliminated).

eval_parse.py: 15 pure parsing functions extracted from evaluate.py
(~370 lines removed). Zero I/O, zero async, independently testable.
evaluate.py drops from ~1510 to ~1140 lines.

Tests: 295 passed (42 new eval_parse + 2 new close_pr), zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:40:23 +01:00
716cc43890 extraction quality: trust hierarchy + verified tagging + telegram review endpoint
Some checks are pending
CI / lint-and-test (push) Waiting to run
Three fixes for conversation-sourced claim quality:

1. Trust hierarchy in extraction prompt: bot-generated numbers are
   flagged as unverified context, not evidence. Directional claims
   are extractable but specific figures require external verification.
   Prevents laundering bot guesses into the KB as evidence.

2. Conversation-sourced claims tagged with verified: false and
   source_type: conversation in frontmatter. Downstream consumers
   (Leo, dashboard) can filter/flag these for verification.

3. GET /api/telegram-extractions endpoint for daily spot-checking.
   Shows recent Telegram-sourced PRs with claim titles, status,
   merge rate, and eval issues. Quick review surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:38:39 +01:00
c8a08023f9 refactor: Phase 2 — wire pr_state into fixer.py and substantive_fixer.py
Some checks are pending
CI / lint-and-test (push) Waiting to run
Fix 4 Forgejo ghost PR bugs flagged by Ganymede:
- fixer.py GC close: DB update ran outside try/except, closing DB even on Forgejo failure
- substantive_fixer.py droppable: NO Forgejo close at all
- substantive_fixer.py auto-enrichment: DB update before Forgejo (reversed order)
- substantive_fixer.py close_and_reextract: replace manual Forgejo+DB with close_pr()

Add start_fixing() and reset_for_reeval() to pr_state.py:
- start_fixing: atomic claim + fix_attempts increment in one statement
- reset_for_reeval: clears all eval state for re-evaluation after fix

Also fixes stale line number comment in merge.py (Ganymede nit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:21:40 +01:00
1e0c1cd788 Write enrichments as file modifications; strengthen correction extraction
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two changes:
1. extract.py: Enrichments now modify existing claim files by appending
   evidence sections. Previously enrichment-only extractions were
   discarded as null-result even when they contained valuable challenges.
2. extraction_prompt.py: Corrections should produce BOTH a claim (the
   corrected knowledge) AND an enrichment (linking to what it corrects).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:12:29 +01:00
1f5eb324f3 refactor: centralize PR state transitions in lib/pr_state.py
Some checks are pending
CI / lint-and-test (push) Waiting to run
Replace 38 hand-crafted UPDATE prs SET status calls across evaluate.py
and merge.py with 7 centralized functions that enforce invariants:
- close_pr: always syncs Forgejo (opt-out for reconciliation)
- approve_pr: raises ValueError on empty domain (prevents NULL bugs)
- mark_merged: always sets merged_at, clears last_error
- mark_conflict: always increments merge_failures, sets merge_cycled
- mark_conflict_permanent: terminal conflict state
- reopen_pr: handles all reopen scenarios (transient, rejection, reeval)
- start_review: atomic claim with bool return

This eliminates the class of bugs that produced 3 incidents:
1. Domain NULL on musings bypass (7 PRs stuck, 20h zero throughput)
2. Forgejo ghost PRs (70 PRs open on Forgejo but closed in DB)
3. Merge_cycled missing on various close paths

Also fixes: 3 close paths in merge.py had DB update before Forgejo call
(reversed order). close_pr does Forgejo first, then DB.

Only remaining raw status transition: _claim_next_pr (approved→merging)
which is an atomic subquery and doesn't have invariant requirements.

20 new tests, 264 total passing, 0 regressions. Net -101 lines in
evaluate.py + merge.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:08:57 +01:00
d073e22e8d Add conversation-aware extraction for Telegram sources
Some checks are pending
CI / lint-and-test (push) Waiting to run
When source format is "conversation", inject specialized extraction
rules that prioritize human corrections/pushback as highest-value
content. Fixes null-result on short but high-signal correction
messages. Maps corrections to existing KB claims as challenges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:05:51 +01:00
552f44ec1c fix: add migration v20 for conflict retry columns + serialize worktree ops
Some checks are pending
CI / lint-and-test (push) Waiting to run
db.py: migration v20 adds conflict_rebase_attempts, merge_failures,
merge_cycled columns (already exist on VPS via manual migration, missing
from code — any future DB rebuild would break retry mechanism).

merge.py: replace retry-with-backoff on config.lock with asyncio.Lock
(_bare_repo_lock) around all worktree add/remove calls. Prevents
contention instead of retrying it. Applied to both _cherry_pick_onto_main
and _merge_reweave_pr.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:19:56 +01:00
e0c9951308 fix: close stale PRs on Forgejo when pipeline DB marks them closed
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two code paths set status='closed' in the pipeline DB without calling
the Forgejo API to close the PR. This caused 50 ghost PRs to accumulate
on Forgejo (dashboard shows review backlog) while the pipeline considered
them done.

- evaluate.py: no-diff stale branch close now calls Forgejo PATCH
- merge.py: permanent conflict close now calls Forgejo PATCH

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:15:58 +01:00
0d3fe95522 Add config.lock retry with jitter to both worktree-add sites
Some checks are pending
CI / lint-and-test (push) Waiting to run
Parallel domain merges race on the bare repo's config file. The single
retry only covered one of two worktree-add call sites and used fixed
delay. Now both sites retry up to 3 times with increasing jitter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:13:32 +01:00
1755580b95 Harden already-merged detection to exact string match
Some checks are pending
CI / lint-and-test (push) Waiting to run
Ganymede review nit: substring match on "already" could false-positive
on future return strings. Pin to the two known values from cherry_pick().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:06:20 +01:00
ad7ee0831e fix(evaluate): set domain + auto_merge on all 5 approval paths
Some checks are pending
CI / lint-and-test (push) Waiting to run
Musings bypass and batch both_approve set status='approved' without
domain or auto_merge. Merge gate requires domain IS NOT NULL and
prefix match OR auto_merge=1. Result: agent PRs deadlocked for 20+ hours.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:03:42 +01:00
10b4e27c28 fix: tighten output gate patterns to eliminate false positives on public content
5 patterns were too broad — matched common English words:
- "extraction" (concept) matched pipeline extraction pattern
- "class X" (English) matched Python class definition pattern
- ".md " (product name) matched file extension pattern
- "threshold" (concept) matched internal metrics pattern

Fixes:
- extraction: require pipeline context words (queue/PR/branch/cron)
- class/def/import: require line-start (actual code, not prose)
- .py/.yaml/.json: require path-like prefix (not bare .md)
- threshold: require pipeline context (cosine/vector/Qdrant)

All 3 Hermes dry-run drafts now pass. 18/18 tests pass.
11/11 system content regression tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:02:08 +01:00
2b58ffc765 Harden output gate: add missing filter patterns for agent names, coordination language, infrastructure domains, UUIDs
Patterns added per Hermes audit:
- All agent names (Epimetheus, Ganymede, Hermes, etc.) as standalone
- Leo/Rio with coordination context (avoids false positives on common words)
- Pentagon, m3ta references
- Coordination language (craft review, substance review, skill graph, eval rubric)
- Infrastructure domains (teleo-codex, livingip.xyz)
- UUID pattern (catches conversation IDs, agent IDs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:02:08 +01:00
50ef90e7d3 Add X content pipeline: output gates + tweet queue + pluggable approval
Output gate (output_gate.py): Deterministic classifier that blocks system/pipeline
messages from reaching public outputs. Pattern-based detection of PR numbers,
deploy logs, diagnostics, infrastructure references.

Tweet queue (x_publisher.py): Submit drafts through output gate + OPSEC filter,
enter approval_queue, auto-post to X via Twitter API v2 on Cory's approval.

Pluggable approval stages (approval_stages.py): Extensible architecture where
adding a new approval stage = implementing ApprovalStage.check(). Current stages:
OutputGate (stage 0), OPSEC (stage 1), Human (stage 10). Designed for future
agent voting, multi-human approval, and decision markets.

Also syncs approvals.py from VPS to local repo (was deployed but never committed).

18 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:02:08 +01:00
f38b1e3c01 fix: handle already-merged PRs + retry worktree config.lock
Some checks are pending
CI / lint-and-test (push) Waiting to run
Two fixes for the 18-PR merge blockage:

1. When cherry-pick returns "already merged" (all commits empty because
   content is already on main), close the PR directly instead of trying
   to push the stale branch SHA to main. The branch ref points at old
   commits that aren't descendants of current main, so the push would
   always fail as non-fast-forward.

2. Retry worktree add once with jittered delay when config.lock
   contention occurs from parallel domain merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:57:28 +01:00
ff357c4bbc fix: remove --force-with-lease from main push to unblock 16 PRs
Some checks are pending
CI / lint-and-test (push) Waiting to run
Forgejo categorically blocks --force-with-lease on protected branches,
even for fast-forward pushes. The cherry-picked branch is already a
descendant of origin/main, so a regular push is a fast-forward by
definition. Non-ff is rejected by default — same safety guarantee.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:52:39 +01:00
25062cf130 Fix health check: accept HTTP 503 (stalled) as healthy
Some checks are pending
CI / lint-and-test (push) Waiting to run
Pipeline /health returns 503 when idle/stalled, which is a valid
running state. Also increase post-restart wait from 15s to 30s
for pipeline HTTP server initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:26:03 +01:00
fe996c3299 feat: add auto-deploy script and systemd units for teleo-infrastructure
Some checks are pending
CI / lint-and-test (push) Waiting to run
Auto-deploy watches teleo-infrastructure (not teleo-codex) and syncs to
VPS working directories. New checkout path: deploy-infra/ (parallel to
existing deploy/ for 48h rollback). Path mapping updated for reorganized
repo structure (lib/, diagnostics/, telegram/ etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:27:23 +01:00
81afcd319f fix: sync all code from VPS — repo is now authoritative source of truth
Some checks are pending
CI / lint-and-test (push) Waiting to run
24 files: 8 pipeline lib modules, 6 diagnostics updates, 4 new diagnostics
modules, telegram bot fix, 5 active operational scripts. Key changes:
- Security: SQL injection prevention (alerting.py), SSL verification
  (review_queue.py), path traversal guard (extract.py)
- Cost tracking: per-PR cost accumulation in evaluate.py
- Auto-recovery: watchdog tier0 reset with retry cap + cooldown
- Extraction: structured edge fields, post-write vector connection
- New modules: vitality, research_tracking, research_routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:18:01 +01:00
d2aec7fee3 feat: reorganize repo with clear directory boundaries and agent ownership
Some checks are pending
CI / lint-and-test (push) Waiting to run
Move scattered root-level files into categorized directories:
- deploy/ — deployment + mirror scripts (Ship)
- scripts/ — one-off backfills + migrations (Ship)
- research/ — nightly research + prompts (Ship)
- docs/ — all operational documentation (shared)

Delete 3 dead cron scripts replaced by pipeline daemon:
- batch-extract-50.sh, evaluate-trigger.sh, extract-cron.sh

Add CODEOWNERS mapping every path to its owning agent.
Add README with directory structure, ownership table, and VPS layout.
Update deploy.sh paths to match new structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:20:13 +01:00
495 changed files with 110945 additions and 4554 deletions

View file

@ -0,0 +1,40 @@
---
name: crabbox
description: Use for remote Linux verification, isolated CI proof, Crabbox lease cleanup, and PR evidence without production deploys or secret forwarding.
---
# Crabbox
Use Crabbox for remote Linux verification and PR proof only.
Allowed jobs:
- `crabbox job run unit`
- `crabbox job run lint-phase1b`
- `crabbox job run ci-contract`
- `crabbox job run phase1b-local-proof`
- `crabbox job run sync-smoke`
Default workflow:
1. Run `crabbox job run --dry-run ci-contract`.
2. Run `crabbox job run --dry-run phase1b-local-proof`.
3. Inspect the planned commands and confirm no production secrets or production deploy commands appear.
4. Run `crabbox job run ci-contract`.
5. Run `crabbox job run phase1b-local-proof`.
6. Save the run id, lease id, stdout, downloaded proof JSON, and JUnit output.
7. Stop the lease unless the CLI has already stopped it.
Boundaries:
- Do not run production deploy commands from Crabbox.
- Do not forward production GitHub, Forgejo, OpenRouter, SSH, Bitwarden, or VPS secrets.
- Do not target the production `decision-engine` repo for sandbox proof.
- Do not mutate the production VPS.
- Do not call Crabbox proof equivalent to production proof unless the lease recreates `/opt/teleo-eval`, systemd services, runtime users, DB paths, timers, and deploy scripts.
Failure handling:
- If sync sanity fails, stop the lease and retry on a fresh lease.
- If a proof script fails, save the full run output and do not summarize it as a pass.
- If a remote box has unknown state, stop it instead of debugging against reused state.

View file

@ -0,0 +1,41 @@
---
name: decision-engine-refinement
description: Use when improving Living IP decision-engine quality, LLM model selection, evaluator prompts, rubrics, replay evals, Rio or Theseus reviewer behavior, or model bakeoffs.
---
# Decision Engine Refinement
Use this skill for quality work, not infrastructure work. Pentagon.run or Crabbox can run remote jobs; this repo owns model judgment, rubric design, prompt/tool refinement, and proof artifacts.
## Workflow
1. Read `docs/llm-refinement-decision-engine.md`.
2. Identify the lane: Rio economics, Theseus model integrity, Leo cross-domain, domain factuality, retrieval quality, or prompt/tool self-upgrade.
3. Build or reuse a replayable fixture before changing prompts or model assignments.
4. Compare baseline vs candidate with the same input, same rubric, and structured verdict format.
5. Record false approves, false rejects, useful disagreements, cost, and latency.
6. Change runtime prompts/models only after the candidate shows a measured improvement with no critical regression.
## Hard Rules
- Do not change live model assignments because one answer sounds better.
- Do not use production DB writes to tune prompts.
- Do not collapse Rio and Theseus into generic "reviewers".
- Do not treat payment, popularity, or engagement as quality approval.
- Do not claim production decision-engine improvement without replay evidence and live/staging readback.
## Agent Responsibilities
- Rio: incentive design, contribution weights, paid-query effects, market/mechanism reasoning, OPSEC, correlated-prior warnings.
- Theseus: model diversity, adversarial evals, disagreement queues, self-upgrade criteria, prompt/tool safety, verifier drift.
- Leo: cross-domain synthesis, fallback review, final arbitration where the route or rubric is ambiguous.
## Expected Artifacts
- fixture file or DB query used for sampling;
- baseline verdict output;
- candidate verdict output;
- summary JSON with quality, cost, latency, and disagreement metrics;
- patch scoped to prompts, model config, rubric docs, or eval harness.
Run `python3 scripts/check_llm_refinement_contract.py` after editing this surface.

View file

@ -0,0 +1,82 @@
---
name: leo-telegram-canary-ops
description: Use for live Leo Telegram tests through authenticated Chrome/Computer Use, including memory, KB audit, staged DB writes, screenshots, and clear separation from external outreach.
---
# Leo Telegram Canary Ops
## Job
Run Telegram-visible Leo canaries when testing Leo itself, with retained screenshots and DB readbacks.
## Trigger Phrases
- "test Leo in Telegram"
- "send the Leo group message"
- "Telegram-visible proof"
- "Chrome Telegram canary"
- "Leo remembers conversation"
- "staged write through Telegram"
## Authorization Boundary
Live Telegram bot messages to the Leo group are authorized when they are test canaries for Leo behavior.
This is different from external outreach. Do not send external outreach, partner messages, public announcements, or non-test communications without exact authorization.
## Required Tooling
Use Computer Use through `node_repl` and the Chrome/Computer Use skill path. Do not use:
- AppleScript,
- `osascript`,
- JXA,
- System Events,
- focus hijacking,
- foregrounding hacks.
## Canary Types
Name the canary before sending:
- bot readiness,
- memory,
- KB audit truth,
- proposed-vs-applied truth,
- open-ended Cory-style triage,
- staged write,
- no-canonical-mutation proof,
- screenshot/readback proof.
## Message Discipline
Use unique markers such as `WL-LIVE-TG-CORY-YYYYMMDD-Tn`.
Ask Leo for machine-checkable reply markers like:
- `LIVE_TG_TURN1_ACK`
- `LIVE_TG_TURN3_STAGED`
- `LIVE_TG_TURN4_READBACK`
Do not rely only on exact-ID prompts. At least one representative canary should be a vague operator-style prompt such as: Cory says the agents are not working, the KB is in the same state as last night, and Leo should be able to manipulate the KB; ask Leo what that means, what it would inspect first, what fixed means, and how it separates proposed, approved, and applied. Keep this canary read-only unless the task explicitly authorizes a staging or apply step.
## Required Proof
After each live Telegram canary:
1. Capture screenshot.
2. Record exact message sent.
3. Record Leo reply marker and substance.
4. Query DB if the canary involves DB state.
5. Record canonical public table counts when proving no mutation.
6. Save a local markdown/json artifact.
7. Sync artifacts to VPS report dir when relevant.
## Current Known Proof
- Live memory and KB audit passed for marker `WL-LIVE-TG-CORY-20260709`.
- Live staged write passed for `WL-LIVE-TG-CORY-20260709-T3`.
- Readback passed for `WL-LIVE-TG-CORY-20260709-T4`.
- Open-ended Cory-style read-only triage passed for `WL-LIVE-TG-CORY-OPEN-20260709`.
Use `docs/reports/leo-working-state-20260709/current-truth-index.md` for artifact paths.

View file

@ -0,0 +1,93 @@
---
name: living-ip-kb-interop
description: Use when giving Hermes, OpenClaw, Claude-style, Pentagon, or other external agents safe read/write access patterns for the Living IP knowledge base.
---
# Living IP KB Interop
Use this skill when an outside agent needs to read from the Living IP knowledge base or propose a write back into it. The default is propose-first, proof-backed, and no-secret.
## Goal
Any Hermes, OpenClaw, Claude-style, or Pentagon agent should be able to:
1. search the knowledge base;
2. read a cited file or record;
3. propose a source, claim, entity, or correction;
4. route the proposal to the right evaluator agents;
5. leave a proof artifact that shows inputs, tools, and no denied actions.
## Read Path
Prefer deterministic local surfaces before asking an LLM:
- repository files under the knowledge base checkout;
- generated claim indexes from `lib/claim_index.py`;
- search helpers in `lib/search.py`;
- copied SQLite state through `teleo-db-operator`;
- retained proof JSON in `.crabbox-results/` or `proof/`.
Read outputs must include file paths, source paths, claim/entity IDs when available, and the exact query used.
## Write Path
All writes are proposals until the normal review/evaluation pipeline accepts them.
Allowed proposal targets:
- source file proposal;
- claim file proposal;
- entity file proposal;
- correction proposal;
- route/evaluator proof artifact.
Required fields:
- source or rationale;
- target domain;
- proposed author/agent;
- route evidence;
- confidence or uncertainty tag;
- citations to existing KB context;
- proof output path.
Do not write directly to main. Do not mutate production `pipeline.db`. Use `teleo-db-operator` for any SQLite write, and only after explicit authorization, backup, transaction, and readback.
## Minimal Tool Contract
Adapters should expose this shape even if their runtime uses different names:
- `kb.search(query, domain?, limit?)`
- `kb.get(path_or_id)`
- `kb.propose_source(markdown, metadata)`
- `kb.propose_claim(markdown, metadata)`
- `kb.propose_entity(markdown, metadata)`
- `kb.route(diff_or_metadata)`
- `kb.proof(path, payload)`
If a runtime cannot implement one of these, record the missing tool as a blocker instead of silently skipping it.
## Denied Actions
- raw Bitwarden export;
- card, token, or password reads;
- production DB writes;
- direct pushes to main;
- public comments or messages;
- hidden Slack, Linear, Telegram, or GitHub sends;
- uncited knowledge writes;
- model-driven edits without route evidence.
## Expected Artifact
Write `.crabbox-results/kb-interop-proof.json` or a caller-specified proof path containing:
- runtime name;
- model/provider if known;
- tools invoked;
- denied tools not invoked;
- query or input fixture;
- cited reads;
- proposed writes;
- route evidence;
- verifier result.

View file

@ -0,0 +1,70 @@
---
name: nousresearch-hermes-agent
description: Use when packaging Living IP agents, skills, prompts, memory, model routing, or decision-engine workflows for NousResearch Hermes Agent.
---
# NousResearch Hermes Agent
Use this skill to adapt Living IP decision-engine behavior to Hermes Agent. Keep the package fixture-first and no-secret by default.
## Current External Surface
As of 2026-06-01, the upstream Hermes Agent README describes:
- model switching via `hermes model`;
- tools via `hermes tools`;
- a messaging gateway for Telegram, Discord, Slack, WhatsApp, Signal, and CLI;
- built-in skill creation and self-improvement;
- cron scheduling;
- terminal backends including local, Docker, SSH, Modal, and Daytona;
- OpenClaw migration commands.
Verify upstream docs before depending on a command in code.
## Living IP Package Shape
Create a package that includes:
- agent identity file for Rio or Theseus;
- skill instructions copied from repo-owned `.agents/skills/*`;
- `living-ip-kb-interop` for read/propose/writeback behavior;
- no-secret tool allowlist;
- fixture replay command;
- model selection notes;
- proof output path.
Do not package production DBs, tokens, API keys, SSH keys, or Bitwarden exports.
## Rio Package
Rio Hermes package should focus on:
- internet finance and mechanism reasoning;
- contribution weights and paid-query effects;
- OPSEC finance filters;
- source-diversity warnings;
- fixture tests for false economic reasoning.
## Theseus Package
Theseus Hermes package should focus on:
- model-diversity evals;
- disagreement queues;
- self-upgrade criteria;
- prompt/tool safety;
- fixture tests for overconfident or poorly grounded model judgments.
## Handoff Contract
Every Hermes handoff must include:
1. install/config snippet;
2. model/provider selection left configurable;
3. tool allowlist;
4. fixture-first demo;
5. no-live-write default;
6. proof artifact path;
7. known blockers.
Do not claim Hermes production integration until a Hermes runtime actually executes the fixture and writes proof.

View file

@ -0,0 +1,70 @@
---
name: openclaw-agent
description: Use when adapting Living IP decision-engine agents, skills, tools, prompt files, or no-secret workflows to OpenClaw agent workspaces.
---
# OpenClaw Agent
Use this skill to package Living IP decision-engine behavior for OpenClaw workspaces. Treat OpenClaw as a distribution/runtime surface, not a new source of truth.
## Current External Surface
As of 2026-06-01, the upstream OpenClaw README describes:
- Node 24 or Node 22.19+ runtime;
- `openclaw onboard --install-daemon`;
- Gateway daemon usage;
- agent prompt files `AGENTS.md`, `SOUL.md`, and `TOOLS.md`;
- workspace skills at `~/.openclaw/workspace/skills/<skill>/SKILL.md`;
- model configuration in OpenClaw config;
- security guidance for DM pairing, allowlists, and sandboxing.
Verify upstream docs before depending on a command in code.
## Living IP Workspace Shape
Create or update:
- `AGENTS.md`: scope, repo boundaries, proof requirements;
- `SOUL.md`: Rio or Theseus identity;
- `TOOLS.md`: bounded tools only;
- `skills/decision-engine-refinement/SKILL.md`;
- `skills/living-ip-kb-interop/SKILL.md`;
- `skills/teleo-db-operator/SKILL.md` only for read-only local copies unless explicitly authorized.
## Tool Policy
Default allow:
- read files;
- run local fixture tests;
- write proof artifacts;
- inspect git diffs;
- query copied SQLite DBs read-only.
Default deny:
- production DB writes;
- token reads;
- Bitwarden vault export;
- live GitHub PR comments;
- public messaging sends;
- broad shell automation against host services.
## Rio And Theseus
- Rio OpenClaw package: economic reasoning, contribution incentives, paid-query guardrails, OPSEC.
- Theseus OpenClaw package: eval integrity, adversarial prompts, model bakeoffs, self-upgrade review.
## Proof Contract
An OpenClaw adapter is useful only if it can run a fixture and produce:
- prompt files used;
- tool allowlist;
- model selected;
- fixture input;
- structured verdict output;
- proof that no denied tools were invoked.
Do not claim OpenClaw production readiness until the package runs in an OpenClaw workspace and writes proof.

View file

@ -0,0 +1,76 @@
---
name: teleo-db-operator
description: Use when reading, auditing, backing up, querying, or safely writing the Teleo pipeline SQLite database, including review_records, audit_log, costs, prs, sources, and contributor feedback loops.
---
# Teleo DB Operator
Default to read-only. The database is evidence for decision-engine refinement, not a scratchpad.
## Discover
1. Read `lib/config.py` for `DB_PATH` and related paths.
2. Prefer local or copied DBs over production DBs.
3. If using production, record whether access is read-only or write-authorized.
4. Never print secret values found near DB paths or shell history.
## Read Path
Use `sqlite3` or Python `sqlite3`.
Recommended read targets:
- `review_records`: evaluator, model, outcome, rejection reason.
- `audit_log`: route decisions, approve/reject events, failure details.
- `costs`: model cost by date/stage.
- `prs`: status, tier, route compatibility fields, verdicts.
- `sources`: priority, feedback, extraction model.
For refinement work, export aggregated JSON or CSV into `.crabbox-results/` or `proof/`, not raw private DB snapshots.
## Write Path
Writes require explicit authorization and a backup.
Required sequence:
1. Create a backup or operate on a copy.
2. Write the exact SQL in a retained artifact.
3. Use `BEGIN IMMEDIATE;`.
4. Apply the minimal mutation.
5. Read back the changed rows.
6. Commit the transaction only after readback is correct.
7. Write a blocker artifact instead of guessing if any precondition is missing.
Never write production prompt/model state as part of an experiment. Experiments should replay fixtures and produce proof first.
## Safety Boundaries
- Do not attach, copy, or commit `pipeline.db`.
- Do not run broad `UPDATE` or `DELETE` without a `WHERE` clause and a prior row count.
- Do not mutate `prs`, `sources`, or contributor state from a model response alone.
- Do not treat local copied DB proof as production proof.
## Useful Queries
```sql
SELECT reviewer, reviewer_model, outcome, rejection_reason, count(*) AS n
FROM review_records
GROUP BY reviewer, reviewer_model, outcome, rejection_reason
ORDER BY n DESC;
```
```sql
SELECT event, count(*) AS n
FROM audit_log
WHERE stage = 'evaluate'
GROUP BY event
ORDER BY n DESC;
```
```sql
SELECT model, stage, calls, input_tokens, output_tokens, cost_usd
FROM costs
ORDER BY date DESC, cost_usd DESC
LIMIT 50;
```

View file

@ -0,0 +1,101 @@
---
name: teleo-gcp-parity-ops
description: Use for Teleo GCP control-plane inventory, Cloud SQL read-only parity, authenticated Chrome routing, VM/runtime comparison, and exact GCP access gates without collapsing VPS proof into GCP proof.
---
# Teleo GCP Parity Ops
## Working Target
Prove the real GCP resource, database, runtime, and Cory-answer paths separately.
Control-plane inventory alone is not database or Leo parity.
## Current Verified Inventory
- Project: `teleo-501523` (`Teleo`).
- Authenticated Console account: redacted `b***@livingip.xyz` via `authuser=5`.
- VM: `teleo-prod-1`, running in `europe-west6-a`.
- VM shape: Debian 12, `e2-standard-4`, 4 vCPU, 16 GB; OS Login metadata is
present. Treat the displayed API scope as metadata, not IAM authorization.
- Cloud SQL: exactly one instance, `teleo-pgvector-standby`.
- Database engine: PostgreSQL `16.14`, Enterprise.
- Region/zone: `europe-west6` / `europe-west6-c`, single-zone.
- Network: private-only `teleo-staging-net`; public IP disabled.
- Connection name: `teleo-501523:europe-west6:teleo-pgvector-standby`.
- Cloud SQL Studio loads, but the observed session exposes no enabled existing
IAM DB principal and selects built-in password authentication.
- Cloud Logging has guest/platform logs but zero seven-day entries matching
`leoclean`, `hermes`, `gateway`, `postgres`, or `teleo-kb`; Ops Agent is not
installed.
- Read-only serial output is current through `2026-07-10T15:57:54Z`. It proves
recurring successful `leoclean-cloudsql-memory-sync.service` cycles. The last
exact `leoclean-gcp-prod-parallel.service` lifecycle event in the retained
buffer is a Hermes start on July 9 with no later exact stop/failure. This is
not a current PID, process, or deployed-SHA readback.
Read these before refreshing:
- `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
- `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
- `docs/reports/leo-working-state-20260709/gcp-parallel-delegation-current.json`
- `docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.json`
## Browser Rule
Use authenticated Chrome/Computer Use for Console-only facts when CLI auth is
stale. Never use AppleScript, `osascript`, JXA, `open`, System Events, or a
focus-stealing fallback. Do not inspect cookies, tokens, saved passwords, or
secret values.
## Required Parity Rows
Track each independently:
1. control-plane project/VM/Cloud SQL inventory;
2. database name, principal, transaction-read-only proof, schema, counts, and
selected row identities;
3. VM process/service/repo/profile/model/bot configuration;
4. no-post `DC-01` through `DC-06` replies and score;
5. cleanup and no-mutation readback.
## Cloud SQL Read-Only Query
Only after Studio is already authenticated with an existing principal:
```sql
begin transaction read only;
select current_database(), current_user,
current_setting('transaction_read_only');
select count(*) from public.claims;
select count(*) from public.sources;
select count(*) from public.claim_evidence;
select count(*) from public.claim_edges;
select count(*) from kb_stage.kb_proposals;
rollback;
```
Do not submit a raw password, create a user, change IAM, enable a flag, open
browser SSH that creates a key, or mutate infrastructure under this read-only
skill.
## Exact Current Gate
Database parity is unproven because Studio requires a database credential and
did not expose an enabled existing IAM principal. The clear CTA is:
```text
Authenticate Cloud SQL Studio with an already-existing authorized database
principal without sharing the credential, then tell Codex: GCP Studio authenticated.
```
Otherwise route IAM/database-user enablement to a separately authorized write
window. Once authenticated, run only the read-only transaction above, retain
sanitized results, and close the agent-created tab.
## Claim Ceiling
Current proof covers the GCP project, VM, Cloud SQL instance, health, version,
private routing surface, and bounded serial unit activity. It does not prove
current gateway PID/restarts, deployed SHA, profile, model/auth, Telegram bot
identity, selected canonical DB, DB contents, VPS-vs-GCP schema/row parity,
safe no-post Cory answers, or cutover.

View file

@ -0,0 +1,70 @@
---
name: teleo-infra-provenance
description: Use when tracing Teleo repo/deploy/runtime provenance, preventing path confusion between GitHub, local checkouts, VPS mirrors, live service cwd, and generated report artifacts.
---
# Teleo Infrastructure Provenance
## Job
Prevent incorrect assumptions about where Leo/Teleo code, runtime state, DB state, and evidence live.
## Trigger Phrases
- "where is Leo running from"
- "VPS provenance"
- "teleo-infrastructure repo"
- "deploy source"
- "why /opt/teleo-eval"
- "what is canonical"
## Surfaces To Separate
- GitHub/canonical repo truth.
- Local Codex workspace checkout.
- VPS live deploy/source mirror.
- Hermes/leoclean runtime profile.
- Postgres canonical DB.
- `kb_stage` proposal ledger.
- Retained proof artifacts under local `outputs/`.
- Synced report artifacts under the VPS profile report directory.
## Current Known Path Map
- Repo-local evidence pack: `docs/reports/leo-working-state-20260709/`
- VPS deploy/source area: `/opt/teleo-eval/workspaces/deploy-infra`
- VPS deploy stamp: `/opt/teleo-eval/.last-deploy-sha`
- Auto-sync: `teleo-auto-deploy.timer` checks `main` every two minutes.
- VPS profile reports: `/home/teleo/.hermes/profiles/leoclean/kb_stage/reports/`
- Live service: `leoclean-gateway.service`
## Required Readbacks
Before claiming provenance:
1. `git -C <path> status --short --branch`
2. `git -C <path> rev-parse HEAD`
3. `/opt/teleo-eval/.last-deploy-sha`
4. latest `teleo-auto-deploy.service` journal decision
5. service PID, start timestamp, restart count, `WorkingDirectory`, and `User`
6. commit delta and file checksums for the runtime paths being compared
7. latest retained report artifact timestamp
## Common Failure
Do not say the repo/deploy split is the direct cause of a Leo outage unless
evidence ties it to the active failure. The split can be a
reproducibility/parity risk without being the immediate runtime cause. A commit
present in the deploy checkout or stamp does not prove a gateway restart,
runtime-profile change, permission migration, worker enablement, or DB apply.
## Output
Return:
- current repo HEADs,
- dirty state,
- live service cwd/user,
- which path owns code changes,
- which path owns runtime reports,
- exact stale/provenance risk if any.

View file

@ -0,0 +1,109 @@
---
name: teleo-kb-db-change-workflow
description: Use for Teleo canonical Postgres changes, proposal normalization, separate review and apply authority, isolated approve_claim canaries, row-level proof, rollback, and production-apply gating.
---
# Teleo KB DB Change Workflow
## Working Target
Move one reviewed knowledge change from `kb_stage.kb_proposals` to exact
canonical rows without confusing approval with application or silently changing
production.
## State Model
- `pending_review`: staged, not approved, canonical rows unchanged.
- `approved`: reviewed intent exists, but `applied_at` may still be null.
- `applied`: the guarded transaction finished and canonical postflight matches.
- `packet_ready_not_executed`: rehearsed artifacts exist; production is unchanged.
Never infer `applied` from chat text, a packet, a clone, or `status=approved`.
## Canonical Contract
The strict `approve_claim` v2 payload may create:
- `public.claims`
- `public.sources`
- `public.claim_evidence`
- `public.claim_edges`
- `public.reasoning_tools`
The lifecycle is split across:
- `scripts/kb_proposal_normalize.py`: fail-closed rich intent to strict payload.
- `scripts/approve_proposal.py`: `kb_review` login approves the exact type/payload.
- `scripts/apply_proposal.py`: operator-only `kb_apply` login writes and verifies.
- `scripts/kb_apply_prereqs.sql`: roles, immutable approval row, gate functions,
ownership, indexes, and exact ACL matrix.
- `scripts/run_approve_claim_isolated_container_canary.sh`: disposable runtime
proof plus read-only live count/service endpoints.
## Authority Invariants
1. `kb_review` and `kb_apply` are separate `NOINHERIT` login roles with no role
memberships.
2. `kb_gate_owner` is `NOLOGIN` and owns the immutable approval table and three
`SECURITY DEFINER` gate functions.
3. The legacy three-argument approval overload must be absent.
4. Review binds DB role, reviewer principal, type, full payload, timestamp, and
note in one immutable row.
5. Apply locks and compares that snapshot before writes, verifies every
payload-controlled field and exact table deltas, then finishes the ledger.
6. Existing semantic evidence/edge rows must match approved weights and owners;
a mismatch rolls back.
7. `kb_apply` remains a trusted operator-only canonical writer. This design does
not claim resistance to a compromised apply credential.
## Operator Path
1. Read the proposal and canonical target rows.
2. Normalize rich intent; reject malformed, duplicate, lossy, or unsupported
candidates instead of guessing.
3. Run focused tests:
```bash
.venv/bin/python -m pytest -q \
tests/test_kb_proposal_normalize.py \
tests/test_approve_proposal.py \
tests/test_apply_proposal.py \
tests/test_kb_apply_prereqs.py
```
4. Run both disposable canaries from the repo root:
```bash
scripts/run_approve_claim_isolated_container_canary.sh \
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
scripts/run_approve_claim_isolated_container_canary.sh \
--normalization-json docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json \
--output docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json
```
5. Require `status=pass`, every check true, expected deltas equal measured
deltas, source hashes matching current files, zero live count deltas, stable
service endpoints, and independent clone/container/workdir cleanup.
6. Treat production deployment, permission migration, worker enablement, and a
production proposal apply as separate authorization windows.
## Current Proof And Claim Ceiling
The retained generic and Helmer v3 receipts each pass `37/37`. Generic proves
`2/2/2/1/1`; Helmer proves `5/13/17/7/1`. Live read-only endpoint counts stayed
`1837/4145/4670/4916/17/26`, and the gateway stayed on PID `2999690` with zero
restarts during those runs.
This proves current-source behavior in disposable PostgreSQL and unchanged live
count/service endpoints. PR #72 source later auto-synchronized to the VPS
checkout without a gateway restart or profile-source delta; the production
permission migration was not applied, the apply worker remained disabled and
inactive, and Helmer remained unapplied. Checkout synchronization alone is not
proof of any of those later state changes. It does not prove byte-for-byte live
content.
Read `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
and both JSON receipts before making a current claim.
Read `docs/reports/leo-working-state-20260709/pr72-vps-auto-deploy-runtime-nonchange-current.md`
before describing the VPS deployment state.

View file

@ -0,0 +1,76 @@
---
name: teleo-leo-onboarding
description: Use when a worker needs fast context on Teleo/Living IP, Leo, the product architecture, current VPS/GCP split, and the July 9 working-state evidence before doing Leo, Teleo, KB, Telegram, VPS, or GCP work.
---
# Teleo / Leo Onboarding
## Job
Orient the worker before action. Build a current, proof-linked understanding of the company/product, Leo's role, the infrastructure surfaces, and the exact claim ceiling.
## Trigger Phrases
- "onboard to Leo"
- "what is Teleo / Living IP"
- "load Leo VPS context"
- "Fable handoff for Leo"
- "before working on Teleo infra"
- "explain the architecture"
## Core Model
- Teleo is the knowledge/agent infrastructure layer behind Leo.
- Leo is the operator-facing agent expected to answer in Telegram, remember operator context, reason from canonical KB state, stage concrete KB changes, and support approved changes becoming canonical DB rows with proof.
- The immediate July 9 issue is not generic bot liveness. It is Cory/m3taversal's expectation that approved KB changes move beyond proposal state when appropriate.
- The VPS is the currently proven Telegram-visible Leo surface.
- GCP is a separate lane. Project, VM, private Cloud SQL inventory, and bounded
serial unit activity are current. Current gateway PID/SHA/model/auth,
canonical DB rows, Cory answers, and cutover are not proven.
## Read First
From the repo root, read:
1. `docs/reports/leo-working-state-20260709/current-truth-index.md`
2. `docs/reports/leo-working-state-20260709/operator-surface-map.md`
3. `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`
4. `docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md`
5. `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
6. `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
7. `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
8. `docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.md`
## Required Status Split
Every status answer must split:
- `VPS runtime`
- `Telegram-visible Leo`
- `KB proposal/staging`
- `Canonical DB apply`
- `GCP parity`
- `Runtime/code provenance`
Do not collapse GCP demo readiness into Telegram completion. Do not collapse proposal approval into canonical DB application.
## Hard Boundaries
- Do not do paid route work or introduce paid-route naming.
- Do not change live VPS Leo runtime behavior unless explicitly authorized.
- Do not production-apply DB packets unless explicitly authorized.
- Do not expose secret contents.
- Do not treat an old summary as current if a fresh readback is cheap.
- Do not call GCP blocked on CLI login when authenticated Chrome can answer the
control-plane question. Conversely, do not submit a raw DB password or create
IAM/users under a read-only lane.
## Output Format
For onboarding handoffs, return:
1. Product/architecture summary in 8 bullets or fewer.
2. Current proof split by VPS, Telegram, DB, GCP.
3. Evidence files actually read.
4. Exact claim ceiling.
5. Next runnable non-production action.

View file

@ -0,0 +1,54 @@
---
name: teleo-proof-handoff
description: Use to package Leo/Teleo evidence and prompts for Fable, Codex, Opus, or future workers so they can continue without rereading the whole thread.
---
# Teleo Proof Handoff
## Job
Create a compact, evidence-indexed handoff that preserves current truth, claim ceilings, exact blockers, and next executable actions.
## Trigger Phrases
- "handoff to Fable"
- "worker onboarding"
- "pack the evidence"
- "resume Leo work"
- "create continuation prompt"
- "what should the next agent know"
## Required Sections
1. `current_canary`: the next runnable action and expected result.
2. `claim_ceiling`: what cannot be claimed yet.
3. `proven`: exact proof bullets with paths.
4. `not_proven`: exact remaining gaps.
5. `do_not_do`: scope boundaries.
6. `files_to_read_first`: no more than 10 files.
7. `commands_to_rerun`: freshness checks.
8. `next_action`: one non-production action to start now.
## Blocker Rule
If a worker claims blocked, require:
- `current_canary`
- `attempted_routes`
- `exact_gate`
- `clear_CTA`
- `next_non_user_action`
Do not accept vague blockers such as "login blocked", "human-origin proof needed", or "no access" without the exact app/account/route and next CTA.
## Fable Prompt
Use `docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md` as the default first prompt.
## Evidence Index
Use `docs/reports/leo-working-state-20260709/current-truth-index.md`.
## Handoff Claim Ceiling
A handoff is not completion. It is complete only if a future worker can start from retained files and run the named canary without asking for context.

View file

@ -0,0 +1,226 @@
---
name: teleo-vps-runtime-ops
description: Use for Leo VPS navigation, service health, Docker/Postgres readbacks, clone DB rehearsals, report sync, and runtime stability verification without changing live Leo behavior.
---
# Teleo VPS Runtime Ops
## Job
Navigate and verify the VPS safely, with exact readbacks and no surprise live-runtime changes.
## Trigger Phrases
- "Leo on VPS"
- "check VPS stability"
- "navigate Teleo VPS"
- "leoclean service"
- "run DB rehearsal on VPS"
- "sync reports to VPS"
## Known Surfaces
- Host: `77.42.65.182`
- SSH user: `root`
- Local key path is available; never print private key contents.
- Service: `leoclean-gateway.service`
- DB container: `teleo-pg`
- DB: `teleo`
- Last retained count endpoints: claims `1837`, sources `4145`, evidence `4670`,
edges `4916`, reasoning tools `17`, proposals `26`. Treat these as stale until
refreshed.
- Profile reports: `/home/teleo/.hermes/profiles/leoclean/kb_stage/reports/`
- Deploy/source area: `/opt/teleo-eval/workspaces/deploy-infra`
## Required Fresh Readbacks
Before claiming runtime state, read:
```bash
systemctl show leoclean-gateway.service -p ActiveState -p SubState -p MainPID -p NRestarts -p ExecMainStartTimestamp -p User -p WorkingDirectory
```
Before claiming DB state, use `docker exec teleo-pg psql -U postgres -d teleo` and query exact tables/rows.
Wrap verification queries in `begin transaction read only; ... rollback;`.
Before claiming cleanup, query disposable DBs:
```sql
select datname from pg_database where datname like 'teleo%rehearsal%20260709' or datname like 'teleo%packet%20260709';
```
## Auto-Deploy Semantics
`teleo-auto-deploy.timer` checks `main` every two minutes. A changed checkout
HEAD or `.last-deploy-sha` does not by itself prove that Leo restarted or that
canonical data changed. For each synchronized commit, also read:
1. `journalctl -u teleo-auto-deploy.service` for the exact deploy decision;
2. the commit delta under `hermes-agent/leoclean-bin/` and
`hermes-agent/leoclean-skills/vps/`;
3. gateway PID, start timestamp, and restart count;
4. `teleo-kb-apply-worker.service` enablement and active state;
5. canonical count endpoints in a read-only transaction.
Treat source checkout sync, runtime profile sync, service restart, permission
migration, worker enablement, and canonical DB apply as separate state changes.
## Mutating Boundaries
Allowed when needed:
- read-only service/DB inspection,
- disposable clone DB creation and drop for rehearsal,
- SQL rollback transactions,
- syncing report artifacts to the report directory,
- no-secret file checks.
Not allowed from this skill alone:
- restarting or changing the live Leo service,
- changing live runtime config,
- production DB commit/apply,
- exposing secret contents,
- deleting non-disposable data.
## Rehearsal Rules
For DB rehearsals:
1. Capture production preflight counts.
2. Create a disposable DB from live state.
3. Run commit SQL only in the disposable DB.
4. Run postflight counts.
5. Run delete rollback if applicable.
6. Drop the disposable DB.
7. Verify production counts stayed unchanged.
8. Verify the disposable DB no longer exists.
For the guarded claim-bundle lifecycle, prefer the retained isolated wrapper:
```bash
scripts/run_approve_claim_isolated_container_canary.sh \
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
```
It must use a disposable unexposed PostgreSQL container, bind its receipt to
current source hashes, compare exact payload projections and table deltas, read
live count/service endpoints without writing them, and independently prove the
container/workdir are absent afterward. Do not install the candidate code into
the live Leo deploy merely to run this canary.
To prove that Leo itself can inspect one lifecycle state through the real
`GatewayRunner` while remaining bound to a disposable full-data clone, run the
checkpoint on the VPS as `teleo` while that clone exists:
```bash
sudo -u teleo install -d -m 700 /home/teleo/leo-checkpoint-reports
sudo -u teleo HOME=/home/teleo \
/home/teleo/.hermes/hermes-agent/venv/bin/python \
scripts/run_leo_clone_bound_handler_checkpoint.py \
--container <disposable-container> \
--db teleo \
--prompt-id <stable-id> \
--prompt "<Cory-style KB question>" \
--expected-state approved \
--copy-model-auth \
--output /home/teleo/leo-checkpoint-reports/leo-clone-bound-checkpoint.json
```
The supplied clone must have Docker label
`com.livingip.leo.checkpoint=disposable`, network mode `none`, no published
ports, no mount source shared with production, and a PostgreSQL system
identifier distinct from production. The harness resolves the name once, pins
all operations to the full container ID, and fails if the name is rebound.
`--copy-model-auth` copies only `auth.json` into a private UUID-scoped profile
and binds only the configured provider's env-backed credential in memory. The
receipt records provider/variable names, never credential values or hashes. The
gateway runs in a dedicated process group with no delivery adapters and only
`skills_list`, `skill_view`, and a terminal handler restricted to the temporary
clone-bound wrapper's read-only verbs. The terminal subprocess must not inherit
the model credential. A timeout must terminate the child process group before
the profile is removed.
The checkpoint must observe at least one successful `teleo-kb` call bound to
the supplied container/database, remove the profile, and prove production row
counts plus row-content fingerprints, gateway state, and live bridge hashes are
unchanged. Every model tool call must be in the allowlist and every KB call must
complete successfully for a clean pass. A correct answer after a failed call
and retry is recovered behavior, not a clean reliability pass. The checkpoint
does not review, approve, apply, restart, post to Telegram, or mutate
production.
After that read-only checkpoint is clean, prove the lower-level
pending-to-canonical lifecycle in the same kind of disposable clone:
```bash
sudo -u teleo HOME=/home/teleo \
/home/teleo/.hermes/hermes-agent/venv/bin/python \
scripts/run_leo_clone_lifecycle_checkpoint.py \
--container <disposable-container> \
--db teleo \
--copy-model-auth \
--operator-review \
--guarded-apply \
--output /home/teleo/leo-checkpoint-reports/leo-clone-lifecycle-checkpoint.json
```
This command gives Leo only one deterministic staging verb. The harness, not
Leo, owns review and guarded apply. A pass requires exact structured receipts
for conversation memory, pending, approved-but-unapplied, and applied state;
exact linked claim/source/evidence rows; an isolated handler reopen using the
same persisted session; zero rejected or nonzero terminal calls; and unchanged
production DB fingerprints, service state, and live bridge hashes. It is a
mutation primitive, not proof that Leo can extract knowledge from arbitrary
documents or tweets.
For the real source-composition checkpoint, first create a fresh full-data
clone with the same disposable label/network/mount rules, then install the gate
and separated ephemeral clone credentials into a private run directory:
```bash
sudo -u teleo HOME=/home/teleo \
/home/teleo/.hermes/hermes-agent/venv/bin/python \
scripts/bootstrap_clone_kb_gate.py \
--container <fresh-disposable-container> \
--db teleo \
--output-dir <private-run-directory>
```
The bootstrap must verify both `kb_review` and `kb_apply` logins and emit only
credential file paths, never secret values or hashes. Then run:
```bash
sudo -u teleo HOME=/home/teleo \
/home/teleo/.hermes/hermes-agent/venv/bin/python \
scripts/run_leo_clone_composition_checkpoint.py \
--container <fresh-disposable-container> \
--db teleo \
--copy-model-auth \
--operator-review \
--guarded-apply \
--review-secrets-file <private-run-directory>/kb-review.env \
--apply-secrets-file <private-run-directory>/kb-apply.env \
--output <private-run-directory>/reports/composition-current.json
```
A pass requires model-driven dedupe search and extraction from supplied source
bytes, exact source hashes and excerpts, linked claims/evidence/conflict edges,
no row change before staging, deterministic normalization, separated review and
apply, exact clone deltas, a new handler process reasoning over exact canonical
rows without supplied IDs, session-marker recall, nonce-bound tool receipts,
unchanged production DB/service/bridge state, and complete clone/profile/secret
cleanup. The retained current receipt is
`docs/reports/leo-working-state-20260709/leo-source-composition-clone-checkpoint-current.json`.
It is not Telegram-visible proof and does not authorize production apply.
## Blocker Format
Do not say "blocked" without:
- `current_canary`
- `attempted_routes`
- `exact_gate`
- `clear_CTA`
- `next_non_user_action`

View file

@ -0,0 +1,137 @@
---
name: working-leo-cory-outcomes
description: "Use when defining, testing, or repairing Leo against Cory/m3taversal's expected outcomes: Telegram memory, critical reasoning, canonical KB truth, proposed-vs-approved-vs-applied state, and guarded DB manipulation."
---
# Working Leo / Cory Outcomes
## Job
Keep Leo work anchored to what Cory/m3taversal appears to mean by "working": not just answers, but remembered context, truthful KB state, and approved concrete changes becoming canonical rows through a guarded proof path.
## Trigger Phrases
- "working Leo"
- "Cory expected outcomes"
- "m3taversal says Leo is broken"
- "able to manipulate the knowledge base"
- "same state as last night"
- "critical reasoning behavior"
## Definition Of Working
A working Leo is a Telegram-facing agent that:
1. Remembers the current operator conversation.
2. Grounds KB answers in canonical Teleo Postgres rows when claiming KB truth.
3. Distinguishes `proposed`, `pending_review`, `approved`, `applied`, and `not applied`.
4. Does not say an approval changed canonical DB state when only `kb_stage` changed.
5. Can answer vague, high-level Cory-style incident prompts without being spoon-fed exact proposal IDs.
6. Can compose the KB from a previously unindexed document, URL, or tweet-like
source: retain a byte/hash-bound source locator, extract atomic claims and
exact evidence excerpts, preserve useful metadata, and link every claim to
its evidence and source.
7. Detects duplicates, conflicts, updates, and insufficient evidence before
staging; uncertainty must remain visible instead of being normalized into a
stronger claim.
8. Can stage those concrete KB changes from Telegram with enough structure for
human review, without making staged content canonical.
9. Can move approved concrete changes through a guarded apply path when authorized.
10. Retains Cory's caveats and review notes in source/evidence/proposal rows.
11. Reasons over claims, evidence, sources, edges, and open conflicts as a graph,
and can explain which rows support or weaken an answer without being given IDs.
12. Rebuilds any compiled identity/workspace artifact deterministically from
canonical DB rows, reports the source rows and freshness boundary, and does
not treat edits to a generated artifact as canonical knowledge.
13. Produces before/after table-level proof and service stability readback.
14. Survives an intentional `leoclean-gateway.service` restart: active before, active after, canonical KB counts unchanged, and a no-post handler smoke still answers.
15. For a reviewed graph bundle, produces exact payload-controlled row
projections, exact table deltas, source-byte binding, and cleanup proof in a
disposable runtime before any production permission or apply window.
## Current Benchmark Cases
- Marker memory: `WL-LIVE-TG-CORY-20260709`.
- Truth correction: `approved != applied`.
- Applied canary: proposal `00957f6c-9883-4015-95a4-6b09367efb0e` and edge `c167933e-d513-4f43-9335-d5d8aeb259f2`.
- Staged write canary: proposal `8dfedb3f-3aa4-4200-970f-4c0016f6869f`, status `pending_review`, with no new public canonical rows after the test timestamp.
- Open-ended triage canary: `WL-LIVE-TG-CORY-OPEN-20260709`, where Leo inferred the likely failure mode from "agents not working / same state as last night" and explained the proposed, approved, and applied split without exact IDs.
- Old rich proposal packets: `14fa5ecc...`, `ac036c9d...`, and `a64df080...` are not to be silently production-applied.
- Guarded apply proof: both generic and real Helmer v3 receipts pass `37/37` in
disposable PostgreSQL; production Helmer remains unapplied.
- Clone-bound handler proof: the real VPS `GatewayRunner` can inspect the full
current-data disposable clone, discover the Helmer proposal, distinguish
`approved` from `applied`, and produce bound bridge-call evidence without a
Telegram post or production change. The proof surface must expose no delivery
adapters or send tool, restrict terminal execution to clone-bound read-only KB
verbs, kill the handler process group on timeout, and compare production
row-content fingerprints before and after. Treat open-ended latency and
retries as a separate reliability dimension; a correct answer after a failed
tool call is recovered behavior, not a clean pass, and one correct answer does
not establish a stable pass rate.
- Source-composition proof: the real VPS `GatewayRunner` passed `34/34` in a
fresh no-send full-data clone. It searched existing knowledge, extracted two
hash-bound conflicting claims from a new document and post, staged a strict
proposal, preserved an immutable separated approval, applied exact canonical
rows, reopened in a new child process, recalled the prior marker, discovered
the new proposal/claims without supplied IDs, read exact evidence/source UUIDs
and edges, and explained `approved != applied`. Production DB fingerprints,
service PID/restarts, and live bridge hashes stayed unchanged; all disposable
containers, volumes, profiles, credentials, and run directories were removed.
This is clone proof, not Telegram delivery or production apply proof.
## Cory Direct Questions
For vague or no-context questions, answer directly and then name the one action
that would change the proof:
- "Did the DB change?": split applied rows, approved-but-unapplied proposals,
pending rows, and canceled rows. Say `Approved is not the same as applied`.
- "Is Helmer in Leo?": say `no, not canonical` until the exact claims, sources,
evidence, edges, reasoning tool, and applied ledger read back from production.
- "Did the decision matrix approve it?": verify matrix tables first; reviewer
approval is not a matrix vote.
- "Are document pointers the blocker?": answer `not just pointer mismatch` and
separate files, source refs, canonical source rows, review, and apply.
- "Can I demo KB mutation?": split staging-demo truth from canonical-apply truth.
- "Did editing SOUL.md change identity?": no canonical identity change without
row IDs plus render/sync postflight.
End no-context answers with exactly one `Next Cory-style follow-up:` line.
## Required Answer Discipline
When Leo or a worker answers about KB state, it must say:
- What source was used: memory, Telegram history, `kb_stage`, canonical `public.*`, or filesystem/runtime.
- Which rows/tables changed.
- Which proposal IDs are only staged/approved.
- Which canonical rows exist.
- What remains pending or deferred.
## Not Done
Leo is not working just because:
- the systemd service is active,
- Bot API calls work,
- a local handler canary passed,
- a proposal was approved,
- a packet was generated,
- a clone rehearsal passed.
- `NRestarts=0` was observed without an intentional restart-cycle proof.
Those are evidence. The target is Telegram-visible behavior plus DB-state truth plus a guarded path to canonical rows.
A synthetic one-claim proposal proves only the guarded mutation primitive. It
does not prove KB composition. Composition requires a source that was absent at
baseline, model-driven extraction into linked source/evidence/claim proposals,
dedupe/conflict readback, guarded canonical apply, and an open-ended answer
grounded in the new rows after an isolated handler restart.
## Proof Files
Use `docs/reports/leo-working-state-20260709/current-truth-index.md` for current artifact paths.
Use `docs/reports/leo-working-state-20260709/leo-source-composition-clone-checkpoint-current.md`
and its JSON companion for the current source-composition claim ceiling.
For restart survival, use `scripts/collect_leo_restart_survival_proof.py --execute-restart`; expected artifacts are `docs/reports/leo-working-state-20260709/leo-restart-survival-proof-current.json` and `.md`.

187
.crabbox.yaml Normal file
View file

@ -0,0 +1,187 @@
profile: teleo-infrastructure-check
provider: hetzner
target: linux
architecture: arm64
class: beast
ttl: 90m
idleTimeout: 20m
capacity:
market: spot
strategy: most-available
fallback: on-demand-after-120s
actions:
workflow: .github/workflows/crabbox.yml
job: hydrate
runnerLabels:
- crabbox
runnerVersion: latest
ephemeral: true
sync:
delete: true
checksum: false
gitSeed: true
fingerprint: true
timeout: 15m
warnFiles: 50000
warnBytes: 5368709120
failFiles: 150000
failBytes: 21474836480
exclude:
- .cache
- .venv
- .pytest_cache
- .ruff_cache
- __pycache__
- "*.pyc"
- "*.db"
- "*.db-wal"
- "*.db-shm"
- "*.log"
- logs
- secrets
- .env
- htmlcov
- dist
- build
- "*.egg-info"
- .turbo
- node_modules
env:
allow:
- CI
- PYTHONWARNINGS
- PHASE1B_AGENT_ROUTING_ENABLED
ssh:
user: crabbox
port: "2222"
# Ordered fallback ports tried after ssh.port; use [] to disable fallback.
fallbackPorts:
- "22"
jobs:
ci-contract:
provider: hetzner
target: linux
architecture: arm64
class: beast
hydrate:
actions: true
githubRunner: false
waitTimeout: 20m
keepAliveMinutes: 90
actions:
workflow: .github/workflows/crabbox.yml
job: hydrate
shell: true
command: >
python3 -m pip install -e '.[dev]' &&
mkdir -p .crabbox-results &&
python3 scripts/check_crabbox_ci_contract.py
--output .crabbox-results/crabbox-ci-contract.json &&
python3 scripts/check_llm_refinement_contract.py
--output .crabbox-results/llm-refinement-contract.json &&
python3 scripts/replay_decision_engine_eval.py
--output .crabbox-results/decision-engine-eval.json
downloads:
- .crabbox-results/crabbox-ci-contract.json
- .crabbox-results/llm-refinement-contract.json
- .crabbox-results/decision-engine-eval.json
stop: always
unit:
provider: hetzner
target: linux
architecture: arm64
class: beast
hydrate:
actions: true
githubRunner: false
waitTimeout: 20m
keepAliveMinutes: 90
actions:
workflow: .github/workflows/crabbox.yml
job: hydrate
shell: true
command: >
python3 -m pip install -e '.[dev]' &&
mkdir -p .crabbox-results &&
python3 -m pytest --junitxml=.crabbox-results/pytest.xml
junit:
- .crabbox-results/pytest.xml
downloads:
- .crabbox-results/pytest.xml
stop: always
lint-phase1b:
provider: hetzner
target: linux
architecture: arm64
class: beast
hydrate:
actions: true
githubRunner: false
waitTimeout: 20m
keepAliveMinutes: 90
actions:
workflow: .github/workflows/crabbox.yml
job: hydrate
shell: true
command: >
python3 -m pip install -e '.[dev]' &&
python3 -m ruff check
lib/agent_routing.py
lib/config.py
lib/db.py
lib/evaluate.py
lib/llm.py
lib/post_extract.py
telegram/approvals.py
scripts/prove_phase1b_local.py
tests/test_agent_routing.py
tests/test_evaluate_agent_routing.py
tests/test_phase1b_end_to_end.py
tests/test_eval_parse.py
tests/test_contributor.py
tests/test_search.py
stop: always
phase1b-local-proof:
provider: hetzner
target: linux
architecture: arm64
class: beast
hydrate:
actions: true
githubRunner: false
waitTimeout: 20m
keepAliveMinutes: 90
actions:
workflow: .github/workflows/crabbox.yml
job: hydrate
shell: true
command: >
python3 -m pip install -e '.[dev]' &&
scripts/crabbox_phase1b_proof.sh
junit:
- .crabbox-results/phase1b-pytest.xml
downloads:
- .crabbox-results/crabbox-ci-contract.json
- proof/phase1b-local-e2e-proof.json
- .crabbox-results/phase1b-pytest.xml
- .crabbox-results/phase1b-proof-summary.json
stop: always
sync-smoke:
provider: hetzner
target: linux
architecture: arm64
class: beast
hydrate:
actions: false
shell: true
command: >
python3 -m compileall
lib
tests
scripts/prove_phase1b_local.py
stop: always

21
.dockerignore Normal file
View file

@ -0,0 +1,21 @@
.git
.venv
__pycache__
*.pyc
.pytest_cache
.ruff_cache
.mypy_cache
.env
*.env
secrets/
telegram-archives/
transcripts/
logs/
pipeline.db
pipeline.db-*
*.sqlite
*.sqlite3
agentcash/
.agentcash/
.hermes/
node_modules/

173
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,173 @@
name: ci
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.11"
CI: "1"
jobs:
lint:
name: Focused lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Ruff focused surface
run: |
python -m ruff check \
lib/agent_routing.py \
lib/config.py \
lib/db.py \
lib/evaluate.py \
lib/llm.py \
lib/post_extract.py \
ops/apply_gcp_iam_split.py \
ops/check_gcp_infra_readiness.py \
ops/run_gcp_infra_execute_canary.py \
ops/apply_gcp_runtime_baseline.py \
ops/check_gcp_service_communications.py \
ops/plan_gcp_iam_split.py \
ops/redact_sqlite_postgres_restore_canary.py \
ops/sqlite_to_postgres_dump.py \
ops/verify_gcp_cloudsql_restore_readback.py \
telegram/approvals.py \
scripts/check_crabbox_ci_contract.py \
scripts/check_llm_refinement_contract.py \
scripts/replay_decision_engine_eval.py \
scripts/prove_phase1b_local.py \
tests/test_agent_routing.py \
tests/test_decision_engine_replay.py \
tests/test_evaluate_agent_routing.py \
tests/test_gcp_artifact_workflow.py \
tests/test_gcp_infra_execute_canary.py \
tests/test_gcp_infra_readiness_checker.py \
tests/test_gcp_runtime_baseline_apply.py \
tests/test_gcp_service_communications.py \
tests/test_gcp_cloudsql_restore_drill.py \
tests/test_gcp_cloudsql_restore_readback.py \
tests/test_gcp_iam_split_apply.py \
tests/test_gcp_iam_split_plan.py \
tests/test_gcp_readiness_workflow.py \
tests/test_phase1b_end_to_end.py \
tests/test_sqlite_to_postgres_dump.py \
tests/test_sqlite_postgres_restore_canary_capsule.py \
tests/test_eval_parse.py \
tests/test_contributor.py \
tests/test_search.py
- name: Shell syntax
run: |
bash -n \
ops/backup_vps_sqlite_kb.sh \
ops/run_gcp_cloudsql_restore_drill.sh \
ops/run_sqlite_postgres_restore_canary.sh
test:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Pytest
run: |
mkdir -p .crabbox-results
python -m pytest --junitxml=.crabbox-results/pytest.xml
- name: Upload test artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: teleo-infrastructure-pytest
path: .crabbox-results/pytest.xml
if-no-files-found: warn
repo-contracts:
name: Repo contracts
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Validate repo-owned contract
run: |
python scripts/check_crabbox_ci_contract.py \
--output .crabbox-results/crabbox-ci-contract.json
python scripts/check_llm_refinement_contract.py \
--output .crabbox-results/llm-refinement-contract.json
python scripts/replay_decision_engine_eval.py \
--output .crabbox-results/decision-engine-eval.json
- name: Upload contract artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: teleo-infrastructure-repo-contracts
path: |
.crabbox-results/crabbox-ci-contract.json
.crabbox-results/llm-refinement-contract.json
.crabbox-results/decision-engine-eval.json
if-no-files-found: error
phase1b-local-proof:
name: Phase 1B local proof
runs-on: ubuntu-latest
needs:
- lint
- test
- repo-contracts
timeout-minutes: 20
env:
PHASE1B_AGENT_ROUTING_ENABLED: "true"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run proof wrapper
run: |
scripts/crabbox_phase1b_proof.sh
- name: Upload proof artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: teleo-infrastructure-phase1b-proof
path: |
.crabbox-results/crabbox-ci-contract.json
proof/phase1b-local-e2e-proof.json
.crabbox-results/phase1b-pytest.xml
.crabbox-results/phase1b-proof-summary.json
if-no-files-found: warn

101
.github/workflows/crabbox.yml vendored Normal file
View file

@ -0,0 +1,101 @@
name: crabbox
on:
workflow_dispatch:
inputs:
ref:
description: "Git ref to hydrate"
required: false
type: string
crabbox_id:
description: "Crabbox lease ID"
required: true
type: string
crabbox_runner_label:
description: "Dynamic Crabbox runner label"
required: true
type: string
crabbox_job:
description: "Hydration job identifier expected by Crabbox"
required: false
default: "hydrate"
type: string
crabbox_keep_alive_minutes:
description: "Minutes to keep the hydrated job alive"
required: false
default: "90"
type: string
permissions:
contents: read
jobs:
hydrate:
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Hydrate
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
if [ -f package-lock.json ]; then npm ci; fi
if [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --frozen-lockfile; fi
if [ -f go.mod ]; then go mod download; fi
- name: Mark Crabbox ready
shell: bash
run: |
job="${{ inputs.crabbox_job }}"
if [ -z "$job" ]; then job=hydrate; fi
mkdir -p "$HOME/.crabbox/actions"
state="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.env"
env_file="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.env.sh"
services_file="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.services"
write_export() {
key="$1"
value="${!key-}"
if [ -n "$value" ]; then
printf 'export %s=%q\n' "$key" "$value"
fi
}
{
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR GITHUB_JOB RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE; do
write_export "$key"
done
} > "${env_file}.tmp"
mv "${env_file}.tmp" "$env_file"
{
echo "# Docker containers visible from the hydrated runner"
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}' 2>/dev/null || true
} > "${services_file}.tmp"
mv "${services_file}.tmp" "$services_file"
tmp="${state}.tmp"
{
echo "WORKSPACE=${GITHUB_WORKSPACE}"
echo "RUN_ID=${GITHUB_RUN_ID}"
echo "JOB=${job}"
echo "ENV_FILE=${env_file}"
echo "SERVICES_FILE=${services_file}"
echo "READY_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$tmp"
mv "$tmp" "$state"
- name: Keep Crabbox job alive
shell: bash
run: |
minutes="${{ inputs.crabbox_keep_alive_minutes }}"
case "$minutes" in
''|*[!0-9]*) minutes=90 ;;
esac
stop="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.stop"
deadline=$(( $(date +%s) + minutes * 60 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
if [ -f "$stop" ]; then
exit 0
fi
sleep 15
done

86
.github/workflows/gcp-artifact.yml vendored Normal file
View file

@ -0,0 +1,86 @@
name: gcp-artifact
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
id-token: write
concurrency:
group: gcp-artifact-${{ github.ref }}
cancel-in-progress: true
env:
PROJECT_ID: teleo-501523
REGION: europe-west6
ARTIFACT_REPOSITORY: teleo
IMAGE_NAME: teleo-pipeline-gcp-staging
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
ARTIFACT_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com
jobs:
build-smoke-push:
name: Build, smoke-run, and push Docker image
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.ARTIFACT_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- name: Configure Artifact Registry Docker auth
run: |
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
- name: Build, smoke-run, and push
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_SHA::7}"
image_uri="${REGION}-docker.pkg.dev/${PROJECT_ID}/${ARTIFACT_REPOSITORY}/${IMAGE_NAME}:${tag}"
image_digest="$(gcloud artifacts docker images describe "${image_uri}" --format='value(image_summary.digest)' 2>/dev/null || true)"
if [[ -n "${image_digest}" ]]; then
echo "Image tag already exists; reusing immutable Artifact Registry image."
docker pull "${image_uri}@${image_digest}"
docker run --rm "${image_uri}@${image_digest}"
else
docker build \
-f Dockerfile.gcp-staging \
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "livingip.revision=${GITHUB_SHA}" \
--label "livingip.surface=teleo-infrastructure" \
--label "livingip.tier=gcp-staging" \
-t "${image_uri}" \
.
docker run --rm "${image_uri}"
docker push "${image_uri}" | tee docker-push.log
image_digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)"
fi
test -n "${image_digest}"
{
echo "image_uri=${image_uri}"
echo "image_digest=${image_digest}"
echo "image_ref=${image_uri}@${image_digest}"
echo "revision=${GITHUB_SHA}"
} > gcp-artifact-image.txt
- name: Upload image receipt
uses: actions/upload-artifact@v4
with:
name: gcp-artifact-image
path: gcp-artifact-image.txt
if-no-files-found: error

132
.github/workflows/gcp-readiness.yml vendored Normal file
View file

@ -0,0 +1,132 @@
name: gcp-readiness
on:
workflow_dispatch:
inputs:
service_account:
description: GCP service account to impersonate for the read-only readiness probe
required: false
default: sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com
restore_canary_capsule_b64:
description: Optional base64-encoded non-secret SQLite-to-Postgres restore canary capsule
required: false
default: ""
permissions:
contents: read
id-token: write
concurrency:
group: gcp-readiness-${{ github.ref }}
cancel-in-progress: true
env:
PROJECT_ID: teleo-501523
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
READINESS_SERVICE_ACCOUNT: ${{ inputs.service_account || 'sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com' }}
RESTORE_CANARY_CAPSULE_B64: ${{ inputs.restore_canary_capsule_b64 || '' }}
READINESS_ARTIFACT_DIR: gcp-readiness-artifacts
jobs:
readiness:
name: Read-only GCP readiness probe
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.READINESS_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- name: Install optional restore canary capsule
if: ${{ env.RESTORE_CANARY_CAPSULE_B64 != '' }}
shell: bash
run: |
set -euo pipefail
mkdir -p "${READINESS_ARTIFACT_DIR}"
capsule_path="${READINESS_ARTIFACT_DIR}/restore-canary-capsule.json"
printf '%s' "${RESTORE_CANARY_CAPSULE_B64}" | base64 --decode > "${capsule_path}"
python3 -m json.tool "${capsule_path}" >/dev/null
echo "TELEO_GCP_RESTORE_CANARY_CAPSULE=${capsule_path}" >> "${GITHUB_ENV}"
- name: Capture gcloud identity
shell: bash
run: |
set +e
mkdir -p "${READINESS_ARTIFACT_DIR}"
{
echo "project=${PROJECT_ID}"
echo "service_account=${READINESS_SERVICE_ACCOUNT}"
echo "workflow_ref=${GITHUB_REF}"
echo "revision=${GITHUB_SHA}"
} > "${READINESS_ARTIFACT_DIR}/context.txt"
gcloud auth list --format=json > "${READINESS_ARTIFACT_DIR}/gcloud-auth-list.json" 2> "${READINESS_ARTIFACT_DIR}/gcloud-auth-list.stderr"
echo "$?" > "${READINESS_ARTIFACT_DIR}/gcloud-auth-list.exitcode"
gcloud config list --format=json > "${READINESS_ARTIFACT_DIR}/gcloud-config-list.json" 2> "${READINESS_ARTIFACT_DIR}/gcloud-config-list.stderr"
echo "$?" > "${READINESS_ARTIFACT_DIR}/gcloud-config-list.exitcode"
- name: Run readiness checker
shell: bash
run: |
set +e
mkdir -p "${READINESS_ARTIFACT_DIR}"
python3 ops/check_gcp_infra_readiness.py \
> "${READINESS_ARTIFACT_DIR}/gcp-infra-readiness.json" \
2> "${READINESS_ARTIFACT_DIR}/gcp-infra-readiness.stderr"
echo "$?" > "${READINESS_ARTIFACT_DIR}/gcp-infra-readiness.exitcode"
python3 - <<'PY'
import os
import json
from pathlib import Path
base = Path(os.environ["READINESS_ARTIFACT_DIR"])
report = {
"artifact": "github_wif_gcp_readiness_probe",
"checker_exitcode": (base / "gcp-infra-readiness.exitcode").read_text().strip(),
"stderr_tail": (base / "gcp-infra-readiness.stderr").read_text()[-2000:],
}
try:
payload = json.loads((base / "gcp-infra-readiness.json").read_text() or "{}")
except json.JSONDecodeError as exc:
report["json_parse_error"] = str(exc)
payload = {}
if payload:
report["pass_count"] = payload.get("pass_count")
report["blocked_count"] = payload.get("blocked_count")
report["fail_count"] = payload.get("fail_count")
report["checks"] = [
{
"name": check.get("name"),
"status": check.get("status"),
"required_tier": check.get("required_tier"),
"current_tier": check.get("current_tier"),
"detail": (check.get("detail") or "")[:500],
}
for check in payload.get("checks", [])
]
(base / "gcp-readiness-summary.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
print(json.dumps(report, indent=2, sort_keys=True))
PY
- name: Upload readiness artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: gcp-readiness
path: ${{ env.READINESS_ARTIFACT_DIR }}/
if-no-files-found: error
- name: Enforce readiness result
if: always()
shell: bash
run: |
code="$(cat "${READINESS_ARTIFACT_DIR}/gcp-infra-readiness.exitcode" 2>/dev/null || echo 1)"
if [ "${code}" != "0" ]; then
echo "GCP readiness checker failed with exit code ${code}. See the uploaded gcp-readiness artifact for exact failed checks."
exit "${code}"
fi

5
.gitignore vendored
View file

@ -20,6 +20,8 @@ logs/
# Test artifacts
.pytest_cache/
.crabbox/
.crabbox-results/
htmlcov/
.coverage
@ -30,3 +32,6 @@ build/
# OS
.DS_Store
# Hermes session artifacts
ops/sessions/

79
CODEOWNERS Normal file
View file

@ -0,0 +1,79 @@
# teleo-infrastructure ownership map
# Each path has ONE owning agent. Owner = accountable for correctness + reviews changes.
# Format: <pattern> <owner>
# Pipeline daemon — entry points
/teleo-pipeline.py @ship
/reweave.py @ship
# Pipeline library — shared Python package
/lib/config.py @ship
/lib/db.py @ship
/lib/connect.py @ship
/lib/log.py @ship
/lib/forgejo.py @ship
/lib/breaker.py @ship
/lib/worktree_lock.py @ship
/lib/domains.py @ship
/lib/costs.py @ship
/lib/llm.py @ship
/lib/merge.py @ship
/lib/cascade.py @ship
/lib/cross_domain.py @ship
/lib/validate.py @ship
/lib/stale_pr.py @ship
/lib/watchdog.py @ship
/lib/feedback.py @ship
/lib/fixer.py @ship
/lib/substantive_fixer.py @ship
/lib/dedup.py @ship
/lib/extract.py @epimetheus
/lib/extraction_prompt.py @epimetheus
/lib/post_extract.py @epimetheus
/lib/pre_screen.py @epimetheus
/lib/entity_batch.py @epimetheus
/lib/entity_queue.py @epimetheus
/lib/evaluate.py @leo
/lib/analytics.py @leo
/lib/attribution.py @leo
/lib/health.py @argus
/lib/search.py @argus
/lib/claim_index.py @argus
/lib/digest.py @argus
# Diagnostics — monitoring dashboard
/diagnostics/ @argus
# Telegram bot
/telegram/ @ship
# Deployment automation
/deploy/ @ship
# Systemd service definitions
/systemd/ @ship
# Agent state management
/agent-state/ @ship
# Research orchestration
/research/ @ship
# Hermes agent
/hermes-agent/ @ship
# One-off scripts and migrations
/scripts/ @ship
# Test suite
/tests/ @ganymede
# Documentation
/docs/ shared
# Config
/pyproject.toml @ship
/.gitignore @ship

30
Dockerfile.gcp-staging Normal file
View file

@ -0,0 +1,30 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIPELINE_BASE=/opt/teleo-eval
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git bash \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml README.md /app/
COPY lib /app/lib
COPY tests /app/tests
COPY scripts /app/scripts
COPY fixtures /app/fixtures
COPY schemas /app/schemas
COPY systemd /app/systemd
COPY deploy /app/deploy
COPY diagnostics /app/diagnostics
COPY telegram /app/telegram
COPY teleo-pipeline.py /app/teleo-pipeline.py
COPY docker/gcp-staging-smoke.sh /usr/local/bin/gcp-staging-smoke
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -e ".[dev]" \
&& chmod +x /usr/local/bin/gcp-staging-smoke
CMD ["/usr/local/bin/gcp-staging-smoke"]

134
README.md Normal file
View file

@ -0,0 +1,134 @@
# teleo-infrastructure
This repo runs the pipeline that processes contributions into the
[teleo-codex](https://github.com/living-ip/teleo-codex) knowledge base.
Every claim on `main` has been extracted from a source, validated for schema
and duplicates, evaluated by at least two independent reviewers, and merged
through an event-sourced audit log. The whole flow is an async Python daemon
talking to a Forgejo git server, an SQLite WAL state store, OpenRouter (for
most LLM calls), and the Anthropic Claude CLI (for Opus deep reviews).
**Production state** (live):
| Metric | Value |
|---|---|
| Claims merged into `main` | 1,546 across 13 domains |
| PRs merged through the pipeline | 1,975 |
| Merge throughput (last 7d) | 508 PRs (~73/day) |
| Review approval rate | 94% |
| Cost per merged claim (last 30d) | $0.10 incl. extract + triage + multi-tier review |
| Production agents | 6 (rio, theseus, leo, vida, astra, clay) |
## Pipeline
Concurrent stage loops in a single daemon (`teleo-pipeline.py`), coordinated
by SQLite. Circuit breakers cap costs, retry budgets cap attempts, and merges
are serialized per-domain to avoid cross-PR conflicts.
```mermaid
flowchart LR
Inbox["inbox/queue/"] --> Extract
Extract["Extract<br/>(Sonnet 4.5)"] --> Validate
Validate["Validate<br/>(tier 0, $0)"] --> Evaluate
Evaluate["Evaluate<br/>(tiered, multi-model)"] --> Merge
Merge["Merge<br/>(Forgejo, domain-serial)"] --> Effects
Effects["Effects<br/>cascade · backlinks · reciprocal edges"]
```
If any reviewer rejects, the PR gets a structured rationale and either
re-extraction guidance (for fixable issues) or a terminal close (for
scope or duplicate problems). Approved merges trigger downstream effects:
- **Cascade** — agents whose beliefs/positions depend on the changed claim get inbox notifications
- **Bidirectional provenance**`sourced_from:` is stamped on each claim at extraction; the source's `claims_extracted:` list is updated post-merge
- **Reciprocal edges** — when a new claim has `supports: [X]`, X's frontmatter is updated with `supports: [new]`
- **Cross-domain index** — entity mentions across domain boundaries are logged for silo detection
## Multi-agent review
Reviews aren't free. Tier classification is deterministic where possible
(changes to `core/` or `foundations/` always go Deep) and otherwise picked
by Haiku based on PR scope. Last 30d distribution: 76% Standard, 21% Light,
2% Deep.
```mermaid
flowchart TD
PR[New PR] --> Classify{Classify}
Classify -->|"core/, foundations/, challenged"| Deep
Classify -->|default| Standard
Classify -->|single claim, low risk| Light
Light["Light tier<br/>Domain agent only"] --> Result
Standard["Standard tier<br/>Domain agent + Leo (Sonnet 4.5)"] --> Result
Deep["Deep tier<br/>Domain agent + Leo (Opus)"] --> Result
Result{Both approve?}
Result -->|yes| MergeOK[Merge]
Result -->|no| Reject[Structured rejection<br/>+ re-extract guidance]
```
Domain agents bring domain expertise: **Rio** (internet-finance), **Vida**
(health), **Astra** (space-development), **Clay** (entertainment),
**Theseus** (ai-alignment). **Leo** brings cross-domain consistency on
every PR. Disagreement between the two reviewers surfaces in `audit_log`
and is tracked as a quality signal, not silenced.
Model diversity isn't cosmetic — same-family models share ~60% of their
errors (Kim et al. ICML 2025). Pipeline mixes Haiku for triage, Gemini 2.5
Flash for domain review, Sonnet 4.5 for Leo standard, Opus for Leo deep.
## Contributor flow
External contributors submit PRs to
[`living-ip/teleo-codex`](https://github.com/living-ip/teleo-codex) on GitHub.
A mirror sync (every 2 minutes) fast-forwards the PR onto Forgejo, where
the pipeline picks it up. From there it's the same flow as agent-authored
PRs — same tiers, same reviewers, same merge rules.
The contributor-facing guide lives in
[`teleo-codex/CONTRIBUTING.md`](https://github.com/living-ip/teleo-codex/blob/main/CONTRIBUTING.md).
## Repository layout
| Directory | What it does |
|-----------------|-----------------------------------------------------------|
| `lib/` | Pipeline modules — config, db, extract, evaluate, merge, cascade |
| `diagnostics/` | Argus monitoring dashboard (4 pages: ops, health, agents, epistemic) |
| `telegram/` | Telegram bot that answers from the knowledge base |
| `research/` | Nightly autonomous research sessions for domain agents |
| `agent-state/` | File-backed state for cross-session agent continuity |
| `deploy/` | Auto-deploy pipeline (Forgejo → working dirs → systemd) |
| `systemd/` | Service definitions for daemon + dashboard + agents |
| `scripts/` | Backfills and one-off migrations |
| `tests/` | pytest suite |
| `docs/` | Architecture specs and operational protocols |
## Ownership
Code review authority is enforced by [`CODEOWNERS`](./CODEOWNERS) — every
file has one accountable agent. The high-level map:
- **Ship** — pipeline core, telegram, deploy, agent-state, research, systemd
- **Epimetheus** — extraction (intake, entity processing, pre-screening, post-extract validation)
- **Leo** — evaluation (claim review, analytics, attribution)
- **Argus** — health (diagnostics dashboard, alerting, claim index, search)
- **Ganymede** — tests (pytest suite, integration, code review gate)
For active sprint work and per-agent in-flight items, see each agent's
status report in their Pentagon profile.
## Development
```bash
pip install -e ".[dev]"
pytest
```
## Operations
Production deployment runs on a single VPS. Runbook, restart procedures,
secret rotation, and on-call live in the private
[`teleo-ops`](https://github.com/living-ip/teleo-ops) repo (request access).
## License
[TBD]

View file

@ -1,283 +0,0 @@
#!/bin/bash
# Batch extract sources from inbox/queue/ — v3 with two-gate skip logic
#
# Uses separate extract/ worktree (not main/ — prevents daemon race condition).
# Skip logic uses two checks instead of local marker files (Ganymede v3 review):
# Gate 1: Is source already in archive/{domain}/? → already processed, dedup
# Gate 2: Does extraction branch exist on Forgejo? → extraction in progress
# Gate 3: Does pipeline.db show ≥3 closed PRs for this source? → zombie, skip
# Gate 4: Does pipeline.db show active OR recently closed PR? → skip (4h cooldown)
# All gates pass → extract
#
# Architecture: Ganymede (two-gate) + Rhea (separate worktrees)
REPO=/opt/teleo-eval/workspaces/extract
MAIN_REPO=/opt/teleo-eval/workspaces/main
EXTRACT=/opt/teleo-eval/openrouter-extract-v2.py
CLEANUP=/opt/teleo-eval/post-extract-cleanup.py
LOG=/opt/teleo-eval/logs/batch-extract-50.log
DB=/opt/teleo-eval/pipeline/pipeline.db
TOKEN=$(cat /opt/teleo-eval/secrets/forgejo-leo-token)
FORGEJO_URL="http://localhost:3000"
MAX=50
MAX_CLOSED=3 # zombie retry limit: skip source after this many closed PRs
COUNT=0
SUCCESS=0
FAILED=0
SKIPPED=0
# Lockfile to prevent concurrent runs
LOCKFILE="/tmp/batch-extract.lock"
if [ -f "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE" 2>/dev/null)
if kill -0 "$pid" 2>/dev/null; then
echo "[$(date)] SKIP: batch extract already running (pid $pid)" >> $LOG
exit 0
fi
rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
echo "[$(date)] Starting batch extraction of $MAX sources" >> $LOG
cd $REPO || exit 1
# Bug fix: don't swallow errors on critical git commands (Ganymede review)
git fetch origin main >> $LOG 2>&1 || { echo "[$(date)] FATAL: fetch origin main failed" >> $LOG; exit 1; }
git checkout -f main >> $LOG 2>&1 || { echo "[$(date)] FATAL: checkout main failed" >> $LOG; exit 1; }
git reset --hard origin/main >> $LOG 2>&1 || { echo "[$(date)] FATAL: reset --hard failed" >> $LOG; exit 1; }
# SHA canary: verify extract worktree matches origin/main (Ganymede review)
LOCAL_SHA=$(git rev-parse HEAD)
REMOTE_SHA=$(git rev-parse origin/main)
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
echo "[$(date)] FATAL: extract worktree diverged from main ($LOCAL_SHA vs $REMOTE_SHA)" >> $LOG
exit 1
fi
# Pre-extraction cleanup: remove queue files that already exist in archive
# This runs on the MAIN worktree (not extract/) so deletions are committed to git.
# Prevents the "queue duplicate reappears after reset --hard" problem.
CLEANED=0
for qfile in $MAIN_REPO/inbox/queue/*.md; do
[ -f "$qfile" ] || continue
qbase=$(basename "$qfile")
if find "$MAIN_REPO/inbox/archive" -name "$qbase" 2>/dev/null | grep -q .; then
rm -f "$qfile"
CLEANED=$((CLEANED + 1))
fi
done
if [ "$CLEANED" -gt 0 ]; then
echo "[$(date)] Cleaned $CLEANED stale queue duplicates" >> $LOG
cd $MAIN_REPO
git add -A inbox/queue/ 2>/dev/null
git commit -m "pipeline: clean $CLEANED stale queue duplicates
Pentagon-Agent: Epimetheus <3D35839A-7722-4740-B93D-51157F7D5E70>" 2>/dev/null
# Push with retry
for attempt in 1 2 3; do
git pull --rebase origin main 2>/dev/null
git push origin main 2>/dev/null && break
sleep 2
done
cd $REPO
git fetch origin main 2>/dev/null
git reset --hard origin/main 2>/dev/null
fi
# Get sources in queue
SOURCES=$(ls inbox/queue/*.md 2>/dev/null | head -$MAX)
# Batch fetch all remote branches once (Ganymede: 1 call instead of 84)
REMOTE_BRANCHES=$(git ls-remote --heads origin 2>/dev/null)
if [ $? -ne 0 ]; then
echo "[$(date)] ABORT: git ls-remote failed — remote unreachable, skipping cycle" >> $LOG
exit 0
fi
for SOURCE in $SOURCES; do
COUNT=$((COUNT + 1))
BASENAME=$(basename "$SOURCE" .md)
BRANCH="extract/$BASENAME"
# Skip conversation archives — valuable content enters through standalone sources,
# inline tags (SOURCE:/CLAIM:), and transcript review. Raw conversations produce
# low-quality claims with schema failures. (Epimetheus session 4)
if grep -q "^format: conversation" "$SOURCE" 2>/dev/null; then
# Move to archive instead of leaving in queue (prevents re-processing)
mv "$SOURCE" "$MAIN_REPO/inbox/archive/telegram/" 2>/dev/null
echo "[$(date)] [$COUNT/$MAX] ARCHIVE $BASENAME (conversation — skipped extraction)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
# Gate 1: Already in archive? Source was already processed — dedup (Ganymede)
if find "$MAIN_REPO/inbox/archive" -name "$BASENAME.md" 2>/dev/null | grep -q .; then
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (already in archive)" >> $LOG
# Delete the queue duplicate
rm -f "$MAIN_REPO/inbox/queue/$BASENAME.md" 2>/dev/null
SKIPPED=$((SKIPPED + 1))
continue
fi
# Gate 2: Branch exists on Forgejo? Extraction already in progress (cached lookup)
# Enhancement: 2-hour staleness check (Ganymede review) — if branch is >2h old
# and PR is unmergeable, close PR + delete branch and re-extract
if echo "$REMOTE_BRANCHES" | grep -q "refs/heads/$BRANCH$"; then
# Check branch age
BRANCH_SHA=$(echo "$REMOTE_BRANCHES" | grep "refs/heads/$BRANCH$" | awk '{print $1}')
BRANCH_AGE_EPOCH=$(git log -1 --format='%ct' "$BRANCH_SHA" 2>/dev/null || echo 0)
NOW_EPOCH=$(date +%s)
AGE_HOURS=$(( (NOW_EPOCH - BRANCH_AGE_EPOCH) / 3600 ))
if [ "$AGE_HOURS" -ge 2 ]; then
# Branch is stale — check if PR is mergeable
# Note: Forgejo head= filter is unreliable. Fetch all open PRs and filter locally.
PR_NUM=$(curl -sf "$FORGEJO_URL/api/v1/repos/teleo/teleo-codex/pulls?state=open&limit=50" \
-H "Authorization: token $TOKEN" | python3 -c "
import sys,json
prs=json.load(sys.stdin)
branch='$BRANCH'
matches=[p for p in prs if p['head']['ref']==branch]
print(matches[0]['number'] if matches else '')
" 2>/dev/null)
if [ -n "$PR_NUM" ]; then
PR_MERGEABLE=$(curl -sf "$FORGEJO_URL/api/v1/repos/teleo/teleo-codex/pulls/$PR_NUM" \
-H "Authorization: token $TOKEN" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("mergeable","true"))' 2>/dev/null)
if [ "$PR_MERGEABLE" = "False" ] || [ "$PR_MERGEABLE" = "false" ]; then
echo "[$(date)] [$COUNT/$MAX] STALE: $BASENAME (${AGE_HOURS}h old, unmergeable PR #$PR_NUM) — closing + re-extracting" >> $LOG
# Close PR with audit comment
curl -sf -X POST "$FORGEJO_URL/api/v1/repos/teleo/teleo-codex/issues/$PR_NUM/comments" \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"Auto-closed: extraction branch stale >2h, conflict unresolvable. Source will be re-extracted from current main."}' > /dev/null 2>&1
curl -sf -X PATCH "$FORGEJO_URL/api/v1/repos/teleo/teleo-codex/pulls/$PR_NUM" \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"state":"closed"}' > /dev/null 2>&1
# Delete remote branch
git push origin --delete "$BRANCH" 2>/dev/null
# Fall through to extraction below
else
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (branch exists ${AGE_HOURS}h, PR #$PR_NUM mergeable — waiting)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
else
# No PR found but branch exists — orphan branch, clean up
echo "[$(date)] [$COUNT/$MAX] STALE: $BASENAME (orphan branch ${AGE_HOURS}h, no PR) — deleting" >> $LOG
git push origin --delete "$BRANCH" 2>/dev/null
# Fall through to extraction
fi
else
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (branch exists — in progress, ${AGE_HOURS}h old)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
fi
# Gate 3: Check pipeline.db for zombie sources — too many closed PRs means
# the source keeps failing eval. Skip after MAX_CLOSED rejections. (Epimetheus)
if [ -f "$DB" ]; then
CLOSED_COUNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM prs WHERE branch = 'extract/$BASENAME' AND status = 'closed'" 2>/dev/null || echo 0)
if [ "$CLOSED_COUNT" -ge "$MAX_CLOSED" ]; then
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (zombie: $CLOSED_COUNT closed PRs >= $MAX_CLOSED limit)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
fi
# Gate 4: Check pipeline.db for active or recently closed PRs — prevents
# re-extraction waste when eval closes a PR and batch-extract runs again
# before the source is manually reviewed. 4h cooldown after closure.
if [ -f "$DB" ]; then
ACTIVE_COUNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM prs WHERE branch = 'extract/$BASENAME' AND status IN ('extracting','approved','merging')" 2>/dev/null || echo 0)
if [ "$ACTIVE_COUNT" -ge 1 ]; then
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (active PR exists)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
RECENT_CLOSED=$(sqlite3 "$DB" "SELECT COUNT(*) FROM prs WHERE branch = 'extract/$BASENAME' AND status = 'closed' AND created_at > datetime('now', '-4 hours')" 2>/dev/null || echo 0)
if [ "$RECENT_CLOSED" -ge 1 ]; then
echo "[$(date)] [$COUNT/$MAX] SKIP $BASENAME (recently closed PR — 4h cooldown)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
fi
echo "[$(date)] [$COUNT/$MAX] Processing $BASENAME" >> $LOG
# Reset to main (log errors — don't swallow)
git checkout -f main >> $LOG 2>&1 || { echo " -> SKIP (checkout main failed)" >> $LOG; SKIPPED=$((SKIPPED + 1)); continue; }
git fetch origin main >> $LOG 2>&1
git reset --hard origin/main >> $LOG 2>&1 || { echo " -> SKIP (reset failed)" >> $LOG; SKIPPED=$((SKIPPED + 1)); continue; }
# Clean stale remote branch (Leo's catch — prevents checkout conflicts)
git push origin --delete "$BRANCH" 2>/dev/null
# Create fresh branch
git branch -D "$BRANCH" 2>/dev/null
git checkout -b "$BRANCH" 2>/dev/null
if [ $? -ne 0 ]; then
echo " -> SKIP (branch creation failed)" >> $LOG
SKIPPED=$((SKIPPED + 1))
continue
fi
# Run extraction
python3 $EXTRACT "$SOURCE" --no-review >> $LOG 2>&1
EXTRACT_RC=$?
if [ $EXTRACT_RC -ne 0 ]; then
FAILED=$((FAILED + 1))
echo " -> FAILED (extract rc=$EXTRACT_RC)" >> $LOG
continue
fi
# Post-extraction cleanup
python3 $CLEANUP $REPO >> $LOG 2>&1
# Check if any files were created/modified
CHANGED=$(git status --porcelain | wc -l | tr -d " ")
if [ "$CHANGED" -eq 0 ]; then
echo " -> No changes (enrichment/null-result only)" >> $LOG
continue
fi
# Commit
git add -A
git commit -m "extract: $BASENAME
Pentagon-Agent: Epimetheus <3D35839A-7722-4740-B93D-51157F7D5E70>" >> $LOG 2>&1
# Push
git push "http://leo:${TOKEN}@localhost:3000/teleo/teleo-codex.git" "$BRANCH" --force >> $LOG 2>&1
# Create PR (include prior art sidecar if available)
PRIOR_ART_FILE="${SOURCE}.prior-art"
PR_BODY=""
if [ -f "$PRIOR_ART_FILE" ]; then
# Escape JSON special chars in prior art content
PR_BODY=$(cat "$PRIOR_ART_FILE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')
PR_BODY=${PR_BODY:1:-1} # Strip outer quotes from json.dumps
fi
curl -sf -X POST "http://localhost:3000/api/v1/repos/teleo/teleo-codex/pulls" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"title\":\"extract: $BASENAME\",\"head\":\"$BRANCH\",\"base\":\"main\",\"body\":\"$PR_BODY\"}" >> /dev/null 2>&1
SUCCESS=$((SUCCESS + 1))
echo " -> SUCCESS ($CHANGED files)" >> $LOG
# Back to main
git checkout -f main >> $LOG 2>&1
# Rate limit
sleep 2
done
echo "[$(date)] Batch complete: $SUCCESS success, $FAILED failed, $SKIPPED skipped (already attempted)" >> $LOG
git checkout -f main >> $LOG 2>&1
git reset --hard origin/main >> $LOG 2>&1

View file

@ -0,0 +1,43 @@
substitutions:
_REGION: europe-west6
_REPOSITORY: teleo
_IMAGE: teleo-pipeline-gcp-staging
_TAG: manual-local
_REVISION: unknown
options:
logging: CLOUD_LOGGING_ONLY
machineType: E2_HIGHCPU_8
serviceAccount: projects/teleo-501523/serviceAccounts/sa-teleo-cloudbuild@teleo-501523.iam.gserviceaccount.com
steps:
- id: build-staging-image
name: gcr.io/cloud-builders/docker
args:
- build
- -f
- Dockerfile.gcp-staging
- --label
- org.opencontainers.image.source=https://github.com/living-ip/teleo-infrastructure
- --label
- org.opencontainers.image.revision=${_REVISION}
- --label
- livingip.revision=${_REVISION}
- --label
- livingip.surface=teleo-infrastructure
- --label
- livingip.tier=gcp-staging
- -t
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/${_IMAGE}:${_TAG}
- .
- id: smoke-test-image-before-push
name: gcr.io/cloud-builders/docker
args:
- run
- --rm
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/${_IMAGE}:${_TAG}
images:
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/${_IMAGE}:${_TAG}

View file

@ -0,0 +1,185 @@
{
"artifact": "teleo_gcp_service_communication_contract",
"schema_version": 1,
"project": "teleo-501523",
"region": "europe-west6",
"network": "teleo-staging-net",
"global_invariants": {
"no_public_database_ip": true,
"no_broad_ssh_or_rdp": true,
"no_default_compute_service_accounts": true,
"secret_values_not_stored_in_contract": true,
"database_connections_encrypted_only": true
},
"allowed_paths": [
{
"name": "github_actions_to_artifact_registry",
"purpose": "Build, smoke-run, and push Teleo staging Docker image.",
"source": {
"type": "github_actions_wif",
"repository": "living-ip/teleo-infrastructure"
},
"identity": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com",
"target": {
"type": "artifact_registry",
"repository": "projects/teleo-501523/locations/europe-west6/repositories/teleo"
},
"protocol": "https",
"ports": [443],
"network_path": "google_apis",
"encryption": "tls",
"allowed_by": [".github/workflows/gcp-artifact.yml", "roles/artifactregistry.writer"],
"forbidden": ["cloudsql.editor", "secretmanager.secretAccessor", "compute.admin"]
},
{
"name": "github_actions_to_readiness_probe",
"purpose": "Read-only GCP readiness probe with retained artifacts.",
"source": {
"type": "github_actions_wif",
"repository": "living-ip/teleo-infrastructure"
},
"identity": "sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com",
"target": {
"type": "gcp_control_plane",
"resources": ["artifact_registry", "compute_metadata", "cloudsql_metadata", "secret_metadata", "storage_metadata"]
},
"protocol": "https",
"ports": [443],
"network_path": "google_apis",
"encryption": "tls",
"allowed_by": [".github/workflows/gcp-readiness.yml", "ops/plan_gcp_iam_split.py"],
"forbidden": ["artifactregistry.writer", "cloudsql.editor", "secretmanager.secretAccessor"]
},
{
"name": "operator_ssh_to_teleo_vms",
"purpose": "Emergency/operator shell access only from one retained operator IPv4 /32.",
"source": {
"type": "operator_ip",
"cidrs": ["<operator-ip>/32"]
},
"identity": "oslogin_operator",
"target": {
"type": "compute_instance_tags",
"tags": ["teleo-prod-ssh", "teleo-staging-ssh"]
},
"protocol": "tcp",
"ports": [22],
"network_path": "vpc_firewall",
"encryption": "ssh",
"allowed_by": ["ops/apply_gcp_runtime_baseline.py", "ops/check_gcp_infra_readiness.py"],
"forbidden": ["broad_ipv4_ingress", "broad_ipv6_ingress", "tcp:3389"]
},
{
"name": "teleo_vms_to_artifact_registry",
"purpose": "Runtime pulls immutable images from Artifact Registry over Private Google Access.",
"source": {
"type": "compute_service_accounts",
"service_accounts": [
"sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com",
"sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"
]
},
"identity": "dedicated_vm_service_accounts",
"target": {
"type": "artifact_registry",
"repository": "projects/teleo-501523/locations/europe-west6/repositories/teleo"
},
"protocol": "https",
"ports": [443],
"network_path": "private_google_access",
"encryption": "tls",
"allowed_by": ["roles/artifactregistry.reader", "ops/apply_gcp_runtime_baseline.py"],
"forbidden": ["default_compute_service_account", "public_image_registry_required"]
},
{
"name": "teleo_vms_to_cloudsql_private",
"purpose": "Application and restore readback connections to the private Cloud SQL standby.",
"source": {
"type": "compute_service_accounts",
"service_accounts": [
"sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com",
"sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"
]
},
"identity": "dedicated_vm_service_accounts",
"target": {
"type": "cloudsql_postgres",
"instance": "teleo-pgvector-standby",
"database": "teleo_kb",
"private_network": "projects/teleo-501523/global/networks/teleo-staging-net",
"public_ip": false
},
"protocol": "postgres",
"ports": [5432],
"network_path": "private_vpc",
"encryption": "encrypted_only",
"allowed_by": ["ops/apply_gcp_runtime_baseline.py", "ops/check_gcp_infra_readiness.py"],
"forbidden": ["public_ip", "ssl_disabled", "authorized_networks_public"]
},
{
"name": "cloudsql_import_from_backup_bucket",
"purpose": "Cloud SQL import operation reads generated restore SQL from the versioned backup bucket.",
"source": {
"type": "cloudsql_instance_service_account",
"discovered_by": "gcloud sql instances describe teleo-pgvector-standby --format=value(serviceAccountEmailAddress)"
},
"identity": "cloudsql_instance_service_account",
"target": {
"type": "gcs_bucket",
"bucket": "gs://teleo-501523-prod-backups",
"prefix": "kb-dumps/cloudsql-restore-drills/"
},
"protocol": "https",
"ports": [443],
"network_path": "google_apis",
"encryption": "tls",
"allowed_by": ["roles/storage.objectAdmin", "ops/apply_gcp_iam_split.py"],
"forbidden": ["allUsers", "allAuthenticatedUsers", "publicAccessPreventionDisabled"]
},
{
"name": "restore_drill_operator_to_cloudsql_admin",
"purpose": "Operator-triggered Cloud SQL import drill and post-import readback.",
"source": {
"type": "operator_or_ci_with_explicit_auth",
"requires_retained_execute_proof": true
},
"identity": "sa-teleo-restore-drill@teleo-501523.iam.gserviceaccount.com",
"target": {
"type": "cloudsql_admin_api",
"instance": "teleo-pgvector-standby"
},
"protocol": "https",
"ports": [443],
"network_path": "google_apis",
"encryption": "tls",
"allowed_by": ["roles/cloudsql.editor", "ops/run_gcp_cloudsql_restore_drill.sh"],
"forbidden": ["unretained_execute", "raw_password_in_logs"]
},
{
"name": "vps_backup_to_gcs_bucket",
"purpose": "Source backup artifacts are uploaded to versioned GCS only through an approved backup identity.",
"source": {
"type": "hetzner_vps_or_operator_export",
"host": "teleo@77.42.65.182"
},
"identity": "approved_backup_writer_or_operator_session",
"target": {
"type": "gcs_bucket",
"bucket": "gs://teleo-501523-prod-backups",
"prefix": "kb-dumps/"
},
"protocol": "https",
"ports": [443],
"network_path": "google_apis",
"encryption": "tls",
"allowed_by": ["ops/backup_vps_sqlite_kb.sh", "ops/run_gcp_cloudsql_restore_drill.sh"],
"forbidden": ["raw_private_key_in_contract", "database_public_ingress", "unversioned_bucket"]
}
],
"not_proven_by_this_contract": [
"that GCP resources currently exist",
"that firewall rules are currently applied",
"that Cloud SQL import/readback has succeeded",
"that production Telegram/Leo traffic has cut over to GCP"
]
}

View file

@ -1,99 +0,0 @@
#!/usr/bin/env bash
# deploy.sh — Deploy pipeline and diagnostics to VPS from repo
# Usage: ./deploy.sh [--dry-run] [--restart]
#
# Requires: committed, clean working tree. Enforces repo-first workflow.
set -euo pipefail
VPS_HOST="teleo@77.42.65.182"
VPS_PIPELINE="/opt/teleo-eval/pipeline"
VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics"
VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DRY_RUN=false
RESTART=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--restart) RESTART=true ;;
--help|-h)
echo "Usage: $0 [--dry-run] [--restart]"
echo " --dry-run Show what would be deployed without doing it"
echo " --restart Restart services after deploy"
exit 0
;;
*) echo "Unknown arg: $arg"; exit 1 ;;
esac
done
# Gate: working tree must be clean
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
echo "ERROR: Uncommitted changes. Commit first, deploy second."
git -C "$REPO_ROOT" status --short
exit 1
fi
echo "Deploying from commit: $(git -C "$REPO_ROOT" log --oneline -1)"
echo ""
# Syntax check all Python files before deploying
echo "=== Pre-deploy syntax check ==="
ERRORS=0
for f in "$REPO_ROOT/ops/pipeline-v2/lib/"*.py "$REPO_ROOT/ops/pipeline-v2/"*.py "$REPO_ROOT/ops/diagnostics/"*.py; do
[ -f "$f" ] || continue
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>/dev/null; then
echo "SYNTAX ERROR: $f"
ERRORS=$((ERRORS + 1))
fi
done
if [ "$ERRORS" -gt 0 ]; then
echo "ERROR: $ERRORS files have syntax errors. Fix before deploying."
exit 1
fi
echo "All files pass syntax check."
echo ""
RSYNC_FLAGS="-avz --exclude='__pycache__' --exclude='*.pyc' --exclude='*.bak*'"
if $DRY_RUN; then
RSYNC_FLAGS="$RSYNC_FLAGS --dry-run"
echo "=== DRY RUN ==="
fi
echo "=== Pipeline lib/ ==="
rsync $RSYNC_FLAGS "$REPO_ROOT/ops/pipeline-v2/lib/" "$VPS_HOST:$VPS_PIPELINE/lib/"
echo ""
echo "=== Pipeline top-level ==="
for f in teleo-pipeline.py reweave.py batch-extract-50.sh; do
[ -f "$REPO_ROOT/ops/pipeline-v2/$f" ] || continue
rsync $RSYNC_FLAGS "$REPO_ROOT/ops/pipeline-v2/$f" "$VPS_HOST:$VPS_PIPELINE/$f"
done
echo ""
echo "=== Diagnostics ==="
rsync $RSYNC_FLAGS "$REPO_ROOT/ops/diagnostics/" "$VPS_HOST:$VPS_DIAGNOSTICS/"
echo ""
echo "=== Agent state ==="
rsync $RSYNC_FLAGS "$REPO_ROOT/ops/agent-state/" "$VPS_HOST:$VPS_AGENT_STATE/"
echo ""
echo "=== Research session ==="
rsync $RSYNC_FLAGS "$REPO_ROOT/ops/research-session.sh" "$VPS_HOST:/opt/teleo-eval/research-session.sh"
echo ""
if $DRY_RUN; then
echo "Dry run complete. No changes made."
exit 0
fi
echo "Deploy complete."
if $RESTART; then
echo ""
echo "=== Restarting services ==="
ssh "$VPS_HOST" "sudo systemctl restart teleo-pipeline teleo-diagnostics"
echo "Services restarted."
fi

223
deploy/auto-deploy.sh Executable file
View file

@ -0,0 +1,223 @@
#!/usr/bin/env bash
# auto-deploy.sh — Pull from Forgejo, sync to working dirs, restart if needed.
# Runs as systemd timer (teleo-auto-deploy.timer) every 2 minutes.
# Exits silently when nothing has changed.
set -euo pipefail
LOCK_FILE="/tmp/teleo-auto-deploy.lock"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
logger -t "auto-deploy" "Another deploy is already running. Skipping."
exit 0
fi
DEPLOY_CHECKOUT="/opt/teleo-eval/workspaces/deploy-infra"
PIPELINE_DIR="/opt/teleo-eval/pipeline"
TELEGRAM_DIR="/opt/teleo-eval/telegram"
DIAGNOSTICS_DIR="/opt/teleo-eval/diagnostics"
AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"
LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"
SYSTEMD_DIR="/etc/systemd/system"
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
LOG_TAG="auto-deploy"
log() { logger -t "$LOG_TAG" "$1"; echo "$(date '+%Y-%m-%d %H:%M:%S') $1"; }
DEPLOY_REMOTE="${TELEO_DEPLOY_REMOTE:-}"
if [ -z "$DEPLOY_REMOTE" ]; then
if git -C "$DEPLOY_CHECKOUT" remote get-url github >/dev/null 2>&1; then
DEPLOY_REMOTE="github"
else
DEPLOY_REMOTE="origin"
fi
fi
if [ ! -d "$DEPLOY_CHECKOUT/.git" ]; then
log "ERROR: Deploy checkout not found at $DEPLOY_CHECKOUT. Run setup first."
exit 1
fi
cd "$DEPLOY_CHECKOUT"
if ! git remote get-url "$DEPLOY_REMOTE" >/dev/null 2>&1; then
log "ERROR: deploy remote '$DEPLOY_REMOTE' is not configured"
exit 1
fi
if ! git fetch "$DEPLOY_REMOTE" main --quiet 2>&1; then
log "ERROR: git fetch failed for $DEPLOY_REMOTE/main"
exit 1
fi
NEW_SHA=$(git rev-parse "$DEPLOY_REMOTE/main")
OLD_SHA=$(cat "$STAMP_FILE" 2>/dev/null || echo "none")
if [ "$NEW_SHA" = "$OLD_SHA" ]; then
exit 0
fi
log "New commits: ${OLD_SHA:0:8} -> ${NEW_SHA:0:8}"
if ! git checkout main --quiet 2>&1; then
log "ERROR: git checkout main failed — dirty tree or corrupted index"
exit 1
fi
if ! git merge --ff-only "$DEPLOY_REMOTE/main" --quiet 2>&1; then
log "ERROR: git merge --ff-only $DEPLOY_REMOTE/main failed. Manual intervention needed."
exit 1
fi
# Syntax check all Python files before copying
ERRORS=0
for f in lib/*.py *.py diagnostics/*.py telegram/*.py tests/*.py; do
[ -f "$f" ] || continue
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>&1; then
log "SYNTAX ERROR: $f"
ERRORS=$((ERRORS + 1))
fi
done
if [ "$ERRORS" -gt 0 ]; then
log "ERROR: $ERRORS syntax errors. Deploy aborted. Fix and push again."
exit 1
fi
log "Syntax check passed"
# Sync to working directories
RSYNC_OPTS=(-az --exclude __pycache__ --exclude '*.pyc' --exclude '*.bak*')
rsync "${RSYNC_OPTS[@]}" lib/ "$PIPELINE_DIR/lib/"
for f in teleo-pipeline.py reweave.py fetch_coins.py pipeline-health-check.py; do
[ -f "$f" ] && rsync "${RSYNC_OPTS[@]}" "$f" "$PIPELINE_DIR/$f"
done
rsync "${RSYNC_OPTS[@]}" telegram/ "$PIPELINE_DIR/telegram/"
rsync "${RSYNC_OPTS[@]}" telegram/ "$TELEGRAM_DIR/"
rsync "${RSYNC_OPTS[@]}" diagnostics/ "$DIAGNOSTICS_DIR/"
rsync "${RSYNC_OPTS[@]}" agent-state/ "$AGENT_STATE_DIR/"
rsync "${RSYNC_OPTS[@]}" tests/ "$PIPELINE_DIR/tests/"
if [ -d hermes-agent/leoclean-bin ]; then
mkdir -p "$LEOCLEAN_BIN_DIR"
rsync "${RSYNC_OPTS[@]}" hermes-agent/leoclean-bin/ "$LEOCLEAN_BIN_DIR/"
fi
if [ -d hermes-agent/leoclean-skills/vps ]; then
mkdir -p "$LEOCLEAN_SKILLS_DIR"
rsync "${RSYNC_OPTS[@]}" hermes-agent/leoclean-skills/vps/ "$LEOCLEAN_SKILLS_DIR/"
fi
[ -f research/research-session.sh ] && rsync "${RSYNC_OPTS[@]}" research/research-session.sh /opt/teleo-eval/research-session.sh
# Safety net: ensure synced .sh files are executable after rsync.
# Keep this bounded to deploy-owned paths: /opt/teleo-eval also contains
# backups and generated state that may be unreadable by the deploy user.
for dir in "$PIPELINE_DIR" "$TELEGRAM_DIR" "$DIAGNOSTICS_DIR" "$AGENT_STATE_DIR" "$LEOCLEAN_BIN_DIR" "$LEOCLEAN_SKILLS_DIR"; do
[ -d "$dir" ] || continue
find "$dir" -maxdepth 3 -name '*.sh' -not -perm -u+x -exec chmod +x {} +
done
[ -f /opt/teleo-eval/research-session.sh ] && chmod u+x /opt/teleo-eval/research-session.sh
log "Files synced"
if [ "$OLD_SHA" = "none" ] || git diff --name-only "$OLD_SHA" "$NEW_SHA" -- systemd/ 2>/dev/null | grep -q .; then
log "Installing systemd units"
for unit in systemd/*.service systemd/*.timer; do
[ -f "$unit" ] || continue
sudo install -m 0644 "$unit" "$SYSTEMD_DIR/$(basename "$unit")"
done
sudo systemctl daemon-reload
if [ -f systemd/teleo-auto-deploy.timer ]; then
sudo systemctl enable --now teleo-auto-deploy.timer >/dev/null
fi
if [ -f systemd/teleo-agent-healthcheck.timer ]; then
sudo systemctl enable --now teleo-agent-healthcheck.timer >/dev/null
fi
fi
# Restart services only if Python files changed
RESTART=""
add_restart() {
case " $RESTART " in
*" $1 "*) ;;
*) RESTART="$RESTART $1" ;;
esac
}
add_restart_if_unit_exists() {
if systemctl list-units --all --full "$1.service" --no-legend 2>/dev/null | grep -q .; then
add_restart "$1"
fi
}
add_restart_if_unit_active() {
if systemctl is-active --quiet "$1.service"; then
add_restart "$1"
fi
}
if [ "$OLD_SHA" != "none" ]; then
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- lib/ teleo-pipeline.py reweave.py telegram/ 2>/dev/null | grep -q '\.py$'; then
add_restart teleo-pipeline
fi
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- telegram/ 2>/dev/null | grep -q '\.py$'; then
add_restart_if_unit_active teleo-agent@leo
add_restart_if_unit_exists teleo-agent@leo-wallet-test
fi
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- systemd/teleo-agent@.service 2>/dev/null | grep -q .; then
add_restart_if_unit_active teleo-agent@leo
add_restart_if_unit_exists teleo-agent@leo-wallet-test
fi
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- diagnostics/ 2>/dev/null | grep -q '\.py$'; then
add_restart teleo-diagnostics
fi
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/ 2>/dev/null | grep -q .; then
add_restart_if_unit_active leoclean-gateway
fi
else
RESTART="teleo-pipeline teleo-diagnostics"
add_restart_if_unit_active teleo-agent@leo
add_restart_if_unit_exists teleo-agent@leo-wallet-test
add_restart_if_unit_active leoclean-gateway
fi
if [ -n "$RESTART" ]; then
log "Restarting:$RESTART"
for svc in $RESTART; do
sudo systemctl restart "$svc"
done
sleep 30
FAIL=0
for svc in $RESTART; do
if systemctl is-active --quiet "$svc"; then
log "$svc: active"
else
log "ERROR: $svc failed to start"
journalctl -u "$svc" -n 5 --no-pager 2>/dev/null || true
FAIL=1
fi
done
if echo "$RESTART" | grep -q "teleo-pipeline"; then
HEALTH_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 http://localhost:8080/health 2>/dev/null || echo "000")
if [ "$HEALTH_CODE" = "200" ] || [ "$HEALTH_CODE" = "503" ]; then
log "pipeline health: OK (HTTP $HEALTH_CODE)"
else
log "WARNING: pipeline health check failed (HTTP $HEALTH_CODE)"
FAIL=1
fi
fi
if echo "$RESTART" | grep -q "teleo-diagnostics"; then
if curl -sf --connect-timeout 3 http://localhost:8081/ops > /dev/null 2>&1; then
log "diagnostics health: OK"
else
log "WARNING: diagnostics health check failed"
FAIL=1
fi
fi
if [ "$FAIL" -gt 0 ]; then
log "WARNING: Smoke test failures. NOT updating stamp. Will retry next cycle. Push a fix."
exit 1
fi
else
log "No Python changes — services not restarted"
fi
echo "$NEW_SHA" > "$STAMP_FILE"
log "Deploy complete: $(git log --oneline -1 "$NEW_SHA")"

128
deploy/deploy.sh Executable file
View file

@ -0,0 +1,128 @@
#!/usr/bin/env bash
# deploy.sh — Deploy pipeline and diagnostics to VPS from repo
# Usage: ./deploy.sh [--dry-run] [--restart]
#
# Requires: committed, clean working tree. Enforces repo-first workflow.
set -euo pipefail
VPS_HOST="teleo@77.42.65.182"
VPS_PIPELINE="/opt/teleo-eval/pipeline"
VPS_TELEGRAM="/opt/teleo-eval/telegram"
VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics"
VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"
VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"
VPS_SYSTEMD="/etc/systemd/system"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DRY_RUN=false
RESTART=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--restart) RESTART=true ;;
--help|-h)
echo "Usage: $0 [--dry-run] [--restart]"
echo " --dry-run Show what would be deployed without doing it"
echo " --restart Restart services after deploy"
exit 0
;;
*) echo "Unknown arg: $arg"; exit 1 ;;
esac
done
# Gate: working tree must be clean
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
echo "ERROR: Uncommitted changes. Commit first, deploy second."
git -C "$REPO_ROOT" status --short
exit 1
fi
echo "Deploying from commit: $(git -C "$REPO_ROOT" log --oneline -1)"
echo ""
# Syntax check all Python files before deploying
echo "=== Pre-deploy syntax check ==="
ERRORS=0
for f in "$REPO_ROOT/lib/"*.py "$REPO_ROOT/"*.py "$REPO_ROOT/diagnostics/"*.py "$REPO_ROOT/telegram/"*.py; do
[ -f "$f" ] || continue
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>/dev/null; then
echo "SYNTAX ERROR: $f"
ERRORS=$((ERRORS + 1))
fi
done
if [ "$ERRORS" -gt 0 ]; then
echo "ERROR: $ERRORS files have syntax errors. Fix before deploying."
exit 1
fi
echo "All files pass syntax check."
echo ""
RSYNC_OPTS=(-avz --exclude __pycache__ --exclude '*.pyc' --exclude '*.bak*')
if $DRY_RUN; then
RSYNC_OPTS+=(--dry-run)
echo "=== DRY RUN ==="
fi
echo "=== Pipeline lib/ ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/lib/" "$VPS_HOST:$VPS_PIPELINE/lib/"
echo ""
echo "=== Pipeline top-level ==="
for f in teleo-pipeline.py reweave.py fetch_coins.py; do
[ -f "$REPO_ROOT/$f" ] || continue
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/$f" "$VPS_HOST:$VPS_PIPELINE/$f"
done
echo ""
echo "=== Telegram bot ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_PIPELINE/telegram/"
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_TELEGRAM/"
echo ""
echo "=== Tests ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/tests/" "$VPS_HOST:$VPS_PIPELINE/tests/"
echo ""
echo "=== Diagnostics ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/diagnostics/" "$VPS_HOST:$VPS_DIAGNOSTICS/"
echo ""
echo "=== Agent state ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/agent-state/" "$VPS_HOST:$VPS_AGENT_STATE/"
echo ""
echo "=== Leoclean bin ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/leoclean-bin/" "$VPS_HOST:$VPS_LEOCLEAN_BIN/"
echo ""
echo "=== Leoclean skills ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/leoclean-skills/vps/" "$VPS_HOST:$VPS_LEOCLEAN_SKILLS/"
echo ""
echo "=== Research session ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/research/research-session.sh" "$VPS_HOST:/opt/teleo-eval/research-session.sh"
echo ""
echo "=== Systemd units ==="
if $DRY_RUN; then
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/systemd/" "$VPS_HOST:/tmp/teleo-systemd-dry-run/"
else
tar -C "$REPO_ROOT/systemd" -cf - . | ssh "$VPS_HOST" "tmpdir=\$(mktemp -d); tar -C \"\$tmpdir\" -xf -; sudo install -m 0644 \"\$tmpdir\"/*.service \"\$tmpdir\"/*.timer '$VPS_SYSTEMD'/; rm -rf \"\$tmpdir\"; sudo systemctl daemon-reload; sudo systemctl enable --now teleo-auto-deploy.timer teleo-agent-healthcheck.timer >/dev/null"
fi
echo ""
if $DRY_RUN; then
echo "Dry run complete. No changes made."
exit 0
fi
echo "Deploy complete."
if $RESTART; then
echo ""
echo "=== Restarting services ==="
ssh "$VPS_HOST" "sudo systemctl restart teleo-pipeline teleo-diagnostics; if systemctl is-active --quiet teleo-agent@leo.service; then sudo systemctl restart teleo-agent@leo; fi; if systemctl list-units --all --full teleo-agent@leo-wallet-test.service --no-legend | grep -q .; then sudo systemctl restart teleo-agent@leo-wallet-test; fi; if systemctl is-active --quiet leoclean-gateway.service; then sudo systemctl restart leoclean-gateway; fi"
echo "Services restarted."
fi

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Install the narrow sudoers rule required by teleo-auto-deploy.service.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SOURCE="$REPO_ROOT/deploy/sudoers/teleo-auto-deploy"
TARGET="/etc/sudoers.d/teleo-auto-deploy"
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: run as root on the VPS" >&2
exit 1
fi
install -m 0440 "$SOURCE" "$TARGET"
visudo -cf "$TARGET"
echo "Installed $TARGET"

120
deploy/setup-infra-mirror.sh Executable file
View file

@ -0,0 +1,120 @@
#!/bin/bash
# One-time setup: prepare the bare mirror repo for teleo-infrastructure.
#
# Prerequisites (must happen BEFORE running this):
# 1. GitHub repo `living-ip/teleo-infrastructure` created (manual via web or
# `gh repo create` — the deploy PAT is fine-grained to teleo-codex only
# and cannot create new repos in the org).
# 2. GitHub PAT updated to include push access on the new repo (or rotate
# to a classic PAT with `repo` scope covering both).
#
# This script is idempotent — safe to re-run.
set -euo pipefail
MIRROR_BASE="/opt/teleo-eval/mirror"
REPO_DIR="$MIRROR_BASE/teleo-infrastructure.git"
FORGEJO_URL="http://localhost:3000/teleo/teleo-infrastructure.git"
GITHUB_REPO="living-ip/teleo-infrastructure"
FORGEJO_TOKEN_FILE="/opt/teleo-eval/secrets/forgejo-admin-token"
GITHUB_PAT_FILE="/opt/teleo-eval/secrets/github-pat"
if [ ! -f "$FORGEJO_TOKEN_FILE" ]; then
echo "ERROR: missing $FORGEJO_TOKEN_FILE" >&2
exit 1
fi
if [ ! -f "$GITHUB_PAT_FILE" ]; then
echo "ERROR: missing $GITHUB_PAT_FILE" >&2
exit 1
fi
FORGEJO_TOKEN=$(cat "$FORGEJO_TOKEN_FILE" | tr -d '[:space:]')
GITHUB_PAT=$(cat "$GITHUB_PAT_FILE" | tr -d '[:space:]')
# Sanity check: GitHub repo must exist before we point a remote at it.
echo "Verifying GitHub repo $GITHUB_REPO exists..."
GH_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $GITHUB_PAT" \
"https://api.github.com/repos/$GITHUB_REPO")
if [ "$GH_STATUS" != "200" ]; then
echo "ERROR: GitHub repo $GITHUB_REPO not accessible (HTTP $GH_STATUS)" >&2
echo "Create it first: gh repo create $GITHUB_REPO --public --description 'Pipeline + diagnostics infra for the LivingIP collective'" >&2
exit 2
fi
echo " OK — $GITHUB_REPO accessible"
# Sanity check: Forgejo repo must exist.
echo "Verifying Forgejo repo teleo/teleo-infrastructure exists..."
FG_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: token $FORGEJO_TOKEN" \
"http://localhost:3000/api/v1/repos/teleo/teleo-infrastructure")
if [ "$FG_STATUS" != "200" ]; then
echo "ERROR: Forgejo repo teleo/teleo-infrastructure not accessible (HTTP $FG_STATUS)" >&2
exit 3
fi
echo " OK — Forgejo repo accessible"
# Init bare mirror if missing
if [ -d "$REPO_DIR" ]; then
echo "Bare repo already exists at $REPO_DIR — skipping init"
else
echo "Creating bare repo at $REPO_DIR..."
mkdir -p "$REPO_DIR"
cd "$REPO_DIR"
git init --bare >/dev/null
chown -R teleo:teleo "$REPO_DIR"
echo " OK — bare repo initialized"
fi
cd "$REPO_DIR"
# Configure remotes (idempotent: set-url succeeds whether remote exists or not)
# Forgejo remote (origin convention is reversed in this codebase: origin=GitHub,
# forgejo=Forgejo, matching the existing teleo-codex.git layout).
FORGEJO_REMOTE_URL="http://github-mirror:${FORGEJO_TOKEN}@localhost:3000/teleo/teleo-infrastructure.git"
# NOTE: "m3taversal" is a placeholder username — for fine-grained PATs the
# username field is decorative; the token does the auth. Matches the existing
# teleo-codex.git remote for consistency. (Ganymede review nit #4.)
GITHUB_REMOTE_URL="https://m3taversal:${GITHUB_PAT}@github.com/${GITHUB_REPO}.git"
if git remote get-url forgejo >/dev/null 2>&1; then
git remote set-url forgejo "$FORGEJO_REMOTE_URL"
echo " Updated forgejo remote URL"
else
git remote add forgejo "$FORGEJO_REMOTE_URL"
echo " Added forgejo remote"
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "$GITHUB_REMOTE_URL"
echo " Updated origin remote URL"
else
git remote add origin "$GITHUB_REMOTE_URL"
echo " Added origin remote"
fi
# Initial fetch from Forgejo
echo "Fetching from Forgejo..."
git fetch forgejo --prune 2>&1 | sed 's/^/ /'
# Initial push to GitHub (will populate the empty repo)
# main_only mode: push ONLY refs/heads/main + tags, mirroring what sync-mirror.sh
# does for this repo on the recurring path. Agent review branches stay Forgejo-only.
echo "Pushing initial main + tags to GitHub..."
git update-ref refs/heads/main refs/remotes/forgejo/main 2>/dev/null || {
echo "ERROR: forgejo/main ref missing — fetch may have failed" >&2
exit 1
}
git push origin "refs/heads/main:refs/heads/main" 2>&1 | sed 's/^/ /' || {
echo "WARN: initial push failed — you may need to authorize the PAT for $GITHUB_REPO" >&2
}
git push origin --tags 2>&1 | sed 's/^/ /' || true
# Final permissions sweep
chown -R teleo:teleo "$REPO_DIR"
echo
echo "Setup complete. Verify with:"
echo " ssh teleo@77.42.65.182 ls -la $REPO_DIR/refs/heads"
echo " /opt/teleo-eval/sync-mirror.sh && tail -50 /opt/teleo-eval/logs/sync.log"

View file

@ -0,0 +1,14 @@
# Narrow service privileges for teleo-auto-deploy.service, which runs as teleo.
# Keep command forms aligned with deploy/auto-deploy.sh.
teleo ALL=(root) NOPASSWD: /usr/bin/systemctl restart leoclean-gateway
teleo ALL=(root) NOPASSWD: /bin/systemctl restart leoclean-gateway
teleo ALL=(root) NOPASSWD: /usr/bin/systemctl restart leoclean-gateway.service
teleo ALL=(root) NOPASSWD: /bin/systemctl restart leoclean-gateway.service
teleo ALL=(root) NOPASSWD: /usr/bin/systemctl status leoclean-gateway
teleo ALL=(root) NOPASSWD: /bin/systemctl status leoclean-gateway
teleo ALL=(root) NOPASSWD: /usr/bin/systemctl status leoclean-gateway.service
teleo ALL=(root) NOPASSWD: /bin/systemctl status leoclean-gateway.service
teleo ALL=(root) NOPASSWD: /usr/bin/journalctl -u leoclean-gateway
teleo ALL=(root) NOPASSWD: /bin/journalctl -u leoclean-gateway
teleo ALL=(root) NOPASSWD: /usr/bin/journalctl -u leoclean-gateway.service
teleo ALL=(root) NOPASSWD: /bin/journalctl -u leoclean-gateway.service

View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Sync source-controlled GCP leoclean skills into the GCP parallel runtime.
#
# Defaults target the non-production GCP leoclean service. This script does not
# touch the production Telegram token and does not read or print secrets.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT="${PROJECT:-teleo-501523}"
ZONE="${ZONE:-europe-west6-a}"
INSTANCE="${INSTANCE:-teleo-prod-1}"
SERVICE="${SERVICE:-leoclean-gcp-prod-parallel.service}"
REMOTE_SKILLS_DIR="${REMOTE_SKILLS_DIR:-/home/teleo/.hermes/profiles/leoclean/skills}"
SOURCE_DIR="${SOURCE_DIR:-$REPO_ROOT/hermes-agent/leoclean-skills/gcp}"
DRY_RUN=false
RESTART=false
VERIFY_ONLY=false
usage() {
cat <<'USAGE'
Usage: deploy/sync-gcp-leoclean-skills.sh [--dry-run] [--restart] [--verify-only]
Environment overrides:
PROJECT GCP project, default teleo-501523
ZONE GCP zone, default europe-west6-a
INSTANCE GCP VM, default teleo-prod-1
SERVICE systemd service, default leoclean-gcp-prod-parallel.service
REMOTE_SKILLS_DIR runtime skills dir, default /home/teleo/.hermes/profiles/leoclean/skills
SOURCE_DIR local source dir, default hermes-agent/leoclean-skills/gcp
USAGE
}
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--restart) RESTART=true ;;
--verify-only) VERIFY_ONLY=true ;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown arg: $arg" >&2
usage >&2
exit 1
;;
esac
done
if [ ! -d "$SOURCE_DIR" ]; then
echo "ERROR: source skill directory not found: $SOURCE_DIR" >&2
exit 1
fi
if [ ! -f "$SOURCE_DIR/teleo-kb-bridge/SKILL.md" ]; then
echo "ERROR: source teleo-kb-bridge skill not found under $SOURCE_DIR" >&2
exit 1
fi
GCP_SSH=(
gcloud compute ssh "$INSTANCE"
--project="$PROJECT"
--zone="$ZONE"
--tunnel-through-iap
)
verify_command=$(cat <<REMOTE
set -euo pipefail
sudo -n -u teleo grep -nE '/kb/claims/<claim_id>|wrap claim IDs, proposal IDs' '$REMOTE_SKILLS_DIR/teleo-kb-bridge/SKILL.md'
systemctl show '$SERVICE' -p ActiveState -p SubState -p NRestarts -p MainPID --no-pager
REMOTE
)
if $DRY_RUN; then
echo "DRY RUN: would stream $SOURCE_DIR to $INSTANCE:$REMOTE_SKILLS_DIR"
echo "DRY RUN: project=$PROJECT zone=$ZONE service=$SERVICE restart=$RESTART verify_only=$VERIFY_ONLY"
exit 0
fi
if $VERIFY_ONLY; then
"${GCP_SSH[@]}" --command="$verify_command"
exit 0
fi
REMOTE_TMP="/tmp/teleo-gcp-leoclean-skills-$(date -u +%Y%m%dT%H%M%SZ)-$$"
sync_command=$(cat <<REMOTE
set -euo pipefail
remote_tmp='$REMOTE_TMP'
remote_skills='$REMOTE_SKILLS_DIR'
service='$SERVICE'
cleanup() { rm -rf "\$remote_tmp"; }
trap cleanup EXIT
rm -rf "\$remote_tmp"
mkdir -p "\$remote_tmp"
tar -C "\$remote_tmp" -xf -
sudo install -d -o teleo -g teleo -m 0755 "\$remote_skills"
tar -C "\$remote_tmp" -cf - . | sudo tar -C "\$remote_skills" -xf -
sudo chown -R teleo:teleo "\$remote_skills"
sudo -n -u teleo grep -nE '/kb/claims/<claim_id>|wrap claim IDs, proposal IDs' "\$remote_skills/teleo-kb-bridge/SKILL.md"
if [ '$RESTART' = true ]; then
sudo systemctl restart "\$service"
fi
systemctl show "\$service" -p ActiveState -p SubState -p NRestarts -p MainPID --no-pager
REMOTE
)
COPYFILE_DISABLE=1 tar --format=ustar -C "$SOURCE_DIR" -cf - . | "${GCP_SSH[@]}" --command="$sync_command"

451
deploy/sync-mirror.sh Executable file
View file

@ -0,0 +1,451 @@
#!/bin/bash
# Bidirectional sync: Forgejo (authoritative) <-> GitHub (public mirror)
# Forgejo wins on conflict. Runs every 2 minutes via cron.
#
# Repos handled (see MIRROR_REPOS below):
# - teleo-codex (mode=bidirectional): full PR roundtrip — fork PR refs from
# GitHub, auto-create Forgejo PR mirrors, link github_pr in pipeline.db.
# - teleo-infrastructure (mode=main_only): one-way sync of branches+tags from
# Forgejo to GitHub. No PR roundtrip — pipeline doesn't process infra PRs;
# external infra PRs land on GitHub for visibility, get reviewed manually.
#
# Security note: GitHub->Forgejo path is for external contributor convenience.
# Never auto-process branches arriving via this path without a PR.
# Eval pipeline and extract cron only act on PRs, not raw branches.
set -euo pipefail
LOG="/opt/teleo-eval/logs/sync.log"
LOCKFILE="/tmp/sync-mirror.lock"
PIPELINE_DB="/opt/teleo-eval/pipeline/pipeline.db"
GITHUB_PAT_FILE="/opt/teleo-eval/secrets/github-pat"
# (forgejo_owner_repo, github_owner_repo, bare_path, mode)
# mode: bidirectional | main_only
MIRROR_REPOS=(
"teleo/teleo-codex living-ip/teleo-codex /opt/teleo-eval/mirror/teleo-codex.git bidirectional"
"teleo/teleo-infrastructure living-ip/teleo-infrastructure /opt/teleo-eval/mirror/teleo-infrastructure.git main_only"
)
REPO_TAG="main"
log() { echo "[$(date -Iseconds)] [$REPO_TAG] $1" >> "$LOG"; }
# Lockfile — prevent concurrent runs (single lock for whole script)
if [ -f "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE" 2>/dev/null)
if kill -0 "$pid" 2>/dev/null; then
exit 0
fi
rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# ─────────────────────────────────────────────────────────────────────────────
# sync_repo: process one mirror entry. Sets module-level FORGEJO_REPO,
# GITHUB_REPO, REPO_DIR, MODE, REPO_TAG used by inner steps.
# ─────────────────────────────────────────────────────────────────────────────
sync_repo() {
FORGEJO_REPO="$1" # e.g. teleo/teleo-codex (path on Forgejo)
GITHUB_REPO="$2" # e.g. living-ip/teleo-codex (path on GitHub)
REPO_DIR="$3" # bare mirror dir
MODE="$4" # bidirectional | main_only
REPO_TAG="${FORGEJO_REPO##*/}" # short name for log prefix
# Pre-flight: bare repo must exist
if [ ! -d "$REPO_DIR" ]; then
log "ERROR: bare repo missing at $REPO_DIR — skipping"
return 0
fi
# Pre-flight: fix permissions if another user touched the mirror dir (Rhea)
BAD_PERMS=$(find "$REPO_DIR" ! -user teleo 2>/dev/null | head -1 || true)
if [ -n "$BAD_PERMS" ]; then
log "Fixing mirror permissions (found: $BAD_PERMS)"
chown -R teleo:teleo "$REPO_DIR" 2>/dev/null || true
fi
cd "$REPO_DIR" || { log "ERROR: cannot cd to $REPO_DIR"; return 0; }
# Step 1: Fetch from Forgejo (must succeed — it's authoritative)
log "Fetching from Forgejo..."
if ! git fetch forgejo --prune >> "$LOG" 2>&1; then
log "ERROR: Forgejo fetch failed — skipping this repo"
return 0
fi
# Step 2: Fetch from GitHub (warn on failure, don't abort)
log "Fetching from GitHub..."
git fetch origin --prune >> "$LOG" 2>&1 || log "WARN: GitHub fetch failed"
# Step 2.1: Fetch GitHub fork PR refs (bidirectional only)
# Fork-based PRs don't create branches on origin — they create refs/pull/N/head.
# main_only repos don't accept fork PRs through the mirror path.
if [ "$MODE" = "bidirectional" ]; then
local PAT
PAT=$(cat "$GITHUB_PAT_FILE" 2>/dev/null | tr -d '[:space:]')
if [ -n "$PAT" ]; then
local OPEN_PRS
OPEN_PRS=$(curl -sf "https://api.github.com/repos/$GITHUB_REPO/pulls?state=open&per_page=100" \
-H "Authorization: token $PAT" 2>/dev/null || echo "[]")
echo "$OPEN_PRS" | python3 -c "
import sys, json
prs = json.load(sys.stdin)
for pr in prs:
head = pr.get('head', {})
base_repo = pr.get('base', {}).get('repo', {}).get('full_name', '')
head_repo = head.get('repo', {}) or {}
head_full = head_repo.get('full_name', '')
if head_full and head_full != base_repo:
print(f\"{pr['number']} {head.get('ref', '')} {head.get('sha', '')}\")
" 2>/dev/null | while read pr_num branch_name head_sha; do
if [ -z "$pr_num" ] || [ -z "$branch_name" ]; then continue; fi
local PR_BRANCH="gh-pr-${pr_num}/${branch_name}"
local EXISTING
EXISTING=$(git rev-parse "refs/heads/$PR_BRANCH" 2>/dev/null || true)
if [ "$EXISTING" = "$head_sha" ]; then continue; fi
git fetch origin "refs/pull/${pr_num}/head:refs/heads/$PR_BRANCH" >> "$LOG" 2>&1 && \
log "Fetched fork PR #$pr_num -> $PR_BRANCH" || \
log "WARN: Failed to fetch fork PR #$pr_num"
done
fi
fi
# Step 2.5: GitHub main -> Forgejo main (ff-only)
# If a PR was merged on GitHub, GitHub main is ahead of Forgejo main.
# Fast-forward Forgejo main to match — safe because ff-only guarantees no divergence.
local GITHUB_MAIN_FF FORGEJO_MAIN_FF
GITHUB_MAIN_FF=$(git rev-parse refs/remotes/origin/main 2>/dev/null || true)
FORGEJO_MAIN_FF=$(git rev-parse refs/remotes/forgejo/main 2>/dev/null || true)
if [ -n "$GITHUB_MAIN_FF" ] && [ -n "$FORGEJO_MAIN_FF" ]; then
if [ "$GITHUB_MAIN_FF" != "$FORGEJO_MAIN_FF" ]; then
if git merge-base --is-ancestor "$FORGEJO_MAIN_FF" "$GITHUB_MAIN_FF"; then
log "GitHub main ($GITHUB_MAIN_FF) ahead of Forgejo main ($FORGEJO_MAIN_FF) — fast-forwarding"
git push forgejo "refs/remotes/origin/main:refs/heads/main" >> "$LOG" 2>&1 && \
log "Forgejo main fast-forwarded to $GITHUB_MAIN_FF" || \
log "WARN: Failed to fast-forward Forgejo main"
fi
fi
fi
# Step 3: Forgejo -> GitHub (primary direction)
log "Syncing Forgejo -> GitHub..."
while read branch; do
[ "$branch" = "HEAD" ] && continue
git update-ref "refs/heads/$branch" "refs/remotes/forgejo/$branch" 2>/dev/null || \
log "WARN: Failed to update ref $branch"
done < <(git for-each-ref --format="%(refname:lstrip=3)" refs/remotes/forgejo/)
# Safety: verify Forgejo main descends from GitHub main before force-pushing
local GITHUB_MAIN FORGEJO_MAIN PUSH_MAIN
GITHUB_MAIN=$(git rev-parse refs/remotes/origin/main 2>/dev/null || true)
FORGEJO_MAIN=$(git rev-parse refs/remotes/forgejo/main 2>/dev/null || true)
PUSH_MAIN=true
if [ -n "$GITHUB_MAIN" ] && [ -n "$FORGEJO_MAIN" ]; then
if ! git merge-base --is-ancestor "$GITHUB_MAIN" "$FORGEJO_MAIN"; then
log "CRITICAL: Forgejo main is NOT a descendant of GitHub main — skipping main push"
log "CRITICAL: GitHub main: $GITHUB_MAIN, Forgejo main: $FORGEJO_MAIN"
PUSH_MAIN=false
fi
fi
if [ "$MODE" = "main_only" ]; then
# Infra-style mirror: push main + tags ONLY. Pre-review agent branches
# (epimetheus/*, ganymede/*, etc.) carry internal context — agent UUIDs,
# in-flight discussion, WIP — and must not land in the public GitHub
# history. (Ganymede review, finding #1.)
if [ "$PUSH_MAIN" = true ]; then
git push origin --force "refs/heads/main:refs/heads/main" >> "$LOG" 2>&1 || \
log "WARN: main push to GitHub failed"
fi
else
# Bidirectional mirror (codex): push all branches so external
# contributors can fork from any branch, not just main.
if [ "$PUSH_MAIN" = true ]; then
git push origin --all --force >> "$LOG" 2>&1 || log "WARN: Push to GitHub failed"
else
# Push all branches except main when main is divergent
while read branch; do
[ "$branch" = "main" ] && continue
[ "$branch" = "HEAD" ] && continue
git push origin --force "refs/heads/$branch:refs/heads/$branch" >> "$LOG" 2>&1 || \
log "WARN: Failed to push $branch to GitHub"
done < <(git for-each-ref --format="%(refname:lstrip=2)" refs/heads/)
fi
fi
git push origin --tags --force >> "$LOG" 2>&1 || log "WARN: Tag push to GitHub failed"
# Step 4: GitHub -> Forgejo + Forgejo PR auto-create (bidirectional only)
if [ "$MODE" = "bidirectional" ]; then
sync_github_to_forgejo_with_prs
fi
# Step 6: Divergence alerting (applies to both modes)
check_divergence
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 4 split out: codex-specific GitHub→Forgejo branch push + PR auto-create.
# Reads FORGEJO_REPO, GITHUB_REPO, PIPELINE_DB, REPO_TAG from sync_repo scope.
# ─────────────────────────────────────────────────────────────────────────────
sync_github_to_forgejo_with_prs() {
log "Checking GitHub-only branches..."
local FORGEJO_HOST="http://localhost:3000/api/v1/repos/$FORGEJO_REPO"
local GITHUB_ONLY
GITHUB_ONLY=$(comm -23 \
<(git for-each-ref --format="%(refname:lstrip=3)" refs/remotes/origin/ | grep -v HEAD | sort) \
<(git for-each-ref --format="%(refname:lstrip=3)" refs/remotes/forgejo/ | grep -v HEAD | sort))
if [ -z "$GITHUB_ONLY" ]; then
log "No new GitHub-only branches"
return 0
fi
local FORGEJO_TOKEN
FORGEJO_TOKEN=$(cat /opt/teleo-eval/secrets/forgejo-admin-token 2>/dev/null)
# Lazy schema for sync-mirror's auto-create tracker. Records (branch, sha)
# pairs we've already auto-created PRs for, so the loop below can skip
# redundant creates after pipeline merge → _delete_remote_branch →
# GitHub-only re-discovery → re-push. Cheap CREATE IF NOT EXISTS on each
# cycle; no migration needed because this table is private to sync-mirror.
sqlite3 "$PIPELINE_DB" "CREATE TABLE IF NOT EXISTS sync_autocreate_tracker (branch TEXT NOT NULL, sha TEXT NOT NULL, pr_number INTEGER, created_at TEXT DEFAULT (datetime('now')), PRIMARY KEY (branch, sha));" 2>/dev/null || true
for branch in $GITHUB_ONLY; do
# Already-tracked gate: if we've previously auto-created a PR for
# this exact (branch, sha), skip the entire push+create sequence.
# Closes the empty-PR loop (research and reweave both observed):
# pipeline merges PR → _delete_remote_branch on Forgejo → next sync
# sees branch GitHub-only (origin still has it) → re-pushes to
# Forgejo → HAS_PR misses (Forgejo ?head= broken; closed PRs scroll
# past 50-item paginated window) → auto-creates fresh PR → pipeline
# merges (empty no-op via cherry-pick / reweave union) → repeat.
# Tracker keys on SHA, so legitimate new commits on the same branch
# produce a new SHA → tracker miss → auto-create proceeds normally.
local BRANCH_SHA TRACKED_PR
if [[ "$branch" == gh-pr-* ]]; then
BRANCH_SHA=$(git rev-parse "refs/heads/$branch" 2>/dev/null || true)
else
BRANCH_SHA=$(git rev-parse "refs/remotes/origin/$branch" 2>/dev/null || true)
fi
if [ -n "$BRANCH_SHA" ]; then
# stderr → $LOG so sustained sqlite3 contention surfaces in ops logs
# rather than silently falling through to a redundant auto-create.
TRACKED_PR=$(sqlite3 "$PIPELINE_DB" "SELECT pr_number FROM sync_autocreate_tracker WHERE branch=$(printf "'%s'" "${branch//\'/\'\'}") AND sha=$(printf "'%s'" "$BRANCH_SHA") LIMIT 1;" 2>>"$LOG" || echo "")
if [ -n "$TRACKED_PR" ]; then
log "Skip auto-create: $branch SHA $BRANCH_SHA already tracked (PR #$TRACKED_PR)"
continue
fi
fi
log "New from GitHub: $branch -> Forgejo"
# Fork PR branches live as local refs (from Step 2.1), not on origin remote
if [[ "$branch" == gh-pr-* ]]; then
git push forgejo "refs/heads/$branch:refs/heads/$branch" >> "$LOG" 2>&1 || {
log "WARN: Failed to push fork PR branch $branch to Forgejo"
continue
}
else
git push forgejo "refs/remotes/origin/$branch:refs/heads/$branch" >> "$LOG" 2>&1 || {
log "WARN: Failed to push $branch to Forgejo"
continue
}
fi
# Skip pipeline-internal branch prefixes (no PR creation)
case "$branch" in
extract/*|ingestion/*) continue ;;
esac
if [ -z "$FORGEJO_TOKEN" ]; then continue; fi
# Check if PR already exists for this branch (open or closed)
# NOTE: Forgejo ?head= filter is broken (ignores head value, returns all PRs).
# Workaround: fetch open+closed PRs, pipe to Python, check head.ref.
local HAS_PR
HAS_PR=$( {
curl -sf "$FORGEJO_HOST/pulls?state=open&limit=50" \
-H "Authorization: token $FORGEJO_TOKEN" 2>/dev/null || echo "[]"
echo ""
curl -sf "$FORGEJO_HOST/pulls?state=closed&sort=created&limit=50" \
-H "Authorization: token $FORGEJO_TOKEN" 2>/dev/null || echo "[]"
} | python3 -c "
import sys, json
branch = sys.argv[1]
for line in sys.stdin:
line = line.strip()
if not line or line == '[]': continue
try:
for pr in json.loads(line):
if pr.get('head', {}).get('ref') == branch:
print('yes'); sys.exit(0)
except: pass
print('no')
" "$branch" 2>/dev/null || echo "no")
if [ "$HAS_PR" = "yes" ]; then continue; fi
# Build PR title — for fork PRs, use the GitHub PR title
local PR_TITLE PAYLOAD RESULT PR_NUM GH_PR_NUM
if [[ "$branch" == gh-pr-* ]]; then
local FORK_GH_NUM PAT_T
FORK_GH_NUM=$(echo "$branch" | sed 's|gh-pr-\([0-9]*\)/.*|\1|')
PAT_T=$(cat "$GITHUB_PAT_FILE" 2>/dev/null | tr -d '[:space:]')
PR_TITLE=$(curl -sf "https://api.github.com/repos/$GITHUB_REPO/pulls/$FORK_GH_NUM" \
-H "Authorization: token $PAT_T" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))" 2>/dev/null || true)
[ -z "$PR_TITLE" ] && PR_TITLE=$(echo "$branch" | sed 's|/|: |;s/-/ /g')
else
PR_TITLE=$(echo "$branch" | sed 's|/|: |;s/-/ /g')
fi
PAYLOAD=$(python3 -c "import sys,json; print(json.dumps({'title':sys.argv[1],'head':sys.argv[2],'base':'main'}))" "$PR_TITLE" "$branch")
RESULT=$(curl -sf -X POST "$FORGEJO_HOST/pulls" \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" 2>/dev/null || echo "")
PR_NUM=$(echo "$RESULT" | grep -o '"number":[0-9]*' | head -1 | grep -o "[0-9]*" || true)
if [ -z "$PR_NUM" ]; then
log "WARN: Failed to auto-create PR for $branch"
continue
fi
log "Auto-created PR #$PR_NUM on Forgejo for $branch"
# Record (branch, sha, pr_number) so the tracker gate above can short-
# circuit the next time we see this exact (branch, sha) combination.
# INSERT OR IGNORE: idempotent if a concurrent run already inserted.
# WARN log on failure: silent INSERT failure under sustained sqlite3
# contention would mask the loop reappearing on the next cycle (HAS_PR
# only saves us while the closed PR is in the 50-item pagination window).
if [ -n "$BRANCH_SHA" ] && [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
if ! sqlite3 "$PIPELINE_DB" "INSERT OR IGNORE INTO sync_autocreate_tracker (branch, sha, pr_number) VALUES ($(printf "'%s'" "${branch//\'/\'\'}"), $(printf "'%s'" "$BRANCH_SHA"), $PR_NUM);" 2>>"$LOG"; then
log "WARN: tracker insert failed for $branch SHA $BRANCH_SHA (PR #$PR_NUM) — duplicate auto-create possible next cycle"
fi
fi
# Step 4.5: Link GitHub PR to Forgejo PR in pipeline DB
if [[ "$branch" == gh-pr-* ]]; then
GH_PR_NUM=$(echo "$branch" | sed 's|gh-pr-\([0-9]*\)/.*|\1|')
else
local PAT
PAT=$(cat "$GITHUB_PAT_FILE" 2>/dev/null | tr -d '[:space:]')
GH_PR_NUM=""
if [ -n "$PAT" ]; then
GH_PR_NUM=$(curl -sf "https://api.github.com/repos/$GITHUB_REPO/pulls?head=living-ip:$branch&state=all" \
-H "Authorization: token $PAT" 2>/dev/null | \
python3 -c "import sys,json; prs=json.load(sys.stdin); print(prs[0]['number'] if prs else '')" 2>/dev/null || true)
fi
fi
if [[ "$GH_PR_NUM" =~ ^[0-9]+$ ]] && [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
sqlite3 "$PIPELINE_DB" "UPDATE prs SET github_pr = $GH_PR_NUM, source_channel = 'github' WHERE number = $PR_NUM;" 2>/dev/null && \
log "Linked GitHub PR #$GH_PR_NUM -> Forgejo PR #$PR_NUM" || \
log "WARN: Failed to link GitHub PR #$GH_PR_NUM to Forgejo PR #$PR_NUM in DB"
fi
done
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 6 split out: divergence alerting. Per-repo state file so each repo
# has its own divergence counter and alert state.
# ─────────────────────────────────────────────────────────────────────────────
check_divergence() {
local DIVERGENCE_FILE="/opt/teleo-eval/logs/.divergence-count.${REPO_TAG}"
git fetch forgejo main --quiet 2>/dev/null || true
git fetch origin main --quiet 2>/dev/null || true
local GH_MAIN_FINAL FG_MAIN_FINAL
GH_MAIN_FINAL=$(git rev-parse refs/remotes/origin/main 2>/dev/null || true)
FG_MAIN_FINAL=$(git rev-parse refs/remotes/forgejo/main 2>/dev/null || true)
if [ -n "$GH_MAIN_FINAL" ] && [ -n "$FG_MAIN_FINAL" ] && [ "$GH_MAIN_FINAL" != "$FG_MAIN_FINAL" ]; then
local PREV
PREV=$(cat "$DIVERGENCE_FILE" 2>/dev/null || echo "0")
if [ "$PREV" = "alerted" ]; then
log "DIVERGENCE: still diverged (already alerted)"
else
local COUNT=$((PREV + 1))
echo "$COUNT" > "$DIVERGENCE_FILE"
log "DIVERGENCE: cycle $COUNT — GitHub=$GH_MAIN_FINAL Forgejo=$FG_MAIN_FINAL"
if [ "$COUNT" -ge 2 ]; then
local BOT_TOKEN ADMIN_CHAT
BOT_TOKEN=$(cat /opt/teleo-eval/secrets/telegram-bot-token 2>/dev/null || true)
ADMIN_CHAT=$(cat /opt/teleo-eval/secrets/admin-chat-id 2>/dev/null || true)
if [ -n "$BOT_TOKEN" ] && [ -n "$ADMIN_CHAT" ]; then
local ALERT_MSG
ALERT_MSG=$(python3 -c "
import json, sys
msg = '⚠️ Mirror divergence detected (' + sys.argv[5] + ')\\n\\n'
msg += f'GitHub main: {sys.argv[1][:8]}\\n'
msg += f'Forgejo main: {sys.argv[2][:8]}\\n'
msg += f'Diverged for {sys.argv[3]} consecutive cycles ({int(sys.argv[3])*2} min)\\n\\n'
msg += 'Check sync-mirror.sh logs: /opt/teleo-eval/logs/sync.log'
print(json.dumps({'chat_id': sys.argv[4], 'text': msg, 'parse_mode': 'HTML'}))
" "$GH_MAIN_FINAL" "$FG_MAIN_FINAL" "$COUNT" "$ADMIN_CHAT" "$REPO_TAG")
if curl -sf -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$ALERT_MSG" >> "$LOG" 2>&1; then
log "DIVERGENCE: alert sent to admin"
echo "alerted" > "$DIVERGENCE_FILE"
else
log "WARN: Failed to send divergence alert (will retry next cycle)"
fi
else
log "WARN: Cannot send divergence alert — missing bot token or admin chat ID"
fi
fi
fi
else
if [ -f "$DIVERGENCE_FILE" ]; then
local PREV
PREV=$(cat "$DIVERGENCE_FILE" 2>/dev/null || echo "0")
if [ "$PREV" != "0" ]; then
log "DIVERGENCE: resolved — repos back in sync"
fi
rm -f "$DIVERGENCE_FILE"
fi
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Main: process each configured mirror in sequence.
# A failure on one repo doesn't block subsequent repos — sync_repo returns 0
# on most error paths to keep the loop going.
# ─────────────────────────────────────────────────────────────────────────────
REPO_TAG="main"
log "Starting sync cycle"
# Step 0: self-heal any gh-pr-* PR rows missing github_pr.
# Runs FIRST — before per-repo work (branch-mirror loop, auto-create-PR block).
# Recovers from races/transient failures in Step 4.5's one-shot link UPDATE.
# Idempotent: SELECT empty when clean, zero-cost path. Same SELECT/UPDATE
# heals historical orphans (PR 4066 picked up on first cron tick post-deploy)
# and future races on subsequent ticks. The branch name encodes the GitHub PR
# number deterministically (gh-pr-{N}/...) so no API call is required.
if [ -f "$PIPELINE_DB" ]; then
sqlite3 -separator '|' "$PIPELINE_DB" \
"SELECT number, branch FROM prs WHERE branch LIKE 'gh-pr-%' AND github_pr IS NULL;" \
2>/dev/null | while IFS='|' read -r pr_num branch; do
# Regex requires >=1 digit — empty/non-numeric branches fail to parse here,
# not just at the empty-guard below. Keeps SQL-integer-safety load-bearing
# on the regex alone. [0-9][0-9]* is the portable BRE form of [0-9]+,
# works on both GNU sed (VPS) and BSD sed (dev macs).
gh_pr_num=$(echo "$branch" | sed -n 's|^gh-pr-\([0-9][0-9]*\)/.*|\1|p')
[ -z "$gh_pr_num" ] && continue
# Both interpolated values are integer-validated upstream (pr_num from
# INTEGER `number` column, gh_pr_num from regex above). No parametric
# binding available in bash sqlite3 — safety relies on those invariants.
if sqlite3 "$PIPELINE_DB" \
"UPDATE prs SET github_pr = $gh_pr_num, source_channel = 'github' WHERE number = $pr_num;" \
2>/dev/null; then
log "self-heal: linked Forgejo PR #$pr_num -> GitHub PR #$gh_pr_num"
fi
done
fi
for entry in "${MIRROR_REPOS[@]}"; do
# Read the 4 fields. `read` splits on $IFS (whitespace) by default.
read -r forgejo_repo github_repo bare_path mode <<< "$entry"
sync_repo "$forgejo_repo" "$github_repo" "$bare_path" "$mode"
done
REPO_TAG="main"
log "Sync cycle complete"

View file

@ -28,12 +28,9 @@ import sqlite3
import json
# Map PR status to Clay's operation color palette
# extract (cyan), new (green), enrich (amber), challenge (red-orange),
# decision (violet), infra (grey)
STATUS_TO_OPERATION = {
'merged': 'new', # green — new knowledge merged
'approved': 'enrich', # amber — approved, enriching KB
# Non-merged statuses map directly to operation — no semantic classification yet.
NON_MERGED_STATUS_TO_OPERATION = {
'approved': 'new', # about to become knowledge
'open': 'extract', # cyan — new extraction in progress
'validating': 'extract', # cyan — being validated
'reviewing': 'extract', # cyan — under review
@ -43,6 +40,51 @@ STATUS_TO_OPERATION = {
'conflict': 'challenge', # red-orange — conflict detected
}
# Maintenance commit_types that land on main but don't represent new knowledge.
_MAINTENANCE_COMMIT_TYPES = {'fix', 'pipeline', 'reweave'}
def classify_pr_operation(status, commit_type, branch, description=None):
"""Derive a Timeline operation from a PR row.
Priority order for MERGED PRs (commit_type wins over branch prefix
extract/* branches with commit_type='enrich' or 'challenge' classify
by commit_type, matching the contributor-role wiring fix):
1. commit_type == 'challenge' OR branch.startswith('challenge/') OR
description contains 'challenged_by' 'challenge'
2. commit_type == 'enrich' OR branch.startswith('enrich/' | 'reweave/')
'enrich'
3. commit_type in _MAINTENANCE_COMMIT_TYPES 'infra'
4. default (commit_type='knowledge'|'extract'|'research'|'entity' or
anything else) 'new'
For non-merged PRs, falls back to NON_MERGED_STATUS_TO_OPERATION.
"""
commit_type = (commit_type or '').lower()
branch = branch or ''
description_lower = (description or '').lower()
if status != 'merged':
return NON_MERGED_STATUS_TO_OPERATION.get(status, 'infra')
# Challenge takes precedence — the signal is inherently more specific.
if (commit_type == 'challenge'
or branch.startswith('challenge/')
or 'challenged_by' in description_lower):
return 'challenge'
if (commit_type == 'enrich'
or branch.startswith('enrich/')
or branch.startswith('reweave/')):
return 'enrich'
if commit_type in _MAINTENANCE_COMMIT_TYPES:
return 'infra'
# Default: legacy 'knowledge', new 'extract', 'research', 'entity',
# unknown/null commit_type → treat as new knowledge.
return 'new'
# Map audit_log stage to operation type
STAGE_TO_OPERATION = {
'ingest': 'extract',
@ -118,6 +160,8 @@ async def handle_activity(request):
Query params:
limit (int, default 100, max 500): number of events to return
cursor (ISO timestamp): return events older than this timestamp
type (str, optional): comma-separated operation types to include
(extract|new|enrich|challenge|infra). If absent, returns all types.
Derives events from two sources:
1. prs table per-PR events with domain, agent, status
@ -131,6 +175,13 @@ async def handle_activity(request):
limit = 100
cursor = request.query.get('cursor')
type_param = request.query.get('type', '').strip()
allowed_ops = None
if type_param:
allowed_ops = {t.strip() for t in type_param.split(',') if t.strip()}
if not allowed_ops:
allowed_ops = None
db_path = request.app['db_path']
try:
@ -143,22 +194,27 @@ async def handle_activity(request):
# Each PR generates events at created_at and merged_at timestamps
pr_query = """
SELECT number, status, domain, agent, branch, source_path,
created_at, merged_at
created_at, merged_at, source_channel, commit_type,
description
FROM prs
WHERE {where_clause}
ORDER BY COALESCE(merged_at, created_at) DESC
LIMIT ?
"""
# Over-fetch when filtering by type so we have enough matching rows after
# post-build filtering. Cap at 2000 to avoid runaway queries.
fetch_limit = min(2000, limit * 5) if allowed_ops else limit + 1
if cursor:
rows = conn.execute(
pr_query.format(where_clause="COALESCE(merged_at, created_at) < ?"),
(cursor, limit + 1)
(cursor, fetch_limit)
).fetchall()
else:
rows = conn.execute(
pr_query.format(where_clause="1=1"),
(limit + 1,)
(fetch_limit,)
).fetchall()
# Known knowledge agents for branch-prefix inference
@ -166,7 +222,14 @@ async def handle_activity(request):
for row in rows:
row_dict = dict(row)
operation = STATUS_TO_OPERATION.get(row_dict['status'], 'infra')
operation = classify_pr_operation(
row_dict['status'],
row_dict.get('commit_type'),
row_dict.get('branch'),
row_dict.get('description'),
)
if allowed_ops and operation not in allowed_ops:
continue
description = pr_description(row_dict)
# Use merged_at if available (more interesting event), else created_at
@ -189,6 +252,7 @@ async def handle_activity(request):
'description': description,
'status': row_dict['status'],
'pr_number': row_dict['number'],
'source_channel': row_dict.get('source_channel') or 'unknown',
})
# Source 2: Audit log events (secondary — pipeline-level)
@ -217,6 +281,8 @@ async def handle_activity(request):
for row in audit_rows:
row_dict = dict(row)
operation = STAGE_TO_OPERATION.get(row_dict['stage'], 'infra')
if allowed_ops and operation not in allowed_ops:
continue
description = audit_description(row_dict)
events.append({
@ -228,6 +294,7 @@ async def handle_activity(request):
'description': description,
'status': None,
'pr_number': None,
'source_channel': None, # audit events not tied to a PR
})
conn.close()

View file

@ -0,0 +1,423 @@
"""Activity feed API — serves contribution events from pipeline.db."""
import re
import sqlite3
import math
import time
from aiohttp import web
DB_PATH = "/opt/teleo-eval/pipeline/pipeline.db"
_cache = {"data": None, "ts": 0}
CACHE_TTL = 60 # 1 minute — activity should feel fresh
# commit_types we surface in the activity feed. `pipeline` is system
# maintenance (reweave/fix auto-runs, zombie cleanup) and stays hidden.
_FEED_COMMIT_TYPES = ("knowledge", "enrich", "challenge", "research", "entity", "extract", "reweave")
# Source-archive slugs follow YYYY-MM-DD-publisher-topic-HASH4 — they're
# inbox archive filenames, not claim slugs. Used as a fallback signal when
# branch/description heuristics miss (e.g. populated descriptions that
# happen to be source titles, not claim insights).
_SOURCE_SLUG_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-.+-[a-f0-9]{4}$")
def _get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout = 10000")
return conn
def _is_source_slug(slug):
return bool(slug and _SOURCE_SLUG_PATTERN.match(slug))
def _classify_event(branch, description, commit_type, candidate_slug=None):
"""Return one of: create | enrich | challenge | source | session_digest | None.
Source-archive PRs are extract/* branches that filed a source into
inbox/archive/ but didn't produce a claim. Session-digest PRs are
agent research/entity commits with no per-claim description they
represent session-level rollups, not specific knowledge artifacts.
"""
commit_type_l = (commit_type or "").lower()
branch = branch or ""
description_lower = (description or "").lower()
has_desc = bool(description and description.strip())
if commit_type_l not in _FEED_COMMIT_TYPES:
return None
# Explicit challenge signals win first.
if (commit_type_l == "challenge"
or branch.startswith("challenge/")
or "challenged_by" in description_lower):
return "challenge"
# Enrichment: reweave edge-connects, enrich/ branches, or commit_type=enrich.
if (commit_type_l == "enrich"
or branch.startswith("enrich/")
or branch.startswith("reweave/")):
return "enrich"
# Research and entity commits with no description are session-level
# rollups (e.g. astra/research-2026-05-11). They have no claim to
# link to — surface as session_digest, not as a phantom create.
if commit_type_l in ("research", "entity") and not has_desc:
return "session_digest"
# Source-only: extract/* with no claim description means inbox archive
# landed but no domain claim was written.
if branch.startswith("extract/") and not has_desc:
return "source"
# Belt-and-suspenders: if the slug we'd surface to the frontend looks
# like an inbox archive filename (date-prefix-hash), treat as source
# regardless of branch/commit_type/description state. Catches cases
# where description leaked but is just a source title, not a claim.
if _is_source_slug(candidate_slug):
return "source"
# Everything else with a description is a new claim.
return "create"
# Internal classifier value -> canonical `kind` enum returned to frontend.
_KIND_MAP = {
"create": "claim_merged",
"enrich": "claim_enriched",
"challenge": "claim_challenged",
"source": "source_archived",
"session_digest": "session_digest",
}
def _archive_slug_from_branch(branch):
"""For extract/YYYY-MM-DD-...-HASH4, return YYYY-MM-DD-... (keep date,
drop the 4-hex hash suffix). Matches inbox/archive filename convention.
"""
if not branch or "/" not in branch:
return ""
slug = branch.split("/", 1)[1]
return re.sub(r"-[a-f0-9]{4}$", "", slug)
def _source_target_url(domain, archive_slug):
"""Forgejo blob URL for an archived source file. Falls back to the
repo-wide inbox/archive directory when domain is unknown so the link
still resolves to something useful instead of a 404.
"""
if not archive_slug:
return None
domain = (domain or "").strip()
if not domain or domain == "unknown":
return "https://git.livingip.xyz/teleo/teleo-codex/src/branch/main/inbox/archive"
return (
"https://git.livingip.xyz/teleo/teleo-codex/src/branch/main/inbox/archive/"
f"{domain}/{archive_slug}.md"
)
def _claim_target_url(claim_slug):
if not claim_slug:
return None
return f"/claims/{claim_slug}"
# Canonical clickthrough URL for an activity-feed event.
#
# Every merged PR in the pipeline.db `prs` table lives on Forgejo at
# git.livingip.xyz/teleo/teleo-codex/pulls/{number}. A small subset (3 of
# 4094 as of 2026-05-13) was additionally mirrored to GitHub and has
# prs.github_pr populated. Prefer GitHub when available (more public-facing
# surface), fall back to Forgejo so every row has a real destination
# instead of None (which makes the frontend whole-row overlay no-op and
# leaves pipeline-attributed events looking dead-on-click).
def _pr_url(pr_number, github_pr):
if github_pr:
return f"https://github.com/living-ip/teleo-codex/pull/{github_pr}"
if pr_number:
return f"https://git.livingip.xyz/teleo/teleo-codex/pulls/{pr_number}"
return None
# Canonicalize contributor labels so frontend links resolve to real
# /contributors/{handle} pages. Pipeline writers (extract.py, manual edits,
# the old backfill_submitted_by.py) historically wrote mixed-case agent
# names with a trailing decorator into prs.submitted_by — e.g.
# "Vida (self-directed)", "pipeline (reweave)", or "@m3taversal".
# These decorated strings do not exist as contributors and 404 the profile
# page. Strip the trailing parenthetical wholesale: valid handles match
# ^[a-z0-9][a-z0-9_-]{0,38}$ (see pipeline/lib/attribution._HANDLE_RE) and
# cannot contain parens, so this is lossless.
_TRAILING_PAREN_RE = re.compile(r"\s*\([^)]*\)\s*$")
def _canonicalize(raw):
if not raw:
return ""
h = raw.strip().lower().lstrip("@")
h = _TRAILING_PAREN_RE.sub("", h).strip()
return h
def _normalize_contributor(submitted_by, agent):
name = _canonicalize(submitted_by)
if name:
return name
name = _canonicalize(agent)
if name and name != "pipeline":
return name
return "pipeline"
def _summary_from_branch(branch):
if not branch:
return ""
parts = branch.split("/", 1)
if len(parts) < 2:
return ""
slug = parts[1]
slug = re.sub(r"^[\d-]+-", "", slug) # strip date prefix
slug = re.sub(r"-[a-f0-9]{4}$", "", slug) # strip hash suffix
return slug.replace("-", " ").strip().capitalize()
def _extract_claim_slugs(description, branch=None):
if not description:
if branch:
parts = branch.split("/", 1)
if len(parts) > 1:
return [parts[1]]
return []
titles = [t.strip() for t in description.split("|") if t.strip()]
slugs = []
for title in titles:
slug = title.lower().strip()
slug = "".join(c if c.isalnum() or c in (" ", "-") else "" for c in slug)
slug = slug.replace(" ", "-").strip("-")
if len(slug) > 10:
slugs.append(slug)
return slugs
def _hot_score(challenge_count, enrich_count, signal_count, hours_since):
numerator = challenge_count * 3 + enrich_count * 2 + signal_count
denominator = max(hours_since, 0.5) ** 1.5
return numerator / denominator
def _build_events():
conn = _get_conn()
try:
placeholders = ",".join("?" * len(_FEED_COMMIT_TYPES))
rows = conn.execute(f"""
SELECT p.number, p.branch, p.domain, p.agent, p.submitted_by,
p.merged_at, p.description, p.commit_type, p.cost_usd,
p.source_channel, p.source_path, p.github_pr
FROM prs p
WHERE p.status = 'merged'
AND p.commit_type IN ({placeholders})
AND p.merged_at IS NOT NULL
ORDER BY p.merged_at DESC
LIMIT 2000
""", _FEED_COMMIT_TYPES).fetchall()
events = []
claim_activity = {} # slug -> {challenges, enriches, signals, first_seen}
for row in rows:
slugs = _extract_claim_slugs(row["description"], row["branch"])
candidate_slug = slugs[0] if slugs else ""
event_type = _classify_event(
row["branch"], row["description"], row["commit_type"],
candidate_slug=candidate_slug,
)
if not event_type:
continue
contributor = _normalize_contributor(row["submitted_by"], row["agent"])
# Hide pipeline-attributed events (reweave/*, ingestion/*) from the
# public activity feed. They're automation maintenance, not
# contributions — the daemon re-knits the graph nightly and ingests
# external sources. Internal diagnostics + CI math still see these
# rows in prs / contribution_events; only the public timeline drops
# them. Mirrors the existing _FEED_COMMIT_TYPES filter (which hides
# commit_type='pipeline') along the contributor axis.
if contributor == "pipeline":
continue
merged_at = row["merged_at"] or ""
domain = row["domain"] or "unknown"
kind = _KIND_MAP.get(event_type, event_type)
ci_map = {
"create": 0.35, "enrich": 0.25, "challenge": 0.40,
"source": 0.15, "session_digest": 0.05,
}
ci_earned = ci_map.get(event_type, 0)
# Source events never carry a claim_slug — no claim was written.
# target_url points at the archived file on Forgejo instead.
if event_type == "source":
archive_slug = _archive_slug_from_branch(row["branch"])
summary_text = _summary_from_branch(row["branch"])
source_display_slug = (
summary_text.lower().replace(" ", "-") or row["branch"]
)
events.append({
"kind": kind,
"type": "source",
"target_url": _source_target_url(domain, archive_slug),
"claim_slug": "",
"source_slug": source_display_slug,
"domain": domain,
"contributor": contributor,
"timestamp": merged_at,
"ci_earned": round(ci_earned, 2),
"summary": summary_text,
"pr_number": row["number"],
"pr_url": _pr_url(row["number"], row["github_pr"]),
"source_channel": row["source_channel"] or "unknown",
})
continue
# Session digests have no clickthrough surface yet (per-agent
# session pages not built). target_url=null so frontend renders
# plain text instead of a broken /claims/research-... link.
if event_type == "session_digest":
summary_text = _summary_from_branch(row["branch"]) or "Research session"
events.append({
"kind": kind,
"type": "session_digest",
"target_url": None,
"claim_slug": "",
"domain": domain,
"contributor": contributor,
"timestamp": merged_at,
"ci_earned": round(ci_earned, 2),
"summary": summary_text,
"pr_number": row["number"],
"pr_url": _pr_url(row["number"], row["github_pr"]),
"source_channel": row["source_channel"] or "unknown",
})
continue
for slug in slugs:
if slug not in claim_activity:
claim_activity[slug] = {
"challenges": 0, "enriches": 0, "signals": 0,
"first_seen": merged_at,
}
if event_type == "challenge":
claim_activity[slug]["challenges"] += 1
elif event_type == "enrich":
claim_activity[slug]["enriches"] += 1
else:
claim_activity[slug]["signals"] += 1
summary_text = ""
if row["description"]:
first_title = row["description"].split("|")[0].strip()
if len(first_title) > 120:
first_title = first_title[:117] + "..."
summary_text = first_title
elif row["branch"]:
summary_text = _summary_from_branch(row["branch"])
for slug in (slugs[:1] if slugs else [""]):
events.append({
"kind": kind,
"type": event_type,
"target_url": _claim_target_url(slug),
"claim_slug": slug,
"domain": domain,
"contributor": contributor,
"timestamp": merged_at,
"ci_earned": round(ci_earned, 2),
"summary": summary_text,
"pr_number": row["number"],
"pr_url": _pr_url(row["number"], row["github_pr"]),
"source_channel": row["source_channel"] or "unknown",
})
return events, claim_activity
finally:
conn.close()
def _sort_events(events, claim_activity, sort_mode, now_ts):
if sort_mode == "recent":
events.sort(key=lambda e: e["timestamp"], reverse=True)
elif sort_mode == "hot":
def hot_key(e):
slug = e["claim_slug"]
ca = claim_activity.get(slug, {"challenges": 0, "enriches": 0, "signals": 0})
try:
from datetime import datetime
evt_time = datetime.fromisoformat(e["timestamp"].replace("Z", "+00:00"))
hours = (now_ts - evt_time.timestamp()) / 3600
except (ValueError, AttributeError):
hours = 9999
return _hot_score(ca["challenges"], ca["enriches"], ca["signals"], hours)
events.sort(key=hot_key, reverse=True)
elif sort_mode == "important":
type_rank = {
"challenge": 0, "enrich": 1, "create": 2,
"source": 3, "session_digest": 4,
}
events.sort(key=lambda e: (type_rank.get(e["type"], 5), -len(e["summary"])))
return events
async def handle_activity_feed(request):
sort_mode = request.query.get("sort", "recent")
if sort_mode not in ("hot", "recent", "important"):
sort_mode = "recent"
domain = request.query.get("domain", "")
contributor = request.query.get("contributor", "")
type_param = request.query.get("type", "")
type_filter = {t.strip() for t in type_param.split(",") if t.strip()} if type_param else None
try:
limit = min(int(request.query.get("limit", "20")), 100)
except ValueError:
limit = 20
try:
offset = max(int(request.query.get("offset", "0")), 0)
except ValueError:
offset = 0
now = time.time()
if _cache["data"] is None or (now - _cache["ts"]) > CACHE_TTL:
_cache["data"] = _build_events()
_cache["ts"] = now
events, claim_activity = _cache["data"]
filtered = events
if domain:
filtered = [e for e in filtered if e["domain"] == domain]
if contributor:
filtered = [e for e in filtered if e["contributor"] == contributor]
if type_filter:
# Accept both legacy `type` values (create/enrich/challenge/source/
# session_digest) and canonical `kind` values (claim_merged/etc.) so
# callers can migrate at their own pace.
filtered = [
e for e in filtered
if e["type"] in type_filter or e.get("kind") in type_filter
]
sorted_events = _sort_events(list(filtered), claim_activity, sort_mode, now)
total = len(sorted_events)
page = sorted_events[offset:offset + limit]
return web.json_response({
"events": page,
"total": total,
"sort": sort_mode,
"offset": offset,
"limit": limit,
}, headers={"Access-Control-Allow-Origin": "*"})
def register(app):
app.router.add_get("/api/activity-feed", handle_activity_feed)

View file

@ -67,6 +67,8 @@ def check_agent_health(conn: sqlite3.Connection) -> list[dict]:
now = datetime.now(timezone.utc)
for r in rows:
agent = r["agent"]
if agent in ("unknown", None):
continue
latest = r["latest"]
if not latest:
continue
@ -157,8 +159,17 @@ def check_quality_regression(conn: sqlite3.Connection) -> list[dict]:
return alerts
_ALLOWED_DIM_EXPRS = frozenset({
"json_extract(detail, '$.agent')",
"json_extract(detail, '$.domain')",
"COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent'))",
})
def _check_approval_by_dimension(conn, alerts, dim_name, dim_expr):
"""Check approval rate regression grouped by a dimension (agent or domain)."""
"""Check approval rate regression grouped by a dimension. dim_expr must be in _ALLOWED_DIM_EXPRS."""
if dim_expr not in _ALLOWED_DIM_EXPRS:
raise ValueError(f"untrusted dim_expr: {dim_expr}")
# 7-day baseline per dimension
baseline_rows = conn.execute(
f"""SELECT {dim_expr} as dim_val,
@ -257,24 +268,22 @@ def check_rejection_spike(conn: sqlite3.Connection) -> list[dict]:
"""Detect single rejection reason exceeding REJECTION_SPIKE_RATIO of recent rejections."""
alerts = []
# Total rejections in 24h
# Total rejected PRs in 24h (prs.eval_issues is the canonical source — Epimetheus 2026-04-02)
total = conn.execute(
"""SELECT COUNT(*) as n FROM audit_log
WHERE stage='evaluate'
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-24 hours')"""
"""SELECT COUNT(*) as n FROM prs
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND created_at > datetime('now', '-24 hours')"""
).fetchone()["n"]
if total < 10:
return alerts # Not enough data
# Count by rejection tag
# Count by rejection tag from prs.eval_issues
tags = conn.execute(
"""SELECT value as tag, COUNT(*) as cnt
FROM audit_log, json_each(json_extract(detail, '$.issues'))
WHERE stage='evaluate'
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-24 hours')
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND created_at > datetime('now', '-24 hours')
GROUP BY tag ORDER BY cnt DESC"""
).fetchall()
@ -306,16 +315,13 @@ def check_stuck_loops(conn: sqlite3.Connection) -> list[dict]:
"""Detect agents repeatedly failing on the same rejection reason."""
alerts = []
# COALESCE: rejection events use $.agent, eval events use $.domain_agent (Epimetheus 2026-03-28)
# Agent + rejection reason from prs table directly (Epimetheus correction 2026-04-02)
rows = conn.execute(
"""SELECT COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent,
value as tag,
COUNT(*) as cnt
FROM audit_log, json_each(json_extract(detail, '$.issues'))
WHERE stage='evaluate'
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-6 hours')
AND COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) IS NOT NULL
"""SELECT agent, value as tag, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND agent IS NOT NULL
AND created_at > datetime('now', '-6 hours')
GROUP BY agent, tag
HAVING cnt > ?""",
(STUCK_LOOP_THRESHOLD,),
@ -403,16 +409,13 @@ def check_domain_rejection_patterns(conn: sqlite3.Connection) -> list[dict]:
"""Track rejection reason shift per domain — surfaces domain maturity issues."""
alerts = []
# Per-domain rejection breakdown in 24h
# Per-domain rejection breakdown in 24h from prs table (Epimetheus correction 2026-04-02)
rows = conn.execute(
"""SELECT json_extract(detail, '$.domain') as domain,
value as tag,
COUNT(*) as cnt
FROM audit_log, json_each(json_extract(detail, '$.issues'))
WHERE stage='evaluate'
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', '-24 hours')
AND json_extract(detail, '$.domain') IS NOT NULL
"""SELECT domain, value as tag, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND domain IS NOT NULL
AND created_at > datetime('now', '-24 hours')
GROUP BY domain, tag
ORDER BY domain, cnt DESC"""
).fetchall()
@ -464,12 +467,11 @@ def generate_failure_report(conn: sqlite3.Connection, agent: str, hours: int = 2
hours = int(hours) # defensive — callers should pass int, but enforce it
rows = conn.execute(
"""SELECT value as tag, COUNT(*) as cnt,
GROUP_CONCAT(DISTINCT json_extract(detail, '$.pr')) as pr_numbers
FROM audit_log, json_each(json_extract(detail, '$.issues'))
WHERE stage='evaluate'
AND event IN ('changes_requested','domain_rejected','tier05_rejected')
AND COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) = ?
AND timestamp > datetime('now', ? || ' hours')
GROUP_CONCAT(DISTINCT number) as pr_numbers
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND agent = ?
AND created_at > datetime('now', ? || ' hours')
GROUP BY tag ORDER BY cnt DESC
LIMIT 5""",
(agent, f"-{hours}"),

View file

@ -26,13 +26,6 @@ async def handle_check(request):
conn = request.app["_alerting_conn_func"]()
try:
alerts = run_all_checks(conn)
except Exception as e:
logger.error("Check failed: %s", e)
return web.json_response({"error": str(e)}, status=500)
global _active_alerts, _last_check
_active_alerts = alerts
_last_check = datetime.now(timezone.utc).isoformat()
# Generate failure reports for agents with stuck loops
failure_reports = {}
@ -41,6 +34,15 @@ async def handle_check(request):
report = generate_failure_report(conn, agent)
if report:
failure_reports[agent] = report
except Exception as e:
logger.error("Check failed: %s", e)
return web.json_response({"error": str(e)}, status=500)
finally:
conn.close()
global _active_alerts, _last_check
_active_alerts = alerts
_last_check = datetime.now(timezone.utc).isoformat()
result = {
"checked_at": _last_check,
@ -104,10 +106,15 @@ async def handle_api_failure_report(request):
hours: lookback window (default 24)
"""
agent = request.match_info["agent"]
hours = int(request.query.get("hours", "24"))
try:
hours = min(int(request.query.get("hours", "24")), 168)
except ValueError:
hours = 24
conn = request.app["_alerting_conn_func"]()
try:
report = generate_failure_report(conn, agent, hours)
finally:
conn.close()
if not report:
return web.json_response({"agent": agent, "status": "no_rejections", "period_hours": hours})

View file

@ -22,10 +22,13 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "pipeline"))
from aiohttp import web
from review_queue_routes import register_review_queue_routes
from daily_digest_routes import register_daily_digest_routes
from response_audit_routes import register_response_audit_routes, RESPONSE_AUDIT_PUBLIC_PATHS
from kb_claim_routes import KB_CLAIM_PUBLIC_PATHS, KB_CLAIM_PUBLIC_PREFIXES, register_kb_claim_routes
from kb_proposal_routes import KB_PROPOSAL_PUBLIC_PATHS, register_kb_proposal_routes
from leaderboard_routes import LEADERBOARD_PUBLIC_PATHS, register_leaderboard_routes
from lib.search import search as kb_search, embed_query, search_qdrant
from response_audit_routes import RESPONSE_AUDIT_PUBLIC_PATHS, register_response_audit_routes
from review_queue_routes import register_review_queue_routes
logger = logging.getLogger("argus")
@ -42,7 +45,7 @@ API_KEY_FILE = Path(os.environ.get("ARGUS_API_KEY_FILE", "/opt/teleo-eval/secret
# Endpoints that skip auth (dashboard is public for now, can lock later)
_PUBLIC_PATHS = frozenset({"/", "/prs", "/ops", "/health", "/agents", "/epistemic", "/legacy", "/audit", "/api/metrics", "/api/snapshots", "/api/vital-signs",
"/api/contributors", "/api/domains", "/api/audit", "/api/yield", "/api/cost-per-claim", "/api/fix-rates", "/api/compute-profile", "/api/review-queue", "/api/daily-digest"})
"/api/contributors", "/api/domains", "/api/audit", "/api/yield", "/api/cost-per-claim", "/api/fix-rates", "/api/compute-profile", "/api/review-queue", "/api/daily-digest", "/api/search"})
def _get_db() -> sqlite3.Connection:
@ -508,7 +511,15 @@ def _load_secret(path: Path) -> str | None:
@web.middleware
async def auth_middleware(request, handler):
"""API key check. Public paths skip auth. Protected paths require X-Api-Key header."""
if request.path in _PUBLIC_PATHS or request.path in RESPONSE_AUDIT_PUBLIC_PATHS or request.path.startswith("/api/response-audit/"):
if (
request.path in _PUBLIC_PATHS
or request.path in RESPONSE_AUDIT_PUBLIC_PATHS
or request.path in LEADERBOARD_PUBLIC_PATHS
or request.path in KB_PROPOSAL_PUBLIC_PATHS
or request.path in KB_CLAIM_PUBLIC_PATHS
or request.path.startswith(KB_CLAIM_PUBLIC_PREFIXES)
or request.path.startswith("/api/response-audit/")
):
return await handler(request)
expected = request.app.get("api_key")
if not expected:
@ -663,38 +674,115 @@ async def handle_api_domains(request):
return web.json_response({"domains": breakdown})
async def handle_api_search(request):
"""GET /api/search — semantic search over claims via Qdrant + graph expansion.
def _qdrant_hits_to_results(hits, include_expanded=False):
"""Shape raw Qdrant hits into Ship's chat-API contract."""
results = []
for h in hits:
payload = h.get("payload", {}) or {}
path = payload.get("claim_path", "") or ""
slug = path.rsplit("/", 1)[-1]
if slug.endswith(".md"):
slug = slug[:-3]
results.append({
"slug": slug,
"path": path,
"title": payload.get("claim_title", ""),
"domain": payload.get("domain"),
"confidence": payload.get("confidence"),
"score": round(float(h.get("score", 0.0) or 0.0), 4),
"body_excerpt": payload.get("snippet", "") or "",
})
return results
Query params:
async def handle_api_search(request):
"""Semantic search over claims via Qdrant.
POST contract (Ship's chat API):
body: {"query": str, "limit": int, "min_score": float?, "domain": str?, "confidence": str?, "exclude": [str]?}
response: {"query": str, "results": [{"slug","path","title","domain","confidence","score","body_excerpt"}], "total": int}
GET (legacy + hackathon debug):
q: search query (required)
domain: filter by domain (optional)
confidence: filter by confidence level (optional)
limit: max results, default 10 (optional)
exclude: comma-separated claim paths to exclude (optional)
expand: enable graph expansion, default true (optional)
limit, domain, confidence, exclude, expand
min_score: if set, bypasses two-pass lib threshold (default lib behavior otherwise)
"""
if request.method == "POST":
try:
body = await request.json()
except Exception:
return web.json_response({"error": "invalid JSON body"}, status=400)
query = (body.get("query") or "").strip()
if not query:
return web.json_response({"error": "query required"}, status=400)
try:
limit = min(int(body.get("limit") or 5), 50)
except (TypeError, ValueError):
return web.json_response({"error": "limit must be int"}, status=400)
try:
min_score = float(body.get("min_score") if body.get("min_score") is not None else 0.25)
except (TypeError, ValueError):
return web.json_response({"error": "min_score must be float"}, status=400)
domain = body.get("domain")
confidence = body.get("confidence")
exclude = body.get("exclude") or None
vector = embed_query(query)
if vector is None:
return web.json_response({"error": "embedding failed"}, status=502)
hits = search_qdrant(vector, limit=limit, domain=domain,
confidence=confidence, exclude=exclude,
score_threshold=min_score)
results = _qdrant_hits_to_results(hits)
return web.json_response({"query": query, "results": results, "total": len(results)})
# GET path
query = request.query.get("q", "").strip()
if not query:
return web.json_response({"error": "q parameter required"}, status=400)
domain = request.query.get("domain")
confidence = request.query.get("confidence")
try:
limit = min(int(request.query.get("limit", "10")), 50)
except ValueError:
return web.json_response({"error": "limit must be int"}, status=400)
exclude_raw = request.query.get("exclude", "")
exclude = [p.strip() for p in exclude_raw.split(",") if p.strip()] if exclude_raw else None
expand = request.query.get("expand", "true").lower() != "false"
min_score_raw = request.query.get("min_score")
# Use shared search library (Layer 1 + Layer 2)
if min_score_raw is not None:
try:
min_score = float(min_score_raw)
except ValueError:
return web.json_response({"error": "min_score must be float"}, status=400)
vector = embed_query(query)
if vector is None:
return web.json_response({"error": "embedding failed"}, status=502)
hits = search_qdrant(vector, limit=limit, domain=domain,
confidence=confidence, exclude=exclude,
score_threshold=min_score)
direct = _qdrant_hits_to_results(hits)
return web.json_response({
"query": query,
"direct_results": direct,
"expanded_results": [],
"total": len(direct),
})
# Default GET: Layer 1 + Layer 2 via lib
result = kb_search(query, expand=expand,
domain=domain, confidence=confidence, exclude=exclude)
if "error" in result:
error = result["error"]
if error == "embedding_failed":
return web.json_response({"error": "embedding failed"}, status=502)
return web.json_response({"error": error}, status=500)
return web.json_response(result)
@ -2268,6 +2356,7 @@ def create_app() -> web.Application:
app.router.add_get("/api/contributors", handle_api_contributors)
app.router.add_get("/api/domains", handle_api_domains)
app.router.add_get("/api/search", handle_api_search)
app.router.add_post("/api/search", handle_api_search)
app.router.add_get("/api/audit", handle_api_audit)
app.router.add_get("/audit", handle_audit_page)
app.router.add_post("/api/usage", handle_api_usage)
@ -2277,9 +2366,28 @@ def create_app() -> web.Application:
register_dashboard_routes(app, lambda: _conn_from_app(app))
register_review_queue_routes(app)
register_daily_digest_routes(app, db_path=str(DB_PATH))
register_kb_claim_routes(app)
register_kb_proposal_routes(app)
# Portfolio
from dashboard_portfolio import register_portfolio_routes
register_portfolio_routes(app, lambda: _conn_from_app(app))
# Response audit - cost tracking + reasoning traces
app["db_path"] = str(DB_PATH)
register_response_audit_routes(app)
# Event-sourced leaderboard (Phase B — reads contribution_events directly)
register_leaderboard_routes(app)
# Timeline activity feed (per-PR + audit_log events for dashboard v2)
from activity_endpoint import handle_activity
app.router.add_get("/api/activity", handle_activity)
# Gamification activity feed (hot/recent/important sort)
from activity_feed_api import register as register_activity_feed
register_activity_feed(app)
# Claims browser + detail
from claims_api import register_claims_routes
register_claims_routes(app)
# Contributor profile (handle lookup, leaderboard with action CI)
from contributor_profile_api import register_contributor_routes
register_contributor_routes(app)
app.on_cleanup.append(_cleanup)
return app

View file

@ -79,12 +79,16 @@ def main():
fm = sfm
break
# `submitted_by` is stored as a canonical handle (lowercase, no @, no
# "(self-directed)" / "(reweave)" suffix). Read consumers normalize via
# attribution.normalize_handle, so writing decorated strings produces
# downstream 404s on /contributors/{handle} (livingip-web timeline).
if fm:
proposed_by = fm.get("proposed_by")
intake_tier = fm.get("intake_tier")
if proposed_by:
contributor = proposed_by.strip().strip('"').strip("'")
contributor = proposed_by.strip().strip('"').strip("'").lower().lstrip("@")
elif intake_tier == "research-task":
# Derive agent from branch prefix
prefix = branch.split("/", 1)[0] if "/" in branch else "unknown"
@ -94,13 +98,12 @@ def main():
"clay": "clay", "astra": "astra", "leo": "leo",
"reweave": "pipeline",
}
agent = agent_map.get(prefix, prefix)
contributor = f"{agent} (self-directed)"
contributor = agent_map.get(prefix, prefix)
elif intake_tier == "directed":
contributor = "@m3taversal"
contributor = "m3taversal"
else:
# Default: if source exists but no proposed_by, it was Cory's submission
contributor = "@m3taversal"
# Default: if source exists but no proposed_by, operator submitted it.
contributor = "m3taversal"
if contributor:
conn.execute(
@ -114,19 +117,19 @@ def main():
agent = branch.split("/", 1)[0]
conn.execute(
"UPDATE prs SET submitted_by = ? WHERE number = ?",
(f"{agent} (self-directed)", pr["number"]),
(agent, pr["number"]),
)
updated += 1
elif branch.startswith("reweave/"):
conn.execute(
"UPDATE prs SET submitted_by = 'pipeline (reweave)' WHERE number = ?",
"UPDATE prs SET submitted_by = 'pipeline' WHERE number = ?",
(pr["number"],),
)
updated += 1
else:
# Everything else (extract/, ingestion/, unknown) → Cory directed it
# Everything else (extract/, ingestion/, unknown) → operator directed it
conn.execute(
"UPDATE prs SET submitted_by = '@m3taversal' WHERE number = ?",
"UPDATE prs SET submitted_by = 'm3taversal' WHERE number = ?",
(pr["number"],),
)
updated += 1

560
diagnostics/claims_api.py Normal file
View file

@ -0,0 +1,560 @@
"""Claims API — list endpoint + canonical claim detail page.
Owner: Argus
Routes:
GET /api/claims list/filter (frontmatter scan, lightweight)
GET /api/claims/{slug} full claim detail (Ship contract)
GET /api/domains domain rollups for sidebar
The detail endpoint is the canonical /claims/{slug} backend per Ship's
2026-04-29 brief. One round-trip, no N+1 cascade. Wikilinks resolved
server-side via titleslug index built from a tree walk.
"""
import json
import re
import sqlite3
import time
from pathlib import Path
import yaml
from aiohttp import web
# Codex tree roots — claims live in three places (Sourcer Apr 26 fix scope)
CODEX_BASE = Path("/opt/teleo-eval/workspaces/main")
CLAIM_TREES = [CODEX_BASE / "domains", CODEX_BASE / "foundations", CODEX_BASE / "core"]
# pipeline.db for joins (review_records, prs, sources)
DB_PATH = "/opt/teleo-eval/pipeline/pipeline.db"
# In-process caches
_list_cache = {"data": None, "ts": 0}
_LIST_CACHE_TTL = 300 # 5 min — list view tolerates staleness
_index_cache = {"by_title": None, "by_stem": None, "ts": 0}
_INDEX_CACHE_TTL = 60 # 1 min — title→slug index for wikilink resolution
CORS_HEADERS = {"Access-Control-Allow-Origin": "*"}
# Wikilink pattern. [[text]] or [[text|alias]] — we keep the link text only.
_WIKILINK_RE = re.compile(r"\[\[([^\]|#]+?)(?:[#|][^\]]*)?\]\]")
# ─── Normalization ─────────────────────────────────────────────────────────
def _normalize_for_match(s):
"""Collapse a title or slug to a comparable form.
Rules (from Ship's brief — match the link-fixer canonicalization):
- lowercase
- hyphen space tolerant (both single space)
- collapse runs of whitespace
- strip leading/trailing whitespace
- drop trailing punctuation that gets stripped from filenames
(`.`, `?`, `!`, `:`, `--`)
NOTE: lib/attribution.py exposes only normalize_handle today, not the
title normalizer Ship referenced. Implementing inline; if a canonical
helper lands later we point at it.
"""
if not s:
return ""
s = str(s).lower().strip()
# Treat hyphens as spaces, then collapse whitespace runs
s = s.replace("-", " ").replace("_", " ")
s = re.sub(r"\s+", " ", s)
# Strip ASCII punctuation that filenames drop
s = re.sub(r"[^\w\s]", "", s)
return s.strip()
# ─── Frontmatter parse ─────────────────────────────────────────────────────
_CODE_FENCE_WRAPPER_RE = re.compile(r"^\s*```(?:markdown|md)?\s*\n(.*?)\n```\s*$", re.DOTALL)
def _split_frontmatter(text):
"""Return (frontmatter_dict, body_str) or (None, None) if not a claim file.
Tolerates files wrapped in a top-level ```markdown ... ``` code fence
some agents have produced these (e.g. Montreal Protocol claim from Astra,
2024-12-09). Unwrap once before frontmatter detection.
"""
if not text:
return None, None
m = _CODE_FENCE_WRAPPER_RE.match(text)
if m:
text = m.group(1)
text = text.lstrip()
if not text.startswith("---"):
return None, None
try:
end = text.index("\n---", 3)
except ValueError:
return None, None
try:
fm = yaml.safe_load(text[3:end])
except Exception:
return None, None
if not isinstance(fm, dict):
return None, None
body = text[end + 4:].lstrip()
return fm, body
def _read_claim_file(filepath):
"""Read a claim file from disk. Returns (frontmatter, body) or (None, None)."""
try:
text = filepath.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return None, None
return _split_frontmatter(text)
# ─── Tree walk + indexing ──────────────────────────────────────────────────
def _walk_claim_files():
"""Yield Path objects for every .md claim file in domains/, foundations/, core/."""
for root in CLAIM_TREES:
if not root.exists():
continue
for f in root.rglob("*.md"):
if f.name == "_map.md":
continue
yield f
def _build_indexes():
"""Build (title→stem, stem→relpath) indexes for wikilink resolution.
Cached for _INDEX_CACHE_TTL. Pulls from claim-index endpoint when
possible (already cached upstream) and falls back to filesystem walk.
"""
now = time.time()
if _index_cache["by_title"] is not None and now - _index_cache["ts"] < _INDEX_CACHE_TTL:
return _index_cache["by_title"], _index_cache["by_stem"]
by_title = {}
by_stem = {}
for f in _walk_claim_files():
stem = f.stem
rel = str(f.relative_to(CODEX_BASE))
by_stem[stem] = rel
# Index by stem-as-normalized too (covers wikilinks that use the slug)
by_title[_normalize_for_match(stem)] = stem
# Also try parsing the title from frontmatter for higher-fidelity matches
fm, _ = _read_claim_file(f)
if fm:
title = fm.get("title")
if title:
key = _normalize_for_match(title)
if key and key not in by_title:
by_title[key] = stem
_index_cache["by_title"] = by_title
_index_cache["by_stem"] = by_stem
_index_cache["ts"] = now
return by_title, by_stem
def _resolve_wikilinks(body, by_title):
"""Extract [[link]] occurrences from body, return {link_text: slug_or_null}."""
out = {}
for match in _WIKILINK_RE.finditer(body or ""):
link_text = match.group(1).strip()
if not link_text or link_text in out:
continue
norm = _normalize_for_match(link_text)
out[link_text] = by_title.get(norm)
return out
# ─── Edge extraction from frontmatter ──────────────────────────────────────
_EDGE_FIELDS = {
"supports": "supports",
"challenges": "challenges",
"challenged_by": "challenges", # canonical: store as challenges direction
"related": "related",
"related_claims": "related",
"depends_on": "depends_on",
}
def _extract_edges(fm, by_title, by_stem):
"""Return edges dict shaped per Ship's contract.
Each edge is {slug, title, exists}. Slug resolved through title index.
"""
edges = {"supports": [], "challenges": [], "related": [], "depends_on": []}
for fm_key, edge_kind in _EDGE_FIELDS.items():
raw = fm.get(fm_key)
if not raw:
continue
items = raw if isinstance(raw, list) else [raw]
for item in items:
if not isinstance(item, str):
continue
text = item.strip()
# Strip wikilink wrapping if present
text = re.sub(r"^\[\[|\]\]$", "", text)
# Strip pipe annotations: "[[link|alias]]" style or "claim | edge_type | date"
text = text.split("|")[0].strip()
if not text:
continue
# Try title match first, fall back to stem match
slug = by_title.get(_normalize_for_match(text))
if not slug and text in by_stem:
slug = text
edges[edge_kind].append({
"slug": slug,
"title": text,
"exists": slug is not None,
})
return edges
# ─── Source provenance ─────────────────────────────────────────────────────
def _resolve_sourced_from(conn, claim_filepath, fm, title, stem):
"""Build sourced_from list for the claim.
Strategy: find PRs that produced this claim (via prs.description LIKE
or branch slug match), look at prs.source_path inbox archive file
parse that source's frontmatter for title/url. Falls back to the raw
`source` string from the claim's own frontmatter.
Both `title` and `stem` must be non-empty caller (handler) already
falls back stemtitle; passing empty values would leak `LIKE '%%'`
and match unrelated PRs.
"""
out = []
seen_paths = set()
pr_rows = []
if (title or "").strip() and (stem or "").strip():
try:
pr_rows = conn.execute(
"""SELECT DISTINCT source_path
FROM prs
WHERE source_path IS NOT NULL AND source_path != ''
AND (description LIKE ? OR branch LIKE ?)
LIMIT 10""",
(f"%{title}%", f"%{stem}%"),
).fetchall()
except sqlite3.OperationalError:
pr_rows = []
for row in pr_rows:
path = row["source_path"]
if not path or path in seen_paths:
continue
seen_paths.add(path)
out.append(_resolve_source_file(path))
# 2. Fallback: parse raw source frontmatter field if no PR match
if not out:
raw = fm.get("source")
if isinstance(raw, str) and raw.strip():
out.append({"path": None, "title": raw.strip()[:200], "url": None})
return out
def _resolve_source_file(rel_path):
"""Given inbox/archive/... path, parse frontmatter for title+url. Best-effort."""
full = CODEX_BASE / rel_path
entry = {"path": rel_path, "title": None, "url": None}
if full.exists():
fm, _ = _read_claim_file(full)
if fm:
entry["title"] = fm.get("title") or fm.get("source") or rel_path
entry["url"] = fm.get("url")
if not entry["title"]:
# Last resort: derive from filename
entry["title"] = Path(rel_path).stem.replace("-", " ")
return entry
# ─── Reviews + PRs ─────────────────────────────────────────────────────────
def _load_pr_history(conn, title, stem):
"""Find PRs that touched this claim and their reviews.
Both title and stem must be non-empty strings empty leaks `LIKE '%%'`
which matches every PR. Handler already populates a fallback so this
is a defense-in-depth guard.
"""
if not (title or "").strip() or not (stem or "").strip():
return [], []
try:
pr_rows = conn.execute(
"""SELECT number, merged_at, commit_type, agent, branch, status
FROM prs
WHERE merged_at IS NOT NULL
AND (description LIKE ? OR branch LIKE ?)
ORDER BY merged_at ASC
LIMIT 50""",
(f"%{title}%", f"%{stem}%"),
).fetchall()
except sqlite3.OperationalError:
return [], []
prs = [
{
"number": r["number"],
"merged_at": r["merged_at"],
"kind": r["commit_type"] or "unknown",
"agent": r["agent"],
"branch": r["branch"],
}
for r in pr_rows
]
pr_numbers = [p["number"] for p in prs]
if not pr_numbers:
return prs, []
placeholders = ",".join("?" * len(pr_numbers))
try:
review_rows = conn.execute(
f"""SELECT pr_number, reviewer, reviewer_model, outcome,
rejection_reason, notes, reviewed_at
FROM review_records
WHERE pr_number IN ({placeholders})
ORDER BY reviewed_at ASC""",
pr_numbers,
).fetchall()
except sqlite3.OperationalError:
review_rows = []
reviews = [
{
"pr_number": r["pr_number"],
"reviewer": r["reviewer"],
"model": r["reviewer_model"],
"outcome": r["outcome"],
"rejection_reason": r["rejection_reason"],
"notes": r["notes"],
"reviewed_at": r["reviewed_at"],
}
for r in review_rows
]
return prs, reviews
# ─── List view (preserved) ─────────────────────────────────────────────────
def _parse_list_entry(filepath):
fm, body = _read_claim_file(filepath)
if not fm or fm.get("type") != "claim":
return None
links = _WIKILINK_RE.findall(body or "")
paragraphs = [p.strip() for p in (body or "").split("\n\n")
if p.strip() and not p.strip().startswith("#")]
summary = paragraphs[0][:300] if paragraphs else ""
return {
"slug": filepath.stem,
"title": fm.get("title", filepath.stem.replace("-", " ")),
"domain": fm.get("domain", "unknown"),
"confidence": fm.get("confidence", "unknown"),
"agent": fm.get("agent"),
"scope": fm.get("scope"),
"created": str(fm.get("created", "")),
"source": fm.get("source", "") if isinstance(fm.get("source"), str) else "",
"sourcer": fm.get("sourcer", ""),
"wiki_link_count": len(links),
"summary": summary,
"challenged_by": fm.get("challenged_by"),
"related_claims": fm.get("related_claims", []),
}
def _load_all_claims_list():
now = time.time()
if _list_cache["data"] and now - _list_cache["ts"] < _LIST_CACHE_TTL:
return _list_cache["data"]
claims = []
for f in _walk_claim_files():
entry = _parse_list_entry(f)
if entry:
claims.append(entry)
_list_cache["data"] = claims
_list_cache["ts"] = now
return claims
# ─── Handlers ──────────────────────────────────────────────────────────────
async def handle_claims(request):
claims = _load_all_claims_list()
domain = request.query.get("domain")
search = request.query.get("q", "").lower()
confidence = request.query.get("confidence")
agent = request.query.get("agent")
sort = request.query.get("sort", "recent")
filtered = claims
if domain:
filtered = [c for c in filtered if c["domain"] == domain]
if confidence:
filtered = [c for c in filtered if c["confidence"] == confidence]
if agent:
filtered = [c for c in filtered if c["agent"] == agent]
if search:
filtered = [c for c in filtered
if search in c["title"].lower() or search in c["summary"].lower()]
if sort == "recent":
filtered.sort(key=lambda c: c["created"], reverse=True)
elif sort == "alpha":
filtered.sort(key=lambda c: c["title"].lower())
elif sort == "domain":
filtered.sort(key=lambda c: (c["domain"], c["title"].lower()))
limit = min(int(request.query.get("limit", "50")), 200)
offset = int(request.query.get("offset", "0"))
page = filtered[offset:offset + limit]
domain_counts = {}
for c in claims:
domain_counts[c["domain"]] = domain_counts.get(c["domain"], 0) + 1
return web.json_response({
"claims": page,
"total": len(filtered),
"offset": offset,
"limit": limit,
"domains": dict(sorted(domain_counts.items(), key=lambda x: -x[1])),
"confidence_levels": sorted(set(c["confidence"] for c in claims)),
"agents": sorted(set(c["agent"] for c in claims if c["agent"])),
}, headers=CORS_HEADERS)
async def handle_claim_detail(request):
"""GET /api/claims/{slug} — canonical claim detail page (Ship contract).
One round-trip, all data resolved server-side. Wikilinks pre-resolved.
"""
requested_slug = request.match_info["slug"]
by_title, by_stem = _build_indexes()
# Resolution order: exact stem → title-normalized (handles description-derived
# slugs from /api/activity-feed that are longer than on-disk file stems) →
# stem-as-prefix (handles description-derived slugs that are shorter than the
# file stem because the description was truncated upstream).
slug = requested_slug
rel_path = by_stem.get(slug)
if not rel_path:
# Title fallback: requested slug = slugified frontmatter title
norm = _normalize_for_match(requested_slug)
resolved_stem = by_title.get(norm)
if resolved_stem:
slug = resolved_stem
rel_path = by_stem.get(resolved_stem)
if not rel_path:
# Prefix fallback: walk stems sharing a common prefix with the request,
# pick longest match. Anchored at 32 chars to avoid spurious hits.
norm_req = _normalize_for_match(requested_slug)
best_stem = None
best_len = 0
for stem in by_stem:
norm_stem = _normalize_for_match(stem)
common = 0
for a, b in zip(norm_req, norm_stem):
if a != b:
break
common += 1
if common >= 32 and common > best_len:
best_stem = stem
best_len = common
if best_stem:
slug = best_stem
rel_path = by_stem.get(best_stem)
if not rel_path:
return web.json_response({"error": "claim not found", "slug": requested_slug},
status=404, headers=CORS_HEADERS)
filepath = CODEX_BASE / rel_path
fm, body = _read_claim_file(filepath)
if not fm:
# File exists at this stem but has no parseable frontmatter — almost
# always a stray enrichment fragment that landed in domains/ without
# being merged into a parent claim. Surfacing as 404 (no claim here)
# not 500: the caller can't act on it differently anyway.
return web.json_response({"error": "claim not found", "slug": slug,
"reason": "file_no_frontmatter"},
status=404, headers=CORS_HEADERS)
# Open read-only DB connection for this request
conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
try:
title = fm.get("title") or slug.replace("-", " ")
prs, reviews = _load_pr_history(conn, title, slug)
sourced_from = _resolve_sourced_from(conn, filepath, fm, title, slug)
finally:
conn.close()
last_review = None
if reviews:
latest = reviews[-1]
last_review = {
"outcome": latest["outcome"],
"reviewer": latest["reviewer"],
"date": (latest["reviewed_at"] or "")[:10],
}
# secondary_domains: explicit list, or empty
secondary = fm.get("secondary_domains") or fm.get("cross_domain_links") or []
if isinstance(secondary, str):
secondary = [secondary]
description = fm.get("description") or ""
edges = _extract_edges(fm, by_title, by_stem)
wikilinks = _resolve_wikilinks(body, by_title)
response = {
"slug": slug,
"title": title,
"domain": fm.get("domain", "unknown"),
"secondary_domains": secondary,
"confidence": fm.get("confidence", "unknown"),
"description": description,
"created": str(fm.get("created", "")),
"last_review": last_review,
"body": body or "",
"sourced_from": sourced_from,
"reviews": reviews,
"prs": prs,
"edges": edges,
"wikilinks": wikilinks,
}
return web.json_response(response, headers=CORS_HEADERS)
async def handle_domains(request):
claims = _load_all_claims_list()
domains = {}
for c in claims:
d = c["domain"]
if d not in domains:
domains[d] = {"name": d, "count": 0, "agents": set(), "confidence_dist": {}}
domains[d]["count"] += 1
if c["agent"]:
domains[d]["agents"].add(c["agent"])
conf = c["confidence"]
domains[d]["confidence_dist"][conf] = domains[d]["confidence_dist"].get(conf, 0) + 1
result = []
for d in sorted(domains.values(), key=lambda x: -x["count"]):
d["agents"] = sorted(d["agents"])
result.append(d)
return web.json_response(result, headers=CORS_HEADERS)
def register_claims_routes(app):
app.router.add_get("/api/claims", handle_claims)
app.router.add_get("/api/claims/{slug}", handle_claim_detail)
app.router.add_get("/api/domains", handle_domains)

View file

@ -0,0 +1,365 @@
"""Contributor profile API — GET /api/contributors/{handle}"""
import sqlite3
import json
import os
import re
import subprocess
from datetime import datetime
DB_PATH = os.environ.get("PIPELINE_DB", "/opt/teleo-eval/pipeline/pipeline.db")
SYSTEM_ACCOUNTS = {"pipeline", "unknown", "teleo-agents", "teleo pipeline"}
CODEX_PATH = "/opt/teleo-eval/workspaces/main"
CI_WEIGHTS = {
"sourcer": 0.15,
"extractor": 0.05,
"challenger": 0.35,
"synthesizer": 0.25,
"reviewer": 0.20,
}
FOUNDING_CUTOFF = "2026-03-15"
BADGE_DEFS = {
"FOUNDING CONTRIBUTOR": {"rarity": "limited", "desc": "Contributed during pre-launch phase"},
"BELIEF MOVER": {"rarity": "rare", "desc": "Challenge that led to a claim revision"},
"KNOWLEDGE SOURCER": {"rarity": "uncommon", "desc": "Source that generated 3+ claims"},
"DOMAIN SPECIALIST": {"rarity": "rare", "desc": "Top 3 CI contributor in a domain"},
"VETERAN": {"rarity": "uncommon", "desc": "10+ accepted contributions"},
"FIRST BLOOD": {"rarity": "common", "desc": "First contribution of any kind"},
"CONTRIBUTOR": {"rarity": "common", "desc": "Account created + first accepted contribution"},
}
def _get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def _compute_ci(row):
total = 0
for role, weight in CI_WEIGHTS.items():
total += (row.get(f"{role}_count", 0) or 0) * weight
return round(total, 2)
def _compute_badges(handle, row, domain_breakdown, conn):
badges = []
first = row.get("first_contribution", "")
if first and first <= FOUNDING_CUTOFF:
badges.append("FOUNDING CONTRIBUTOR")
claims = row.get("claims_merged", 0) or 0
if claims > 0:
badges.append("CONTRIBUTOR")
badges.append("FIRST BLOOD")
if claims >= 10:
badges.append("VETERAN")
challenger = row.get("challenger_count", 0) or 0
challenge_ci = row.get("_challenge_count_from_scores", 0)
if challenger > 0 or challenge_ci > 0:
badges.append("BELIEF MOVER")
sourcer = row.get("sourcer_count", 0) or 0
if sourcer >= 3:
badges.append("KNOWLEDGE SOURCER")
return badges
def _get_domain_breakdown(handle, conn):
rows = conn.execute("""
SELECT domain, COUNT(*) as cnt
FROM prs
WHERE status='merged' AND (LOWER(agent)=LOWER(?) OR LOWER(submitted_by)=LOWER(?))
AND domain IS NOT NULL
GROUP BY domain ORDER BY cnt DESC
""", (handle, handle)).fetchall()
return {r["domain"]: r["cnt"] for r in rows}
def _get_contribution_timeline(handle, conn, limit=20):
rows = conn.execute("""
SELECT number, domain, status, created_at, description, commit_type, source_path
FROM prs
WHERE status='merged' AND (LOWER(agent)=LOWER(?) OR LOWER(submitted_by)=LOWER(?))
ORDER BY created_at DESC LIMIT ?
""", (handle, handle, limit)).fetchall()
timeline = []
for r in rows:
desc = r["description"] or ""
if not desc and r["source_path"]:
desc = os.path.basename(r["source_path"]).replace("-", " ").replace(".md", "")
timeline.append({
"pr_number": r["number"],
"domain": r["domain"],
"date": r["created_at"][:10] if r["created_at"] else None,
"type": _classify_commit(r["commit_type"]),
"summary": desc[:200] if desc else None,
})
return timeline
def _classify_commit(commit_type):
if not commit_type:
return "create"
ct = commit_type.lower()
if "challenge" in ct:
return "challenge"
if "enrich" in ct or "update" in ct or "reweave" in ct:
return "enrich"
return "create"
def _get_review_stats(handle, conn):
rows = conn.execute("""
SELECT outcome, COUNT(*) as cnt
FROM review_records
WHERE LOWER(agent) = LOWER(?)
GROUP BY outcome
""", (handle,)).fetchall()
stats = {}
for r in rows:
stats[r["outcome"]] = r["cnt"]
return stats
def _get_action_ci(handle, conn):
"""Get action-type CI from contribution_scores table.
Checks both exact handle and common variants (with/without suffix).
"""
h = handle.lower()
base = re.sub(r"[-_]\w+\d+$", "", h)
variants = list({h, base}) if base and base != h else [h]
try:
placeholders = ",".join("?" for _ in variants)
rows = conn.execute(f"""
SELECT event_type, SUM(ci_earned) as total, COUNT(*) as cnt
FROM contribution_scores
WHERE LOWER(contributor) IN ({placeholders})
GROUP BY event_type
""", variants).fetchall()
except Exception:
return None
if not rows:
return None
breakdown = {}
total = 0.0
for r in rows:
breakdown[r["event_type"]] = {
"count": r["cnt"],
"ci": round(r["total"], 4),
}
total += r["total"]
return {
"total": round(total, 4),
"breakdown": breakdown,
}
def _get_git_contributor(handle):
"""Fallback: check git log for contributors not in pipeline.db."""
try:
result = subprocess.run(
["git", "log", "--all", "--format=%H|%an|%ae|%aI", "--diff-filter=A", "--", "domains/"],
capture_output=True, text=True, cwd=CODEX_PATH, timeout=30
)
if result.returncode != 0:
return None
claims = []
for line in result.stdout.strip().split("\n"):
if not line:
continue
parts = line.split("|", 3)
if len(parts) < 4:
continue
sha, name, email, date = parts
if handle.lower() in name.lower() or handle.lower() in email.lower():
claims.append({"sha": sha, "author": name, "email": email, "date": date[:10]})
if not claims:
return None
return {
"handle": handle,
"display_name": claims[0]["author"],
"email": claims[0]["email"],
"first_contribution": min(c["date"] for c in claims),
"last_contribution": max(c["date"] for c in claims),
"claims_merged": len(claims),
"sourcer_count": 0,
"extractor_count": 0,
"challenger_count": 0,
"synthesizer_count": 0,
"reviewer_count": 0,
}
except Exception:
return None
def get_contributor_profile(handle):
conn = _get_conn()
try:
row = conn.execute(
"SELECT * FROM contributors WHERE LOWER(handle) = LOWER(?)", (handle,)
).fetchone()
if row:
data = dict(row)
else:
git_data = _get_git_contributor(handle)
if git_data:
data = git_data
else:
return None
ci_score = _compute_ci(data)
action_ci = _get_action_ci(handle, conn)
domain_breakdown = _get_domain_breakdown(handle, conn)
timeline = _get_contribution_timeline(handle, conn)
review_stats = _get_review_stats(handle, conn)
if action_ci and "challenge" in action_ci.get("breakdown", {}):
data["_challenge_count_from_scores"] = action_ci["breakdown"]["challenge"]["count"]
badges = _compute_badges(handle, data, domain_breakdown, conn)
# For git-only contributors, build domain breakdown from git
if not domain_breakdown and not row:
domain_breakdown = _git_domain_breakdown(handle)
hero_badge = None
rarity_order = ["limited", "rare", "uncommon", "common"]
for rarity in rarity_order:
for b in badges:
if BADGE_DEFS.get(b, {}).get("rarity") == rarity:
hero_badge = b
break
if hero_badge:
break
role_breakdown = {
"sourcer": data.get("sourcer_count", 0) or 0,
"extractor": data.get("extractor_count", 0) or 0,
"challenger": data.get("challenger_count", 0) or 0,
"synthesizer": data.get("synthesizer_count", 0) or 0,
"reviewer": data.get("reviewer_count", 0) or 0,
}
total_roles = sum(role_breakdown.values())
role_pct = {}
for k, v in role_breakdown.items():
role_pct[k] = round(v / total_roles * 100) if total_roles > 0 else 0
return {
"handle": data.get("handle", handle),
"display_name": data.get("display_name"),
"ci_score": ci_score,
"action_ci": action_ci,
"primary_ci": action_ci["total"] if action_ci else ci_score,
"hero_badge": hero_badge,
"badges": [{"name": b, **BADGE_DEFS.get(b, {})} for b in badges],
"joined": data.get("first_contribution"),
"last_active": data.get("last_contribution"),
"claims_merged": data.get("claims_merged", 0) or 0,
"principal": data.get("principal"),
"role_breakdown": role_breakdown,
"role_percentages": role_pct,
"domain_breakdown": domain_breakdown,
"review_stats": review_stats,
"contribution_timeline": timeline,
"active_domains": list(domain_breakdown.keys()),
}
finally:
conn.close()
def _git_domain_breakdown(handle):
"""For git-only contributors, count claims by domain from file paths."""
try:
result = subprocess.run(
["git", "log", "--all", "--name-only", "--format=COMMIT|%an", "--diff-filter=A", "--", "domains/"],
capture_output=True, text=True, cwd=CODEX_PATH, timeout=30
)
if result.returncode != 0:
return {}
domains = {}
current_match = False
for line in result.stdout.strip().split("\n"):
if line.startswith("COMMIT|"):
author = line.split("|", 1)[1]
current_match = handle.lower() in author.lower()
elif current_match and line.startswith("domains/"):
parts = line.split("/")
if len(parts) >= 2:
domain = parts[1]
domains[domain] = domains.get(domain, 0) + 1
return domains
except Exception:
return {}
async def handle_contributor_profile(request):
from aiohttp import web
handle = request.match_info["handle"]
profile = get_contributor_profile(handle)
if profile is None:
return web.json_response({"error": f"Contributor '{handle}' not found"}, status=404)
return web.json_response(profile)
async def handle_contributors_list(request):
from aiohttp import web
conn = _get_conn()
try:
min_claims = int(request.query.get("min_claims", "1"))
rows = conn.execute("""
SELECT handle, display_name, first_contribution, last_contribution,
sourcer_count, extractor_count, challenger_count, synthesizer_count,
reviewer_count, claims_merged, principal
FROM contributors
WHERE claims_merged >= ?
ORDER BY claims_merged DESC
""", (min_claims,)).fetchall()
contributors = []
for r in rows:
data = dict(r)
if data["handle"].lower() in SYSTEM_ACCOUNTS:
continue
ci = _compute_ci(data)
action_ci = _get_action_ci(data["handle"], conn)
action_total = action_ci["total"] if action_ci else 0.0
contributors.append({
"handle": data["handle"],
"display_name": data["display_name"],
"ci_score": ci,
"action_ci": action_total,
"primary_ci": action_total if action_total > 0 else ci,
"claims_merged": data["claims_merged"],
"first_contribution": data["first_contribution"],
"last_contribution": data["last_contribution"],
"principal": data["principal"],
})
return web.json_response({
"contributors": contributors,
"total": len(contributors),
})
finally:
conn.close()
def register_contributor_routes(app):
app.router.add_get("/api/contributors/list", handle_contributors_list)
app.router.add_get("/api/contributors/{handle}", handle_contributor_profile)

View file

@ -74,7 +74,7 @@ def render_epistemic_page(vital_signs: dict, now: datetime) -> str:
<div style="font-size:40px;margin-bottom:12px;opacity:0.3">&#9881;</div>
<div style="color:#8b949e">
Multi-model agreement rate requires the <code>model_evals</code> table.<br>
<span style="font-size:12px">Blocked on: model_evals table creation (Theseus 2 Phase 3)</span>
<span style="font-size:12px">Blocked on: model_evals table creation (Ship Phase 3)</span>
</div>
<div style="margin-top:16px;font-size:12px;color:#8b949e">
Current eval models: Haiku (triage), GPT-4o (domain), Sonnet/Opus (Leo).<br>
@ -194,12 +194,6 @@ fetch('/api/review-summary?days=30')
reasonRows += '<tr><td><code>' + esc(r.reason) + '</code></td><td>' + r.count + '</td></tr>';
}}
// Disagreement types
let disagreeRows = '';
for (const d of (data.disagreement_types || [])) {{
disagreeRows += '<tr><td>' + esc(d.type) + '</td><td>' + d.count + '</td></tr>';
}}
el.innerHTML = `
<div class="grid">
<div class="card"><div class="label">Total Reviews</div><div class="hero-value">${{data.total}}</div></div>
@ -215,13 +209,6 @@ fetch('/api/review-summary?days=30')
${{reasonRows || '<tr><td colspan="2" style="color:#8b949e">No rejections</td></tr>'}}
</table>
</div>
<div class="card">
<div style="font-weight:600;margin-bottom:8px">Disagreement Types</div>
<table>
<tr><th>Type</th><th>Count</th></tr>
${{disagreeRows || '<tr><td colspan="2" style="color:#8b949e">No disagreements</td></tr>'}}
</table>
</div>
</div>`;
}}).catch(() => {{
document.getElementById('review-container').innerHTML =

View file

@ -0,0 +1,408 @@
"""Portfolio dashboard — fixes empty chart by:
1. Computing NAV server-side in the history API (not client-side from nulls)
2. Only returning dates with valid NAV data
3. Showing data points when sparse
"""
import json
import sqlite3
import logging
from html import escape as esc
from datetime import datetime, timezone
from aiohttp import web
from shared_ui import render_page
logger = logging.getLogger("argus.portfolio")
CSS = """
.hero-chart { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.hero-chart h2 { color: #c9d1d9; font-size: 18px; margin-bottom: 12px; }
.range-btns { display: flex; gap: 4px; margin-bottom: 12px; }
.range-btn { background: #21262d; border: 1px solid #30363d; color: #8b949e; padding: 5px 14px;
border-radius: 4px; cursor: pointer; font-size: 12px; }
.range-btn.active { background: #1f6feb33; border-color: #58a6ff; color: #58a6ff; }
.ptable-wrap { overflow-x: auto; margin-top: 20px; }
.ptable { width: 100%; border-collapse: collapse; font-size: 13px; }
.ptable th { background: #161b22; color: #8b949e; font-size: 11px; text-transform: uppercase;
letter-spacing: 0.5px; padding: 10px 12px; text-align: right; border-bottom: 1px solid #30363d;
cursor: pointer; user-select: none; white-space: nowrap; }
.ptable th:first-child { text-align: left; position: sticky; left: 0; background: #161b22; z-index: 1; }
.ptable th:hover { color: #c9d1d9; }
.ptable th.sorted-asc::after { content: ' \\25B2'; font-size: 9px; }
.ptable th.sorted-desc::after { content: ' \\25BC'; font-size: 9px; }
.ptable td { padding: 10px 12px; text-align: right; border-bottom: 1px solid #21262d; color: #c9d1d9; }
.ptable td:first-child { text-align: left; position: sticky; left: 0; background: #0d1117; z-index: 1; font-weight: 600; }
.ptable tr:hover td { background: #161b22; }
.ptable tr:hover td:first-child { background: #161b22; }
.summary-row td { font-weight: 700; border-top: 2px solid #30363d; background: #161b22 !important; }
.premium { color: #f85149; }
.discount { color: #3fb950; }
.near-nav { color: #d29922; }
"""
def _fmt_usd(v):
if v is None:
return '\u2014'
if abs(v) >= 1_000_000:
return f'${v / 1_000_000:.1f}M'
if abs(v) >= 1_000:
return f'${v / 1_000:.0f}K'
return f'${v:,.0f}'
def _fmt_price(v):
if v is None:
return '\u2014'
if v >= 100:
return f'${v:,.0f}'
if v >= 1:
return f'${v:.2f}'
if v >= 0.01:
return f'${v:.4f}'
return f'${v:.6f}'
def _fmt_ratio(v):
if v is None or v == 0:
return '\u2014'
return f'{v:.2f}x'
def _ratio_class(v):
if v is None or v == 0:
return ''
if v > 1.5:
return 'premium'
if v < 0.9:
return 'discount'
if v <= 1.1:
return 'near-nav'
return ''
def render_portfolio_page(coins: list[dict], now: datetime) -> str:
if not coins:
body = '<div style="padding:40px;text-align:center;color:#8b949e;">No coin data yet.</div>'
return render_page("Portfolio", "Ownership coin portfolio", "/portfolio", body,
extra_css=CSS, timestamp=now.strftime("%Y-%m-%d %H:%M UTC"))
total_mcap = sum(c.get('market_cap_usd') or 0 for c in coins)
total_treasury = sum(c.get('treasury_usd') or 0 for c in coins)
hero_chart = """
<div class="hero-chart">
<h2>Price / NAV per Token</h2>
<div class="range-btns">
<button class="range-btn" onclick="setRange(this, 30)">30d</button>
<button class="range-btn active" onclick="setRange(this, 90)">90d</button>
<button class="range-btn" onclick="setRange(this, 180)">180d</button>
<button class="range-btn" onclick="setRange(this, 365)">All</button>
</div>
<canvas id="ratio-chart" height="320" style="max-height:320px"></canvas>
</div>
"""
header = """<div class="ptable-wrap"><table class="ptable" id="coin-table">
<thead><tr>
<th data-col="name">Coin</th>
<th data-col="price">Price</th>
<th data-col="nav">NAV / Token</th>
<th data-col="ratio">Price / NAV</th>
<th data-col="treasury">Treasury</th>
<th data-col="mcap">Market Cap</th>
</tr></thead><tbody>"""
rows = ''
for c in coins:
name = c.get('name', '?')
ticker = c.get('ticker', '')
price = c.get('price_usd')
nav = c.get('nav_per_token')
ratio = c.get('price_nav_ratio')
treasury = c.get('treasury_usd')
mcap = c.get('market_cap_usd')
label = esc(name)
if ticker:
label += f' <span style="color:#8b949e;font-size:11px;">{esc(ticker)}</span>'
rows += f"""<tr>
<td>{label}</td>
<td>{_fmt_price(price)}</td>
<td>{_fmt_price(nav)}</td>
<td class="{_ratio_class(ratio)}">{_fmt_ratio(ratio)}</td>
<td>{_fmt_usd(treasury)}</td>
<td>{_fmt_usd(mcap)}</td>
</tr>"""
rows += f"""<tr class="summary-row">
<td>Total ({len(coins)})</td>
<td></td><td></td><td></td>
<td>{_fmt_usd(total_treasury)}</td>
<td>{_fmt_usd(total_mcap)}</td>
</tr>"""
table = header + rows + '</tbody></table></div>'
scripts = """<script>
const COLORS = ['#58a6ff','#3fb950','#f0883e','#d29922','#f85149','#bc8cff','#39d353','#79c0ff','#ff7b72','#a5d6ff'];
let chart = null;
function setRange(btn, days) {
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadChart(days);
}
function loadChart(days) {
fetch('/api/portfolio/nav-ratios?days=' + days)
.then(r => r.json())
.then(data => {
const dates = data.dates || [];
const series = data.series || {};
if (dates.length === 0) {
if (chart) chart.destroy();
chart = null;
const ctx = document.getElementById('ratio-chart').getContext('2d');
ctx.fillStyle = '#8b949e';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('No NAV data yet — accumulating daily snapshots', ctx.canvas.width / 2, 160);
return;
}
const sparse = dates.length <= 10;
const datasets = [];
let i = 0;
for (const [name, ratios] of Object.entries(series)) {
const hasData = ratios.some(v => v !== null);
if (!hasData) { i++; continue; }
datasets.push({
label: name,
data: ratios,
borderColor: COLORS[i % COLORS.length],
backgroundColor: COLORS[i % COLORS.length] + '33',
borderWidth: 2,
tension: 0.3,
spanGaps: true,
pointRadius: sparse ? 4 : 0,
pointHoverRadius: 6,
fill: false,
});
i++;
}
if (chart) chart.destroy();
const ctx = document.getElementById('ratio-chart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: { labels: dates, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#8b949e', font: { size: 11 }, usePointStyle: true, boxWidth: 8 }, position: 'top' },
tooltip: { mode: 'index', intersect: false,
callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.parsed.y != null ? ctx.parsed.y.toFixed(2) + 'x' : 'n/a') }
},
annotation: {
annotations: {
navLine: {
type: 'line',
yMin: 1, yMax: 1,
borderColor: '#3fb95088',
borderWidth: 2,
borderDash: [6, 4],
label: {
display: true,
content: '1.0x = NAV',
position: 'end',
backgroundColor: '#3fb95033',
color: '#3fb950',
font: { size: 10 },
}
}
}
}
},
scales: {
x: { ticks: { color: '#8b949e', maxTicksLimit: 12 }, grid: { display: false } },
y: { ticks: { color: '#8b949e', callback: v => v.toFixed(1) + 'x' }, grid: { color: '#21262d' },
suggestedMin: 0 }
}
}
});
});
}
// Table sorting
function sortTable(col) {
const table = document.getElementById('coin-table');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr:not(.summary-row)'));
const summaryRow = tbody.querySelector('.summary-row');
const th = table.querySelectorAll('th')[col];
const asc = th.classList.contains('sorted-asc');
table.querySelectorAll('th').forEach(h => h.classList.remove('sorted-asc','sorted-desc'));
th.classList.add(asc ? 'sorted-desc' : 'sorted-asc');
rows.sort((a, b) => {
let va = a.cells[col].textContent.replace(/[$,+%x\\u2014]/g,'').trim();
let vb = b.cells[col].textContent.replace(/[$,+%x\\u2014]/g,'').trim();
const na = parseFloat(va) || 0, nb = parseFloat(vb) || 0;
if (col === 0) return asc ? vb.localeCompare(va) : va.localeCompare(vb);
return asc ? na - nb : nb - na;
});
rows.forEach(r => tbody.appendChild(r));
if (summaryRow) tbody.appendChild(summaryRow);
}
document.querySelectorAll('#coin-table th').forEach((th, i) => {
th.addEventListener('click', () => sortTable(i));
});
loadChart(90);
</script>"""
body = hero_chart + table
return render_page("Portfolio", "Ownership coin portfolio", "/portfolio", body,
scripts=scripts, extra_css=CSS,
timestamp=now.strftime("%Y-%m-%d %H:%M UTC"))
# ── API handlers ────────────────────────────────────────────────────────────
def _get_db(request):
return request.app["_portfolio_conn"]()
def _compute_nav(row):
"""Compute NAV per token and Price/NAV ratio from a snapshot row dict."""
treas = (row.get('treasury_multisig_usd') or 0) + (row.get('lp_usdc_total') or 0)
adj = row.get('adjusted_circulating_supply') or 0
price = row.get('price_usd') or 0
nav = treas / adj if adj > 0 else 0
ratio = price / nav if nav > 0 else 0
return treas, nav, ratio
async def handle_portfolio_page(request):
conn = _get_db(request)
try:
rows = conn.execute("""
SELECT * FROM coin_snapshots
WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM coin_snapshots)
ORDER BY market_cap_usd DESC
""").fetchall()
coins = []
for r in rows:
d = dict(r)
treas, nav, ratio = _compute_nav(d)
d['treasury_usd'] = treas
d['nav_per_token'] = nav
d['price_nav_ratio'] = ratio
coins.append(d)
now = datetime.now(timezone.utc)
html = render_portfolio_page(coins, now)
return web.Response(text=html, content_type='text/html')
finally:
conn.close()
async def handle_nav_ratios(request):
"""Server-side computed NAV ratios — only returns dates with valid data."""
conn = _get_db(request)
try:
try:
days = min(int(request.query.get('days', '90')), 365)
except (ValueError, TypeError):
days = 90
rows = conn.execute("""
SELECT name, snapshot_date, price_usd, treasury_multisig_usd,
lp_usdc_total, adjusted_circulating_supply
FROM coin_snapshots
WHERE snapshot_date >= date('now', ? || ' days')
AND adjusted_circulating_supply IS NOT NULL
AND adjusted_circulating_supply > 0
ORDER BY name, snapshot_date
""", (f'-{days}',)).fetchall()
coin_ratios = {}
all_dates = set()
for r in rows:
d = dict(r)
name = d['name']
date = d['snapshot_date']
_, nav, ratio = _compute_nav(d)
if nav > 0 and ratio > 0:
if name not in coin_ratios:
coin_ratios[name] = {}
coin_ratios[name][date] = round(ratio, 3)
all_dates.add(date)
sorted_dates = sorted(all_dates)
series = {}
for name, date_map in coin_ratios.items():
series[name] = [date_map.get(d) for d in sorted_dates]
return web.json_response({
'dates': sorted_dates,
'series': series,
})
finally:
conn.close()
async def handle_portfolio_history(request):
conn = _get_db(request)
try:
try:
days = min(int(request.query.get('days', '90')), 365)
except (ValueError, TypeError):
days = 90
rows = conn.execute("""
SELECT * FROM coin_snapshots
WHERE snapshot_date >= date('now', ? || ' days')
ORDER BY name, snapshot_date
""", (f'-{days}',)).fetchall()
history = {}
for r in rows:
d = dict(r)
key = d['name']
if key not in history:
history[key] = []
history[key].append(d)
return web.json_response({'history': history})
finally:
conn.close()
async def handle_portfolio_latest(request):
conn = _get_db(request)
try:
rows = conn.execute("""
SELECT * FROM coin_snapshots
WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM coin_snapshots)
ORDER BY market_cap_usd DESC
""").fetchall()
coins = []
for r in rows:
d = dict(r)
treas, nav, ratio = _compute_nav(d)
d['treasury_usd'] = treas
d['nav_per_token'] = nav
d['price_nav_ratio'] = ratio
coins.append(d)
return web.json_response({'coins': coins, 'date': coins[0]['snapshot_date'] if coins else None})
finally:
conn.close()
def register_portfolio_routes(app, get_conn):
app["_portfolio_conn"] = get_conn
app.router.add_get("/portfolio", handle_portfolio_page)
app.router.add_get("/api/portfolio/nav-ratios", handle_nav_ratios)
app.router.add_get("/api/portfolio/history", handle_portfolio_history)
app.router.add_get("/api/portfolio/latest", handle_portfolio_latest)

View file

@ -1,8 +1,8 @@
"""PR Lifecycle dashboard — single-page view of every PR through the pipeline.
Sortable table: PR#, summary, claims, domain, contributor, outcome, evals, evaluator, cost, date.
Click any row to expand: claim titles, eval chain, timeline, reviews, issues.
Hero cards: total PRs, merge rate, total claims, est. cost.
Sortable table: PR#, summary, claims, domain, outcome, evals, evaluator, cost, date.
Click any row to expand: timeline, claim list, issues summary.
Hero cards: total PRs, merge rate, median eval rounds, total claims, total cost.
Data sources: prs table, audit_log (eval rounds), review_records.
Owner: Ship
@ -14,7 +14,7 @@ from shared_ui import render_page
EXTRA_CSS = """
.content-wrapper { max-width: 1600px !important; }
.page-content { max-width: 1600px !important; }
.filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.filters select, .filters input {
background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
@ -22,15 +22,14 @@ EXTRA_CSS = """
.filters select:focus, .filters input:focus { border-color: #58a6ff; outline: none; }
.pr-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.pr-table th:nth-child(1) { width: 50px; } /* PR# */
.pr-table th:nth-child(2) { width: 28%; } /* Summary */
.pr-table th:nth-child(2) { width: 30%; } /* Summary */
.pr-table th:nth-child(3) { width: 50px; } /* Claims */
.pr-table th:nth-child(4) { width: 11%; } /* Domain */
.pr-table th:nth-child(5) { width: 10%; } /* Contributor */
.pr-table th:nth-child(6) { width: 10%; } /* Outcome */
.pr-table th:nth-child(7) { width: 44px; } /* Evals */
.pr-table th:nth-child(8) { width: 12%; } /* Evaluator */
.pr-table th:nth-child(9) { width: 60px; } /* Cost */
.pr-table th:nth-child(10) { width: 80px; } /* Date */
.pr-table th:nth-child(4) { width: 12%; } /* Domain */
.pr-table th:nth-child(5) { width: 10%; } /* Outcome */
.pr-table th:nth-child(6) { width: 50px; } /* Evals */
.pr-table th:nth-child(7) { width: 16%; } /* Evaluator */
.pr-table th:nth-child(8) { width: 70px; } /* Cost */
.pr-table th:nth-child(9) { width: 90px; } /* Date */
.pr-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 8px 6px; }
.pr-table td:nth-child(2) { white-space: normal; overflow: visible; line-height: 1.4; }
.pr-table th { cursor: pointer; user-select: none; position: relative; padding: 8px 18px 8px 6px; }
@ -49,24 +48,22 @@ EXTRA_CSS = """
.pr-table .pr-link:hover { text-decoration: underline; }
.pr-table td .summary-text { font-size: 12px; color: #c9d1d9; }
.pr-table td .review-snippet { font-size: 11px; color: #f85149; margin-top: 2px; opacity: 0.8; }
.pr-table td .model-tag { font-size: 10px; color: #6e7681; background: #161b22; border-radius: 3px; padding: 1px 4px; }
.pr-table td .contributor-tag { font-size: 11px; color: #d2a8ff; }
.pr-table td .contributor-self { font-size: 11px; color: #6e7681; font-style: italic; }
.pr-table td .model-tag { font-size: 9px; color: #6e7681; background: #21262d; border-radius: 3px; padding: 1px 4px; display: inline-block; margin: 1px 0; }
.pr-table td .expand-chevron { display: inline-block; width: 12px; color: #484f58; font-size: 10px; transition: transform 0.2s; }
.pr-table tr.expanded .expand-chevron { transform: rotate(90deg); color: #58a6ff; }
.pr-table td .cost-val { font-size: 12px; color: #8b949e; }
.pr-table td .claims-count { font-size: 13px; color: #c9d1d9; text-align: center; }
.pr-table td .evals-count { font-size: 13px; text-align: center; }
.trace-panel { background: #0d1117; border: 1px solid #30363d; border-radius: 8px;
padding: 16px; margin: 4px 0 8px 0; font-size: 12px; display: none; }
.trace-panel.open { display: block; }
.trace-panel h4 { color: #58a6ff; font-size: 12px; margin: 12px 0 6px 0; }
.trace-panel h4:first-child { margin-top: 0; }
.claim-list { list-style: none; padding: 0; margin: 0; }
.claim-list li { padding: 4px 0 4px 16px; border-left: 2px solid #238636; color: #c9d1d9; font-size: 12px; line-height: 1.5; }
.claim-list li .claim-confidence { font-size: 10px; color: #8b949e; margin-left: 6px; }
.issues-box { background: #1c1210; border: 1px solid #f8514933; border-radius: 6px;
.trace-panel .section-title { color: #58a6ff; font-size: 12px; font-weight: 600; margin: 12px 0 6px; }
.trace-panel .section-title:first-child { margin-top: 0; }
.trace-panel .claim-list { list-style: none; padding: 0; margin: 0; }
.trace-panel .claim-list li { padding: 4px 0; border-bottom: 1px solid #21262d; color: #c9d1d9; font-size: 12px; }
.trace-panel .claim-list li:last-child { border-bottom: none; }
.trace-panel .issues-box { background: #1c1017; border: 1px solid #f8514930; border-radius: 6px;
padding: 8px 12px; margin: 4px 0; font-size: 12px; color: #f85149; }
.eval-chain { background: #161b22; border-radius: 6px; padding: 8px 12px; margin: 4px 0; font-size: 12px; }
.eval-chain .chain-step { display: inline-block; margin-right: 6px; }
.eval-chain .chain-arrow { color: #484f58; margin: 0 4px; }
.trace-timeline { list-style: none; padding: 0; }
.trace-timeline li { padding: 4px 0; border-left: 2px solid #30363d; padding-left: 12px; margin-left: 8px; }
.trace-timeline li .ts { color: #484f58; font-size: 11px; }
@ -76,6 +73,12 @@ EXTRA_CSS = """
.trace-timeline li.ev-changes .ev { color: #d29922; }
.review-text { background: #161b22; padding: 8px 12px; border-radius: 4px;
margin: 4px 0; white-space: pre-wrap; font-size: 11px; color: #8b949e; max-height: 200px; overflow-y: auto; }
.eval-chain { background: #161b22; border-radius: 6px; padding: 8px 12px; margin: 4px 0 8px;
font-size: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.eval-chain .step { display: flex; align-items: center; gap: 4px; }
.eval-chain .step-label { color: #8b949e; font-size: 11px; }
.eval-chain .step-model { color: #c9d1d9; font-size: 11px; font-weight: 600; }
.eval-chain .arrow { color: #484f58; }
.pagination { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 16px; }
.pagination button { background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; }
@ -93,6 +96,7 @@ def render_prs_page(now: datetime) -> str:
<div class="grid" id="hero-cards">
<div class="card"><div class="label">Total PRs</div><div class="value blue" id="kpi-total">--</div><div class="detail" id="kpi-total-detail"></div></div>
<div class="card"><div class="label">Merge Rate</div><div class="value green" id="kpi-merge-rate">--</div><div class="detail" id="kpi-merge-detail"></div></div>
<div class="card"><div class="label">Median Eval Rounds</div><div class="value" id="kpi-rounds">--</div><div class="detail" id="kpi-rounds-detail"></div></div>
<div class="card"><div class="label">Total Claims</div><div class="value blue" id="kpi-claims">--</div><div class="detail" id="kpi-claims-detail"></div></div>
<div class="card"><div class="label">Est. Cost</div><div class="value" id="kpi-cost">--</div><div class="detail" id="kpi-cost-detail"></div></div>
</div>
@ -100,7 +104,6 @@ def render_prs_page(now: datetime) -> str:
<!-- Filters -->
<div class="filters">
<select id="filter-domain"><option value="">All Domains</option></select>
<select id="filter-contributor"><option value="">All Contributors</option></select>
<select id="filter-outcome">
<option value="">All Outcomes</option>
<option value="merged">Merged</option>
@ -130,10 +133,9 @@ def render_prs_page(now: datetime) -> str:
<th data-col="summary">Summary <span class="sort-arrow">&#9650;</span></th>
<th data-col="claims_count">Claims <span class="sort-arrow">&#9650;</span></th>
<th data-col="domain">Domain <span class="sort-arrow">&#9650;</span></th>
<th data-col="submitted_by">Contributor <span class="sort-arrow">&#9650;</span></th>
<th data-col="status">Outcome <span class="sort-arrow">&#9650;</span></th>
<th data-col="eval_rounds">Evals <span class="sort-arrow">&#9650;</span></th>
<th data-col="evaluator_label">Evaluator <span class="sort-arrow">&#9650;</span></th>
<th data-col="evaluator">Evaluator <span class="sort-arrow">&#9650;</span></th>
<th data-col="est_cost">Cost <span class="sort-arrow">&#9650;</span></th>
<th data-col="created_at">Date <span class="sort-arrow">&#9650;</span></th>
</tr>
@ -150,71 +152,42 @@ def render_prs_page(now: datetime) -> str:
</div>
"""
# Use single-quoted JS strings throughout to avoid Python/HTML escaping issues
scripts = """<script>
var PAGE_SIZE = 50;
var FORGEJO = 'https://git.livingip.xyz/teleo/teleo-codex/pulls/';
var allData = [];
var filtered = [];
var sortCol = 'number';
var sortAsc = false;
var page = 0;
var expandedPr = null;
// Tier-based cost estimates (per eval round)
var TIER_COSTS = {
'DEEP': 0.145, // Haiku triage + Gemini Flash domain + Opus Leo
'STANDARD': 0.043, // Haiku triage + Gemini Flash domain + Sonnet Leo
'LIGHT': 0.027 // Haiku triage + Gemini Flash domain only
};
function estimateCost(pr) {
var tier = pr.tier || 'STANDARD';
var rounds = pr.eval_rounds || 1;
var baseCost = TIER_COSTS[tier] || TIER_COSTS['STANDARD'];
return baseCost * rounds;
}
function fmtCost(val) {
if (val == null || val === 0) return '--';
return '$' + val.toFixed(3);
}
const PAGE_SIZE = 50;
const FORGEJO = 'https://git.livingip.xyz/teleo/teleo-codex/pulls/';
let allData = [];
let filtered = [];
let sortCol = 'number';
let sortAsc = false;
let page = 0;
let expandedPr = null;
function loadData() {
var days = document.getElementById('filter-days').value;
var url = '/api/pr-lifecycle' + (days !== '0' ? '?days=' + days : '?days=9999');
fetch(url).then(function(r) { return r.json(); }).then(function(data) {
allData = data.prs || [];
// Compute derived fields
allData.forEach(function(p) {
p.est_cost = estimateCost(p);
// Evaluator label for sorting
p.evaluator_label = p.domain_agent || p.agent || '--';
});
populateFilters(allData);
updateKPIs(data);
applyFilters();
}).catch(function() {
document.getElementById('pr-tbody').innerHTML =
'<tr><td colspan="10" style="text-align:center;color:#f85149;">Failed to load data</td></tr>';
'<tr><td colspan="9" style="text-align:center;color:#f85149;">Failed to load data</td></tr>';
});
}
function populateFilters(prs) {
var domains = [], contribs = [], seenD = {}, seenC = {};
var domains = [], seenD = {};
prs.forEach(function(p) {
if (p.domain && !seenD[p.domain]) { seenD[p.domain] = 1; domains.push(p.domain); }
var c = p.submitted_by || 'unknown';
if (!seenC[c]) { seenC[c] = 1; contribs.push(c); }
});
domains.sort(); contribs.sort();
domains.sort();
var domSel = document.getElementById('filter-domain');
var conSel = document.getElementById('filter-contributor');
var curDom = domSel.value, curCon = conSel.value;
var curDom = domSel.value;
domSel.innerHTML = '<option value="">All Domains</option>' +
domains.map(function(d) { return '<option value="' + esc(d) + '">' + esc(d) + '</option>'; }).join('');
conSel.innerHTML = '<option value="">All Contributors</option>' +
contribs.map(function(c) { return '<option value="' + esc(c) + '">' + esc(c) + '</option>'; }).join('');
domSel.value = curDom; conSel.value = curCon;
domSel.value = curDom;
}
function updateKPIs(data) {
@ -226,29 +199,47 @@ def render_prs_page(now: datetime) -> str:
document.getElementById('kpi-merge-rate').textContent = fmtPct(rate);
document.getElementById('kpi-merge-detail').textContent = fmtNum(data.open) + ' open';
var totalClaims = 0, mergedClaims = 0, totalCost = 0;
document.getElementById('kpi-rounds').textContent =
data.median_rounds != null ? data.median_rounds.toFixed(1) : '--';
document.getElementById('kpi-rounds-detail').textContent =
data.max_rounds != null ? 'max: ' + data.max_rounds : '';
var totalClaims = 0, mergedClaims = 0;
var totalCost = 0;
var actualCount = 0, estCount = 0;
(data.prs || []).forEach(function(p) {
totalClaims += (p.claims_count || 1);
if (p.status === 'merged') mergedClaims += (p.claims_count || 1);
totalCost += estimateCost(p);
totalCost += (p.cost || 0);
if (p.cost_is_actual) actualCount++; else estCount++;
});
document.getElementById('kpi-claims').textContent = fmtNum(totalClaims);
document.getElementById('kpi-claims-detail').textContent = fmtNum(mergedClaims) + ' merged';
// Show actual DB total if available, otherwise sum from PRs
var costLabel = '';
if (data.actual_total_cost > 0) {
document.getElementById('kpi-cost').textContent = '$' + data.actual_total_cost.toFixed(2);
costLabel = 'from costs table';
} else if (actualCount > 0) {
document.getElementById('kpi-cost').textContent = '$' + totalCost.toFixed(2);
var perClaim = totalClaims > 0 ? totalCost / totalClaims : 0;
document.getElementById('kpi-cost-detail').textContent = '$' + perClaim.toFixed(3) + '/claim';
costLabel = actualCount + ' actual, ' + estCount + ' est.';
} else {
document.getElementById('kpi-cost').textContent = '$' + totalCost.toFixed(2);
costLabel = 'ALL ESTIMATED';
}
var costPerClaim = totalClaims > 0 ? totalCost / totalClaims : 0;
document.getElementById('kpi-cost-detail').textContent =
'$' + costPerClaim.toFixed(3) + '/claim \u00b7 ' + costLabel;
}
function applyFilters() {
var dom = document.getElementById('filter-domain').value;
var con = document.getElementById('filter-contributor').value;
var out = document.getElementById('filter-outcome').value;
var tier = document.getElementById('filter-tier').value;
filtered = allData.filter(function(p) {
if (dom && p.domain !== dom) return false;
if (con && (p.submitted_by || 'unknown') !== con) return false;
if (out && p.status !== out) return false;
if (tier && p.tier !== tier) return false;
return true;
@ -278,6 +269,19 @@ def render_prs_page(now: datetime) -> str:
return s.length > n ? s.substring(0, n) + '...' : s;
}
function shortModel(m) {
if (!m) return '';
// Shorten model names for display
if (m.indexOf('gemini-2.5-flash') !== -1) return 'Gemini Flash';
if (m.indexOf('claude-sonnet') !== -1 || m.indexOf('sonnet-4') !== -1) return 'Sonnet';
if (m.indexOf('claude-opus') !== -1 || m.indexOf('opus') !== -1) return 'Opus';
if (m.indexOf('haiku') !== -1) return 'Haiku';
if (m.indexOf('gpt-4o') !== -1) return 'GPT-4o';
// fallback: strip provider prefix
var parts = m.split('/');
return parts[parts.length - 1];
}
function renderTable() {
var tbody = document.getElementById('pr-tbody');
var start = page * PAGE_SIZE;
@ -285,7 +289,7 @@ def render_prs_page(now: datetime) -> str:
var totalPages = Math.ceil(filtered.length / PAGE_SIZE);
if (slice.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;color:#8b949e;">No PRs match filters</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:#8b949e;">No PRs match filters</td></tr>';
return;
}
@ -297,37 +301,40 @@ def render_prs_page(now: datetime) -> str:
(p.tier || '').toLowerCase() === 'standard' ? 'tier-standard' : 'tier-light';
var date = p.created_at ? p.created_at.substring(0, 10) : '--';
// Summary: first claim title
// Summary
var summary = p.summary || '--';
var reviewSnippet = '';
if (p.status === 'closed' && p.review_snippet) {
reviewSnippet = '<div class="review-snippet">' + esc(truncate(p.review_snippet, 120)) + '</div>';
}
// Outcome with tier badge
var outcomeLabel = esc(p.status || '--');
var tierBadge = p.tier ? ' <span class="' + tierClass + '" style="font-size:10px;">' + esc(p.tier) + '</span>' : '';
// Review snippet for issues
var reviewSnippet = '';
if (p.review_snippet) {
reviewSnippet = '<div class="review-snippet">' + esc(truncate(p.review_snippet, 100)) + '</div>';
}
// Contributor display
var contributor = p.submitted_by || '--';
var contribClass = 'contributor-tag';
if (contributor.indexOf('self-directed') >= 0 || contributor === 'unknown') {
contribClass = 'contributor-self';
}
// Evaluator: domain agent + model tag
// Evaluator column: domain agent + model
var evaluator = '';
if (p.domain_agent) {
var modelShort = '';
if (p.domain_model) {
var m = p.domain_model;
if (m.indexOf('gemini') >= 0) modelShort = 'Gemini Flash';
else if (m.indexOf('gpt-4o') >= 0) modelShort = 'GPT-4o';
else if (m.indexOf('sonnet') >= 0) modelShort = 'Sonnet';
else modelShort = m.split('/').pop();
evaluator = '<div style="font-size:12px;color:#c9d1d9;">' + esc(p.domain_agent) + '</div>';
}
evaluator = esc(p.domain_agent) + (modelShort ? ' <span class="model-tag">' + esc(modelShort) + '</span>' : '');
if (p.domain_model) {
evaluator += '<div class="model-tag">' + esc(shortModel(p.domain_model)) + '</div>';
}
if (p.leo_model) {
evaluator += '<div class="model-tag">' + esc(shortModel(p.leo_model)) + '</div>';
}
if (!evaluator) evaluator = '<span style="color:#484f58;">--</span>';
// Cost actual from DB or estimated (flagged)
var costStr;
if (p.cost != null && p.cost > 0) {
if (p.cost_is_actual) {
costStr = '<span class="cost-val">$' + p.cost.toFixed(3) + '</span>';
} else {
costStr = '<span class="cost-val" style="opacity:0.5;" title="Estimated — no actual cost tracked">~$' + p.cost.toFixed(3) + '</span>';
}
} else {
costStr = '<span style="color:#484f58;">--</span>';
}
rows.push(
@ -335,17 +342,16 @@ def render_prs_page(now: datetime) -> str:
'<td><span class="expand-chevron">&#9654;</span> ' +
'<a class="pr-link" href="' + FORGEJO + p.number + '" target="_blank" rel="noopener" onclick="event.stopPropagation();">#' + p.number + '</a></td>' +
'<td style="white-space:normal;"><span class="summary-text">' + esc(summary) + '</span>' + reviewSnippet + '</td>' +
'<td style="text-align:center;">' + (p.claims_count || 1) + '</td>' +
'<td style="text-align:center;">' + (p.claims_count || '--') + '</td>' +
'<td>' + esc(p.domain || '--') + '</td>' +
'<td><span class="' + contribClass + '">' + esc(truncate(contributor, 20)) + '</span></td>' +
'<td class="' + outClass + '">' + esc(p.status || '--') + tierBadge + '</td>' +
'<td class="' + outClass + '">' + outcomeLabel + tierBadge + '</td>' +
'<td style="text-align:center;">' + (p.eval_rounds || '--') + '</td>' +
'<td>' + evaluator + '</td>' +
'<td>' + fmtCost(p.est_cost) + '</td>' +
'<td>' + costStr + '</td>' +
'<td>' + date + '</td>' +
'</tr>' +
'<tr id="trace-' + p.number + '" style="display:none;"><td colspan="10" style="padding:0;">' +
'<div class="trace-panel" id="panel-' + p.number + '">Loading...</div>' +
'<tr id="trace-' + p.number + '" style="display:none;"><td colspan="9" style="padding:0;">' +
'<div class="trace-panel" id="panel-' + p.number + '">Loading trace...</div>' +
'</td></tr>'
);
});
@ -408,34 +414,46 @@ def render_prs_page(now: datetime) -> str:
});
function loadTrace(pr, panel) {
// Find the PR data for claim titles
// Also find this PR in allData for claim list
var prData = null;
for (var i = 0; i < allData.length; i++) {
if (allData[i].number == pr) { prData = allData[i]; break; }
}
allData.forEach(function(p) { if (p.number == pr) prData = p; });
fetch('/api/trace/' + pr).then(function(r) { return r.json(); }).then(function(data) {
var html = '';
// Claims contained in this PR
if (prData && prData.description) {
var titles = prData.description.split('|').map(function(t) { return t.trim(); }).filter(Boolean);
if (titles.length > 0) {
html += '<h4>Claims (' + titles.length + ')</h4>';
// --- Claims contained in this PR ---
if (prData && prData.claim_titles && prData.claim_titles.length > 0) {
html += '<div class="section-title">Claims (' + prData.claim_titles.length + ')</div>';
html += '<ul class="claim-list">';
titles.forEach(function(t) {
prData.claim_titles.forEach(function(t) {
html += '<li>' + esc(t) + '</li>';
});
html += '</ul>';
}
}
// Issues (if any)
// --- Issues summary ---
var issues = [];
if (data.timeline) {
data.timeline.forEach(function(ev) {
if (ev.detail && ev.detail.issues) {
var iss = ev.detail.issues;
if (typeof iss === 'string') { try { iss = JSON.parse(iss); } catch(e) { iss = [iss]; } }
if (Array.isArray(iss)) {
iss.forEach(function(i) {
var label = String(i).replace(/_/g, ' ');
if (issues.indexOf(label) === -1) issues.push(label);
});
}
}
});
}
if (prData && prData.review_snippet) {
html += '<div class="issues-box">' + esc(prData.review_snippet) + '</div>';
} else if (issues.length > 0) {
html += '<div class="issues-box">Issues: ' + issues.map(esc).join(', ') + '</div>';
}
// Eval chain with models
// --- Eval chain (who reviewed with what model) ---
var models = {};
if (data.timeline) {
data.timeline.forEach(function(ev) {
@ -446,38 +464,23 @@ def render_prs_page(now: datetime) -> str:
}
});
}
html += '<div class="eval-chain"><strong style="color:#58a6ff;">Eval Chain:</strong> ';
var chain = [];
if (models['triage.haiku_triage'] || models['triage.deterministic_triage']) {
chain.push('<span class="chain-step">Triage <span class="model-tag">' +
esc(models['triage.haiku_triage'] || 'deterministic') + '</span></span>');
}
if (models['domain_review']) {
chain.push('<span class="chain-step">Domain <span class="model-tag">' +
esc(models['domain_review']) + '</span></span>');
}
if (models['leo_review']) {
chain.push('<span class="chain-step">Leo <span class="model-tag">' +
esc(models['leo_review']) + '</span></span>');
}
html += chain.length > 0 ? chain.join('<span class="chain-arrow">&#8594;</span>') :
'<span style="color:#484f58;">No model data</span>';
html += '</div>';
// Source + contributor metadata
if (data.pr) {
html += '<div style="margin:8px 0;font-size:12px;color:#8b949e;">';
if (data.pr.source_path) html += 'Source: <span style="color:#c9d1d9;">' + esc(data.pr.source_path) + '</span> &middot; ';
if (prData && prData.submitted_by) html += 'Contributor: <span style="color:#d2a8ff;">' + esc(prData.submitted_by) + '</span> &middot; ';
if (data.pr.tier) html += 'Tier: <span style="color:#c9d1d9;">' + esc(data.pr.tier) + '</span> &middot; ';
html += '<a class="pr-link" href="' + FORGEJO + pr + '" target="_blank">View on Forgejo</a>';
if (Object.keys(models).length > 0) {
html += '<div class="eval-chain">';
html += '<strong style="color:#58a6ff;">Eval chain:</strong> ';
var parts = [];
if (models['triage.haiku_triage'] || models['triage.deterministic_triage'])
parts.push('<span class="step"><span class="step-label">Triage</span> <span class="step-model">' + shortModel(models['triage.haiku_triage'] || 'deterministic') + '</span></span>');
if (models['domain_review'])
parts.push('<span class="step"><span class="step-label">Domain</span> <span class="step-model">' + shortModel(models['domain_review']) + '</span></span>');
if (models['leo_review'])
parts.push('<span class="step"><span class="step-label">Leo</span> <span class="step-model">' + shortModel(models['leo_review']) + '</span></span>');
html += parts.length > 0 ? parts.join(' <span class="arrow">&#8594;</span> ') : '<span style="color:#484f58;">No model data</span>';
html += '</div>';
}
// Timeline
// --- Timeline ---
if (data.timeline && data.timeline.length > 0) {
html += '<h4>Timeline</h4>';
html += '<div class="section-title">Timeline</div>';
html += '<ul class="trace-timeline">';
data.timeline.forEach(function(ev) {
var cls = ev.event === 'approved' ? 'ev-approved' :
@ -488,7 +491,7 @@ def render_prs_page(now: datetime) -> str:
if (ev.detail) {
if (ev.detail.tier) detail += ' tier=' + ev.detail.tier;
if (ev.detail.reason) detail += ' &#8212; ' + esc(ev.detail.reason);
if (ev.detail.model) detail += ' [' + esc(ev.detail.model) + ']';
if (ev.detail.model) detail += ' [' + esc(shortModel(ev.detail.model)) + ']';
if (ev.detail.review_text) {
detail += '<div class="review-text">' + esc(ev.detail.review_text).substring(0, 2000) + '</div>';
}
@ -506,19 +509,19 @@ def render_prs_page(now: datetime) -> str:
});
html += '</ul>';
} else {
html += '<div style="color:#484f58;font-size:12px;margin:8px 0;">No timeline events</div>';
html += '<div style="color:#484f58;font-size:12px;margin-top:8px;">No timeline events</div>';
}
// Reviews
// --- Reviews ---
if (data.reviews && data.reviews.length > 0) {
html += '<h4>Reviews</h4>';
html += '<div class="section-title">Reviews</div>';
data.reviews.forEach(function(r) {
var cls = r.outcome === 'approved' ? 'badge-green' :
r.outcome === 'rejected' ? 'badge-red' : 'badge-yellow';
html += '<div style="margin:4px 0;">' +
'<span class="badge ' + cls + '">' + esc(r.outcome) + '</span> ' +
'<span style="color:#8b949e;font-size:11px;">' + esc(r.reviewer || '') + ' ' +
(r.model ? '[' + esc(r.model) + ']' : '') + ' ' +
(r.model ? '[' + esc(shortModel(r.model)) + ']' : '') + ' ' +
(r.reviewed_at || '').substring(0, 19) + '</span>';
if (r.rejection_reason) {
html += ' <code>' + esc(r.rejection_reason) + '</code>';
@ -537,7 +540,7 @@ def render_prs_page(now: datetime) -> str:
}
// Filter listeners
['filter-domain', 'filter-contributor', 'filter-outcome', 'filter-tier'].forEach(function(id) {
['filter-domain', 'filter-outcome', 'filter-tier'].forEach(function(id) {
document.getElementById(id).addEventListener('change', applyFilters);
});
document.getElementById('filter-days').addEventListener('change', loadData);

View file

@ -10,6 +10,7 @@ Endpoints:
Owner: Argus
"""
import asyncio
import json
import logging
import os
@ -17,6 +18,7 @@ import sqlite3
import statistics
import time
import urllib.request
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
@ -61,6 +63,7 @@ async def handle_stage_times(request):
Returns median minutes between consecutive stages.
"""
conn = request.app["_get_conn"]()
try:
hours = int(request.query.get("hours", "24"))
# Get per-PR event timestamps
@ -117,6 +120,8 @@ async def handle_stage_times(request):
}
return web.json_response({"hours": hours, "stages": stage_times})
finally:
conn.close()
# ─── GET /api/herfindahl ──────────────────────────────────────────────────
@ -127,6 +132,7 @@ async def handle_herfindahl(request):
HHI = sum of (domain_share^2). 1.0 = single domain, lower = more diverse.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
rows = conn.execute(
@ -164,6 +170,8 @@ async def handle_herfindahl(request):
"total_merged": total,
"days": days,
})
finally:
conn.close()
# ─── GET /api/agent-state ─────────────────────────────────────────────────
@ -226,13 +234,14 @@ async def handle_agent_state(request):
async def handle_extraction_yield_by_domain(request):
"""Sources → claims conversion rate per domain."""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
# Sources per domain (approximate from PR source_path domain)
source_counts = conn.execute(
"""SELECT domain, COUNT(DISTINCT source_url) as sources
"""SELECT domain, COUNT(DISTINCT path) as sources
FROM sources s
JOIN prs p ON p.source_path LIKE '%' || s.url || '%'
JOIN prs p ON p.source_path LIKE '%' || s.path || '%'
WHERE s.created_at > datetime('now', ? || ' days')
GROUP BY domain""",
(f"-{days}",),
@ -269,6 +278,8 @@ async def handle_extraction_yield_by_domain(request):
domains.sort(key=lambda x: x["merged"], reverse=True)
return web.json_response({"days": days, "domains": domains})
finally:
conn.close()
# ─── GET /api/agents-dashboard ─────────────────────────────────────────────
@ -281,6 +292,7 @@ async def handle_agents_dashboard(request):
All in one response to avoid N client-side fetches.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
# Per-agent merged + rejected counts
@ -380,6 +392,8 @@ async def handle_agents_dashboard(request):
pass
return web.json_response({"days": days, "agents": agents})
finally:
conn.close()
# ─── GET /api/cascade-coverage ────────────────────────────────────────────
@ -390,6 +404,7 @@ async def handle_cascade_coverage(request):
Returns: triggered count, by-agent breakdown, claims affected.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
triggered = conn.execute(
@ -431,6 +446,8 @@ async def handle_cascade_coverage(request):
for r in triggered
]
insufficient_data = total_triggered < 5
return web.json_response({
"days": days,
"total_triggered": total_triggered,
@ -439,7 +456,10 @@ async def handle_cascade_coverage(request):
"total_notifications": summaries["total_notifications"] if summaries else 0,
"merges_with_cascade": summaries["total_merges_with_cascade"] if summaries else 0,
"by_agent": by_agent,
"insufficient_data": insufficient_data,
})
finally:
conn.close()
# ─── GET /api/review-summary ─────────────────────────────────────────────
@ -451,6 +471,7 @@ async def handle_review_summary(request):
disagreement_type columns.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
# Check if table exists and has data
@ -474,7 +495,7 @@ async def handle_review_summary(request):
(f"-{days}",),
).fetchall()
# Rejection reasons
# Rejection reasons — try review_records first, fall back to prs.eval_issues
reasons = conn.execute(
"""SELECT rejection_reason, COUNT(*) as cnt
FROM review_records
@ -484,15 +505,17 @@ async def handle_review_summary(request):
(f"-{days}",),
).fetchall()
# Disagreement types
disagreements = conn.execute(
"""SELECT disagreement_type, COUNT(*) as cnt
FROM review_records
WHERE disagreement_type IS NOT NULL
AND reviewed_at > datetime('now', ? || ' days')
GROUP BY disagreement_type ORDER BY cnt DESC""",
rejection_source = "review_records"
if not reasons:
reasons = conn.execute(
"""SELECT value AS rejection_reason, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND created_at > datetime('now', ? || ' days')
GROUP BY value ORDER BY cnt DESC""",
(f"-{days}",),
).fetchall()
rejection_source = "prs.eval_issues"
# Per-reviewer breakdown
reviewers = conn.execute(
@ -525,7 +548,7 @@ async def handle_review_summary(request):
"total": total,
"outcomes": {r["outcome"]: r["cnt"] for r in outcomes},
"rejection_reasons": [{"reason": r["rejection_reason"], "count": r["cnt"]} for r in reasons],
"disagreement_types": [{"type": r["disagreement_type"], "count": r["cnt"]} for r in disagreements],
"rejection_source": rejection_source,
"reviewers": [
{"reviewer": r["reviewer"], "approved": r["approved"], "approved_with_changes": r["approved_with_changes"],
"rejected": r["rejected"], "total": r["total"]}
@ -537,6 +560,126 @@ async def handle_review_summary(request):
for r in domains
],
})
finally:
conn.close()
# ─── GET /api/agent-scorecard ──────────────────────────────────────────────
async def handle_agent_scorecard(request):
"""Per-agent scorecard: PRs submitted, review outcomes, rejection reasons.
Data from review_records (structured reviews) + prs (submission counts).
Falls back to prs.eval_issues for rejection reasons when review_records
has no rejections yet.
"""
conn = request.app["_get_conn"]()
try:
try:
days = min(int(request.query.get("days", "30")), 90)
except ValueError:
days = 30
day_filter = f"-{days}"
# PRs submitted per agent
prs_by_agent = conn.execute(
"""SELECT agent, COUNT(*) as cnt FROM prs
WHERE agent IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY agent""",
(day_filter,),
).fetchall()
prs_map = {r["agent"]: r["cnt"] for r in prs_by_agent}
# Review outcomes from review_records
review_data = {}
try:
reviews = conn.execute(
"""SELECT reviewer as agent, outcome, COUNT(*) as cnt
FROM review_records
WHERE reviewed_at > datetime('now', ? || ' days')
GROUP BY reviewer, outcome""",
(day_filter,),
).fetchall()
for r in reviews:
agent = r["agent"]
if agent not in review_data:
review_data[agent] = {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0}
review_data[agent][r["outcome"].replace("-", "_")] = r["cnt"]
review_data[agent]["total"] += r["cnt"]
except sqlite3.OperationalError:
pass
# If review_records is empty, fall back to audit_log eval events
if not review_data:
evals = conn.execute(
"""SELECT
COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent,
event, COUNT(*) as cnt
FROM audit_log
WHERE stage='evaluate'
AND event IN ('approved','changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', ? || ' days')
GROUP BY agent, event""",
(day_filter,),
).fetchall()
for r in evals:
agent = r["agent"]
if not agent:
continue
if agent not in review_data:
review_data[agent] = {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0}
if r["event"] == "approved":
review_data[agent]["approved"] += r["cnt"]
elif r["event"] == "changes_requested": # fixer auto-remediated; equivalent in pre-review_records era
review_data[agent]["approved_with_changes"] += r["cnt"]
else:
review_data[agent]["rejected"] += r["cnt"]
review_data[agent]["total"] += r["cnt"]
# Rejection reasons from prs.eval_issues (canonical source)
reason_rows = conn.execute(
"""SELECT agent, value as reason, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND agent IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY agent, reason ORDER BY agent, cnt DESC""",
(day_filter,),
).fetchall()
reasons_map = {}
for r in reason_rows:
if r["agent"] not in reasons_map:
reasons_map[r["agent"]] = {}
reasons_map[r["agent"]][r["reason"]] = r["cnt"]
# Build scorecards
all_agents = sorted(set(list(prs_map.keys()) + list(review_data.keys())))
scorecards = []
for agent in all_agents:
if agent in ("unknown", None):
continue
rd = review_data.get(agent, {"approved": 0, "approved_with_changes": 0, "rejected": 0, "total": 0})
total_reviews = rd["total"]
approved = rd["approved"]
approved_wc = rd["approved_with_changes"]
rejected = rd["rejected"]
approval_rate = ((approved + approved_wc) / total_reviews * 100) if total_reviews else 0
scorecards.append({
"agent": agent,
"total_prs": prs_map.get(agent, 0),
"total_reviews": total_reviews,
"approved": approved,
"approved_with_changes": approved_wc,
"rejected": rejected,
"approval_rate": round(approval_rate, 1),
"rejection_reasons": reasons_map.get(agent, {}),
})
scorecards.sort(key=lambda x: x["total_reviews"], reverse=True)
return web.json_response({"days": days, "scorecards": scorecards})
finally:
conn.close()
# ─── Trace endpoint ────────────────────────────────────────────────────────
@ -549,11 +692,8 @@ async def handle_trace(request: web.Request) -> web.Response:
One thread, every stage, chronological.
"""
trace_id = request.match_info["trace_id"]
get_conn = request.app["_get_conn"]
conn = get_conn()
# Audit log events (the backbone)
# Try trace_id first, fall back to PR number in detail JSON
conn = request.app["_get_conn"]()
try:
events = conn.execute(
"""SELECT timestamp, stage, event, detail
FROM audit_log
@ -563,7 +703,6 @@ async def handle_trace(request: web.Request) -> web.Response:
).fetchall()
if not events:
# Fallback: match by PR number in detail JSON (for rows without trace_id)
events = conn.execute(
"""SELECT timestamp, stage, event, detail
FROM audit_log
@ -572,7 +711,6 @@ async def handle_trace(request: web.Request) -> web.Response:
(trace_id,),
).fetchall()
# Review records for this PR
reviews = conn.execute(
"""SELECT reviewed_at, reviewer, reviewer_model, outcome,
rejection_reason, disagreement_type, notes, claim_path
@ -582,7 +720,6 @@ async def handle_trace(request: web.Request) -> web.Response:
(trace_id,),
).fetchall()
# PR metadata
pr = conn.execute(
"""SELECT number, source_path, domain, agent, tier, status,
origin, created_at, merged_at
@ -608,6 +745,8 @@ async def handle_trace(request: web.Request) -> web.Response:
}
return web.json_response(result)
finally:
conn.close()
# ─── GET /api/growth ──────────────────────────────────────────────────────
@ -618,6 +757,7 @@ async def handle_growth(request):
Returns daily data points with running totals for each series.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "90"))
# Daily new sources
@ -709,6 +849,8 @@ async def handle_growth(request):
"merged": m_total,
},
})
finally:
conn.close()
import re
@ -723,23 +865,36 @@ async def handle_pr_lifecycle(request):
Joins prs + audit_log (eval rounds) + review_records.
"""
conn = request.app["_get_conn"]()
try:
days = int(request.query.get("days", "30"))
day_clause = "AND p.created_at > datetime('now', ? || ' days')" if days < 9999 else ""
params = (f"-{days}",) if days < 9999 else ()
# Base PR data
# Base PR data (include cost_usd for actual cost tracking)
pr_rows = conn.execute(
f"""SELECT p.number, p.agent, p.domain, p.tier, p.status,
p.created_at, p.merged_at, p.leo_verdict, p.description,
p.domain_agent, p.domain_model, p.branch, p.submitted_by,
p.source_path
p.domain_agent, p.domain_model, p.branch, p.cost_usd
FROM prs p
WHERE 1=1 {day_clause}
ORDER BY p.number DESC""",
params,
).fetchall()
# Actual costs from costs table (aggregated, same date window as PRs)
cost_day_clause = "AND date > date('now', ? || ' days')" if days < 9999 else ""
actual_cost_rows = conn.execute(
f"""SELECT SUM(cost_usd) as total_actual_cost,
SUM(calls) as total_calls,
SUM(input_tokens) as total_input_tokens,
SUM(output_tokens) as total_output_tokens
FROM costs
WHERE cost_usd > 0 {cost_day_clause}""",
params,
).fetchone()
actual_total_cost = actual_cost_rows["total_actual_cost"] if actual_cost_rows and actual_cost_rows["total_actual_cost"] else 0
# Eval round counts per PR (from audit_log)
eval_rows = conn.execute(
f"""SELECT CAST(json_extract(detail, '$.pr') AS INTEGER) as pr,
@ -802,6 +957,19 @@ async def handle_pr_lifecycle(request):
except (json.JSONDecodeError, TypeError):
pass
TIER_COST_EST = {
"LIGHT": 0.002,
"STANDARD": 0.018,
"DEEP": 0.12,
}
EXTRACT_COST_EST = 0.025
LEO_MODEL_BY_TIER = {
"DEEP": "claude-opus-4-20250514",
"STANDARD": "anthropic/claude-sonnet-4.5",
"LIGHT": None,
}
# Build PR list
prs = []
ttm_values = []
@ -839,38 +1007,46 @@ async def handle_pr_lifecycle(request):
elif status == "open":
open_count += 1
# Claims count from pipe-separated description titles
desc = r["description"] or ""
claims_count = desc.count("|") + 1 if desc.strip() else 1
claim_titles = [t.strip() for t in desc.split("|") if t.strip()] if desc.strip() else []
claims_count = len(claim_titles) if claim_titles else 1
# Summary: first claim title from description, fallback to branch name
summary = None
if desc.strip():
first_title = desc.split("|")[0].strip()
summary = first_title[:120] if first_title else None
if claim_titles:
summary = claim_titles[0][:120]
if not summary:
branch = r["branch"] or ""
# Use prefix as category if present: "extract/...", "reweave/...", etc.
prefix = ""
if "/" in branch:
prefix = branch.split("/", 1)[0]
branch = branch.split("/", 1)[1]
# Strip date prefix like "2026-04-06-" or "2026-02-00-"
branch = _DATE_PREFIX_RE.sub("", branch)
# Strip trailing hash suffix like "-116d" or "-2cb1"
branch = re.sub(r"-[0-9a-f]{4}$", "", branch)
if branch:
summary = branch.replace("-", " ").replace("_", " ").strip()[:120]
elif prefix:
summary = prefix # "reweave", "ingestion", etc.
summary = prefix
tier = r["tier"] or "STANDARD"
actual_cost = r["cost_usd"] if r["cost_usd"] and r["cost_usd"] > 0 else None
if actual_cost is not None:
cost = round(actual_cost, 4)
cost_is_actual = True
else:
eval_cost = TIER_COST_EST.get(tier, 0.018) * max(rounds, 1)
cost = round(EXTRACT_COST_EST + eval_cost, 4)
cost_is_actual = False
leo_model = LEO_MODEL_BY_TIER.get(tier)
prs.append({
"number": pr_num,
"agent": r["agent"],
"domain": r["domain"],
"tier": r["tier"],
"tier": tier,
"status": status,
"claims_count": claims_count,
"claim_titles": claim_titles,
"eval_rounds": rounds,
"ttm_minutes": round(ttm, 1) if ttm is not None else None,
"created_at": r["created_at"],
@ -880,10 +1056,11 @@ async def handle_pr_lifecycle(request):
"summary": summary,
"description": desc if desc.strip() else None,
"review_snippet": snippet_map.get(pr_num),
"submitted_by": r["submitted_by"],
"source_path": r["source_path"],
"domain_agent": r["domain_agent"],
"domain_model": r["domain_model"],
"leo_model": leo_model,
"cost": cost,
"cost_is_actual": cost_is_actual,
})
# Summary KPIs
@ -903,18 +1080,215 @@ async def handle_pr_lifecycle(request):
return None
return vals[int(len(vals) * 0.9)]
# Compute cost summary: actual where available, estimated where not
total_actual = sum(p["cost"] for p in prs if p["cost_is_actual"])
total_estimated = sum(p["cost"] for p in prs if not p["cost_is_actual"])
prs_with_actual_cost = sum(1 for p in prs if p["cost_is_actual"])
med_ttm = median(ttm_values)
med_rounds = median(round_values)
return web.json_response({
"days": days,
"total": len(prs),
"merged": merged_count,
"closed": closed_count,
"open": open_count,
"median_ttm": round(median(ttm_values), 1) if median(ttm_values) is not None else None,
"median_ttm": round(med_ttm, 1) if med_ttm is not None else None,
"p90_ttm": round(p90(ttm_values), 1) if p90(ttm_values) is not None else None,
"median_rounds": round(median(round_values), 1) if median(round_values) is not None else None,
"median_rounds": round(med_rounds, 1) if med_rounds is not None else None,
"max_rounds": max(round_values) if round_values else None,
"actual_total_cost": round(actual_total_cost, 2),
"cost_summary": {
"total_actual": round(total_actual, 2),
"total_estimated": round(total_estimated, 2),
"prs_with_actual_cost": prs_with_actual_cost,
"prs_with_estimated_cost": len(prs) - prs_with_actual_cost,
},
"prs": prs,
})
finally:
conn.close()
# ─── GET /api/telegram-extractions ───────────────────────────────────────
async def handle_telegram_extractions(request):
"""Review surface for Telegram conversation extractions.
Shows recent PRs sourced from Telegram conversations with claim titles,
status, and source info. Designed for quick daily spot-checking.
Query params:
days (int): lookback window (default 7, max 90)
"""
conn = request.app["_get_conn"]()
try:
days = min(int(request.query.get("days", "7")), 90)
day_filter = f"-{days}"
# Find PRs from Telegram sources (source_path contains 'telegram' or submitted_by is @m3taversal via bot)
rows = conn.execute(
"""SELECT p.number, p.agent, p.domain, p.tier, p.status,
p.created_at, p.merged_at, p.description, p.source_path,
p.submitted_by, p.branch, p.eval_issues, p.leo_verdict
FROM prs p
WHERE (p.source_path LIKE '%telegram%' OR p.source_path LIKE '%futardio%')
AND p.created_at > datetime('now', ? || ' days')
ORDER BY p.number DESC""",
(day_filter,),
).fetchall()
prs = []
for r in rows:
desc = r["description"] or ""
claim_titles = [t.strip() for t in desc.split("|") if t.strip()] if desc.strip() else []
issues = None
if r["eval_issues"]:
try:
issues = json.loads(r["eval_issues"]) if isinstance(r["eval_issues"], str) else r["eval_issues"]
except (json.JSONDecodeError, TypeError):
pass
prs.append({
"number": r["number"],
"agent": r["agent"],
"domain": r["domain"],
"tier": r["tier"],
"status": r["status"],
"created_at": r["created_at"],
"merged_at": r["merged_at"],
"claim_titles": claim_titles,
"source_path": r["source_path"],
"submitted_by": r["submitted_by"],
"eval_issues": issues,
"leo_verdict": r["leo_verdict"],
})
# Summary stats
merged = sum(1 for p in prs if p["status"] == "merged")
closed = sum(1 for p in prs if p["status"] == "closed")
open_prs = sum(1 for p in prs if p["status"] == "open")
return web.json_response({
"days": days,
"total": len(prs),
"merged": merged,
"closed": closed,
"open": open_prs,
"merge_rate": round(merged / len(prs) * 100, 1) if prs else 0,
"prs": prs,
})
finally:
conn.close()
# ─── GET /api/contributor-growth ─────────────────────────────────────────
CODEX_WORKTREE = Path(os.environ.get("MAIN_WORKTREE", "/opt/teleo-eval/workspaces/main"))
FOUNDING_CUTOFF = "2026-03-15"
CONTRIBUTOR_EXCLUDE = {"Teleo Agents", "Teleo Pipeline"}
_growth_cache: dict | None = None
_growth_cache_ts: float = 0
GROWTH_CACHE_TTL = 300
async def handle_contributor_growth(request):
"""Cumulative unique contributors and claims over time from git log.
Returns time-series data for Chart.js line charts.
Cached for 5 minutes since git log is expensive.
"""
global _growth_cache, _growth_cache_ts
now = time.monotonic()
if _growth_cache is not None and (now - _growth_cache_ts) < GROWTH_CACHE_TTL:
return web.json_response(_growth_cache)
codex_path = str(CODEX_WORKTREE)
if not CODEX_WORKTREE.exists():
return web.json_response(
{"error": "codex worktree not found", "path": codex_path}, status=404
)
proc = await asyncio.create_subprocess_exec(
"git", "log", "--format=%ad|%an", "--date=format:%Y-%m-%d", "--all",
cwd=codex_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
return web.json_response(
{"error": "git log failed", "detail": stderr.decode()[:500]}, status=500
)
first_seen: dict[str, str] = {}
daily_commits: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for line in stdout.decode().strip().split("\n"):
if "|" not in line:
continue
date, author = line.split("|", 1)
if author in CONTRIBUTOR_EXCLUDE:
continue
daily_commits[date][author] += 1
if author not in first_seen or date < first_seen[author]:
first_seen[author] = date
by_date: dict[str, list[str]] = defaultdict(list)
for author, date in first_seen.items():
by_date[date].append(author)
contributors_timeline = []
seen: set[str] = set()
for date in sorted(by_date.keys()):
new_authors = by_date[date]
seen.update(new_authors)
contributors_timeline.append({
"date": date,
"cumulative": len(seen),
"new": [{"name": a, "founding": date <= FOUNDING_CUTOFF} for a in sorted(new_authors)],
})
proc2 = await asyncio.create_subprocess_exec(
"git", "log", "--format=%ad", "--date=format:%Y-%m-%d",
"--all", "--diff-filter=A", "--", "domains/*.md",
cwd=codex_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout2, _ = await proc2.communicate()
claim_counts: dict[str, int] = defaultdict(int)
for line in stdout2.decode().strip().split("\n"):
line = line.strip()
if line:
claim_counts[line] += 1
claims_timeline = []
cumulative = 0
for date in sorted(claim_counts.keys()):
cumulative += claim_counts[date]
claims_timeline.append({"date": date, "cumulative": cumulative, "added": claim_counts[date]})
all_contributors = set(first_seen.keys())
founding = sorted(a for a in all_contributors if first_seen[a] <= FOUNDING_CUTOFF)
result = {
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"summary": {
"total_contributors": len(all_contributors),
"founding_contributors": founding,
"total_claims": cumulative,
"days_active": (datetime.now(timezone.utc) - datetime(2026, 3, 5, tzinfo=timezone.utc)).days,
},
"cumulative_contributors": contributors_timeline,
"cumulative_claims": claims_timeline,
}
_growth_cache = result
_growth_cache_ts = now
return web.json_response(result)
# ─── Registration ──────────────────────────────────────────────────────────
@ -929,6 +1303,47 @@ def register_dashboard_routes(app: web.Application, get_conn):
app.router.add_get("/api/agents-dashboard", handle_agents_dashboard)
app.router.add_get("/api/cascade-coverage", handle_cascade_coverage)
app.router.add_get("/api/review-summary", handle_review_summary)
app.router.add_get("/api/agent-scorecard", handle_agent_scorecard)
app.router.add_get("/api/trace/{trace_id}", handle_trace)
app.router.add_get("/api/growth", handle_growth)
app.router.add_get("/api/pr-lifecycle", handle_pr_lifecycle)
app.router.add_get("/api/telegram-extractions", handle_telegram_extractions)
app.router.add_get("/api/contributor-growth", handle_contributor_growth)
app.router.add_get("/api/digest/latest", handle_digest_latest)
app.router.add_get("/api/contributor-graph", handle_contributor_graph)
async def handle_digest_latest(request):
"""GET /api/digest/latest — return the most recent scoring digest."""
import json as _json
digest_path = "/opt/teleo-eval/logs/scoring-digest-latest.json"
try:
with open(digest_path) as f:
data = _json.load(f)
return web.json_response(data)
except FileNotFoundError:
return web.json_response({"error": "No digest available yet"}, status=404)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def handle_contributor_graph(request):
"""GET /api/contributor-graph — serve the PNG chart."""
import subprocess, os
png_path = "/opt/teleo-eval/static/contributor-graph.png"
# Regenerate if older than 1 hour or missing
regen = not os.path.exists(png_path)
if not regen:
age = __import__('time').time() - os.path.getmtime(png_path)
regen = age > 3600
if regen:
try:
subprocess.run(
['python3', '/opt/teleo-eval/scripts/contributor-graph.py'],
timeout=30, capture_output=True
)
except Exception:
pass
if not os.path.exists(png_path):
return web.Response(text='Chart not available', status=503)
return web.FileResponse(png_path, headers={'Content-Type': 'image/png'})

View file

@ -0,0 +1,309 @@
"""Read-only canonical KB claim routes for Argus.
These routes show the Postgres-backed claim graph that Leo uses through the
``teleo-kb`` bridge: one claim, its evidence rows, and its graph edges.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import sys
from datetime import datetime, timezone
from html import escape
from pathlib import Path
from typing import Any
from aiohttp import web
from shared_ui import render_page
ROOT = Path(__file__).resolve().parent.parent
SCRIPT_DIR_CANDIDATES = [
ROOT / "scripts",
Path(os.environ.get("TELEO_INFRA_REPO_DIR", "/opt/teleo-eval/workspaces/deploy-infra")) / "scripts",
]
for scripts_dir in SCRIPT_DIR_CANDIDATES:
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
import kb_proposal_review_packet as proposal_review # noqa: E402
logger = logging.getLogger("argus.kb_claims")
KB_CLAIM_PUBLIC_PATHS = frozenset()
KB_CLAIM_PUBLIC_PREFIXES = ("/kb/claims/", "/api/kb/claims/")
KB_CLAIM_LOADER_KEY = web.AppKey("kb_claim_loader", object)
UUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)
def is_claim_id(value: Any) -> bool:
return bool(UUID_RE.match(str(value or "")))
def claim_path(claim_id: str) -> str:
return f"/kb/claims/{claim_id}"
def claim_link_html(claim_id: Any, *, label: str | None = None) -> str:
value = str(claim_id or "")
if not is_claim_id(value):
return f"<code>{escape(value or 'unknown')}</code>"
text = label or value
return f'<a class="claim-link" href="{claim_path(value)}"><code>{escape(text)}</code></a>'
def short_claim_link_html(claim_id: Any) -> str:
value = str(claim_id or "")
if not is_claim_id(value):
return f"<code>{escape(value or 'unknown')}</code>"
return claim_link_html(value, label=value[:8])
def _db_args() -> argparse.Namespace:
return argparse.Namespace(
secrets_file=os.environ.get(
"KB_CLAIM_REVIEW_SECRETS_FILE",
os.environ.get(
"KB_PROPOSAL_REVIEW_SECRETS_FILE",
os.environ.get("KB_APPLY_SECRETS_FILE", proposal_review.ap.DEFAULT_SECRETS_FILE),
),
),
container=os.environ.get("KB_CLAIM_REVIEW_CONTAINER", proposal_review.ap.DEFAULT_CONTAINER),
db=os.environ.get("KB_CLAIM_REVIEW_DB", proposal_review.ap.DEFAULT_DB),
host=os.environ.get("KB_CLAIM_REVIEW_HOST", proposal_review.ap.DEFAULT_HOST),
role=os.environ.get("KB_CLAIM_REVIEW_ROLE", proposal_review.ap.DEFAULT_ROLE),
)
def _load_claim_from_db(claim_id: str) -> dict[str, Any] | None:
password = proposal_review.ap.load_password(_db_args().secrets_file)
args = _db_args()
sql = f"""
with target as (
select c.id,
c.type,
c.text,
c.status,
c.confidence,
c.tags,
c.superseded_by,
c.created_at,
c.updated_at
from public.claims c
where c.id = {proposal_review.ap.sql_literal(claim_id)}::uuid
)
select jsonb_build_object(
'claim', jsonb_build_object(
'id', target.id::text,
'type', target.type,
'text', target.text,
'status', target.status,
'confidence', target.confidence,
'tags', coalesce(to_jsonb(target.tags), '[]'::jsonb),
'superseded_by', target.superseded_by::text,
'created_at', target.created_at::text,
'updated_at', target.updated_at::text
),
'evidence', coalesce((
select jsonb_agg(row_data order by rn)
from (
select row_number() over (
order by ce.role::text,
ce.weight desc nulls last,
s.storage_path nulls last,
s.url nulls last
) as rn,
jsonb_build_object(
'role', ce.role::text,
'weight', ce.weight,
'source_type', s.source_type,
'url', s.url,
'storage_path', s.storage_path,
'excerpt', left(coalesce(s.excerpt, ''), 1200)
) as row_data
from public.claim_evidence ce
join public.sources s on s.id = ce.source_id
where ce.claim_id = target.id
limit 30
) evidence_rows
), '[]'::jsonb),
'edges', coalesce((
select jsonb_agg(row_data order by rn)
from (
select row_number() over (
order by e.edge_type::text,
other.text
) as rn,
jsonb_build_object(
'direction', case when e.from_claim = target.id then 'outgoing' else 'incoming' end,
'edge_type', e.edge_type::text,
'connected_id', other.id::text,
'connected_text', other.text,
'connected_status', other.status,
'connected_confidence', other.confidence
) as row_data
from public.claim_edges e
join public.claims other
on other.id = case when e.from_claim = target.id then e.to_claim else e.from_claim end
where e.from_claim = target.id or e.to_claim = target.id
limit 40
) edge_rows
), '[]'::jsonb)
)::text
from target;
"""
out = proposal_review.ap.run_psql(args, sql, password).strip()
return json.loads(out) if out else None
def _load_claim(request: web.Request, claim_id: str) -> dict[str, Any] | None:
loader = request.app.get(KB_CLAIM_LOADER_KEY)
return loader(claim_id) if loader else _load_claim_from_db(claim_id)
def _metadata_table(claim: dict[str, Any]) -> str:
rows = [
("ID", claim_link_html(claim.get("id"))),
("Status", f"<code>{escape(str(claim.get('status') or 'unknown'))}</code>"),
("Type", f"<code>{escape(str(claim.get('type') or 'unknown'))}</code>"),
("Confidence", f"<code>{escape(str(claim.get('confidence') or 'unknown'))}</code>"),
("Tags", f"<code>{escape(', '.join(claim.get('tags') or []) or 'none')}</code>"),
("Created", f"<code>{escape(str(claim.get('created_at') or 'unknown'))}</code>"),
("Updated", f"<code>{escape(str(claim.get('updated_at') or 'unknown'))}</code>"),
]
if claim.get("superseded_by"):
rows.append(("Superseded by", claim_link_html(claim["superseded_by"])))
return "\n".join(f"<tr><th>{escape(label)}</th><td>{value}</td></tr>" for label, value in rows)
def _render_evidence(evidence: list[dict[str, Any]]) -> str:
if not evidence:
return '<div class="empty-state">No evidence rows found for this claim.</div>'
rows = []
for row in evidence:
source = row.get("url") or row.get("storage_path") or "no source pointer"
source_html = escape(str(source))
if row.get("url"):
source_html = f'<a href="{escape(str(row["url"]))}">{source_html}</a>'
rows.append(
"<tr>"
f"<td><code>{escape(str(row.get('role') or 'unknown'))}</code></td>"
f"<td><code>{escape(str(row.get('weight') or ''))}</code></td>"
f"<td><code>{escape(str(row.get('source_type') or 'unknown'))}</code></td>"
f"<td>{source_html}</td>"
f"<td>{escape(str(row.get('excerpt') or ''))}</td>"
"</tr>"
)
return (
"<table class=\"claim-table\"><thead><tr><th>Role</th><th>Weight</th>"
"<th>Source type</th><th>Source</th><th>Excerpt</th></tr></thead>"
f"<tbody>{''.join(rows)}</tbody></table>"
)
def _render_edges(edges: list[dict[str, Any]]) -> str:
if not edges:
return '<div class="empty-state">No graph edges found for this claim.</div>'
rows = []
for row in edges:
rows.append(
"<tr>"
f"<td><code>{escape(str(row.get('direction') or 'unknown'))}</code></td>"
f"<td><code>{escape(str(row.get('edge_type') or 'unknown'))}</code></td>"
f"<td>{short_claim_link_html(row.get('connected_id'))}</td>"
f"<td>{escape(str(row.get('connected_text') or ''))}</td>"
f"<td><code>{escape(str(row.get('connected_status') or ''))}</code></td>"
"</tr>"
)
return (
"<table class=\"claim-table\"><thead><tr><th>Direction</th><th>Edge</th>"
"<th>Connected claim</th><th>Text</th><th>Status</th></tr></thead>"
f"<tbody>{''.join(rows)}</tbody></table>"
)
def render_kb_claim_page(data: dict[str, Any]) -> str:
claim = data["claim"]
generated_at = datetime.now(timezone.utc).isoformat()
body = f"""
<div class="claim-card">
<div class="label">Canonical claim</div>
<p class="claim-text">{escape(str(claim.get("text") or ""))}</p>
<table class="proposal-detail"><tbody>{_metadata_table(claim)}</tbody></table>
</div>
<div class="section">
<div class="section-title">Evidence</div>
{_render_evidence(data.get("evidence") or [])}
</div>
<div class="section">
<div class="section-title">Edges</div>
{_render_edges(data.get("edges") or [])}
</div>"""
extra_css = """
.claim-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.claim-text { margin-top: 8px; color: #f0f6fc; font-size: 17px; line-height: 1.45; }
.claim-link { color: #58a6ff; text-decoration: none; }
.claim-link:hover { text-decoration: underline; }
.claim-table td { vertical-align: top; line-height: 1.35; }
.claim-table td:nth-child(4), .claim-table td:nth-child(5) { overflow-wrap: anywhere; }
.proposal-detail { margin-top: 14px; }
.proposal-detail th { width: 150px; vertical-align: top; }
.empty-state { color: #8b949e; background: #161b22; border: 1px solid #30363d;
border-radius: 8px; padding: 14px; }
"""
return render_page(
"KB Claim",
"Canonical claim, evidence rows, and graph edges",
"/kb-proposals",
body,
extra_css=extra_css,
timestamp=generated_at,
)
async def handle_api_kb_claim(request: web.Request) -> web.Response:
claim_id = request.match_info["claim_id"]
if not is_claim_id(claim_id):
return web.json_response({"error": "invalid_claim_id", "claim_id": claim_id}, status=400)
try:
data = _load_claim(request, claim_id)
except BaseException as exc:
logger.exception("KB claim API load failed")
return web.json_response({"error": "kb_claim_load_failed", "detail": str(exc)}, status=500)
if not data:
return web.json_response({"error": "claim_not_found", "claim_id": claim_id}, status=404)
return web.json_response(data)
async def handle_kb_claim_page(request: web.Request) -> web.Response:
claim_id = request.match_info["claim_id"]
if not is_claim_id(claim_id):
return web.Response(text="Invalid claim id", status=400)
try:
data = _load_claim(request, claim_id)
except BaseException as exc:
logger.exception("KB claim page load failed")
return web.Response(
text=render_page(
"KB Claim",
"Canonical claim, evidence rows, and graph edges",
"/kb-proposals",
f'<div class="alert-banner alert-critical">Failed to load KB claim: {escape(str(exc))}</div>',
),
content_type="text/html",
status=500,
)
if not data:
return web.Response(text="Claim not found", status=404)
return web.Response(text=render_kb_claim_page(data), content_type="text/html")
def register_kb_claim_routes(app: web.Application) -> None:
app.router.add_get("/api/kb/claims/{claim_id}", handle_api_kb_claim)
app.router.add_get("/kb/claims/{claim_id}", handle_kb_claim_page)

View file

@ -0,0 +1,535 @@
"""Read-only KB proposal review routes for Argus.
This surface is the operator-visible bridge between Leo's Telegram KB reasoning
and the narrow apply worker. It intentionally performs no approve/reject/apply
mutation; it only renders the same packets produced by
``scripts/kb_proposal_review_packet.py``.
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from collections import Counter
from datetime import datetime, timezone
from html import escape
from pathlib import Path
from typing import Any
from urllib.parse import urlencode
from aiohttp import web
from shared_ui import render_page
ROOT = Path(__file__).resolve().parent.parent
SCRIPT_DIR_CANDIDATES = [
ROOT / "scripts",
Path(os.environ.get("TELEO_INFRA_REPO_DIR", "/opt/teleo-eval/workspaces/deploy-infra")) / "scripts",
]
for scripts_dir in SCRIPT_DIR_CANDIDATES:
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
import kb_proposal_normalize as proposal_normalize # noqa: E402
import kb_proposal_review_packet as proposal_review # noqa: E402
from kb_claim_routes import claim_link_html # noqa: E402
logger = logging.getLogger("argus.kb_proposals")
KB_PROPOSAL_PUBLIC_PATHS = frozenset({"/kb-proposals", "/api/kb-proposals"})
KB_PROPOSAL_LOADER_KEY = web.AppKey("kb_proposal_loader", object)
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
def _query_limit(value: str | None) -> int:
if not value:
return DEFAULT_LIMIT
try:
limit = int(value)
except ValueError:
return DEFAULT_LIMIT
return max(1, min(limit, MAX_LIMIT))
def _query_filters(request: web.Request) -> dict[str, Any]:
status = request.query.get("status", "approved").strip() or "approved"
proposal_id = (request.query.get("proposal_id") or "").strip()
if proposal_id:
status = ""
return {
"status": status,
"proposal_id": proposal_id,
"limit": _query_limit(request.query.get("limit")),
}
def _db_args(filters: dict[str, Any]) -> argparse.Namespace:
return argparse.Namespace(
proposal_id=filters["proposal_id"] or None,
status=filters["status"] or None,
limit=filters["limit"],
secrets_file=os.environ.get(
"KB_PROPOSAL_REVIEW_SECRETS_FILE",
os.environ.get("KB_APPLY_SECRETS_FILE", proposal_review.ap.DEFAULT_SECRETS_FILE),
),
container=os.environ.get("KB_PROPOSAL_REVIEW_CONTAINER", proposal_review.ap.DEFAULT_CONTAINER),
db=os.environ.get("KB_PROPOSAL_REVIEW_DB", proposal_review.ap.DEFAULT_DB),
host=os.environ.get("KB_PROPOSAL_REVIEW_HOST", proposal_review.ap.DEFAULT_HOST),
role=os.environ.get("KB_PROPOSAL_REVIEW_ROLE", proposal_review.ap.DEFAULT_ROLE),
)
def _load_packets(request: web.Request, filters: dict[str, Any]) -> list[dict[str, Any]]:
loader = request.app.get(KB_PROPOSAL_LOADER_KEY)
proposals = loader(_db_args(filters)) if loader else proposal_review.load_from_db(_db_args(filters))
packets = []
for proposal in proposals:
packet = proposal_review.classify_proposal(proposal)
packet["normalization_preview"] = proposal_normalize.normalize_proposal(proposal)
packet["apply_preview"] = build_apply_preview(proposal, packet)
packets.append(packet)
return packets
def _counts(packets: list[dict[str, Any]], key: str) -> dict[str, int]:
return dict(Counter(str(packet.get(key) or "unknown") for packet in packets))
def build_kb_proposal_response(packets: list[dict[str, Any]], filters: dict[str, Any]) -> dict[str, Any]:
return {
"generated_at": datetime.now(timezone.utc).isoformat(),
"filters": filters,
"total": len(packets),
"status_counts": _counts(packets, "status"),
"review_state_counts": _counts(packets, "review_state"),
"worker_applyable_count": sum(1 for packet in packets if packet.get("worker_applyable")),
"packets": packets,
"read_only": True,
}
def _badge_class(value: str) -> str:
if value in {"applied", "approved_applyable"}:
return "badge-green"
if value in {"approved", "approved_needs_apply_payload", "needs_human_review", "pending_review"}:
return "badge-yellow"
if value in {"unsupported_by_apply_worker", "not_ready"}:
return "badge-red"
return "badge-blue"
def _badge(value: Any) -> str:
text = escape(str(value or "unknown"))
return f'<span class="badge {_badge_class(str(value or ""))}">{text}</span>'
def _join_values(values: list[Any]) -> str:
if not values:
return '<span class="muted">none</span>'
return ", ".join(escape(str(value)) for value in values)
def _code(value: Any) -> str:
return f"<code>{escape(str(value))}</code>"
def _payload_dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _payload_list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _apply_preview_row(
*,
action: str,
table: str,
target: str,
claim_ids: list[str] | None = None,
details: dict[str, Any] | None = None,
source: str = "proposal",
) -> dict[str, Any]:
return {
"action": action,
"table": table,
"target": target,
"claim_ids": claim_ids or [],
"details": details or {},
"source": source,
}
def _strict_apply_preview_rows(
proposal_type: str | None,
apply_payload: dict[str, Any],
*,
source: str = "proposal.apply_payload",
) -> list[dict[str, Any]]:
if proposal_type == "add_edge":
from_claim = str(apply_payload.get("from_claim") or "")
to_claim = str(apply_payload.get("to_claim") or "")
edge_type = str(apply_payload.get("edge_type") or "")
return [
_apply_preview_row(
action="insert_if_missing",
table="public.claim_edges",
target=f"{from_claim} -> {to_claim} ({edge_type or 'edge_type missing'})",
claim_ids=[claim_id for claim_id in [from_claim, to_claim] if claim_id],
details={
"from_claim": from_claim,
"to_claim": to_claim,
"edge_type": edge_type,
"weight": apply_payload.get("weight"),
},
source=source,
)
]
if proposal_type == "attach_evidence":
rows = []
for evidence in _payload_list(apply_payload.get("evidence")):
ev = _payload_dict(evidence)
claim_id = str(ev.get("claim_id") or "")
source_id = str(ev.get("source_id") or "")
role = str(ev.get("role") or "grounds")
rows.append(
_apply_preview_row(
action="insert_if_missing",
table="public.claim_evidence",
target=f"{claim_id} <= {source_id} ({role})",
claim_ids=[claim_id] if claim_id else [],
details={
"claim_id": claim_id,
"source_id": source_id,
"role": role,
"weight": ev.get("weight"),
},
source=source,
)
)
return rows
if proposal_type == "revise_strategy":
agent_id = str(apply_payload.get("agent_id") or "")
strategy_nodes = _payload_list(apply_payload.get("strategy_nodes"))
return [
_apply_preview_row(
action="update_then_insert",
table="public.strategies",
target=f"new active strategy for agent {agent_id or 'unknown'}",
details={
"agent_id": agent_id,
"strategy_keys": sorted(_payload_dict(apply_payload.get("strategy")).keys()),
},
source=source,
),
_apply_preview_row(
action="retire_then_insert",
table="public.strategy_nodes",
target=f"{len(strategy_nodes)} replacement strategy node(s)",
details={"agent_id": agent_id, "strategy_node_count": len(strategy_nodes)},
source=source,
),
]
return [
_apply_preview_row(
action="blocked",
table="none",
target=f"unsupported proposal_type {proposal_type or 'unknown'}",
details={"proposal_type": proposal_type},
source=source,
)
]
def build_apply_preview(proposal: dict[str, Any], packet: dict[str, Any]) -> dict[str, Any]:
"""Build a read-only, row-level preview of what an apply step would touch."""
payload = _payload_dict(proposal.get("payload"))
apply_payload = _payload_dict(payload.get("apply_payload"))
proposal_type = proposal.get("proposal_type")
normalization = _payload_dict(packet.get("normalization_preview"))
if proposal.get("status") == "applied":
return {
"state": "applied",
"rows": [],
"blocked_fragments": [],
"note": "Proposal is already applied; use the canonical claim/edge/evidence readback.",
}
if apply_payload and proposal_type in proposal_review.ap.APPLYABLE_TYPES:
return {
"state": "strict_apply_payload_ready",
"rows": _strict_apply_preview_rows(proposal_type, apply_payload),
"blocked_fragments": [],
"note": "Preview only; the page does not execute canonical writes.",
}
child_rows = []
for index, child in enumerate(_payload_list(normalization.get("strict_child_proposals"))):
child_payload = _payload_dict(_payload_dict(child).get("payload"))
child_apply_payload = _payload_dict(child_payload.get("apply_payload"))
child_type = str(_payload_dict(child).get("proposal_type") or "")
child_rows.extend(
_strict_apply_preview_rows(
child_type,
child_apply_payload,
source=f"normalization.strict_child_proposals[{index}]",
)
)
blocked = _payload_list(normalization.get("blocked_fragments"))
if child_rows and not blocked:
state = "strict_child_proposals_ready"
note = "Preview of strict child proposals; reviewer still needs to stage those children before apply."
elif child_rows:
state = "partial_preview_blocked_fragments"
note = "Some strict child rows can be previewed; blocked fragments still need canonical IDs or schema decisions."
else:
state = "not_applyable_yet"
note = "No canonical write preview is safe until the proposal has a strict apply_payload or strict child proposals."
return {
"state": state,
"rows": child_rows,
"blocked_fragments": blocked,
"note": note,
}
def _status_link(label: str, status: str, filters: dict[str, Any]) -> str:
query = urlencode({"status": status, "limit": filters["limit"]})
return f'<a class="filter-link" href="/kb-proposals?{query}">{escape(label)}</a>'
def _apply_preview_html(preview: dict[str, Any]) -> str:
state = _badge(preview.get("state"))
rows = _payload_list(preview.get("rows"))
blocked = _payload_list(preview.get("blocked_fragments"))
note = escape(str(preview.get("note") or ""))
row_html = ""
if rows:
rendered_rows = []
for row in rows:
details = _payload_dict(_payload_dict(row).get("details"))
detail_text = ", ".join(
f"{escape(str(key))}={escape(str(value))}"
for key, value in details.items()
if value not in (None, "")
)
claim_links = " ".join(
claim_link_html(claim_id, label=str(claim_id)[:8])
for claim_id in _payload_list(_payload_dict(row).get("claim_ids"))
) or '<span class="muted">none</span>'
detail_html = escape(detail_text) if detail_text else '<span class="muted">none</span>'
rendered_rows.append(
"<tr>"
f"<td>{_code(_payload_dict(row).get('action') or 'unknown')}</td>"
f"<td>{_code(_payload_dict(row).get('table') or 'unknown')}</td>"
f"<td>{escape(str(_payload_dict(row).get('target') or ''))}</td>"
f"<td>{claim_links}</td>"
f"<td>{detail_html}</td>"
"</tr>"
)
row_html = f"""
<table class="proposal-detail apply-preview-table">
<thead><tr><th>Action</th><th>Table</th><th>Target</th><th>Claims</th><th>Details</th></tr></thead>
<tbody>{''.join(rendered_rows)}</tbody>
</table>"""
else:
row_html = '<p class="muted">No canonical row preview is executable yet.</p>'
blocked_html = ""
if blocked:
blocked_rows = []
for fragment in blocked:
frag = _payload_dict(fragment)
missing = ", ".join(escape(str(value)) for value in _payload_list(frag.get("missing"))) or "unknown"
blocked_rows.append(
"<tr>"
f"<td>{_code(frag.get('kind') or 'fragment')}</td>"
f"<td>{escape(str(frag.get('reason') or 'blocked'))}</td>"
f"<td>{missing}</td>"
"</tr>"
)
blocked_html = f"""
<div class="label preview-label">Blocked fragments</div>
<table class="proposal-detail apply-preview-table">
<thead><tr><th>Kind</th><th>Reason</th><th>Missing</th></tr></thead>
<tbody>{''.join(blocked_rows)}</tbody>
</table>"""
return f"""
<div class="next-action apply-preview">
<div class="label">Apply preview</div>
<p>{state} <span class="muted">{note}</span></p>
{row_html}
{blocked_html}
</div>"""
def _packet_card(packet: dict[str, Any]) -> str:
auth = packet.get("identity_and_authorization") or {}
summary = packet.get("payload_summary") or {}
applyability = packet.get("applyability") or {}
proposal_id_raw = str(packet.get("proposal_id") or "unknown")
proposal_id = _code(proposal_id_raw)
proposal_kind = escape(str(summary.get("proposal_kind") or "unknown"))
next_action = escape(str(packet.get("next_admin_action") or "No next action recorded."))
source_ref = escape(str(auth.get("source_ref") or "unknown"))
channel = escape(str(auth.get("channel") or "unknown"))
reviewed_by = escape(str(auth.get("reviewed_by_handle") or "unreviewed"))
rows = [
("Status", _badge(packet.get("status"))),
("Review state", _badge(packet.get("review_state"))),
("Worker applyable", "yes" if packet.get("worker_applyable") else "no"),
("Has strict apply payload", "yes" if packet.get("has_apply_payload") else "no"),
("Proposal kind", proposal_kind),
("Reviewed by", reviewed_by),
("Channel", channel),
("Source ref", source_ref),
("Old claim id", claim_link_html(summary.get("old_claim_id") or "unknown")),
("Claim candidates", _code(summary.get("claim_candidate_count", 0))),
("Source candidates", _code(summary.get("source_candidate_count", 0))),
("Supersession edges", _code(summary.get("supersession_edge_count", 0))),
("Missing contract", _join_values(applyability.get("missing_contract") or [])),
]
normalization = packet.get("normalization_preview") or {}
if normalization:
rows.extend(
[
("Normalization", _badge(normalization.get("normalization_state"))),
("Strict child proposals", _code(normalization.get("strict_child_count", 0))),
("Blocked fragments", _code(normalization.get("blocked_count", 0))),
]
)
details = "\n".join(
f"<tr><th>{escape(label)}</th><td>{value}</td></tr>" for label, value in rows
)
normalization_html = ""
if normalization:
normalization_html = f"""
<div class="next-action">
<div class="label">Normalization action</div>
<p>{escape(str(normalization.get("next_normalization_action") or "No normalization action recorded."))}</p>
</div>"""
apply_preview_html = _apply_preview_html(packet.get("apply_preview") or {})
return f"""
<article class="proposal-card">
<div class="proposal-card-header">
<h2>{proposal_id}</h2>
</div>
<table class="proposal-detail">
<tbody>{details}</tbody>
</table>
<div class="next-action">
<div class="label">Next admin action</div>
<p>{next_action}</p>
</div>
{normalization_html}
{apply_preview_html}
</article>"""
def render_kb_proposals_page(data: dict[str, Any]) -> str:
filters = data["filters"]
packets = data["packets"]
generated_at = escape(str(data["generated_at"]))
cards = "\n".join(_packet_card(packet) for packet in packets)
if not cards:
cards = '<div class="alert-banner alert-info">No proposals matched this filter.</div>'
filter_html = " ".join(
[
_status_link("Approved", "approved", filters),
_status_link("Pending review", "pending_review", filters),
_status_link("Applied", "applied", filters),
]
)
body = f"""
<div class="alert-banner alert-info">
Read-only review surface. It shows proposal intent, applyability, missing contracts,
and the next admin action; it does not approve, reject, or apply proposals.
</div>
<div class="grid">
<div class="card"><div class="label">Matching proposals</div><div class="value">{data["total"]}</div></div>
<div class="card"><div class="label">Worker applyable</div><div class="value">{data["worker_applyable_count"]}</div></div>
<div class="card"><div class="label">Current status filter</div><div class="value small-value">{escape(filters.get("status") or "by id")}</div></div>
</div>
<div class="section">
<div class="section-title">Filters</div>
<div class="filters">{filter_html}</div>
</div>
<div class="section">
<div class="section-title">Proposal Review Packets</div>
<div class="proposal-list">{cards}</div>
</div>"""
extra_css = """
.small-value { font-size: 16px !important; overflow-wrap: anywhere; }
.filters { display: flex; flex-wrap: wrap; gap: 8px; }
.filter-link { color: #58a6ff; background: #161b22; border: 1px solid #30363d;
border-radius: 6px; padding: 6px 10px; text-decoration: none; font-size: 13px; }
.filter-link:hover { background: #21262d; }
.claim-link { color: #58a6ff; text-decoration: none; }
.claim-link:hover { text-decoration: underline; }
.proposal-list { display: grid; gap: 16px; }
.proposal-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.proposal-card-header { display: flex; justify-content: space-between; gap: 12px; align-items: start; }
.proposal-card h2 { color: #c9d1d9; font-size: 15px; overflow-wrap: anywhere; }
.proposal-detail { margin-top: 12px; }
.proposal-detail th { width: 210px; vertical-align: top; }
.proposal-detail td { overflow-wrap: anywhere; }
.next-action { margin-top: 14px; padding: 12px; border: 1px solid #30363d; border-radius: 6px; background: #0d1117; }
.next-action .label { color: #8b949e; font-size: 11px; text-transform: uppercase; margin-bottom: 6px; }
.next-action p { color: #c9d1d9; line-height: 1.4; }
.muted { color: #8b949e; }
"""
return render_page(
"KB Proposals",
"Review Leo KB proposal packets before any apply step",
"/kb-proposals",
body,
extra_css=extra_css,
timestamp=generated_at,
)
async def handle_api_kb_proposals(request: web.Request) -> web.Response:
filters = _query_filters(request)
try:
packets = _load_packets(request, filters)
except BaseException as exc:
logger.exception("KB proposal packet load failed")
return web.json_response({"error": "kb_proposal_load_failed", "detail": str(exc)}, status=500)
return web.json_response(build_kb_proposal_response(packets, filters))
async def handle_kb_proposals_page(request: web.Request) -> web.Response:
filters = _query_filters(request)
try:
packets = _load_packets(request, filters)
except BaseException as exc:
logger.exception("KB proposal packet page failed")
return web.Response(
text=render_page(
"KB Proposals",
"Review Leo KB proposal packets before any apply step",
"/kb-proposals",
f'<div class="alert-banner alert-critical">Failed to load KB proposals: {escape(str(exc))}</div>',
),
content_type="text/html",
status=500,
)
data = build_kb_proposal_response(packets, filters)
return web.Response(text=render_kb_proposals_page(data), content_type="text/html")
def register_kb_proposal_routes(app: web.Application) -> None:
app.router.add_get("/api/kb-proposals", handle_api_kb_proposals)
app.router.add_get("/kb-proposals", handle_kb_proposals_page)

View file

@ -0,0 +1,166 @@
"""Leaderboard endpoint reading from event-sourced contribution_events.
Owner: Argus
Source of truth: pipeline.db contribution_events (Epimetheus, schema v25)
Reads contribution_events GROUP BY handle, computes CI as SUM(weight),
joins contributors for kind, returns sorted leaderboard with role breakdown.
Roles + weights (Phase A):
author 0.30 | challenger 0.25 | synthesizer 0.20 | originator 0.15 | evaluator 0.05
Endpoints:
GET /api/leaderboard?window=all_time|Nd|Nh&domain=&kind=person|agent|org|all&limit=100
"""
import logging
import re
import sqlite3
from aiohttp import web
logger = logging.getLogger("argus.leaderboard_routes")
ROLE_KEYS = ("author", "challenger", "synthesizer", "originator", "evaluator")
KIND_VALUES = ("person", "agent", "org", "all")
# Public path set so auth middleware lets it through
LEADERBOARD_PUBLIC_PATHS = frozenset({"/api/leaderboard"})
def _conn(app):
"""Read-only connection to pipeline.db."""
db_path = app["db_path"]
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
return conn
def _parse_window(raw):
"""Parse window param. Returns (sql_clause, params_tuple, label).
Accepts: 'all_time' (default), 'Nd' (last N days), 'Nh' (last N hours).
Caps N at 365d / 8760h to prevent abuse.
"""
if not raw or raw == "all_time":
return ("", (), "all_time")
m = re.fullmatch(r"(\d+)([dh])", raw.strip().lower())
if not m:
return ("", (), "all_time")
n = int(m.group(1))
unit = m.group(2)
# Note: WHERE clause is composed via " AND ".join(...) — do NOT prefix with "AND ".
if unit == "d":
n = min(n, 365)
return ("ce.timestamp >= datetime('now', ?)", (f"-{n} days",), f"{n}d")
n = min(n, 8760)
return ("ce.timestamp >= datetime('now', ?)", (f"-{n} hours",), f"{n}h")
async def handle_leaderboard(request):
"""GET /api/leaderboard.
Query params:
window: 'all_time' (default) | 'Nd' (e.g. '7d') | 'Nh' (e.g. '24h')
domain: filter by domain (optional)
kind: 'person' (default) | 'agent' | 'org' | 'all'
limit: max entries (default 100, max 500)
"""
window_clause, window_params, window_label = _parse_window(request.query.get("window"))
domain = request.query.get("domain")
kind = request.query.get("kind", "person")
if kind not in KIND_VALUES:
kind = "person"
try:
limit = min(int(request.query.get("limit", "100")), 500)
except (ValueError, TypeError):
limit = 100
where = ["1=1", window_clause] if window_clause else ["1=1"]
params = list(window_params)
if domain:
where.append("ce.domain = ?")
params.append(domain)
if kind != "all":
where.append("COALESCE(c.kind, 'person') = ?")
params.append(kind)
where_sql = " AND ".join([w for w in where if w])
conn = _conn(request.app)
try:
# Aggregate per handle: total CI, per-role breakdown, event count, first/last timestamp
# LEFT JOIN contributors so handles in events but not in contributors still appear
# (defaults to kind='person' via COALESCE).
rows = conn.execute(f"""
SELECT
ce.handle,
COALESCE(c.kind, 'person') AS kind,
ROUND(SUM(ce.weight), 4) AS ci,
COUNT(*) AS events_count,
MIN(ce.timestamp) AS first_contribution,
MAX(ce.timestamp) AS last_contribution,
SUM(CASE WHEN ce.role='author' THEN ce.weight ELSE 0 END) AS ci_author,
SUM(CASE WHEN ce.role='challenger' THEN ce.weight ELSE 0 END) AS ci_challenger,
SUM(CASE WHEN ce.role='synthesizer' THEN ce.weight ELSE 0 END) AS ci_synthesizer,
SUM(CASE WHEN ce.role='originator' THEN ce.weight ELSE 0 END) AS ci_originator,
SUM(CASE WHEN ce.role='evaluator' THEN ce.weight ELSE 0 END) AS ci_evaluator,
COUNT(DISTINCT ce.domain) AS domain_count,
COUNT(DISTINCT ce.pr_number) AS pr_count
FROM contribution_events ce
LEFT JOIN contributors c ON c.handle = ce.handle
WHERE {where_sql}
GROUP BY ce.handle, COALESCE(c.kind, 'person')
ORDER BY ci DESC, last_contribution DESC
LIMIT ?
""", (*params, limit + 1)).fetchall() # +1 to detect overflow
has_more = len(rows) > limit
rows = rows[:limit]
# Total count of distinct handles matching filters (without limit)
total_row = conn.execute(f"""
SELECT COUNT(DISTINCT ce.handle) AS total
FROM contribution_events ce
LEFT JOIN contributors c ON c.handle = ce.handle
WHERE {where_sql}
""", params).fetchone()
total = total_row["total"] if total_row else 0
leaderboard = []
for r in rows:
leaderboard.append({
"handle": r["handle"],
"kind": r["kind"],
"ci": r["ci"],
"ci_breakdown": {
"author": round(r["ci_author"] or 0, 4),
"challenger": round(r["ci_challenger"] or 0, 4),
"synthesizer": round(r["ci_synthesizer"] or 0, 4),
"originator": round(r["ci_originator"] or 0, 4),
"evaluator": round(r["ci_evaluator"] or 0, 4),
},
"events_count": r["events_count"],
"domain_count": r["domain_count"],
"pr_count": r["pr_count"],
"first_contribution": r["first_contribution"],
"last_contribution": r["last_contribution"],
})
return web.json_response({
"window": window_label,
"domain": domain,
"kind_filter": kind,
"total": total,
"shown": len(leaderboard),
"has_more": has_more,
"source": "contribution_events", # explicit so consumers know the data origin
"leaderboard": leaderboard,
})
finally:
conn.close()
def register_leaderboard_routes(app: web.Application):
"""Register /api/leaderboard. Requires app['db_path'] to be set."""
app.router.add_get("/api/leaderboard", handle_leaderboard)

View file

@ -0,0 +1,279 @@
"""Dashboard API routes for research session + cost tracking.
Argus-side read-only endpoints. These query the data that
research_tracking.py writes to pipeline.db.
Add to app.py after alerting_routes setup.
"""
import json
import sqlite3
from aiohttp import web
def _conn(app):
"""Read-only connection to pipeline.db."""
db_path = app["db_path"]
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
return conn
async def handle_api_research_sessions(request):
"""GET /api/research-sessions?agent=&domain=&days=7
Returns research sessions with linked sources and cost data.
"""
agent = request.query.get("agent")
domain = request.query.get("domain")
try:
days = int(request.query.get("days", 7))
except (ValueError, TypeError):
days = 7
conn = _conn(request.app)
try:
where = ["rs.started_at >= datetime('now', ?)"]
params = [f"-{days} days"]
if agent:
where.append("rs.agent = ?")
params.append(agent)
if domain:
where.append("rs.domain = ?")
params.append(domain)
where_clause = " AND ".join(where)
sessions = conn.execute(f"""
SELECT rs.*,
GROUP_CONCAT(s.path, '||') as source_paths,
GROUP_CONCAT(s.status, '||') as source_statuses,
GROUP_CONCAT(s.claims_count, '||') as source_claims,
GROUP_CONCAT(COALESCE(s.cost_usd, 0), '||') as source_costs
FROM research_sessions rs
LEFT JOIN sources s ON s.session_id = rs.id
WHERE {where_clause}
GROUP BY rs.id
ORDER BY rs.started_at DESC
""", params).fetchall()
result = []
for s in sessions:
sources = []
if s["source_paths"]:
paths = s["source_paths"].split("||")
statuses = (s["source_statuses"] or "").split("||")
claims = (s["source_claims"] or "").split("||")
costs = (s["source_costs"] or "").split("||")
for i, p in enumerate(paths):
sources.append({
"path": p,
"status": statuses[i] if i < len(statuses) else None,
"claims_count": int(claims[i]) if i < len(claims) and claims[i] else 0,
"extraction_cost": float(costs[i]) if i < len(costs) and costs[i] else 0,
})
result.append({
"id": s["id"],
"agent": s["agent"],
"domain": s["domain"],
"topic": s["topic"],
"reasoning": s["reasoning"],
"summary": s["summary"],
"sources_planned": s["sources_planned"],
"sources_produced": s["sources_produced"],
"model": s["model"],
"input_tokens": s["input_tokens"],
"output_tokens": s["output_tokens"],
"research_cost": s["cost_usd"],
"extraction_cost": sum(src["extraction_cost"] for src in sources),
"total_cost": s["cost_usd"] + sum(src["extraction_cost"] for src in sources),
"total_claims": sum(src["claims_count"] for src in sources),
"status": s["status"],
"started_at": s["started_at"],
"completed_at": s["completed_at"],
"sources": sources,
})
# Summary stats
total_sessions = len(result)
total_cost = sum(r["total_cost"] for r in result)
total_claims = sum(r["total_claims"] for r in result)
total_sources = sum(r["sources_produced"] for r in result)
return web.json_response({
"summary": {
"sessions": total_sessions,
"total_cost": round(total_cost, 2),
"total_claims": total_claims,
"total_sources": total_sources,
"avg_cost_per_claim": round(total_cost / total_claims, 4) if total_claims else 0,
"avg_cost_per_session": round(total_cost / total_sessions, 4) if total_sessions else 0,
},
"sessions": result,
})
finally:
conn.close()
async def handle_api_costs(request):
"""GET /api/costs?days=14&by=stage|model|date
Comprehensive cost breakdown. Works with EXISTING data in costs table
plus the new extraction costs once backfilled.
"""
try:
days = int(request.query.get("days", 14))
except (ValueError, TypeError):
days = 14
group_by = request.query.get("by", "stage")
conn = _conn(request.app)
try:
valid_groups = {"stage", "model", "date"}
if group_by not in valid_groups:
group_by = "stage"
rows = conn.execute(f"""
SELECT {group_by},
SUM(calls) as total_calls,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(cost_usd) as total_cost
FROM costs
WHERE date >= date('now', ?)
GROUP BY {group_by}
ORDER BY total_cost DESC
""", (f"-{days} days",)).fetchall()
result = []
for r in rows:
result.append({
group_by: r[group_by],
"calls": r["total_calls"],
"input_tokens": r["total_input"],
"output_tokens": r["total_output"],
"cost_usd": round(r["total_cost"], 4),
})
grand_total = sum(r["cost_usd"] for r in result)
# Also get per-agent cost from sources table (extraction costs)
agent_costs = conn.execute("""
SELECT p.agent,
COUNT(DISTINCT s.path) as sources,
SUM(s.cost_usd) as extraction_cost,
SUM(s.claims_count) as claims
FROM sources s
LEFT JOIN prs p ON p.source_path = s.path
WHERE s.cost_usd > 0
GROUP BY p.agent
ORDER BY extraction_cost DESC
""").fetchall()
agent_breakdown = []
for r in agent_costs:
agent_breakdown.append({
"agent": r["agent"] or "unlinked",
"sources": r["sources"],
"extraction_cost": round(r["extraction_cost"], 2),
"claims": r["claims"],
"cost_per_claim": round(r["extraction_cost"] / r["claims"], 4) if r["claims"] else 0,
})
return web.json_response({
"period_days": days,
"grand_total": round(grand_total, 2),
"by_" + group_by: result,
"by_agent": agent_breakdown,
})
finally:
conn.close()
async def handle_api_source_detail(request):
"""GET /api/source/{path}
Full lifecycle of a single source: research session extraction claims eval outcomes.
"""
source_path = request.match_info["path"]
conn = _conn(request.app)
try:
# Try exact match first, fall back to suffix match (anchored)
source = conn.execute(
"SELECT * FROM sources WHERE path = ?",
(source_path,),
).fetchone()
if not source:
# Suffix match — anchor with / prefix to avoid substring hits
source = conn.execute(
"SELECT * FROM sources WHERE path LIKE ? ORDER BY length(path) LIMIT 1",
(f"%/{source_path}",),
).fetchone()
if not source:
return web.json_response({"error": "Source not found"}, status=404)
result = dict(source)
# Get research session if linked
if source["session_id"]:
session = conn.execute(
"SELECT * FROM research_sessions WHERE id = ?",
(source["session_id"],),
).fetchone()
result["research_session"] = dict(session) if session else None
else:
result["research_session"] = None
# Get PRs from this source
prs = conn.execute(
"SELECT number, status, domain, agent, tier, leo_verdict, domain_verdict, "
"cost_usd, created_at, merged_at, commit_type, transient_retries, substantive_retries, last_error "
"FROM prs WHERE source_path = ?",
(source["path"],),
).fetchall()
result["prs"] = [dict(p) for p in prs]
# Get eval events from audit_log for those PRs
# NOTE: audit_log.detail is mixed — some rows are JSON (evaluate events),
# some are plain text. Use json_valid() to filter safely.
pr_numbers = [p["number"] for p in prs]
if pr_numbers:
placeholders = ",".join("?" * len(pr_numbers))
evals = conn.execute(f"""
SELECT * FROM audit_log
WHERE stage = 'evaluate'
AND json_valid(detail)
AND json_extract(detail, '$.pr') IN ({placeholders})
ORDER BY timestamp
""", pr_numbers).fetchall()
result["eval_history"] = [
{"timestamp": e["timestamp"], "event": e["event"],
"detail": json.loads(e["detail"]) if e["detail"] else None}
for e in evals
]
else:
result["eval_history"] = []
return web.json_response(result)
finally:
conn.close()
def setup_research_routes(app):
"""Register research tracking routes. Call from create_app()."""
app.router.add_get("/api/research-sessions", handle_api_research_sessions)
app.router.add_get("/api/costs", handle_api_costs)
app.router.add_get("/api/source/{path:.+}", handle_api_source_detail)
# Public paths to add to auth middleware
RESEARCH_PUBLIC_PATHS = frozenset({
"/api/research-sessions",
"/api/costs",
})
# /api/source/{path} needs prefix matching — add to auth middleware:
# if path.startswith("/api/source/"): allow

View file

@ -0,0 +1,419 @@
"""Research session tracking + cost attribution for the Teleo pipeline.
This module adds three capabilities:
1. research_sessions table tracks WHY agents researched, what they found interesting,
session cost, and links to generated sources
2. Extraction cost attribution writes per-source cost to sources.cost_usd after extraction
3. Source claim linkage ensures prs.source_path is always populated
Designed for Epimetheus to integrate into the pipeline. Argus built the spec;
Ganymede reviews; Epimetheus wires it in.
Data flow:
Agent research session research_sessions row (with reasoning + summary)
sources created (with session_id FK)
extraction runs (cost written to sources.cost_usd + costs table)
PRs created (source_path populated)
claims merged (traceable back to session)
"""
import json
import logging
import sqlite3
from datetime import datetime
from typing import Optional
logger = logging.getLogger("research_tracking")
# ---------------------------------------------------------------------------
# Migration v11: research_sessions table + sources.session_id FK
# (v9 is current; v10 is Epimetheus's eval pipeline migration)
# ---------------------------------------------------------------------------
MIGRATION_V11_SQL = """
-- Research session tracking table
CREATE TABLE IF NOT EXISTS research_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent TEXT NOT NULL,
-- Which agent ran the research (leo, rio, astra, etc.)
domain TEXT,
-- Primary domain of the research
topic TEXT NOT NULL,
-- What they researched (short description)
reasoning TEXT,
-- WHY they chose this topic (agent's own explanation)
summary TEXT,
-- What they found most interesting/relevant
sources_planned INTEGER DEFAULT 0,
-- How many sources they intended to produce
sources_produced INTEGER DEFAULT 0,
-- How many actually materialized
model TEXT,
-- Model used for research (e.g. claude-opus-4-6)
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0,
-- Total research session cost (LLM calls for discovery + writing)
status TEXT DEFAULT 'running',
-- running, completed, failed, partial
started_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
metadata TEXT DEFAULT '{}'
-- JSON: any extra context (prompt version, search queries used, etc.)
);
CREATE INDEX IF NOT EXISTS idx_rs_agent ON research_sessions(agent);
CREATE INDEX IF NOT EXISTS idx_rs_domain ON research_sessions(domain);
CREATE INDEX IF NOT EXISTS idx_rs_started ON research_sessions(started_at);
-- Add session_id FK to sources table
ALTER TABLE sources ADD COLUMN session_id INTEGER REFERENCES research_sessions(id);
CREATE INDEX IF NOT EXISTS idx_sources_session ON sources(session_id);
-- Record migration
INSERT INTO schema_version (version) VALUES (11);
"""
# ---------------------------------------------------------------------------
# Cost attribution: write extraction cost to sources.cost_usd
# ---------------------------------------------------------------------------
# Pricing per million tokens (as of March 2026)
MODEL_PRICING = {
"anthropic/claude-sonnet-4.5": {"input": 3.00, "output": 15.00},
"anthropic/claude-sonnet-4-5": {"input": 3.00, "output": 15.00},
"anthropic/claude-haiku-4.5": {"input": 0.80, "output": 4.00},
"anthropic/claude-haiku-4-5-20251001": {"input": 0.80, "output": 4.00},
"minimax/minimax-m2.5": {"input": 0.14, "output": 0.56},
}
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
"""Calculate USD cost from model name and token counts."""
pricing = MODEL_PRICING.get(model)
if not pricing:
# Default to Sonnet 4.5 pricing as conservative estimate
logger.warning("Unknown model %s — using Sonnet 4.5 pricing", model)
pricing = {"input": 3.00, "output": 15.00}
return (input_tokens * pricing["input"] + output_tokens * pricing["output"]) / 1_000_000
def record_extraction_cost(
conn: sqlite3.Connection,
source_path: str,
model: str,
input_tokens: int,
output_tokens: int,
):
"""Write extraction cost to both sources.cost_usd and costs table.
Call this after each successful extraction call in openrouter-extract-v2.py.
This is the missing link the CSV logger records tokens but never writes
cost back to the DB.
"""
cost = calculate_cost(model, input_tokens, output_tokens)
# Update source row
conn.execute(
"UPDATE sources SET cost_usd = cost_usd + ?, extraction_model = ? WHERE path = ?",
(cost, model, source_path),
)
# Also record in costs table for dashboard aggregation
date = datetime.utcnow().strftime("%Y-%m-%d")
conn.execute(
"""INSERT INTO costs (date, model, stage, calls, input_tokens, output_tokens, cost_usd)
VALUES (?, ?, 'extraction', 1, ?, ?, ?)
ON CONFLICT(date, model, stage)
DO UPDATE SET calls = calls + 1,
input_tokens = input_tokens + excluded.input_tokens,
output_tokens = output_tokens + excluded.output_tokens,
cost_usd = cost_usd + excluded.cost_usd""",
(date, model, input_tokens, output_tokens, cost),
)
conn.commit()
logger.info(
"Recorded extraction cost for %s: $%.4f (%d in, %d out, %s)",
source_path, cost, input_tokens, output_tokens, model,
)
return cost
# ---------------------------------------------------------------------------
# Research session lifecycle
# ---------------------------------------------------------------------------
def start_session(
conn: sqlite3.Connection,
agent: str,
topic: str,
domain: Optional[str] = None,
reasoning: Optional[str] = None,
sources_planned: int = 0,
model: Optional[str] = None,
metadata: Optional[dict] = None,
) -> int:
"""Call at the START of a research session. Returns session_id.
The agent should call this before it begins producing sources,
explaining what it plans to research and why.
"""
cur = conn.execute(
"""INSERT INTO research_sessions
(agent, domain, topic, reasoning, sources_planned, model, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(
agent,
domain,
topic,
reasoning,
sources_planned,
model,
json.dumps(metadata or {}),
),
)
conn.commit()
session_id = cur.lastrowid
logger.info("Started research session #%d: %s / %s", session_id, agent, topic)
return session_id
def link_source_to_session(
conn: sqlite3.Connection,
source_path: str,
session_id: int,
):
"""Link a source file to its research session.
Call this when a source is written to inbox/ during a research session.
"""
conn.execute(
"UPDATE sources SET session_id = ? WHERE path = ?",
(session_id, source_path),
)
conn.execute(
"""UPDATE research_sessions
SET sources_produced = sources_produced + 1
WHERE id = ?""",
(session_id,),
)
conn.commit()
def complete_session(
conn: sqlite3.Connection,
session_id: int,
summary: str,
input_tokens: int = 0,
output_tokens: int = 0,
cost_usd: float = 0,
status: str = "completed",
):
"""Call at the END of a research session.
The agent should summarize what it found most interesting/relevant.
Cost should include ALL LLM calls made during the session (web search,
analysis, source writing everything).
"""
conn.execute(
"""UPDATE research_sessions
SET summary = ?, input_tokens = ?, output_tokens = ?,
cost_usd = ?, status = ?, completed_at = datetime('now')
WHERE id = ?""",
(summary, input_tokens, output_tokens, cost_usd, status, session_id),
)
conn.commit()
logger.info("Completed research session #%d: %s", session_id, status)
# ---------------------------------------------------------------------------
# Source → PR linkage fix
# ---------------------------------------------------------------------------
def ensure_source_path_on_pr(
conn: sqlite3.Connection,
pr_number: int,
source_path: str,
):
"""Ensure prs.source_path is populated. Call during PR creation.
Currently 0/1451 PRs have source_path set. This is the fix.
"""
conn.execute(
"UPDATE prs SET source_path = ? WHERE number = ? AND (source_path IS NULL OR source_path = '')",
(source_path, pr_number),
)
conn.commit()
# ---------------------------------------------------------------------------
# Backfill: attribute extraction costs from existing CSV log
# ---------------------------------------------------------------------------
def backfill_extraction_costs(conn: sqlite3.Connection, csv_path: str):
"""One-time backfill: read openrouter-usage.csv and write costs to sources + costs tables.
Run once to fill in the ~$338 of extraction costs that were logged to CSV
but never written to the database.
Safe to re-run only updates sources where cost_usd = 0, so partial
runs can be resumed without double-counting.
"""
import csv
count = 0
total_cost = 0.0
with open(csv_path) as f:
reader = csv.DictReader(f)
for row in reader:
source_file = row.get("source_file", "")
model = row.get("model", "")
try:
in_tok = int(row.get("input_tokens", 0) or 0)
out_tok = int(row.get("output_tokens", 0) or 0)
except (ValueError, TypeError):
continue
cost = calculate_cost(model, in_tok, out_tok)
if cost <= 0:
continue
# Try to match source_file to sources.path
# CSV has filename, DB has full path — match on exact suffix
# Use ORDER BY length(path) to prefer shortest (most specific) match
matched = conn.execute(
"SELECT path FROM sources WHERE path LIKE ? AND cost_usd = 0 ORDER BY length(path) LIMIT 1",
(f"%/{source_file}" if "/" not in source_file else f"%{source_file}",),
).fetchone()
if matched:
conn.execute(
"UPDATE sources SET cost_usd = ?, extraction_model = ? WHERE path = ?",
(cost, model, matched[0]),
)
# Always record in costs table
date = row.get("date", "unknown")
conn.execute(
"""INSERT INTO costs (date, model, stage, calls, input_tokens, output_tokens, cost_usd)
VALUES (?, ?, 'extraction', 1, ?, ?, ?)
ON CONFLICT(date, model, stage)
DO UPDATE SET calls = calls + 1,
input_tokens = input_tokens + excluded.input_tokens,
output_tokens = output_tokens + excluded.output_tokens,
cost_usd = cost_usd + excluded.cost_usd""",
(date, model, in_tok, out_tok, cost),
)
count += 1
total_cost += cost
conn.commit()
logger.info("Backfilled %d extraction cost records, total $%.2f", count, total_cost)
return count, total_cost
# ---------------------------------------------------------------------------
# Backfill: populate prs.source_path from branch naming convention
# ---------------------------------------------------------------------------
def backfill_source_paths(conn: sqlite3.Connection):
"""One-time backfill: derive source_path for existing PRs from branch names.
Branch format: extract/YYYY-MM-DD-source-name or similar patterns.
Source path format: inbox/queue/YYYY-MM-DD-source-name.md
"""
rows = conn.execute(
"SELECT number, branch FROM prs WHERE source_path IS NULL AND branch IS NOT NULL"
).fetchall()
count = 0
for number, branch in rows:
# Try to extract source name from branch
# Common patterns: extract/source-name, claims/source-name
parts = branch.split("/", 1)
if len(parts) < 2:
continue
source_stem = parts[1]
# Try to find matching source in DB — exact suffix match, shortest path wins
matched = conn.execute(
"SELECT path FROM sources WHERE path LIKE ? ORDER BY length(path) LIMIT 1",
(f"%/{source_stem}%" if source_stem else "",),
).fetchone()
if matched:
conn.execute(
"UPDATE prs SET source_path = ? WHERE number = ?",
(matched[0], number),
)
count += 1
conn.commit()
logger.info("Backfilled source_path for %d PRs", count)
return count
# ---------------------------------------------------------------------------
# Integration points (for Epimetheus to wire in)
# ---------------------------------------------------------------------------
INTEGRATION_GUIDE = """
## Where to wire this in
### 1. openrouter-extract-v2.py — after successful extraction call
from research_tracking import record_extraction_cost
# After line 430 (content, usage = call_openrouter(...))
# After line 672 (log_usage(...))
record_extraction_cost(
conn, args.source_file, args.model,
usage.get("prompt_tokens", 0),
usage.get("completion_tokens", 0),
)
### 2. Agent research scripts — wrap research sessions
from research_tracking import start_session, link_source_to_session, complete_session
# At start of research:
session_id = start_session(conn, agent="leo", topic="weapons stigmatization campaigns",
domain="grand-strategy",
reasoning="Following up on EU AI Act national security exclusion — exploring how stigmatization
campaigns have historically driven arms control policy",
sources_planned=6, model="claude-opus-4-6")
# As each source is written:
link_source_to_session(conn, source_path, session_id)
# At end of research:
complete_session(conn, session_id,
summary="Ottawa Treaty mine ban model is the strongest parallel to AI weapons — same
3-condition framework (humanitarian harm + low military utility + civil society
coalition). Ukraine Shahed case is a near-miss triggering event.",
input_tokens=total_in, output_tokens=total_out, cost_usd=total_cost)
### 3. PR creation in lib/merge.py or lib/validate.py — ensure source_path
from research_tracking import ensure_source_path_on_pr
# When creating a PR, pass the source:
ensure_source_path_on_pr(conn, pr_number, source_path)
### 4. One-time backfills (run manually after migration)
from research_tracking import backfill_extraction_costs, backfill_source_paths
backfill_extraction_costs(conn, "/opt/teleo-eval/logs/openrouter-usage.csv")
backfill_source_paths(conn)
### 5. Migration
Run MIGRATION_V11_SQL against pipeline.db after backing up.
"""

View file

@ -140,7 +140,7 @@ async def fetch_review_queue(
if forgejo_token:
headers["Authorization"] = f"token {forgejo_token}"
connector = aiohttp.TCPConnector(ssl=False)
connector = aiohttp.TCPConnector() # Default SSL verification — Forgejo token must not be exposed to MITM
async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
# Fetch open PRs
url = f"{FORGEJO_BASE}/repos/{REPO}/pulls?state=open&limit=50&sort=oldest"

View file

@ -9,8 +9,10 @@ PAGES = [
{"path": "/prs", "label": "PRs", "icon": "&#9998;"},
{"path": "/ops", "label": "Operations", "icon": "&#9881;"},
{"path": "/health", "label": "Knowledge Health", "icon": "&#9829;"},
{"path": "/kb-proposals", "label": "KB Proposals", "icon": "&#9874;"},
{"path": "/agents", "label": "Agents", "icon": "&#9733;"},
{"path": "/epistemic", "label": "Epistemic", "icon": "&#9878;"},
{"path": "/portfolio", "label": "Portfolio", "icon": "&#9733;"},
]

629
diagnostics/vitality.py Normal file
View file

@ -0,0 +1,629 @@
"""Agent Vitality Diagnostics — data collection and schema.
Records daily vitality snapshots per agent across 10 dimensions.
Designed as the objective function for agent "aliveness" ranking.
Owner: Ship (data collection) + Argus (storage, API, dashboard)
Data sources: pipeline.db (read-only), claim-index API, agent-state filesystem, review_records
Dimension keys (agreed with Leo 2026-04-08):
knowledge_output, knowledge_quality, contributor_engagement,
review_performance, spend_efficiency, autonomy,
infrastructure_health, social_reach, capital, external_impact
"""
import json
import logging
import os
import sqlite3
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
logger = logging.getLogger("vitality")
# Known domain agents and their primary domains
AGENT_DOMAINS = {
"rio": ["internet-finance"],
"theseus": ["collective-intelligence", "living-agents"],
"astra": ["space-development", "energy", "manufacturing", "robotics"],
"vida": ["health"],
"clay": ["entertainment", "cultural-dynamics"],
"leo": ["grand-strategy", "teleohumanity"],
"hermes": [], # communications, no domain
"rhea": [], # infrastructure ops, no domain
"ganymede": [], # code review, no domain
"epimetheus": [], # pipeline, no domain
"oberon": [], # dashboard, no domain
"argus": [], # diagnostics, no domain
"ship": [], # engineering, no domain
}
# Agent file path prefixes — for matching claims by location, not just domain field.
# Handles claims in core/ and foundations/ that may not have a standard domain field
# in the claim-index (domain derived from directory path).
AGENT_PATHS = {
"rio": ["domains/internet-finance/"],
"theseus": ["domains/ai-alignment/", "core/living-agents/", "core/collective-intelligence/",
"foundations/collective-intelligence/"],
"astra": ["domains/space-development/", "domains/energy/",
"domains/manufacturing/", "domains/robotics/"],
"vida": ["domains/health/"],
"clay": ["domains/entertainment/", "foundations/cultural-dynamics/"],
"leo": ["core/grand-strategy/", "core/teleohumanity/", "core/mechanisms/",
"core/living-capital/", "foundations/teleological-economics/",
"foundations/critical-systems/"],
}
ALL_AGENTS = list(AGENT_DOMAINS.keys())
# Agent-state directory (VPS filesystem)
AGENT_STATE_DIR = Path(os.environ.get(
"AGENT_STATE_DIR", "/opt/teleo-eval/agent-state"
))
MIGRATION_SQL = """
CREATE TABLE IF NOT EXISTS vitality_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_name TEXT NOT NULL,
dimension TEXT NOT NULL,
metric TEXT NOT NULL,
value REAL NOT NULL DEFAULT 0,
unit TEXT NOT NULL DEFAULT '',
source TEXT,
recorded_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(agent_name, dimension, metric, recorded_at)
);
CREATE INDEX IF NOT EXISTS idx_vitality_agent_time
ON vitality_snapshots(agent_name, recorded_at);
CREATE INDEX IF NOT EXISTS idx_vitality_dimension
ON vitality_snapshots(dimension, recorded_at);
"""
# Add source column if missing (idempotent upgrade from v1 schema)
UPGRADE_SQL = """
ALTER TABLE vitality_snapshots ADD COLUMN source TEXT;
"""
def ensure_schema(db_path: str):
"""Create vitality_snapshots table if it doesn't exist."""
conn = sqlite3.connect(db_path, timeout=30)
try:
conn.executescript(MIGRATION_SQL)
try:
conn.execute(UPGRADE_SQL)
except sqlite3.OperationalError:
pass # column already exists
conn.commit()
logger.info("vitality_snapshots schema ensured")
finally:
conn.close()
def _fetch_claim_index(url: str = "http://localhost:8080/claim-index") -> dict | None:
"""Fetch claim-index from pipeline health API."""
try:
req = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
except Exception as e:
logger.warning("claim-index fetch failed: %s", e)
return None
def _ro_conn(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=30)
conn.row_factory = sqlite3.Row
return conn
# ---------------------------------------------------------------------------
# Dimension 1: knowledge_output — "How much has this agent produced?"
# ---------------------------------------------------------------------------
def collect_knowledge_output(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Claims merged, domain count, PRs submitted."""
metrics = []
row = conn.execute(
"SELECT COUNT(*) as cnt FROM prs WHERE agent = ? AND status = 'merged'",
(agent,),
).fetchone()
metrics.append({"metric": "claims_merged", "value": row["cnt"], "unit": "claims"})
row = conn.execute(
"SELECT COUNT(DISTINCT domain) as cnt FROM prs "
"WHERE agent = ? AND domain IS NOT NULL AND status = 'merged'",
(agent,),
).fetchone()
metrics.append({"metric": "domains_contributed", "value": row["cnt"], "unit": "domains"})
row = conn.execute(
"SELECT COUNT(*) as cnt FROM prs WHERE agent = ? AND created_at > datetime('now', '-7 days')",
(agent,),
).fetchone()
metrics.append({"metric": "prs_7d", "value": row["cnt"], "unit": "PRs"})
return metrics
# ---------------------------------------------------------------------------
# Dimension 2: knowledge_quality — "How good is the output?"
# ---------------------------------------------------------------------------
def collect_knowledge_quality(
conn: sqlite3.Connection, claim_index: dict | None, agent: str
) -> list[dict]:
"""Evidence density, challenge rate, cross-domain links, domain coverage."""
metrics = []
agent_domains = AGENT_DOMAINS.get(agent, [])
# Challenge rate = challenge PRs / total PRs
rows = conn.execute(
"SELECT commit_type, COUNT(*) as cnt FROM prs "
"WHERE agent = ? AND commit_type IS NOT NULL GROUP BY commit_type",
(agent,),
).fetchall()
total = sum(r["cnt"] for r in rows)
type_counts = {r["commit_type"]: r["cnt"] for r in rows}
challenge_rate = type_counts.get("challenge", 0) / total if total > 0 else 0
metrics.append({"metric": "challenge_rate", "value": round(challenge_rate, 4), "unit": "ratio"})
# Activity breadth (distinct commit types)
metrics.append({"metric": "activity_breadth", "value": len(type_counts), "unit": "types"})
# Evidence density + cross-domain links from claim-index
# Match by domain field OR file path prefix (catches core/, foundations/ claims)
agent_paths = AGENT_PATHS.get(agent, [])
if claim_index and (agent_domains or agent_paths):
claims = claim_index.get("claims", [])
agent_claims = [
c for c in claims
if c.get("domain") in agent_domains
or any(c.get("file", "").startswith(p) for p in agent_paths)
]
total_claims = len(agent_claims)
# Evidence density: claims with incoming links / total claims
linked = sum(1 for c in agent_claims if c.get("incoming_count", 0) > 0)
density = linked / total_claims if total_claims > 0 else 0
metrics.append({"metric": "evidence_density", "value": round(density, 4), "unit": "ratio"})
# Cross-domain links
cross_domain = sum(
1 for c in agent_claims
for link in c.get("outgoing_links", [])
if any(d in link for d in claim_index.get("domains", {}).keys()
if d not in agent_domains)
)
metrics.append({"metric": "cross_domain_links", "value": cross_domain, "unit": "links"})
# Domain coverage: agent's claims / average domain size
domains_data = claim_index.get("domains", {})
agent_claim_count = sum(domains_data.get(d, 0) for d in agent_domains)
avg_domain_size = (sum(domains_data.values()) / len(domains_data)) if domains_data else 1
coverage = min(agent_claim_count / avg_domain_size, 1.0) if avg_domain_size > 0 else 0
metrics.append({"metric": "domain_coverage", "value": round(coverage, 4), "unit": "ratio"})
else:
metrics.append({"metric": "evidence_density", "value": 0, "unit": "ratio"})
metrics.append({"metric": "cross_domain_links", "value": 0, "unit": "links"})
metrics.append({"metric": "domain_coverage", "value": 0, "unit": "ratio"})
return metrics
# ---------------------------------------------------------------------------
# Dimension 3: contributor_engagement — "Who contributes to this agent's domain?"
# ---------------------------------------------------------------------------
def collect_contributor_engagement(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Unique submitters to this agent's domain."""
row = conn.execute(
"SELECT COUNT(DISTINCT submitted_by) as cnt FROM prs "
"WHERE agent = ? AND submitted_by IS NOT NULL AND submitted_by != ''",
(agent,),
).fetchone()
return [
{"metric": "unique_submitters", "value": row["cnt"], "unit": "contributors"},
]
# ---------------------------------------------------------------------------
# Dimension 4: review_performance — "How good is the evaluator feedback loop?"
# ---------------------------------------------------------------------------
def collect_review_performance(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Approval rate, rejection reasons from review_records."""
metrics = []
# Check if review_records table exists
table_check = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='review_records'"
).fetchone()
if not table_check:
return [
{"metric": "approval_rate", "value": 0, "unit": "ratio"},
{"metric": "total_reviews", "value": 0, "unit": "reviews"},
]
# Overall approval rate for this agent's claims (join through prs table)
row = conn.execute(
"SELECT COUNT(*) as total, "
"SUM(CASE WHEN r.outcome = 'approved' THEN 1 ELSE 0 END) as approved, "
"SUM(CASE WHEN r.outcome = 'approved-with-changes' THEN 1 ELSE 0 END) as with_changes, "
"SUM(CASE WHEN r.outcome = 'rejected' THEN 1 ELSE 0 END) as rejected "
"FROM review_records r "
"JOIN prs p ON r.pr_number = p.pr_number "
"WHERE LOWER(p.agent) = LOWER(?)",
(agent,),
).fetchone()
total = row["total"] or 0
approved = (row["approved"] or 0) + (row["with_changes"] or 0)
rejected = row["rejected"] or 0
approval_rate = approved / total if total > 0 else 0
metrics.append({"metric": "total_reviews", "value": total, "unit": "reviews"})
metrics.append({"metric": "approval_rate", "value": round(approval_rate, 4), "unit": "ratio"})
metrics.append({"metric": "approved", "value": row["approved"] or 0, "unit": "reviews"})
metrics.append({"metric": "approved_with_changes", "value": row["with_changes"] or 0, "unit": "reviews"})
metrics.append({"metric": "rejected", "value": rejected, "unit": "reviews"})
# Top rejection reasons (last 30 days)
reasons = conn.execute(
"SELECT r.rejection_reason, COUNT(*) as cnt FROM review_records r "
"JOIN prs p ON r.pr_number = p.pr_number "
"WHERE LOWER(p.agent) = LOWER(?) AND r.outcome = 'rejected' "
"AND r.rejection_reason IS NOT NULL "
"AND r.review_date > datetime('now', '-30 days') "
"GROUP BY r.rejection_reason ORDER BY cnt DESC",
(agent,),
).fetchall()
for r in reasons:
metrics.append({
"metric": f"rejection_{r['rejection_reason']}",
"value": r["cnt"],
"unit": "rejections",
})
return metrics
# ---------------------------------------------------------------------------
# Dimension 5: spend_efficiency — "What does it cost per merged claim?"
# ---------------------------------------------------------------------------
def collect_spend_efficiency(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Cost per merged claim, total spend, response costs."""
metrics = []
# Pipeline cost attributed to this agent (from prs.cost_usd)
row = conn.execute(
"SELECT COALESCE(SUM(cost_usd), 0) as cost, COUNT(*) as merged "
"FROM prs WHERE agent = ? AND status = 'merged'",
(agent,),
).fetchone()
total_cost = row["cost"] or 0
merged = row["merged"] or 0
cost_per_claim = total_cost / merged if merged > 0 else 0
metrics.append({"metric": "total_pipeline_cost", "value": round(total_cost, 4), "unit": "USD"})
metrics.append({"metric": "cost_per_merged_claim", "value": round(cost_per_claim, 4), "unit": "USD"})
# Response audit costs (Telegram bot) — per-agent
row = conn.execute(
"SELECT COALESCE(SUM(generation_cost), 0) as cost, COUNT(*) as cnt "
"FROM response_audit WHERE agent = ?",
(agent,),
).fetchone()
metrics.append({"metric": "response_cost_total", "value": round(row["cost"], 4), "unit": "USD"})
metrics.append({"metric": "total_responses", "value": row["cnt"], "unit": "responses"})
# 24h spend snapshot
row = conn.execute(
"SELECT COALESCE(SUM(generation_cost), 0) as cost "
"FROM response_audit WHERE agent = ? AND timestamp > datetime('now', '-24 hours')",
(agent,),
).fetchone()
metrics.append({"metric": "response_cost_24h", "value": round(row["cost"], 4), "unit": "USD"})
return metrics
# ---------------------------------------------------------------------------
# Dimension 6: autonomy — "How independently does this agent act?"
# ---------------------------------------------------------------------------
def collect_autonomy(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Self-directed actions, active days."""
metrics = []
# Autonomous responses in last 24h
row = conn.execute(
"SELECT COUNT(*) as cnt FROM response_audit "
"WHERE agent = ? AND timestamp > datetime('now', '-24 hours')",
(agent,),
).fetchone()
metrics.append({"metric": "autonomous_responses_24h", "value": row["cnt"], "unit": "actions"})
# Active days in last 7
row = conn.execute(
"SELECT COUNT(DISTINCT date(created_at)) as days FROM prs "
"WHERE agent = ? AND created_at > datetime('now', '-7 days')",
(agent,),
).fetchone()
metrics.append({"metric": "active_days_7d", "value": row["days"], "unit": "days"})
return metrics
# ---------------------------------------------------------------------------
# Dimension 7: infrastructure_health — "Is the agent's machinery working?"
# ---------------------------------------------------------------------------
def collect_infrastructure_health(conn: sqlite3.Connection, agent: str) -> list[dict]:
"""Circuit breakers, PR success rate, agent-state liveness."""
metrics = []
# Circuit breakers
rows = conn.execute(
"SELECT name, state FROM circuit_breakers WHERE name LIKE ?",
(f"%{agent}%",),
).fetchall()
open_breakers = sum(1 for r in rows if r["state"] != "closed")
metrics.append({"metric": "open_circuit_breakers", "value": open_breakers, "unit": "breakers"})
# PR success rate last 7 days
row = conn.execute(
"SELECT COUNT(*) as total, "
"SUM(CASE WHEN status='merged' THEN 1 ELSE 0 END) as merged "
"FROM prs WHERE agent = ? AND created_at > datetime('now', '-7 days')",
(agent,),
).fetchone()
total = row["total"]
rate = row["merged"] / total if total > 0 else 0
metrics.append({"metric": "merge_rate_7d", "value": round(rate, 4), "unit": "ratio"})
# Agent-state liveness (read metrics.json from filesystem)
state_file = AGENT_STATE_DIR / agent / "metrics.json"
if state_file.exists():
try:
with open(state_file) as f:
state = json.load(f)
lifetime = state.get("lifetime", {})
metrics.append({
"metric": "sessions_total",
"value": lifetime.get("sessions_total", 0),
"unit": "sessions",
})
metrics.append({
"metric": "sessions_timeout",
"value": lifetime.get("sessions_timeout", 0),
"unit": "sessions",
})
metrics.append({
"metric": "sessions_error",
"value": lifetime.get("sessions_error", 0),
"unit": "sessions",
})
except (json.JSONDecodeError, OSError) as e:
logger.warning("Failed to read agent-state for %s: %s", agent, e)
return metrics
# ---------------------------------------------------------------------------
# Dimensions 8-10: Stubs (no data sources yet)
# ---------------------------------------------------------------------------
def collect_social_reach(agent: str) -> list[dict]:
"""Social dimension: stub zeros until X API accounts are active."""
return [
{"metric": "followers", "value": 0, "unit": "followers"},
{"metric": "impressions_7d", "value": 0, "unit": "impressions"},
{"metric": "engagement_rate", "value": 0, "unit": "ratio"},
]
def collect_capital(agent: str) -> list[dict]:
"""Capital dimension: stub zeros until treasury/revenue tracking exists."""
return [
{"metric": "aum", "value": 0, "unit": "USD"},
{"metric": "treasury", "value": 0, "unit": "USD"},
]
def collect_external_impact(agent: str) -> list[dict]:
"""External impact dimension: stub zeros until manual tracking exists."""
return [
{"metric": "decisions_informed", "value": 0, "unit": "decisions"},
{"metric": "deals_sourced", "value": 0, "unit": "deals"},
]
# ---------------------------------------------------------------------------
# Orchestration
# ---------------------------------------------------------------------------
DIMENSION_MAP = {
"knowledge_output": lambda conn, ci, agent: collect_knowledge_output(conn, agent),
"knowledge_quality": collect_knowledge_quality,
"contributor_engagement": lambda conn, ci, agent: collect_contributor_engagement(conn, agent),
"review_performance": lambda conn, ci, agent: collect_review_performance(conn, agent),
"spend_efficiency": lambda conn, ci, agent: collect_spend_efficiency(conn, agent),
"autonomy": lambda conn, ci, agent: collect_autonomy(conn, agent),
"infrastructure_health": lambda conn, ci, agent: collect_infrastructure_health(conn, agent),
"social_reach": lambda conn, ci, agent: collect_social_reach(agent),
"capital": lambda conn, ci, agent: collect_capital(agent),
"external_impact": lambda conn, ci, agent: collect_external_impact(agent),
}
def collect_all_for_agent(
db_path: str,
agent: str,
claim_index_url: str = "http://localhost:8080/claim-index",
) -> dict:
"""Collect all 10 vitality dimensions for a single agent.
Returns {dimension: [metrics]}.
"""
claim_index = _fetch_claim_index(claim_index_url)
conn = _ro_conn(db_path)
try:
result = {}
for dim_key, collector in DIMENSION_MAP.items():
try:
result[dim_key] = collector(conn, claim_index, agent)
except Exception as e:
logger.error("collector %s failed for %s: %s", dim_key, agent, e)
result[dim_key] = []
return result
finally:
conn.close()
def collect_system_aggregate(
db_path: str,
claim_index_url: str = "http://localhost:8080/claim-index",
) -> dict:
"""System-level aggregate vitality metrics."""
claim_index = _fetch_claim_index(claim_index_url)
conn = _ro_conn(db_path)
try:
metrics = {}
# Knowledge totals
total_claims = claim_index["total_claims"] if claim_index else 0
orphan_ratio = claim_index.get("orphan_ratio", 0) if claim_index else 0
domain_count = len(claim_index.get("domains", {})) if claim_index else 0
metrics["knowledge_output"] = [
{"metric": "total_claims", "value": total_claims, "unit": "claims"},
{"metric": "total_domains", "value": domain_count, "unit": "domains"},
{"metric": "orphan_ratio", "value": round(orphan_ratio, 4), "unit": "ratio"},
]
# Cross-domain citation rate
if claim_index:
claims = claim_index.get("claims", [])
total_links = sum(c.get("outgoing_count", 0) for c in claims)
cross_domain = 0
for c in claims:
src_domain = c.get("domain")
for link in c.get("outgoing_links", []):
linked_claims = [
x for x in claims
if x.get("stem") in link or x.get("file", "").endswith(link + ".md")
]
for lc in linked_claims:
if lc.get("domain") != src_domain:
cross_domain += 1
metrics["knowledge_quality"] = [
{"metric": "cross_domain_citation_rate",
"value": round(cross_domain / max(total_links, 1), 4),
"unit": "ratio"},
]
# Pipeline throughput
row = conn.execute(
"SELECT COUNT(*) as merged FROM prs "
"WHERE status='merged' AND merged_at > datetime('now', '-24 hours')"
).fetchone()
row2 = conn.execute("SELECT COUNT(*) as total FROM sources").fetchone()
row3 = conn.execute(
"SELECT COUNT(*) as pending FROM prs "
"WHERE status NOT IN ('merged','rejected','closed')"
).fetchone()
metrics["infrastructure_health"] = [
{"metric": "prs_merged_24h", "value": row["merged"], "unit": "PRs/day"},
{"metric": "total_sources", "value": row2["total"], "unit": "sources"},
{"metric": "queue_depth", "value": row3["pending"], "unit": "PRs"},
]
# Total spend
row = conn.execute(
"SELECT COALESCE(SUM(cost_usd), 0) as cost "
"FROM costs WHERE date > date('now', '-1 day')"
).fetchone()
row2 = conn.execute(
"SELECT COALESCE(SUM(generation_cost), 0) as cost FROM response_audit "
"WHERE timestamp > datetime('now', '-24 hours')"
).fetchone()
metrics["spend_efficiency"] = [
{"metric": "pipeline_cost_24h", "value": round(row["cost"], 4), "unit": "USD"},
{"metric": "response_cost_24h", "value": round(row2["cost"], 4), "unit": "USD"},
{"metric": "total_cost_24h",
"value": round(row["cost"] + row2["cost"], 4), "unit": "USD"},
]
# Stubs
metrics["social_reach"] = [{"metric": "total_followers", "value": 0, "unit": "followers"}]
metrics["capital"] = [{"metric": "total_aum", "value": 0, "unit": "USD"}]
return metrics
finally:
conn.close()
def record_snapshot(
db_path: str,
claim_index_url: str = "http://localhost:8080/claim-index",
):
"""Run a full vitality snapshot — one row per agent per dimension per metric."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
rows = []
# Per-agent snapshots
for agent in ALL_AGENTS:
try:
dimensions = collect_all_for_agent(db_path, agent, claim_index_url)
for dim_name, metrics in dimensions.items():
collector_name = f"{dim_name}_collector"
for m in metrics:
rows.append((
agent, dim_name, m["metric"], m["value"],
m["unit"], collector_name, now,
))
except Exception as e:
logger.error("vitality collection failed for %s: %s", agent, e)
# System aggregate
try:
system = collect_system_aggregate(db_path, claim_index_url)
for dim_name, metrics in system.items():
for m in metrics:
rows.append((
"_system", dim_name, m["metric"], m["value"],
m["unit"], "system_aggregate", now,
))
except Exception as e:
logger.error("vitality system aggregate failed: %s", e)
# Write all rows
ensure_schema(db_path)
conn = sqlite3.connect(db_path, timeout=30)
try:
conn.executemany(
"INSERT OR REPLACE INTO vitality_snapshots "
"(agent_name, dimension, metric, value, unit, source, recorded_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
rows,
)
conn.commit()
logger.info(
"vitality snapshot recorded: %d rows for %d agents + system",
len(rows), len(ALL_AGENTS),
)
return {"rows_written": len(rows), "agents": len(ALL_AGENTS), "recorded_at": now}
finally:
conn.close()
if __name__ == "__main__":
"""CLI: python3 vitality.py [db_path] — runs a snapshot."""
import sys
logging.basicConfig(level=logging.INFO)
db = sys.argv[1] if len(sys.argv) > 1 else "/opt/teleo-eval/pipeline/pipeline.db"
result = record_snapshot(db)
print(json.dumps(result, indent=2))

View file

@ -0,0 +1,293 @@
"""Vitality API routes for Argus diagnostics dashboard.
Endpoints:
GET /api/vitality latest snapshot + time-series for all agents or one
GET /api/vitality/snapshot trigger a new snapshot (POST-like via GET for cron curl)
GET /api/vitality/leaderboard agents ranked by composite vitality score
Owner: Argus
"""
import json
import logging
import sqlite3
from pathlib import Path
from aiohttp import web
from vitality import (
ALL_AGENTS,
MIGRATION_SQL,
collect_all_for_agent,
collect_system_aggregate,
record_snapshot,
)
logger = logging.getLogger("argus.vitality")
# Composite vitality weights — Leo-approved 2026-04-08
# Dimension keys match Ship's refactored vitality.py DIMENSION_MAP
VITALITY_WEIGHTS = {
"knowledge_output": 0.30, # primary output — highest weight
"knowledge_quality": 0.20, # was "diversity" — quality of output
"contributor_engagement": 0.15, # attracting external contributors
"review_performance": 0.00, # new dim, zero until review_records populated
"autonomy": 0.15, # independent action
"infrastructure_health": 0.05, # machinery working
"spend_efficiency": 0.05, # cost discipline
"social_reach": 0.00, # zero until accounts active
"capital": 0.00, # zero until treasury exists
"external_impact": 0.00, # zero until measurable
}
# Public paths (no auth required)
VITALITY_PUBLIC_PATHS = frozenset({
"/api/vitality",
"/api/vitality/snapshot",
"/api/vitality/leaderboard",
})
def _ro_conn(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=30)
conn.row_factory = sqlite3.Row
return conn
async def handle_vitality(request: web.Request) -> web.Response:
"""GET /api/vitality?agent=<name>&days=7
Returns latest snapshot and time-series data.
If agent is specified, returns that agent only. Otherwise returns all.
"""
db_path = request.app["db_path"]
agent = request.query.get("agent")
try:
days = min(int(request.query.get("days", "7")), 90)
except ValueError:
days = 7
conn = _ro_conn(db_path)
try:
# Check if table exists
table_check = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='vitality_snapshots'"
).fetchone()
if not table_check:
return web.json_response({
"error": "No vitality data yet. Trigger a snapshot first via /api/vitality/snapshot",
"has_data": False
})
# Latest snapshot timestamp
latest = conn.execute(
"SELECT MAX(recorded_at) as ts FROM vitality_snapshots"
).fetchone()
latest_ts = latest["ts"] if latest else None
if not latest_ts:
return web.json_response({"has_data": False})
# Latest snapshot data
if agent:
agents_filter = [agent]
else:
agents_filter = ALL_AGENTS + ["_system"]
result = {"latest_snapshot": latest_ts, "agents": {}}
for a in agents_filter:
rows = conn.execute(
"SELECT dimension, metric, value, unit FROM vitality_snapshots "
"WHERE agent_name = ? AND recorded_at = ?",
(a, latest_ts)
).fetchall()
if not rows:
continue
dimensions = {}
for r in rows:
dim = r["dimension"]
if dim not in dimensions:
dimensions[dim] = []
dimensions[dim].append({
"metric": r["metric"],
"value": r["value"],
"unit": r["unit"],
})
result["agents"][a] = dimensions
# Time-series for trend charts (one data point per snapshot)
ts_query_agent = agent if agent else "_system"
ts_rows = conn.execute(
"SELECT recorded_at, dimension, metric, value "
"FROM vitality_snapshots "
"WHERE agent_name = ? AND recorded_at > datetime('now', ?)"
"ORDER BY recorded_at",
(ts_query_agent, f"-{days} days")
).fetchall()
time_series = {}
for r in ts_rows:
key = f"{r['dimension']}.{r['metric']}"
if key not in time_series:
time_series[key] = []
time_series[key].append({
"t": r["recorded_at"],
"v": r["value"],
})
result["time_series"] = time_series
result["has_data"] = True
return web.json_response(result)
finally:
conn.close()
async def handle_vitality_snapshot(request: web.Request) -> web.Response:
"""GET /api/vitality/snapshot — trigger a new snapshot collection.
Used by cron: curl http://localhost:8081/api/vitality/snapshot
Requires ?confirm=1 to prevent accidental triggers from crawlers/prefetch.
"""
if request.query.get("confirm") != "1":
return web.json_response(
{"status": "noop", "error": "Add ?confirm=1 to trigger a snapshot write"},
status=400,
)
db_path = request.app["db_path"]
claim_index_url = request.app.get("claim_index_url", "http://localhost:8080/claim-index")
try:
result = record_snapshot(db_path, claim_index_url)
return web.json_response({"status": "ok", **result})
except Exception as e:
logger.error("vitality snapshot failed: %s", e)
return web.json_response({"status": "error", "error": str(e)}, status=500)
async def handle_vitality_leaderboard(request: web.Request) -> web.Response:
"""GET /api/vitality/leaderboard — agents ranked by composite vitality score.
Scoring approach:
- Each dimension gets a 0-1 normalized score based on the metric values
- Weighted sum produces composite score
- Agents ranked by composite score descending
"""
db_path = request.app["db_path"]
conn = _ro_conn(db_path)
try:
table_check = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='vitality_snapshots'"
).fetchone()
if not table_check:
return web.json_response({"error": "No vitality data yet", "has_data": False})
latest = conn.execute(
"SELECT MAX(recorded_at) as ts FROM vitality_snapshots"
).fetchone()
if not latest or not latest["ts"]:
return web.json_response({"has_data": False})
latest_ts = latest["ts"]
# Collect all agents' latest data
agent_scores = []
for agent in ALL_AGENTS:
rows = conn.execute(
"SELECT dimension, metric, value FROM vitality_snapshots "
"WHERE agent_name = ? AND recorded_at = ?",
(agent, latest_ts)
).fetchall()
if not rows:
continue
dims = {}
for r in rows:
dim = r["dimension"]
if dim not in dims:
dims[dim] = {}
dims[dim][r["metric"]] = r["value"]
# Normalize each dimension to 0-1
# Dimension keys match Ship's refactored vitality.py DIMENSION_MAP
dim_scores = {}
# knowledge_output: claims_merged (cap at 100 = 1.0)
ko = dims.get("knowledge_output", {})
claims = ko.get("claims_merged", 0)
dim_scores["knowledge_output"] = min(claims / 100, 1.0)
# knowledge_quality: challenge_rate + breadth + evidence_density + domain_coverage
kq = dims.get("knowledge_quality", {})
cr = kq.get("challenge_rate", 0)
breadth = kq.get("activity_breadth", 0)
evidence = kq.get("evidence_density", 0)
coverage = kq.get("domain_coverage", 0)
dim_scores["knowledge_quality"] = min(
(cr / 0.1 * 0.2 + breadth / 4 * 0.2 + evidence * 0.3 + coverage * 0.3), 1.0
)
# contributor_engagement: unique_submitters (cap at 5 = 1.0)
ce = dims.get("contributor_engagement", {})
dim_scores["contributor_engagement"] = min(ce.get("unique_submitters", 0) / 5, 1.0)
# review_performance: approval_rate from review_records (0 until populated)
rp = dims.get("review_performance", {})
dim_scores["review_performance"] = rp.get("approval_rate", 0)
# autonomy: active_days_7d (7 = 1.0)
am = dims.get("autonomy", {})
dim_scores["autonomy"] = min(am.get("active_days_7d", 0) / 7, 1.0)
# infrastructure_health: merge_rate_7d directly (already 0-1)
ih = dims.get("infrastructure_health", {})
dim_scores["infrastructure_health"] = ih.get("merge_rate_7d", 0)
# spend_efficiency: inverted — lower cost per claim is better
se = dims.get("spend_efficiency", {})
daily_cost = se.get("response_cost_24h", 0)
dim_scores["spend_efficiency"] = max(1.0 - daily_cost / 10.0, 0)
# Social/Capital/External: stubbed at 0
dim_scores["social_reach"] = 0
dim_scores["capital"] = 0
dim_scores["external_impact"] = 0
# Composite weighted score
composite = sum(
dim_scores.get(dim, 0) * weight
for dim, weight in VITALITY_WEIGHTS.items()
)
agent_scores.append({
"agent": agent,
"composite_score": round(composite, 4),
"dimension_scores": {k: round(v, 4) for k, v in dim_scores.items()},
"raw_highlights": {
"claims_merged": int(claims),
"merge_rate": round(ih.get("merge_rate_7d", 0) * 100, 1),
"active_days": int(am.get("active_days_7d", 0)),
"challenge_rate": round(cr * 100, 1),
"evidence_density": round(evidence * 100, 1),
},
})
# Sort by composite score descending
agent_scores.sort(key=lambda x: x["composite_score"], reverse=True)
return web.json_response({
"has_data": True,
"snapshot_at": latest_ts,
"leaderboard": agent_scores,
})
finally:
conn.close()
def register_vitality_routes(app: web.Application):
"""Register vitality endpoints on the aiohttp app."""
app.router.add_get("/api/vitality", handle_vitality)
app.router.add_get("/api/vitality/snapshot", handle_vitality_snapshot)
app.router.add_get("/api/vitality/leaderboard", handle_vitality_leaderboard)

View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
cd /app
python -m compileall -q lib telegram diagnostics teleo-pipeline.py
python -m pytest -q \
tests/test_agent_routing.py \
tests/test_decision_engine_replay.py \
tests/test_phase1b_end_to_end.py \
tests/test_research_eval_schema_sql.py \
tests/test_teleo_agent_systemd.py
python - <<'PY'
import json
from lib.agent_routing import classify_pr_route
route = classify_pr_route(
"diff --git a/domains/internet-finance/x402-wallets.md b/domains/internet-finance/x402-wallets.md\n"
"+AI systems route agents around x402 payments and agent wallets.\n"
)
print(json.dumps({
"smoke": "teleo-gcp-staging-ok",
"primary_agent": route.primary_agent,
"required_agents": list(route.required_agents),
"route_kind": route.route_kind,
}, sort_keys=True))
PY

View file

@ -0,0 +1,54 @@
{
"status": "blocked_remote_execution",
"scope": "crabbox remote proof",
"attempted_discovery": [
"verified Crabbox CLI is installed at /Users/user/.local/bin/crabbox",
"ran crabbox job list",
"ran crabbox sync-plan",
"ran crabbox job run --dry-run unit",
"ran crabbox job run --dry-run phase1b-local-proof",
"checked presence of CRABBOX_COORDINATOR, CRABBOX_COORDINATOR_TOKEN, HCLOUD_TOKEN, HETZNER_TOKEN, GH_TOKEN, and GITHUB_TOKEN without printing values",
"loaded retained Bitwarden session from /tmp/bw_session without printing the session value",
"ran bw status and bw sync",
"checked Bitwarden organization, collection, and item counts",
"checked visible Bitwarden item names and metadata only",
"scanned visible Bitwarden item names and notes for crabbox, hcloud, hetzner, and coordinator terms without printing note or secret values"
],
"exact_blocker": "Crabbox provider execution still lacks a real provider credential: HCLOUD_TOKEN, HETZNER_TOKEN, CRABBOX_COORDINATOR, and CRABBOX_COORDINATOR_TOKEN are unset, and the visible Bitwarden org collection contains only Anthropic API Key, Leo twitter, and LivingIPbot Github, with no Crabbox, HCloud, Hetzner, or coordinator metadata match.",
"why_it_cannot_be_solved_autonomously": "A remote Crabbox lease requires a real Hetzner or Crabbox broker credential. The repo can safely commit CI/CD config, dry-run plans, and blocker artifacts, but it cannot fabricate the provider credential or commit secret values.",
"exact_next_action": "Add a scoped Hetzner/Crabbox broker credential to Bitwarden or GitHub environment secrets as HCLOUD_TOKEN, HETZNER_TOKEN, CRABBOX_COORDINATOR, or CRABBOX_COORDINATOR_TOKEN, then rerun crabbox doctor --json and crabbox job run phase1b-local-proof from teleo-infrastructure.",
"safe_local_status": {
"crabbox_cli_installed": "0.22.1",
"job_list": "passes",
"sync_plan": "217 files, 2.4 MiB",
"unit_dry_run": "passes",
"phase1b_proof_dry_run": "passes",
"ci_contract_guard": "passes",
"phase1b_proof_wrapper": "131 passed, 8 proof cases succeeded, all six agents seen",
"full_pytest": "422 passed",
"crabbox_doctor": "fails only provider credential check: HCLOUD_TOKEN or HETZNER_TOKEN is required",
"bitwarden_status": "unlocked",
"bitwarden_organizations": 1,
"bitwarden_collections": 1,
"bitwarden_items_visible": 3,
"bitwarden_matching_crabbox_or_hetzner_items": 0
},
"secret_commit_policy": {
"allowed_to_commit": [
"workflow files",
"Crabbox config with secret slot names omitted",
"proof scripts",
"machine-readable blocker artifacts",
"docs and agent skills"
],
"not_allowed_to_commit": [
"Bitwarden item values",
"Bitwarden vault exports",
"provider tokens",
"GitHub bot tokens",
"OpenRouter keys",
"SSH private keys",
"production databases"
]
}
}

96
docs/crabbox.md Normal file
View file

@ -0,0 +1,96 @@
# Crabbox Remote Proof
Crabbox is the remote execution layer for `teleo-infrastructure`. It is not the production deploy system.
## Goals
- Run Python tests on a disposable or warm remote Linux box.
- Prove the CI/Crabbox contract without network access before remote runs.
- Run the Phase 1B local proof script remotely.
- Retain JUnit and machine-readable proof artifacts.
- Give agents a bounded job list instead of arbitrary cloud shell access.
## Non-Goals
- No production deploys.
- No production secrets.
- No production VPS mutation.
- No production `decision-engine` PR comments from Crabbox jobs.
## Required Local Setup
Crabbox CLI 0.22.1 or newer:
```bash
crabbox --version
```
One of:
```bash
crabbox login --url "$CRABBOX_COORDINATOR"
```
or direct Hetzner operator env:
```bash
export HCLOUD_TOKEN="..."
```
Do not commit either value.
## Jobs
```bash
crabbox job list
crabbox job run --dry-run ci-contract
crabbox job run --dry-run unit
crabbox job run --dry-run phase1b-local-proof
crabbox job run ci-contract
crabbox job run unit
crabbox job run phase1b-local-proof
```
`ci-contract` writes:
- `.crabbox-results/crabbox-ci-contract.json`
`phase1b-local-proof` writes:
- `.crabbox-results/crabbox-ci-contract.json`
- `proof/phase1b-local-e2e-proof.json`
- `.crabbox-results/phase1b-pytest.xml`
- `.crabbox-results/phase1b-proof-summary.json`
The contract proof checks that:
- Crabbox exposes only the named bounded jobs.
- sync excludes secret/runtime files such as `.env`, `secrets`, DBs, logs, caches, and virtualenvs.
- `.crabbox.yaml` contains no token-bearing env names.
- Leo routes are explicit: Leo-owned domains, fallback routes, and top-2 cross-domain routes that include Leo are covered, while Phase 1B does not silently preserve Leo as a universal second reviewer.
## Secret Boundary
Allowed:
- `CI`
- `PYTHONWARNINGS`
- `PHASE1B_AGENT_ROUTING_ENABLED`
- broker token in user config
- direct `HCLOUD_TOKEN` or `HETZNER_TOKEN` in local operator env
- GitHub environment secrets named `HCLOUD_TOKEN` or `HETZNER_TOKEN` for an explicitly dispatched remote proof workflow
Not allowed:
- production GitHub admin token
- production Forgejo token
- production OpenRouter key
- production SSH keys
- Bitwarden exports
- prod `pipeline.db`
Bitwarden may be used as the human/operator source of truth for secret lookup and GitHub secret setup, but no Bitwarden item value, vault export, or copied secret belongs in this repo. The committed config may name required secret slots; it must not contain the values.
## Proof Boundary
Crabbox remote proof proves repo behavior on a remote Linux lease. It does not prove production parity unless the lease recreates the production runtime paths, systemd services, timers, DB path, and deploy script behavior.

View file

@ -0,0 +1,284 @@
# GCP CI/CD and Redundancy Hardening
This records the current GCP hardening contract for `teleo-501523`.
## Current-State Rule
The latest retained `gcp-readiness` artifact is authoritative for what is live
right now. The resource lists below define the target runtime contract and the
checks this lane enforces; do not treat them as current production proof unless
the current readiness run passes.
## Target Runtime Contract
- Artifact Registry Docker repositories exist in `europe-west6`:
- `teleo`
- `livingip-web`
- Both repositories use immutable tags and active vulnerability scanning.
- `cloudbuild.gcp-staging.yaml` builds the staging Teleo image, runs the image smoke test, then pushes to Artifact Registry.
- Cloud Build runs as the dedicated service account:
- `sa-teleo-cloudbuild@teleo-501523.iam.gserviceaccount.com`
- The dedicated Cloud Build account has only the roles required for the current build/publish path:
- `roles/artifactregistry.writer`
- `roles/logging.logWriter`
- `roles/storage.objectViewer`
- GitHub Actions can publish Artifact Registry images through Workload Identity Federation:
- workflow: `.github/workflows/gcp-artifact.yml`
- provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github`
- service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`
- repository scope: `living-ip/teleo-infrastructure`
- Backup buckets are versioned and use uniform bucket-level access:
- `gs://teleo-501523-prod-backups`
- `gs://teleo-501523-leoclean-backups`
- VM boot disks have a daily 7-day snapshot policy:
- `teleo-prod-1`
- `teleo-staging-1`
- Cloud SQL standby target exists for KB restore/replication drills:
- instance: `teleo-pgvector-standby`
- database: `teleo_kb`
- version: `POSTGRES_16`
- private IP only on `teleo-staging-net`
- encrypted-only SQL connections
- automated backups and point-in-time recovery enabled
- deletion protection enabled
- Source-side Teleo DB/KB export canary exists:
- script: `ops/backup_vps_sqlite_kb.sh`
- source DB: `/opt/teleo-eval/pipeline/pipeline.db`
- source Leo/KB files: `workspaces/*/agents/leo` plus `agent-state`
- excludes secrets and logs
- Local SQLite-to-Postgres restore canary exists:
- scripts: `ops/sqlite_to_postgres_dump.py` and `ops/run_sqlite_postgres_restore_canary.sh`
- target: disposable local `postgres:16-alpine` shadow schema
- verifies source SQLite integrity and per-table source/target row-count parity
## How To Build
Automatic Artifact Registry publishing runs on pushes to `main` through GitHub Actions. To run the same lane manually from GitHub:
```bash
gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main
```
The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact.
For a read-only GCP posture probe through the same Workload Identity path:
```bash
gh workflow run gcp-readiness.yml --repo living-ip/teleo-infrastructure --ref main
```
This workflow runs `ops/check_gcp_infra_readiness.py` from GitHub Actions and
uploads stdout, stderr, exit code, and a summary as the `gcp-readiness` artifact.
It is intentionally non-mutating and defaults to
`sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com`.
If you need to test a specific service account during IAM repair, pass it
explicitly:
```bash
gh workflow run gcp-readiness.yml \
--repo living-ip/teleo-infrastructure \
--ref main \
-f service_account=sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com
```
If you also want GitHub readiness to include a local SQLite-to-Postgres restore
canary proof without uploading private backup paths or generated SQL, pass a
redacted capsule:
```bash
python3 ops/redact_sqlite_postgres_restore_canary.py \
--proof outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-<timestamp>.json \
--output outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-capsule-<timestamp>.json
CAPSULE_B64="$(base64 < outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-capsule-<timestamp>.json | tr -d '\n')"
gh workflow run gcp-readiness.yml \
--repo living-ip/teleo-infrastructure \
--ref main \
-f restore_canary_capsule_b64="${CAPSULE_B64}"
```
This only upgrades the local restore-preflight row. It is not GCP DB redundancy
until the Cloud SQL import and target-count readback also pass.
For a local/manual Cloud Build proof:
```bash
REVISION="$(git rev-parse HEAD)"
TAG="$(git rev-parse --short=7 HEAD)-manual-$(date -u +%Y%m%d-%H%M%S)"
gcloud builds submit \
--project=teleo-501523 \
--config=cloudbuild.gcp-staging.yaml \
--substitutions="_TAG=${TAG},_REVISION=${REVISION}"
```
Expected result:
- `build-staging-image` succeeds.
- `smoke-test-image-before-push` succeeds.
- A Docker image is pushed to:
`europe-west6-docker.pkg.dev/teleo-501523/teleo/teleo-pipeline-gcp-staging:${TAG}`
## How To Check Readiness
Run from the repository root:
```bash
python3 ops/check_gcp_infra_readiness.py
```
The check is read-only and prints no secret values. It verifies:
- Artifact Registry immutability and vulnerability scanning.
- Cloud Build config contract.
- Dedicated Cloud Build service account and roles.
- GitHub Actions WIF Artifact Registry publishing contract.
- Network ingress posture:
- no enabled broad SSH/RDP ingress;
- Teleo SSH rules are scoped to `/32` source ranges and target tags.
- Runtime service-account posture for the prod/staging VMs.
- Compute disk snapshot policy attachment.
- Backup bucket versioning and uniform access.
- Cloud SQL standby target posture.
- Source SQLite/KB backup/export repeatability.
- Whether an approved source KB/Postgres dump or replication credential exists.
- Whether source data has actually been restored or replicated into GCP and queried.
- Whether the GitHub WIF readiness workflow exists for non-local readback.
## Current Boundaries
The GCP Docker build/publish path is live through manual Cloud Build and GitHub Actions Workload Identity Federation. Native Cloud Build GitHub triggers are not configured because this project currently has no Cloud Build repository connection.
Database redundancy is not complete. The current project now has a GCP Cloud SQL/Postgres standby target, backup buckets, VM disk snapshots, and a repeatable source SQLite/KB export script. It does not yet have source-data restore or replication into GCP. Do not claim DB parity until one of these is true:
- the existing canonical KB database is replicated into GCP and read back; or
- GCP Cloud SQL/Postgres becomes the canonical database and production services read/write it; or
- an explicitly approved standby restore drill proves that a GCP database can be restored and queried from the retained backups.
Do not call the empty `teleo-pgvector-standby` instance redundancy by itself. It only counts after source data, restore/replication, access controls, and query readback are proven.
The local restore canary narrows the remaining gap: the source SQLite backup can
be converted and restored into PostgreSQL with table/row-count parity, but the
same import still needs to run against the GCP Cloud SQL standby through an
approved GCP auth and network path.
After Cloud SQL import, use the generated `target-counts.sql` and verify it with:
```bash
python3 ops/verify_gcp_cloudsql_restore_readback.py \
--drill-proof outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-restore-drill-<timestamp>.json \
--target-counts-csv outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-target-counts-<timestamp>.csv \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-restore-readback-verification-<timestamp>.json
```
The verifier must return `status = pass` before claiming row-count parity in GCP.
## IAM Split Plan
Do not make `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com` the broad
infra account. Keep it scoped to Docker image publishing.
Use the generated plan for the next privilege boundary:
```bash
python3 ops/plan_gcp_iam_split.py --format json
python3 ops/plan_gcp_iam_split.py --format shell
```
For an idempotent retained apply attempt, use:
```bash
python3 ops/apply_gcp_iam_split.py \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-iam-split-apply-dry-run.json
python3 ops/apply_gcp_iam_split.py \
--execute \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-iam-split-apply-execute.json
```
The first command is dry-run only. The second command mutates IAM and must run
from an authenticated GCP admin shell. It is safe to re-run: existing service
accounts are skipped, and IAM binding commands are additive/idempotent.
The plan creates two separate accounts:
- `sa-teleo-readiness@teleo-501523.iam.gserviceaccount.com`
- GitHub WIF account for read-only readiness checks.
- Needs read-only roles for Artifact Registry, IAM/service-account/WIF
metadata, Compute/network posture, Cloud SQL metadata, backup buckets, and
Secret Manager metadata.
- `sa-teleo-restore-drill@teleo-501523.iam.gserviceaccount.com`
- Operator account for explicit Cloud SQL restore drills.
- Needs Cloud SQL edit rights for the import operation and object-admin access
to the restore bucket path.
Cloud SQL imports also require the Cloud SQL instance service account to read the
GCS object. The plan includes a command that discovers that instance service
account and grants it `roles/storage.objectAdmin` on
`gs://teleo-501523-prod-backups`.
This plan is not itself a completed redundancy proof. DB redundancy is complete
only after the restore drill imports source data into Cloud SQL and the retained
`target-counts.sql` readback matches the source/local restore proof.
## Runtime Baseline Runner
Do not create or repair the GCP runtime baseline manually in the console if an
audited runner can do it. The runtime baseline runner is dry-run by default:
```bash
python3 ops/apply_gcp_runtime_baseline.py \
--admin-ssh-cidr <operator-ip>/32 \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-runtime-baseline-dry-run.json
```
The dry-run proof records the exact service accounts, network, firewall, VM,
snapshot, backup bucket, secret, and Cloud SQL operations needed for the
readiness checker. It does not prove that those resources exist.
To apply after an authenticated GCP admin session is available:
```bash
export TELEO_CLOUDSQL_POSTGRES_PASSWORD='<store locally; do not commit or print>'
python3 ops/apply_gcp_runtime_baseline.py \
--execute \
--admin-ssh-cidr <operator-ip>/32 \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-runtime-baseline-execute.json
```
`--execute` refuses to run without a single trusted IPv4 `/32` SSH CIDR. The
Cloud SQL password is passed through the environment and redacted from retained
operation commands. After execute mode succeeds, rerun `gcp-readiness.yml` with
the dedicated readiness service account and then run the Cloud SQL restore
drill/readback verifier before claiming database redundancy.
## Communication Posture
The service-to-service communication contract is declared in:
- `config/gcp-service-communications.json`
Validate it locally or in CI with:
```bash
python3 ops/check_gcp_service_communications.py \
--contract config/gcp-service-communications.json \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-service-communications-check.json
```
The contract currently requires:
- GitHub Actions artifact publishing only through `sa-artifact-builder`.
- GitHub Actions readiness only through `sa-teleo-readiness`.
- emergency SSH only from one operator IPv4 `/32` to Teleo target tags.
- VM image pulls over Private Google Access.
- Cloud SQL only on private VPC paths with encrypted-only PostgreSQL.
- Cloud SQL imports only from the versioned backup bucket through approved
service accounts.
- no broad SSH/RDP, no public database IP, no default Compute Engine service
accounts, and no raw secret values in the contract.
This is still a contract until GCP readiness passes. Live proof requires the
current `gcp-readiness` artifact to show the matching firewall, VM service
accounts, bucket, and Cloud SQL checks passing.

View file

@ -0,0 +1,224 @@
# KB Restore / Replication Runbook
This runbook is for proving Living IP KB/database redundancy on GCP.
Current source reality:
- canonical runtime DB: `/opt/teleo-eval/pipeline/pipeline.db`
- engine: SQLite WAL
- canonical Leo files:
- `/opt/teleo-eval/workspaces/main/agents/leo`
- `/opt/teleo-eval/workspaces/research-leo/agents/leo`
- `/opt/teleo-eval/agent-state`
The standby target already exists:
- project: `teleo-501523`
- instance: `teleo-pgvector-standby`
- database: `teleo_kb`
- region: `europe-west6`
- network: `teleo-staging-net`
- private IP: `10.61.0.3`
- admin password secret: `gcp-teleo-pgvector-standby-postgres-password`
Do not call this redundancy complete until source data has been restored or replicated and queried from GCP.
## Source Backup Canary
Create a consistent source backup without stopping the VPS service:
```bash
ops/backup_vps_sqlite_kb.sh
```
The script:
- uses SQLite `.backup` against `/opt/teleo-eval/pipeline/pipeline.db`;
- compresses and hashes the backup on the VPS;
- archives Leo/KB files while excluding `secrets` and logs;
- copies both artifacts locally;
- verifies SHA-256 matches;
- runs `PRAGMA integrity_check` on a local restored SQLite copy;
- records proof under `outputs/gcp-infra-hardening-20260707/proofs/`.
This proves source exportability and local restore integrity. It does not prove GCP DB redundancy until a GCP restore/import/query canary also passes.
## Local SQLite-To-Postgres Restore Canary
Before importing into Cloud SQL, prove that the current SQLite backup can be
converted and restored into PostgreSQL without row loss:
```bash
SQLITE_BACKUP=./outputs/gcp-infra-hardening-20260707/private-backups/teleo-pipeline-sqlite-<timestamp>.db.gz \
ops/run_sqlite_postgres_restore_canary.sh
```
The canary:
- generates a PostgreSQL import script with `ops/sqlite_to_postgres_dump.py`;
- recreates a shadow schema in a disposable `postgres:16-alpine` container;
- imports all user tables from the SQLite backup;
- compares source and target row counts for every table;
- writes a proof JSON under `outputs/gcp-infra-hardening-20260707/proofs/`;
- removes only its temporary canary container.
This is a local restore/parity proof, not GCP redundancy by itself. It is the
preflight that should pass before the same generated import is applied through
the approved Cloud SQL connector/VPC path.
To pass this local preflight into a clean GitHub readiness run without uploading
private backup paths, generated SQL, or target-count CSVs, create a redacted
capsule from the proof:
```bash
python3 ops/redact_sqlite_postgres_restore_canary.py \
--proof outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-<timestamp>.json \
--output outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-capsule-<timestamp>.json
```
The capsule keeps only non-secret evidence: proof hash, backup hash, source and
target table/row counts, conversion notes/stats, and the redacted-field list.
It does not prove that Cloud SQL imported the data; it only proves the local
SQLite-to-Postgres parity preflight.
To include the capsule in GitHub readiness:
```bash
CAPSULE_B64="$(base64 < outputs/gcp-infra-hardening-20260707/proofs/sqlite-postgres-restore-canary-capsule-<timestamp>.json | tr -d '\n')"
gh workflow run gcp-readiness.yml \
--repo living-ip/teleo-infrastructure \
--ref main \
-f restore_canary_capsule_b64="${CAPSULE_B64}"
```
## Cloud SQL Restore Drill Runner
Prepare the exact GCS import and Cloud SQL import operation without mutating GCP:
```bash
SQLITE_BACKUP=./outputs/gcp-infra-hardening-20260707/private-backups/teleo-pipeline-sqlite-<timestamp>.db.gz \
ops/run_gcp_cloudsql_restore_drill.sh
```
Execute it only from an authenticated operator environment that can write the
versioned backup bucket and administer the standby Cloud SQL instance:
```bash
EXECUTE=1 \
SQLITE_BACKUP=./outputs/gcp-infra-hardening-20260707/private-backups/teleo-pipeline-sqlite-<timestamp>.db.gz \
ops/run_gcp_cloudsql_restore_drill.sh
```
The runner:
- regenerates the explicit PostgreSQL import script;
- targets the shadow schema `teleo_restore` inside `teleo_kb`;
- uploads the import script to `gs://teleo-501523-prod-backups/kb-dumps/cloudsql-restore-drills/...` when `EXECUTE=1`;
- starts and waits for `gcloud sql import sql`;
- writes `target-counts.sql` for the required trusted VPC/Cloud SQL connector query readback.
The import operation alone is still not the final proof. The final proof needs
`target-counts.sql` run against `teleo-pgvector-standby` and compared to the
source counts in the drill proof.
After the import operation is `DONE`, run the generated count query from a
trusted VPC runtime or Cloud SQL connector path and retain CSV output:
```bash
psql "$TELEO_CLOUDSQL_DATABASE_URL" \
--csv \
-f outputs/gcp-infra-hardening-20260707/private-cloudsql-restore-drills/gcp-cloudsql-restore-drill-<timestamp>/target-counts.sql \
> outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-target-counts-<timestamp>.csv
```
Then compare the Cloud SQL readback to the source proof:
```bash
python3 ops/verify_gcp_cloudsql_restore_readback.py \
--drill-proof outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-restore-drill-<timestamp>.json \
--target-counts-csv outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-target-counts-<timestamp>.csv \
--output outputs/gcp-infra-hardening-20260707/proofs/gcp-cloudsql-restore-readback-verification-<timestamp>.json
```
Only a `status = pass` verifier output is enough for row-count parity. It still
does not prove application cutover or continuous replication.
## Required Proof
A successful restore or replication canary must retain:
- source dataset identity:
- source host or dump artifact;
- dump timestamp or replication slot timestamp;
- source schema/database name.
- transfer proof:
- dump object path in a versioned bucket, or logical replication subscription details;
- row/table counts before import where available.
- target proof:
- `teleo-pgvector-standby` readback;
- `teleo_kb` database readback;
- extension readback for `vector` if the restored schema needs pgvector;
- representative query readback for core KB tables.
- failure boundary:
- exact missing secret, source access, schema incompatibility, extension issue, or import error.
## One-Shot SQLite Export / GCP Restore Path
Use this while the canonical DB remains SQLite on the VPS and we need a GCP restore drill.
1. Run `ops/backup_vps_sqlite_kb.sh`.
2. Upload the resulting SQLite backup and Leo/KB tarball to a versioned GCS bucket such as `gs://teleo-501523-prod-backups/kb-dumps/`.
3. Run the local SQLite-to-Postgres restore canary above and retain its proof.
4. Run `ops/run_gcp_cloudsql_restore_drill.sh` in dry-run mode to generate the GCS import plan.
5. Run `EXECUTE=1 ops/run_gcp_cloudsql_restore_drill.sh` from an authenticated operator environment to upload and import the generated SQL. Do not run blind string rewrites against the SQLite dump.
6. Install required extensions on Cloud SQL:
```sql
create extension if not exists vector;
```
7. From a trusted VPC runtime or Cloud SQL connector path, run readbacks:
```sql
select current_database();
select extname, extversion from pg_extension where extname = 'vector';
select schemaname, tablename from pg_tables where schemaname not in ('pg_catalog', 'information_schema') order by 1, 2 limit 50;
```
8. Retain the SQLite backup hash, GCS object generation, import/conversion operation, query output, and row-count sample.
9. Run `ops/verify_gcp_cloudsql_restore_readback.py` and retain a passing parity proof.
## Logical Replication Path
Use this only if the canonical source becomes Postgres or a Postgres mirror exists. SQLite cannot be logically replicated into Cloud SQL Postgres without an intermediate conversion/sync layer.
Required source privileges:
- replication-capable source user;
- publication over the intended schemas/tables;
- network path from GCP to source, or source-to-GCP path through an approved proxy/tunnel.
Required target steps:
```sql
create extension if not exists vector;
create subscription <subscription_name>
connection '<redacted source connection string>'
publication <publication_name>;
```
Retain only redacted connection metadata. Do not commit or paste credentials.
## Current Blocker
As of this run, GCP has the standby target, a repeatable SQLite/KB export script,
and a local SQLite-to-Postgres restore canary. It does not have an approved GCP
upload/restore credential in the current local session, nor a retained Cloud SQL
import/query proof. That is why the readiness checker still reports:
- `kb_source_restore_access = blocked`
- `kb_restore_or_replication = blocked`
The next real canary is:
source SQLite/KB backup -> upload to versioned GCS bucket -> convert/import into `teleo_kb` -> install/verify `vector` if needed -> run representative KB queries on GCP -> retain proof.

View file

@ -0,0 +1,236 @@
# LLM Refinement And Decision Engine Program
Created: 2026-06-01
Status: active direction
## Product Outcome
The decision engine should become the best judgment layer for Living IP: it routes knowledge changes to the right agent identities, tests competing LLMs against the same rubric, learns from disagreement, and improves prompts/tools only when measured deltas prove the change.
Pentagon.run should own disposable infrastructure and remote execution. This repo should own decision quality: rubrics, prompts, model selection, route evidence, database feedback loops, and agent tool packages.
## What Rio And Theseus Become
### Rio
Rio becomes the economic and incentive-quality evaluator.
Rio owns:
- contribution weights and role economics;
- paid-query effects and anti-pay-to-pollute rules;
- market, mechanism, futarchy, x402, token, and capital-formation reasoning;
- source-diversity and correlated-prior warnings;
- OPSEC for finance, deal terms, token economics, and internal allocations;
- model tests that expose weak economic reasoning.
Rio should not be "the crypto agent". Rio should be the agent that asks whether the system's incentives create useful knowledge or garbage incentives.
### Theseus
Theseus becomes the model-integrity and agent-refinement evaluator.
Theseus owns:
- model diversity and correlated-blind-spot measurement;
- adversarial eval rubrics;
- prompt/tool safety and self-upgrade criteria;
- disagreement queues and verifier-divergence analysis;
- LLM capability evidence and agent-system architecture;
- tests that expose hallucinated certainty, weak causal claims, and prompt-injection fragility.
Theseus should not be "the AI safety agent". Theseus should be the agent that asks whether the decision system can be trusted when the models are persuasive but wrong.
## Decision Engine Loop
```mermaid
flowchart TD
PR["Decision-engine PR or source record"] --> Route["Deterministic route evidence"]
Route --> Reviewers["Required agent reviewers"]
Reviewers --> Rubric["Shared rubric"]
Rubric --> ModelA["Primary model"]
Rubric --> ModelB["Independent model family"]
ModelA --> Verdicts["Structured verdicts"]
ModelB --> Verdicts
Verdicts --> Disagree{"Disagreement?"}
Disagree -->|yes| Queue["Disagreement queue"]
Disagree -->|no| Metrics["Calibration metrics"]
Queue --> HumanOrLeo["Leo or human arbitration"]
HumanOrLeo --> Metrics
Metrics --> DB["SQLite feedback state"]
DB --> Refine["Prompt, tool, or model proposal"]
Refine --> Delta["Before/after eval harness"]
Delta -->|passes| Update["Commit refinement"]
Delta -->|fails| Archive["Archive failed refinement"]
```
## Model Portfolio
The goal is not to pick one favorite model. The goal is to assign models to failure modes.
| Lane | Primary evaluator | Independent check | Why |
| --- | --- | --- | --- |
| Fast triage | cheap small model | deterministic route evidence | triage should be cheap and overridable |
| Domain review | routed agent prompt | different model family | catch domain-specific errors without same-family agreement bias |
| Deep review | strongest available reasoning model | non-Claude or non-primary family | deep review is for structural claims and disagreement |
| Economic reasoning | Rio rubric | model with strong quantitative/mechanism reasoning | tests incentive design, paid-query effects, and contribution weights |
| Agent/refinement safety | Theseus rubric | model with strong adversarial critique | tests tool safety, self-upgrades, and evaluator drift |
Candidate models should enter only through a harness:
1. fixed input set;
2. fixed rubric;
3. structured verdict JSON;
4. cost and latency recorded;
5. disagreement categories stored;
6. before/after comparison against current baseline.
No model switch is accepted because it "sounds better" on one example.
## Refinement Workstreams
### R0: Model Discovery Registry
Create a registry before arguing about model preference. The registry should track:
- hosted frontier models;
- open-weight Hugging Face candidates;
- local or edge candidates;
- small, cheap triage models;
- larger reasoning models, including future in-house or 27B-class candidates;
- license, hardware, context, latency, cost, tool support, and known failure modes.
The registry does not bless a model. It decides which model deserves a bakeoff fixture.
### R1: Rubric Packets
Create a small rubric packet for each evaluator role:
- `rio-economics-rubric`
- `theseus-model-integrity-rubric`
- `leo-cross-domain-rubric`
- domain-specific factuality rubrics
Each packet must define allowed verdicts, rejection tags, must-check criteria, and examples of false positives.
### R2: Evaluation Corpus
Build a replayable corpus from existing PRs:
- approved clean PRs;
- rejected PRs by issue tag;
- Rio/Theseus cross-domain PRs;
- paid-query or contribution-weight examples;
- adversarial malformed claims;
- near-duplicate and OPSEC edge cases.
Use local fixture data first. Production DB sampling requires the DB operator skill.
### R3: Model Bakeoff
Run each candidate model against the same corpus and emit:
- accuracy against expected disposition;
- false-approve count;
- false-reject count;
- issue-tag precision;
- average latency;
- estimated cost;
- disagreement matrix by model pair.
The highest-signal metric is not raw approval rate. It is false approvals on bad claims plus useful disagreement on ambiguous claims.
### R4: Feedback Loop
Use `review_records`, `audit_log`, `costs`, and PR state to find:
- recurring model failure categories;
- agents with repeated same-tag rejections;
- prompts that produce vague reviews;
- cost spikes without quality gain;
- routes that keep requiring manual override.
Every prompt/tool change should include a before/after proof over this loop.
### R5: Agent Runtime Packages
Package the same decision-engine contract for:
- NousResearch Hermes Agent: skill/memory/model-switching oriented.
- OpenClaw: workspace skill plus `AGENTS.md`, `SOUL.md`, `TOOLS.md` oriented.
- Claude-style, Pentagon, or other persistent agents: skill-oriented knowledge-base read/write interop.
Both packages should be fixture-first and no-secret by default. They are distribution surfaces for the decision engine, not separate evaluators with their own truth.
### R6: Knowledge-Base Interop
Any Hermes, OpenClaw, or Claude-style agent should be able to read information from the Living IP knowledge base and propose writes back into it.
The contract is:
- read through deterministic search, claim indexes, copied SQLite state, or cited repo files;
- propose source, claim, entity, correction, and route artifacts;
- never write directly to main;
- never mutate production `pipeline.db` from a model response;
- leave proof showing the exact query, cited reads, proposed write, and route evidence.
Use `.agents/skills/living-ip-kb-interop/SKILL.md` for runtime-neutral KB access, and `.agents/skills/teleo-db-operator/SKILL.md` for SQLite-specific work.
## DB Usage Boundary
Default is read-only.
Writes are allowed only when all are true:
- the target DB is local, staging, or explicitly authorized production;
- a backup or copy exists;
- the write is wrapped in a transaction;
- the exact query is retained in a proof artifact;
- the post-write readback is retained.
Never let an agent tune prompts by mutating production state directly.
## Pentagon.run Boundary
Pentagon.run should own:
- disposable VPS setup;
- Crabbox or remote proof execution;
- Hetzner lifecycle;
- runner cleanup;
- infra receipts.
- persistent agent teammates, company-brain infrastructure, and agent-to-agent transport when that is their managed stack.
This repo should own:
- decision-engine quality;
- model and prompt experiments;
- agent skills and adapter handoffs;
- database feedback analysis;
- proof schemas for eval quality.
Raw cards and secrets are not agent runtime inputs. Human operators may decide vendor billing and spend policy, but repo artifacts should only name secret slots, scoped tokens, spend limits, receipts, and setup checklists.
## Transcript-Derived Requirements
The 2026-06-01 working transcript adds these requirements:
- LLM/refinement work should focus on model discovery, compression, context strategy, and decision-engine quality while Pentagon handles cloud/persistent-agent infrastructure.
- Rio should be the first place to route Meteora, LP, x402, futarchy, paid-query, and contribution-incentive questions.
- Theseus should own the skill/MCP/refinement path that makes model judgment portable across Hermes, OpenClaw, Claude-style agents, and Pentagon-style company brains.
- The knowledge-writing path should turn large founder/source corpora into structured, reviewable knowledge packets, not shallow summaries.
- Slack, Linear, email, billing, and provider accounts are external collaboration setup. They should unblock people, but they are not prerequisites for local fixture, rubric, and proof work.
## Next Implementation Slice
1. Add `docs/model-discovery-registry.md`.
2. Add `scripts/replay_decision_engine_eval.py` with local fixture mode.
3. Add `fixtures/decision-engine-eval/*.json`.
4. Store verdict outputs in `.crabbox-results/decision-engine-eval.json`.
5. Add one Rio economics fixture and one Theseus model-integrity fixture.
6. Add one KB interop fixture that searches existing context and proposes a write without touching main or production DB.
7. Compare current prompt versus one candidate prompt before touching runtime prompts.
Do not start by changing live model assignments.
Run `python3 scripts/replay_decision_engine_eval.py` after changing fixture, rubric, registry, or candidate-output formats.

View file

@ -0,0 +1,75 @@
# Model Discovery Registry
Created: 2026-06-01
Status: candidate registry, not model approval
This registry exists to decide which models deserve a Living IP bakeoff fixture. It does not choose production models and it does not replace measured replay results.
## Rules
- Use official provider docs, model cards, or source repositories for every entry.
- Treat all model specs, prices, context limits, and aliases as volatile.
- Do not switch runtime model assignments from this document alone.
- Promote a model only after `scripts/replay_decision_engine_eval.py` shows no critical regression on the same fixture set.
- Prefer different model families for independent review so agreement is not just same-family correlation.
## Candidate Matrix
| Candidate | Surface | Why It Is Worth Testing | First Living IP Lane | Source |
| --- | --- | --- | --- | --- |
| GPT-5.5 / GPT-5.4 family | Hosted API | Strong general reasoning and agentic task baseline; useful as a frontier comparison point. | deep review, Leo arbitration | [OpenAI models](https://platform.openai.com/docs/models) |
| GPT-5 lower-latency variants | Hosted API | Possible cheap triage candidates; exact model IDs must be re-verified before a bakeoff run. | fast triage | [OpenAI models](https://platform.openai.com/docs/models) |
| gpt-oss-120b | Open-weight | Open-weight reasoning candidate for on-prem or Pentagon-managed inference; needs hardware/cost proof. | Theseus model integrity | [OpenAI open models](https://openai.com/open-models/) |
| gpt-oss-20b | Open-weight | Smaller local/edge candidate for cheap first-pass triage and portable demos. | fast triage, local harness | [OpenAI open models](https://openai.com/open-models/) |
| Claude Opus 4.8 | Hosted API | Complex-reasoning candidate for highest-stakes arbitration. | Leo arbitration, deep review | [Anthropic models overview](https://docs.anthropic.com/en/docs/about-claude/models) |
| Claude Sonnet 4.6 | Hosted API | Speed/intelligence tradeoff candidate for domain review. | domain review | [Anthropic models overview](https://docs.anthropic.com/en/docs/about-claude/models) |
| Claude Haiku 4.5 | Hosted API | Low-latency candidate for cheap reviewer pre-checks. | fast triage | [Anthropic models overview](https://docs.anthropic.com/en/docs/about-claude/models) |
| Gemini 3.5 Flash | Hosted API | Agentic/coding-oriented candidate from a different model family. | independent second review | [Gemini API models](https://ai.google.dev/gemini-api/docs/models) |
| Gemini 3.1 Pro | Hosted API | Complex problem-solving candidate from a non-primary model family. | deep review | [Gemini API models](https://ai.google.dev/gemini-api/docs/models) |
| Mistral Medium 3.5 | Hosted or open surface per provider docs | Agentic/coding candidate with a non-US-primary model family. | independent second review | [Mistral models overview](https://docs.mistral.ai/getting-started/models/) |
| Mistral Small 4 | Hosted or open surface per provider docs | Efficient hybrid instruct/reasoning/coding candidate. | fast triage, domain review | [Mistral models overview](https://docs.mistral.ai/getting-started/models/) |
| Mistral Large 3 | Open-weight | Large open-weight comparison point for self-hosted evaluation. | deep review | [Mistral models overview](https://docs.mistral.ai/getting-started/models/) |
| Devstral 2 | Hosted or open surface per provider docs | Code-agent candidate for tools, repository work, and adapter tasks. | Theseus tool integrity | [Mistral models overview](https://docs.mistral.ai/getting-started/models/) |
| Hermes 4 70B | Open-weight / provider-hosted | Nous-aligned model with structured output and tool-use relevance for Hermes Agent packaging. | Hermes adapter, Theseus | [NousResearch Hermes 4 70B](https://huggingface.co/NousResearch/Hermes-4-70B) |
| Qwen3.5 9B | Open-weight | Small multimodal/open-weight candidate for local and edge experiments. | fast triage, local harness | [Qwen3.5 9B model card](https://huggingface.co/Qwen/Qwen3.5-9B) |
## Bakeoff Intake Fields
Each candidate needs a retained record before a real bakeoff:
- provider or local runtime;
- exact model ID or pinned snapshot;
- source URL;
- license or terms surface;
- context window and max output if verified;
- structured-output support;
- tool/function calling support;
- expected hardware or hosted cost;
- latency estimate;
- privacy and data-retention posture;
- failure mode hypothesis;
- first fixture lane.
## First Bakeoff Order
1. Cheap triage: exact-ID-verified GPT-5 lower-latency variant, Claude Haiku 4.5, Mistral Small 4, Qwen3.5 9B, gpt-oss-20b.
2. Theseus integrity: Gemini 3.5 Flash, Hermes 4 70B, Devstral 2, gpt-oss-120b.
3. Rio economics: GPT-5.5/5.4, Claude Sonnet 4.6, Gemini 3.1 Pro, Mistral Medium 3.5.
4. Deep arbitration: Claude Opus 4.8, GPT-5.5, Gemini 3.1 Pro, Mistral Large 3.
## Promotion Gate
A model can move from registry to runtime proposal only if the replay proof includes:
- exact model ID;
- fixture count;
- route accuracy;
- false approvals;
- false rejects;
- missing required issue tags;
- average latency;
- cost estimate;
- disagreement matrix against current baseline;
- one paragraph explaining why the observed disagreements are useful.
Zero false approvals on known-bad fixtures is a hard gate for evaluator roles.

View file

@ -0,0 +1,996 @@
# Phase 1b Agent Routing Spec
Created: 2026-05-29
Status: active draft
Owner: Epimetheus pipeline implementation, with m3taversal as scope owner and Fwaz as VPS/runtime owner
## Product Outcome Contract
Phase 1b makes the knowledge-base evaluation engine behave like a six-agent review system instead of a generic triage stack.
When a contribution changes the `decision-engine` KB, the pipeline must decide which Hermes agent identity is responsible for judging that change, run the required review or reviews, post agent-specific verdicts, and then let the existing merge or feedback machinery continue.
The user-visible outcome is not a new frontend. It is a PR review trail showing that the right agent or agents reviewed the right KB mutation.
## Non-Goals
This spec does not implement:
- Twitter/X posting.
- x402, wallet, payment, or funding flows.
- Decision markets, agent bidding, stake-weighted quorum, or prediction-market review.
- Full general user-input routing outside the PR evaluation path.
- Separate GitHub accounts for each agent.
- A full Forgejo-to-GitHub daemon rewrite beyond what Phase 1b needs.
- A dashboard redesign.
- Production deployment without staging or VPS proof.
## Program Decomposition
This is a medium-sized control-plane change with five execution lanes:
1. Agent identity routing.
2. Eval pipeline integration.
3. GitHub identity and bot comment posture.
4. Reporting and contributor compatibility.
5. Staging and production proof.
The implementation can remain in one PR only if lanes 1 through 4 are tightly tested and the staging proof remains a separate operator task. If the eval integration diff grows beyond the files named in this spec, split into:
- PR 1: route contract and tests.
- PR 2: eval integration and mocked state tests.
- PR 3: GitHub/comment idempotency and reporting compatibility.
- PR 4 or operator runbook: staging proof artifacts.
Child specs:
- `docs/phase1b/agent-identity-router-spec.md`
- `docs/phase1b/eval-pipeline-integration-spec.md`
- `docs/phase1b/github-identity-bot-posture-spec.md`
- `docs/phase1b/reporting-contributor-compatibility-spec.md`
- `docs/phase1b/staging-proof-spec.md`
## Priority Matrix
| Rank | Workstream | Recurrence | Value | Readiness | Current state | Issue/spec mapping | Thread-claimed status | Verified implementation/proof status | Recommended next move |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | Canonical repo and eval target | Repeated confusion between `teleo-codex`, `teleo-kb`, and `decision-engine`. | Critical | Ready now | Confirmed by user: `decision-engine`. Some code still has Forgejo/teleo-codex defaults. | This spec, `handoff/phase1-step3-script-migration.md` | Clarified in chat. | Partially reflected in repo; not unified in daemon modules. | Make Phase 1b route/proof explicitly target `decision-engine`. |
| 2 | Agent identity routing | Repeated confusion between domain folders and agent ownership. | Critical | Ready now | Existing `lib/domains.py` is folder-first. | This spec | m3taversal clarified identity-first routing. | Initial local patch is insufficient. | Replace with identity-scored route contract. |
| 3 | Cross-domain review | Raised as scope expansion during clarification. | High | Ready now | Not implemented. | This spec | m3taversal confirmed cap at top 2. | No code proof. | Add top-2 required reviewer aggregation. |
| 4 | Single master bot account | GitHub bot/PAT issue was noted as blocker. | High | Ready now | Phase 1 handoff already documents single `livingIPbot` posture. | `handoff/phase1-step3-script-migration.md` | Separate identities ideal, likely too complex. | Handoff-only. | Use master bot comments with agent verdict tags. |
| 5 | Staging proof | User asked how to test without mutating prod VPS. | Critical for production | Draft gated | Needs VPS clone or Crabbox/staging access. | This spec | Proposed, not executed. | No proof. | Run after code PR passes local checks. |
## Goal
Implement Phase 1b for the `decision-engine` knowledge base: pipeline-v2 evaluates each incoming KB pull request by routing it to the Hermes agent identity that owns the relevant domain of judgment.
The implementation lives in `teleo-infrastructure`. The canonical KB repo for this phase is `living-ip/decision-engine`.
Phase 1b is complete only when single-domain and cross-domain PRs are routed to the expected required reviewer agents, verdicts are posted in the existing `VERDICT:AGENT:*` format, and the merge or feedback path continues from those verdicts.
## User-Journey Contract
Contributor or agent flow:
1. A contributor or agent opens a PR against `living-ip/decision-engine`.
2. The PR changes one or more KB files.
3. Pipeline-v2 discovers the PR and fetches its diff.
4. The router scores Hermes agent identities from the diff, file paths, branch metadata, and eventually PR metadata.
5. The pipeline runs the required reviewer agents.
6. The master bot posts verdict comments that clearly name the agent identity in `VERDICT:AGENT:*` tags.
7. If all required reviewers approve, the existing approval and merge path continues.
8. If any required reviewer requests changes, the existing feedback/retry path continues.
Operator flow:
1. Operator can inspect a PR and see why each agent was selected.
2. Operator can inspect pipeline logs or audit rows and see route scores, required agents, verdicts, and aggregate result.
3. Operator can distinguish local proof, staging proof, and production proof.
## Existing-Spec Inventory
| Existing doc | Relevance | Decision | Reason |
| --- | --- | --- | --- |
| `handoff/phase1-step3-script-migration.md` | Establishes the Phase 1 move from Forgejo `teleo-codex` toward GitHub `living-ip/decision-engine`, and documents the single master bot account posture. | Reuse as context. | It owns migration history, not the Phase 1b routing implementation. |
| `handoff/deprecated/eval-scripts.md` | Confirms old eval dispatcher/worker scripts are dead and `lib/evaluate.py::evaluate_cycle` owns live eval behavior. | Reuse as context. | It prevents work from targeting retired scripts. |
| `docs/ARCHITECTURE.md` | Describes pipeline-v2 stages, SQLite state, Forgejo-era runtime topology, and existing evaluate/merge loops. | Reuse as context. | It is broader architecture; this spec is a Phase 1b delta spec. |
| `docs/multi-model-eval-architecture.md` | Documents the prior Leo-first plus second-model evaluation theory. | Supersede for Phase 1b eval routing only. | Phase 1b now routes to domain-owner agent identities, with capped top-2 cross-domain review. The old doc remains useful for later calibration. |
| `docs/queue.md` | Mentions domain evolution such as `ai-alignment` to `ai-systems`. | Reuse as signal. | It supports the identity-scored router rather than folder-only routing. |
## Current Implementation Audit
Current relevant implementation state:
- `teleo-pipeline.py` runs pipeline-v2 as a single async daemon.
- `lib/evaluate.py::evaluate_cycle` is the active eval loop.
- `lib/evaluate.py::evaluate_pr` currently detects a domain, runs a domain review, then runs Leo review for non-LIGHT PRs.
- `lib/domains.py` contains a folder-first `DOMAIN_AGENT_MAP`.
- `lib/llm.py` contains prompt templates and `run_domain_review`, `run_batch_domain_review`, and `run_leo_review`.
- `lib/eval_parse.py::parse_verdict` parses `VERDICT:AGENT:APPROVE` and `VERDICT:AGENT:REQUEST_CHANGES`.
- `pipeline-health-check.py` is GitHub-oriented and points at `living-ip/decision-engine`.
- `lib/forgejo.py`, `lib/evaluate.py`, and `lib/merge.py` still use Forgejo-named abstractions as the primary API surface.
- Per-agent GitHub identity is deferred; Phase 1 uses one master bot account.
Fwaz clarification on 2026-05-29:
- Separate GitHub identities are still ideal and blocked on GitHub/PAT setup; Phase 1b must not require them to land the routed-eval path.
- Current production update behavior is `pull -> services recognize pull -> edit on VPS -> PR to Leo`; this is useful context, not the desired long-term control model.
- New desired rule is no direct production self-upgrades: agents open PRs, and production deploys exact reviewed/tested SHAs approved and signed by Leo.
- Crabbox is acceptable as the long-term disposable staging/test-box direction, while a production-like clone remains the highest-fidelity proof for systemd/VPS paths.
This branch implementation now includes:
- `lib/agent_routing.py` with a pure identity-scored route contract.
- `PHASE1B_AGENT_ROUTING_ENABLED`, defaulting off.
- A Phase 1b eval path that runs routed required agents and disables stale domain batching under the flag.
- Focused tests for six-agent routing, top-2 cross-domain routing, verdict parsing, and mocked eval aggregation.
## Goal-Vs-Repo-Truth Diff
Desired Phase 1b behavior:
- Route PRs against `decision-engine`, not `teleo-codex`.
- Classify by agent identity ownership, not only by folder path.
- Run exactly the required reviewer agents.
- Use one master bot account if separate GitHub identities are too complex.
- Preserve the existing verdict comment format.
- Preserve existing merge and feedback behavior.
- Support cross-domain PRs by requiring the top 2 routed agents.
Pre-implementation repo truth:
- Pipeline eval still has a two-stage review shape: domain review plus Leo review.
- Folder-domain mapping exists, but agent identity scoring does not.
- Cross-domain review is not implemented as multiple required reviewer agents.
- Batch eval can group rows before fetching diffs, which risks routing unclassified rows through `general`.
- GitHub migration is partial: some scripts target GitHub `decision-engine`, but live daemon modules still have Forgejo-era names and assumptions.
## Completion Percent And Remaining Delta
Estimated implementation progress on this branch:
- B1 classifier foundation: 100 percent locally, pending staging calibration.
- B2 routing layer: 75 percent locally behind a default-off feature flag.
- Cross-domain top-2 review: 75 percent locally through mocked eval proof.
- Local proof suite: 85 percent for router/eval/parser scope.
- Staging or VPS proof: 0 percent.
Remaining delta:
1. Decide whether the production Phase 1b transport stays Forgejo-first for cutover or switches direct to GitHub `decision-engine` before staging.
2. Update reporting/health compatibility beyond `review_records` if staging shows false readiness.
3. Prove against staging before production.
4. Deploy only an exact reviewed/tested SHA after Leo signoff.
## Closure, Endpoint, And Deployment Truth
Local closure means:
- Focused tests pass in `teleo-infrastructure`.
- A PR exists with the Phase 1b routing implementation and proof notes.
Staging closure means:
- A cloned or disposable staging runtime is pointed at a sandbox `decision-engine`.
- Six single-domain sandbox PRs and one cross-domain sandbox PR complete the expected eval path.
- A machine-readable proof artifact captures routes, required agents, verdicts, status transitions, git SHAs, and logs.
Production closure means:
- The exact reviewed SHA is deployed to the production VPS.
- Production pipeline runs real `decision-engine` PRs through Phase 1b routing.
- All six agents have completed at least one live review cycle.
- Pipeline remains stable for at least 24 hours after cutover.
Without VPS or staging access, only local closure can be claimed.
## Critical Assumptions And Invalidators
Assumptions:
- `decision-engine` is the canonical KB repo for Phase 1b.
- The active eval implementation is `teleo-infrastructure/lib/evaluate.py`, not retired shell scripts.
- One master bot account is acceptable for Phase 1b verdict comments.
- Required reviewer identity is encoded in the verdict tag, not necessarily in the GitHub account identity.
- Agent state files in `decision-engine/agents/{agent}` are the right identity context source when present.
Invalidators:
- Production pipeline is still wired to a different canonical repo.
- The VPS runs code not represented by current `teleo-infrastructure`.
- Branch protection requires separate GitHub identities before comments or reviews count.
- Agent identity files are absent or materially different on the VPS.
- Cross-domain review must include more than top 2 reviewers.
## State And Truth Contract
The routing implementation must record or expose:
- PR number.
- Primary agent.
- Required agents.
- Route kind: `single`, `multi`, or `escalated`.
- Route scores by agent.
- Route evidence: path, branch, title, diff keyword, or fallback.
- Verdict per required agent.
- Aggregate result.
- Failure reason for missing or unparseable verdicts.
This can be stored first in audit log details and test artifacts. A DB schema migration is optional for Phase 1b unless downstream dashboards require queryable route fields.
### Route Decision Schema
The route decision should be serializable without importing Python classes. Use this JSON shape in audit rows and proof artifacts:
```json
{
"pr": 123,
"repo": "living-ip/decision-engine",
"route_version": "phase1b-v1",
"route_kind": "single",
"primary_agent": "Rio",
"required_agents": ["Rio"],
"scores": {
"Leo": 0,
"Theseus": 1,
"Rio": 9,
"Vida": 0,
"Clay": 0,
"Astra": 0
},
"evidence": [
{
"agent": "Rio",
"signal": "path",
"weight": 5,
"value": "domains/internet-finance/example.md"
}
],
"fallback": false
}
```
`route_kind` values:
- `single`: one required reviewer.
- `multi`: two required reviewers from cross-domain scoring.
- `fallback`: no confident route, Leo required.
- `escalated`: route exceeded simple review bounds and was capped by policy.
### Verdict State Schema
Aggregate review state should be serializable as:
```json
{
"pr": 123,
"required_agents": ["Theseus", "Rio"],
"agent_verdicts": {
"Theseus": "approve",
"Rio": "request_changes"
},
"aggregate_verdict": "request_changes",
"blocking_agents": ["Rio"],
"missing_agents": [],
"unparseable_agents": [],
"transport_failed_agents": []
}
```
Aggregate states:
- `approve`: all required agents approved.
- `request_changes`: at least one required agent requested changes or produced unparseable content.
- `retry`: at least one required review failed for transport reasons and should not burn the PR as a substantive rejection.
## Measurement Contract
Minimum metrics:
- `route_single_count`
- `route_multi_count`
- `route_escalated_count`
- `review_required_agent_count`
- `review_missing_verdict_count`
- `review_request_changes_count`
- `review_approve_count`
- `route_fallback_count`
Minimum proof matrix:
| Case | Expected route |
| --- | --- |
| grand strategy PR | Leo |
| ai systems or ai alignment PR | Theseus |
| internet finance or x402 PR | Rio |
| health PR | Vida |
| entertainment PR | Clay |
| space, robotics, energy, or advanced manufacturing PR | Astra |
| ai plus x402 PR | Theseus and Rio |
| collective ai goals PR | Leo and Theseus, if both score in top 2 |
## Score-To-100 Closure Plan
Preparedness score before implementation: 35/100.
| Score band | Closure move | Evidence that moves score |
| --- | --- | --- |
| 35 -> 50 | Route contract implemented and unit-tested. | `test_agent_routing.py` proves six single-agent routes, broadened identity ownership, top-2 cross-domain routes, and fallback behavior. |
| 50 -> 65 | Eval integration mocked locally. | Mocked eval tests prove required agents are invoked, default Leo review is removed, and aggregate verdicts drive approve/request-changes behavior. |
| 65 -> 75 | API/comment compatibility proven locally. | Tests prove all six verdict tags parse and master-bot comment bodies preserve existing parser expectations. |
| 75 -> 85 | Staging clone or disposable test box runs sandbox PR proof. | Six single-domain sandbox PRs plus one cross-domain sandbox PR produce expected comments and state transitions. |
| 85 -> 95 | Production deploy of exact reviewed SHA. | VPS deploy log, service restart readback, and route/proof artifact for first real PRs. |
| 95 -> 100 | 24-hour production stability. | 24-hour daemon readback with no duplicate comments, no stuck review rows, no production fallback spike, and all six agents represented in verdict history. |
The implementation PR can be merged at 65-75 if reviewers accept staging as a deploy gate. It cannot claim Phase 1b complete below 100.
## Backend Work Required
### 1. Agent identity router
Create or refactor into `lib/agent_routing.py` unless the existing `lib/domains.py` remains clearly small enough.
Define:
```python
AgentRoute(
primary_agent: str,
required_agents: tuple[str, ...],
route_kind: str,
scores: dict[str, int],
evidence: list[dict],
)
```
Router signals:
- Path signals from `domains/`, `entities/`, `core/`, `foundations/`, and `agents/`.
- Branch prefix signals such as `rio/`, `theseus/`, `astra/`, `leo/`.
- Keyword signals from path, filename, branch, PR title/body when available, and capped diff text.
- Agent identity ownership map.
Agent identity ownership map:
| Agent | Owns |
| --- | --- |
| Leo | grand strategy, teleohumanity goals, collective AI self-understanding, meta strategy, nested collective intelligence concepts |
| Theseus | AI systems, AI alignment, AI governance, agent systems, safety, evaluation |
| Rio | internet finance, living capital, markets, crypto, futarchy, x402, payments, capital formation |
| Vida | health, healthcare, medicine, prevention, clinical systems, mental health, biohealth |
| Clay | entertainment, media, culture, IP, fandom, narrative, consumer attention |
| Astra | space development, robotics, energy, advanced manufacturing, physical frontier infrastructure |
Routing rules:
- If only one agent crosses the threshold, require that agent.
- If more than one agent crosses the threshold, require the top 2 agents.
- If no agent crosses threshold, fallback to Leo with route kind `fallback`.
- Tie break by score, then deterministic configured order.
Implementation constraints:
- The router must be deterministic.
- The router must be pure and side-effect free.
- Route scores must be explainable through evidence entries.
- Folder paths should be strong evidence, not the whole classifier.
- Keyword scoring must not require paid inference.
- LLM classification may be added later only as shadow-mode evidence.
Recommended scoring starter:
| Signal | Weight |
| --- | --- |
| Path directly under known primary ownership area | 8 |
| Path under broadened ownership area | 6 |
| Branch prefix matches agent | 4 |
| Filename keyword matches ownership | 3 |
| Diff keyword matches ownership | 1 per capped hit |
| PR title/body keyword matches ownership, if available | 2 |
Top-2 selection:
- Include the highest-scoring agent.
- Include a second agent only if its score is at least 40 percent of the first score and at least the minimum threshold.
- Minimum threshold starts at 4.
- Never include more than two required agents in Phase 1b.
### 2. Eval layer integration
Modify `lib/evaluate.py`:
- Fetch PR diff.
- Build route from diff and branch.
- Store or audit route decision.
- Run required reviewer agents.
- Aggregate verdicts.
- Remove default Leo second-review for normal single-agent PRs.
- Keep existing bypasses for musings and reweave unless m3taversal changes policy.
- Revisit batch eval: disable batching for Phase 1b or classify before batching.
Implementation sequence:
1. Add pure route builder and tests.
2. Add review aggregation helper and tests.
3. Add `run_agent_review` while leaving existing `run_domain_review` and `run_leo_review` intact.
4. Switch individual `evaluate_pr` path to the new router behind a feature flag such as `PHASE1B_AGENT_ROUTING_ENABLED`.
5. Disable batch domain eval when the feature flag is enabled unless route-aware batching is implemented in the same PR.
6. Remove or bypass the default Leo second-review when the feature flag is enabled.
7. Preserve old behavior when the feature flag is disabled.
Feature flag requirement:
```text
PHASE1B_AGENT_ROUTING_ENABLED=false by default until staging proof exists.
```
The PR may set tests against enabled behavior without changing the production default.
### 3. Agent review runner
Modify or add in `lib/llm.py`:
```python
async def run_agent_review(diff: str, files: str, agent: str, route: AgentRoute) -> tuple[str | None, dict]:
...
```
Prompt must include:
- Agent identity context when available.
- Route evidence.
- Existing eval criteria.
- Required verdict tag for that exact agent.
Continue using one master bot account for comments. The bot comment body must identify the routed agent via the verdict tag.
Agent context lookup order:
1. Runtime-configured KB worktree path, expected to point at `decision-engine`.
2. Existing `config.MAIN_WORKTREE` if production still uses that convention.
3. Explicit test fixture path in unit tests.
Context files:
- `agents/{agent}/identity.md`
- `agents/{agent}/beliefs.md`
- `agents/{agent}/reasoning.md`
- `agents/{agent}/skills.md`
Missing context files:
- Log a warning.
- Include an audit evidence entry.
- Continue with the generic agent prompt.
- Do not crash the eval cycle.
### 4. Verdict aggregation
Add helper:
```python
aggregate_agent_verdicts(required_agents, reviews) -> AggregateVerdict
```
Rules:
- All required agents approve: approved.
- Any required agent requests changes: request changes.
- Transport failure: reopen for retry.
- Missing or unparseable verdict: request changes unless transport failure is explicit.
Comment format:
Preferred for one required agent:
```text
<review text>
<!-- VERDICT:RIO:APPROVE -->
```
Preferred for two required agents:
```text
## Theseus review
<review text>
<!-- VERDICT:THESEUS:APPROVE -->
## Rio review
<review text>
<!-- VERDICT:RIO:REQUEST_CHANGES -->
```
Two separate comments are acceptable if simpler and less risky for existing parsers.
### 5. Contributor and dashboard compatibility
Audit and update:
- `lib/contributor.py` assumptions that Leo reviews every PR.
- `pipeline-health-check.py` verdict parsing if needed.
- Any dashboard code assuming only `leo_verdict` plus `domain_verdict`.
Avoid broad dashboard redesign in Phase 1b. If dashboards need richer route state, add an audit artifact first and defer UI.
## Frontend Work Required
No frontend work is required for Phase 1b.
`livingip-web` Phase 1c can later reuse the same router as pre-PR guidance, but Phase 1b acceptance is based on `decision-engine` PR evaluation.
## Operator Work Required
Operator or infrastructure owner must provide before production proof:
- Current production deployed SHA for `teleo-infrastructure`.
- Current production KB target and worktree path.
- Current systemd units and restart commands.
- Staging clone or disposable test runner access.
- Sandbox `decision-engine` target or clear permission to create one.
- Staging token set with no production mutation authority.
- Rollback SHA and rollback command.
If these are unavailable, implementation can continue locally but production proof must remain blocked.
## Expected Runtime And User-Visible Behavior
Single-domain PR:
1. Pipeline detects route.
2. Required agents has one name.
3. Master bot posts one review comment with `VERDICT:AGENT:*`.
4. Existing merge or feedback path continues.
Cross-domain PR:
1. Pipeline detects route.
2. Required agents has two names.
3. Master bot posts one review comment per required agent, or one structured comment with separate verdict sections if that is simpler.
4. Merge requires both approvals.
5. Any request changes blocks and feeds back.
The user-visible proof is PR comments and final PR disposition.
## Staging Proof Contract
Staging must be production-like enough to test pipeline behavior but quarantined from production side effects.
Required staging safety controls:
- Production services disabled before any daemon starts.
- Production GitHub tokens removed or replaced.
- Production OpenRouter/Claude/Hermes keys removed or replaced unless explicitly approved for staging spend.
- Sandbox `decision-engine` repo configured.
- Auto-merge either disabled or constrained to sandbox repo.
- Hostname clearly changed to staging.
Required proof artifact:
```json
{
"phase": "1b",
"environment": "staging",
"teleo_infrastructure_sha": "...",
"decision_engine_sha": "...",
"pipeline_db_schema": 26,
"feature_flags": {
"PHASE1B_AGENT_ROUTING_ENABLED": "true"
},
"test_prs": [
{
"case": "internet-finance",
"pr": 1,
"required_agents": ["Rio"],
"verdicts": {"Rio": "approve"},
"final_state": "approved"
}
],
"cross_domain_pr": {
"required_agents": ["Theseus", "Rio"],
"final_state": "approved_or_feedback"
},
"prod_services_disabled": true,
"proof_generated_at": "2026-05-29T00:00:00Z"
}
```
Staging proof does not satisfy the 24-hour production stability gate.
## Validation And Test Matrix
Unit tests:
- `test_agent_routing.py`
- routes six primary ownership cases.
- routes broadened Astra cases: energy, robotics, advanced manufacturing.
- routes Leo meta cases: collective AI goals, teleohumanity strategy.
- routes Theseus AI systems cases.
- routes Rio x402 and internet finance cases.
- caps cross-domain to top 2 agents.
- has deterministic tie breaking.
Parser tests:
- Existing `test_eval_parse.py` remains valid.
- Add explicit verdict parse coverage for all six agent names.
Mocked eval integration tests:
- One required agent calls one runner and posts one verdict.
- Two required agents call two runners and post two verdicts.
- One request changes blocks aggregate approval.
- Transport failure reopens for retry.
- Default Leo second-review does not run unless Leo is routed.
Batch tests:
- If batching remains enabled, batch grouping must use route decisions, not stale DB domain.
- If batching is disabled for Phase 1b, assert cross-domain and single-domain PRs still process individually.
Smoke commands:
```bash
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install 'aiohttp>=3.9,<4' 'pytest>=8' 'pytest-asyncio>=0.23' 'ruff>=0.3' pyyaml
python3 -m pytest tests/test_agent_routing.py tests/test_evaluate_agent_routing.py tests/test_eval_parse.py
```
If local `pytest` is unavailable, that is a tooling blocker for full local proof, not an implementation blocker.
## CI/CD, Release, And Pre-Push Gate Contract
Pre-push required:
- `python3 -m pytest` for the focused routing/eval test set.
- `python3 -m ruff check lib tests` if dev deps are installed.
- Manual scan that no secrets are printed or committed.
PR required:
- Summary of routing rule.
- Test output.
- Known non-prod proof boundary.
- Statement that production acceptance still requires staging or VPS proof.
Deploy required:
- Exact reviewed SHA.
- Staging proof bundle first.
- Production service restart plan.
- Rollback SHA.
Release phases:
| Phase | Feature flag | Environment | Required proof |
| --- | --- | --- | --- |
| Local implementation | Enabled only in tests | Local | Unit and mocked eval tests. |
| Staging shadow | Enabled against sandbox repo | Staging clone or Crabbox-like box | Seven sandbox PR proof artifact. |
| Production shadow | Optional, no merge mutation if supported | Production | Route decisions logged without changing verdict path. |
| Production cutover | Enabled | Production | Real PR verdicts by required agents. |
| Production closure | Enabled | Production | 24-hour stability plus all six agents represented. |
Rollback:
- Flip `PHASE1B_AGENT_ROUTING_ENABLED=false`.
- Restart `teleo-pipeline.service`.
- Confirm eval path returns to prior behavior.
- If code rollback is required, deploy the previous exact SHA and restart service.
- Keep proof artifact explaining why rollback occurred.
Pre-push commands:
```bash
python3 -m pytest tests/test_agent_routing.py tests/test_evaluate_agent_routing.py tests/test_eval_parse.py
python3 -m ruff check lib tests
git diff --check
```
If dev dependencies are missing, install with:
```bash
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install 'aiohttp>=3.9,<4' 'pytest>=8' 'pytest-asyncio>=0.23' 'ruff>=0.3' pyyaml
```
## Independent CLI Audit Contract
A reviewer should be able to run:
```bash
git diff --stat
git diff -- lib/agent_routing.py lib/domains.py lib/evaluate.py lib/llm.py tests/
python3 -m pytest tests/test_agent_routing.py tests/test_evaluate_agent_routing.py
```
The audit should confirm:
- No direct production credentials are introduced.
- `decision-engine` is the target in docs/config where Phase 1b needs it.
- No old eval scripts are revived.
- Default Leo second-review is not silently preserved for all PRs.
- Multi-agent PRs require top 2 reviewer approvals.
## Outside-The-Box Fix Paths
If identity-scored keyword routing is too noisy:
- Use folder-first routing for strong path evidence and identity scoring only for ambiguous or cross-domain cases.
- Add a cheap LLM classifier in shadow mode only, comparing against deterministic router decisions.
- Require contributors/frontends to include an explicit domain or agent hint in PR metadata.
If live GitHub identity constraints block separate agent comments:
- Keep one master bot account and agent-specific verdict tags.
- Defer separate GitHub identities to Phase 2.
If staging VPS access is delayed:
- Use a disposable Hetzner clone when available.
- Use Crabbox or another remote test box for local dirty checkout proof.
- Use a mocked local fake GitHub/Forgejo API server for the eval loop.
## Maintenance Capture
Same-tranche maintenance that is justified now:
- Extract route scoring into a dedicated module if `lib/domains.py` would become too broad.
- Keep backward-compatible wrappers for existing `agent_for_domain` and `detect_domain_from_diff` until downstream callers are migrated.
- Add tests around the existing bug-prone batch grouping surface.
Maintenance to avoid now:
- Full Forgejo-to-GitHub daemon rewrite unless needed for the Phase 1b PR.
- Dashboard redesign.
- Contributor credit redesign beyond removing "Leo reviews every PR" assumptions.
- Separate GitHub identities per agent.
- Payment, wallet, Twitter, or decision-market work.
## Parallelization And Fanout
| Workstream | Classification | Owner | Notes |
| --- | --- | --- | --- |
| Agent identity router and tests | local_owner | Codex current turn | Core implementation surface. Do not fan out because it owns central route contract. |
| Eval layer integration and mocked tests | local_owner | Codex current turn | Needs tight coupling with router semantics. |
| Staging VPS clone proof | draft_gated | Fwaz or infrastructure owner | Requires VPS/provider access and secret quarantine. |
| GitHub identity model | draft_gated | Fwaz plus m3taversal | Deferred unless master bot account becomes unacceptable. |
| Dashboard/reporting polish | do_not_parallelize | Later | Avoid until route state contract is stable. |
### Workstream Sub-Spec: Agent Identity Router
Classification: local_owner
Owned files:
- `lib/agent_routing.py` if created.
- `lib/domains.py` compatibility wrappers.
- `tests/test_agent_routing.py`.
Forbidden files:
- `lib/evaluate.py` except imports needed for route type compatibility.
- Any runtime secrets.
- Any production config defaults outside route feature flags.
Binary done condition:
- Pure route function returns expected required agents for every row in the proof matrix.
- Tests prove deterministic top-2 behavior and fallback behavior.
Verification commands:
```bash
python3 -m pytest tests/test_agent_routing.py
```
Non-claims:
- Does not prove PR comment posting.
- Does not prove production target wiring.
Prompt-ready handoff:
```text
implement phase 1b agent identity routing in teleo-infrastructure. own only route module and route tests. preserve compatibility wrappers. route decision must be pure, deterministic, evidence-bearing, and top-2 capped for cross-domain cases. do not touch production API or eval state transitions.
```
### Workstream Sub-Spec: Eval Integration
Classification: local_owner
Owned files:
- `lib/evaluate.py`
- `lib/llm.py`
- `lib/eval_parse.py` only if parser normalization is required.
- `tests/test_evaluate_agent_routing.py`
- `tests/test_eval_parse.py`
Forbidden files:
- Old deprecated eval shell scripts.
- Deploy scripts unless a feature flag must be exposed.
- Dashboard UI except parser-compatible health checks.
Binary done condition:
- With `PHASE1B_AGENT_ROUTING_ENABLED=true`, eval invokes only required reviewer agents.
- With flag disabled, prior behavior remains available.
- One request-changes verdict blocks aggregate approval.
- All approve verdicts continue to existing approval path.
Verification commands:
```bash
python3 -m pytest tests/test_evaluate_agent_routing.py tests/test_eval_parse.py
```
Non-claims:
- Does not prove live GitHub or VPS behavior.
- Does not prove separate agent GitHub identities.
Prompt-ready handoff:
```text
wire phase 1b routing into teleo-infrastructure eval path behind a feature flag. use required agents from the route result, run agent-specific reviews, aggregate verdicts, and preserve merge/feedback semantics. do not revive deprecated scripts or remove rollback path.
```
### Workstream Sub-Spec: Staging Proof
Classification: draft_gated
Owned files and surfaces:
- Staging VPS or disposable remote test box.
- Sandbox `decision-engine` repo.
- Staging secrets.
- Machine-readable proof artifact.
Forbidden files and surfaces:
- Production VPS services.
- Production GitHub repo.
- Production secrets.
- Mainnet/payment/Twitter surfaces.
Binary done condition:
- Six single-domain PRs and one cross-domain PR produce expected required-agent verdicts and final dispositions in staging.
Verification commands:
```bash
systemctl status teleo-pipeline
journalctl -u teleo-pipeline --since "1 hour ago"
sqlite3 /path/to/pipeline.db "select number, status, domain_agent, leo_verdict, domain_verdict from prs order by number desc limit 20;"
gh pr view --repo living-ip/decision-engine-sandbox PR_NUMBER --comments
```
Non-claims:
- Does not prove production 24-hour stability.
Prompt-ready handoff:
```text
create a quarantined staging proof for phase 1b. clone or provision a disposable server, disable production services and secrets before starting pipeline, point to a sandbox decision-engine repo, run six single-domain prs plus one cross-domain pr, and save a machine-readable proof artifact. do not mutate production.
```
Worker-ready ticket for later staging proof:
```text
title: phase 1b staging proof on cloned vps
owned surfaces: staging vps, sandbox decision-engine repo, staging secrets, proof artifact
forbidden surfaces: production vps services, production github repo, production secrets
done condition: six single-domain prs plus one cross-domain pr produce expected required-agent verdicts and final dispositions
verification commands: systemd status readback, pipeline log scrape, sqlite route query, github pr comment readback
non-claims: does not prove 24h production stability
preferred executor: human/fwaz with codex support
handoff: create staging clone, disable prod services, inject sandbox config, run phase 1b proof script, save machine-readable proof
```
## Acceptance Criteria
Local PR acceptance:
- Focused tests pass.
- Router returns correct single-agent routes.
- Router returns top-2 required agents for cross-domain cases.
- Eval layer invokes only required reviewer agents.
- Verdict aggregation handles all approve, request changes, transport failure, and missing verdict.
- Existing verdict format remains parseable.
- No production readiness claim is made.
Staging acceptance:
- Staging environment cannot mutate production.
- Six single-domain sandbox PRs complete.
- One cross-domain sandbox PR completes.
- Required reviewer agents match proof matrix.
- Proof artifact is retained.
Production exit:
- Exact reviewed SHA deployed.
- All six agents produce at least one verdict in their domain.
- At least one cross-domain PR proves top-2 review behavior.
- Pipeline stable for 24 hours.
## Readiness And Claim Boundaries
Allowed claims after local implementation:
- "Route logic is implemented and locally tested."
- "Mocked eval integration proves required-agent invocation and aggregation."
- "The implementation PR is ready for staging proof."
Forbidden claims after local implementation:
- "Phase 1b is complete."
- "Production is ready."
- "All six agents have demonstrated live review cycles."
- "The VPS is safely updated."
Allowed claims after staging proof:
- "Phase 1b passed sandbox staging proof."
- "The exact SHA is eligible for production cutover review."
Forbidden claims after staging proof:
- "Production is stable."
- "Live `decision-engine` PRs are proven."
Allowed claims after production 24-hour proof:
- "Phase 1b production exit criteria are met."
## Spec Quality Self-Audit
Required execution-grade headings present:
- Current Implementation Audit: present.
- Goal-Vs-Repo-Truth Diff: present.
- Completion Percent And Remaining Delta: present.
- Closure, Endpoint, And Deployment Truth: present.
- Critical Assumptions And Invalidators: present.
- State And Truth Contract: present.
- Measurement Contract: present.
- Backend Work Required: present.
- Frontend Work Required: present.
- Expected Runtime And User-Visible Behavior: present.
- Validation And Test Matrix: present.
- CI/CD, Release, And Pre-Push Gate Contract: present.
- Independent CLI Audit Contract: present.
- Outside-The-Box Fix Paths: present.
- Maintenance Capture: present.
- Parallelization And Fanout: present.
Additional spec-of-spec coverage:
- Product Outcome Contract: present.
- Non-Goals: present.
- Program Decomposition: present.
- Priority Matrix: present.
- Score-To-100 Closure Plan: present.
- Workstream sub-specs: present.
- Staging Proof Contract: present.
- Rollback contract: present.
Known incompleteness:
- This spec cannot name the exact production deploy command until Fwaz or VPS truth confirms it.
- This spec cannot name the exact sandbox repo until the operator creates or selects it.
- This spec cannot prove whether production daemon code exactly matches local `teleo-infrastructure` until VPS readback exists.
## Assistant-Added Caveats
This spec intentionally expands B1/B2 from folder-domain routing to identity-scored agent routing because m3taversal clarified that agent identities should route and folders are only signals. That is the right product interpretation, but it increases implementation scope versus the original simple path classifier.
This spec does not claim production readiness without staging or VPS proof.

31
docs/phase1b/README.md Normal file
View file

@ -0,0 +1,31 @@
# Phase 1b Spec Index
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Scope
Phase 1b is the `decision-engine` PR evaluation router. It sends each KB mutation to the owning Hermes agent identity, supports top-2 cross-domain review, posts parseable `VERDICT:AGENT:*` comments through one master bot account, preserves existing merge or feedback behavior, and proves the change in staging before production cutover.
## Specs
| Workstream | Spec | Implementation posture |
| --- | --- | --- |
| Agent identity router | `docs/phase1b/agent-identity-router-spec.md` | ready_now |
| Eval pipeline integration | `docs/phase1b/eval-pipeline-integration-spec.md` | ready_now after router contract freezes |
| GitHub identity and bot comments | `docs/phase1b/github-identity-bot-posture-spec.md` | ready_now after canonical target config freezes |
| Reporting and contributor compatibility | `docs/phase1b/reporting-contributor-compatibility-spec.md` | ready_now after verdict state shape freezes |
| Staging proof | `docs/phase1b/staging-proof-spec.md` | draft_gated on staging/VPS or disposable remote access |
| Staging blocker | `docs/phase1b/staging-blocker.json` | external_only |
## Execution Order
1. Implement router contract and tests.
2. Wire eval pipeline to required reviewer agents under a feature flag.
3. Route comments through the canonical GitHub target with idempotency markers.
4. Update reporting and contributor accounting to read reviewer sets rather than fixed Leo plus domain slots.
5. Run staging proof on a clone or disposable remote target before production cutover.
## Claim Boundary
These specs plus the Phase 1b branch prove only local implementation behavior. A production completion claim requires merged code, passing tests, staging proof, exact production SHA deployment, Leo signoff, and 24-hour production daemon stability.

View file

@ -0,0 +1,338 @@
# Phase 1b Child Spec: Agent Identity Router
Created: 2026-05-29
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Product Outcome Contract
The router decides which Hermes agent identity should review a `decision-engine` KB PR. It must route by agent ownership, with file paths as strong evidence but not the only source of truth.
## Goal
Implement a pure, deterministic, evidence-bearing route scorer that returns one or two required reviewer agents for a PR.
## Non-Goals
- Do not call paid LLMs for routing.
- Do not post PR comments.
- Do not mutate pipeline DB state.
- Do not deploy to VPS.
- Do not implement general user-input routing outside PR evaluation.
## Current Implementation Audit
Current relevant code:
- `lib/domains.py` contains `DOMAIN_AGENT_MAP`, `agent_for_domain`, `detect_domain_from_diff`, and `detect_domain_from_branch`.
- `lib/agent_routing.py` now owns the Phase 1b identity-scored route contract.
- The obsolete local `DomainRoute` folder-first draft and its draft tests were removed before this branch was committed.
- Cross-domain PRs now require the top 2 routed agents locally, with `route_kind="escalated"` when more than two agents scored.
Existing implementation truth:
- The repo already has domain detection that can be reused for path signals.
- The new route tests cover six primary agents, broadened ownership domains, top-2 cross-domain routing, fallback, and deterministic repeat behavior.
- The existing map includes adjacent domains such as `mechanisms`, `living-capital`, `living-agents`, `critical-systems`, `collective-intelligence`, `teleological-economics`, and `cultural-dynamics`.
- The product owner clarified that Phase 1b should use agent identities to route, not only folder names.
## Existing-Spec Inventory
| Existing doc | Relevance | Decision |
| --- | --- | --- |
| `docs/phase1b-agent-routing-spec.md` | Umbrella source of truth. | Reuse. |
| `docs/queue.md` | Notes `ai-alignment` domain evolution. | Reuse as a signal for Theseus ownership. |
| `docs/ARCHITECTURE.md` | Describes eval stage shape. | Context only. |
## Goal-Vs-Repo-Truth Diff
Goal:
- Return `AgentRoute` with `primary_agent`, `required_agents`, `route_kind`, `scores`, and `evidence`.
- Cap cross-domain routes at top 2 agents.
- Treat folders as evidence, not the complete classifier.
- Be testable without network, DB, GitHub, or LLM calls.
Repo truth:
- Existing classifier returns one folder-domain string or `None`.
- No scores, evidence, or top-2 agent set exist.
- Existing tests do not cover identity-broadened ownership.
## Completion Percent And Remaining Delta
Current completion on this branch: 100 percent for local route logic, 0 percent for staging route calibration.
Remaining delta:
1. Review the route weights against real recent `decision-engine` PRs.
2. Calibrate ambiguous keyword cases from staging evidence.
3. Decide whether escalated routes should remain top-2 total or become Leo plus top-2 later.
## Closure, Endpoint, And Deployment Truth
Local closure:
- Route tests pass.
- No network or DB dependency exists in route tests.
Staging closure:
- Staging proof artifact records route scores and evidence for seven sandbox PRs.
Production closure:
- Live PR audit rows show route evidence and required agents.
This child spec alone cannot prove staging or production behavior.
## Critical Assumptions And Invalidators
Assumptions:
- `decision-engine` file layout is close enough to current local clone for path signals to apply.
- Agent identity ownership from m3taversal is authoritative.
- Top-2 cap is acceptable for cross-domain cases.
Invalidators:
- Product owner changes cross-domain rule from top 2 to all touched agents.
- Agent ownership boundaries change materially.
- Production PR metadata lacks branch or changed-file data.
## State And Truth Contract
Route output schema:
```python
AgentRoute(
primary_agent="Rio",
required_agents=("Rio",),
route_kind="single",
scores={"Leo": 0, "Theseus": 1, "Rio": 9, "Vida": 0, "Clay": 0, "Astra": 0},
evidence=[
{"agent": "Rio", "signal": "path", "weight": 8, "value": "domains/internet-finance/foo.md"}
],
fallback=False,
)
```
`route_kind` values:
- `single`
- `multi`
- `fallback`
- `escalated`
`required_agents` must never contain more than two agents in Phase 1b.
## Measurement Contract
Required route fixture cases:
| Fixture | Expected |
| --- | --- |
| `domains/grand-strategy/foo.md` | Leo |
| `domains/ai-alignment/foo.md` | Theseus |
| `domains/internet-finance/foo.md` | Rio |
| `domains/health/foo.md` | Vida |
| `domains/entertainment/foo.md` | Clay |
| `domains/space-development/foo.md` | Astra |
| `domains/energy/foo.md` | Astra |
| `domains/robotics/foo.md` | Astra |
| `domains/manufacturing/foo.md` | Astra |
| `core/living-capital/foo.md` | Rio |
| `core/living-agents/foo.md` | Theseus |
| `foundations/cultural-dynamics/foo.md` | Clay |
| AI plus x402 diff | Theseus and Rio |
| collective AI goals diff | Leo and Theseus |
Minimum quality metrics:
- `route_fixture_pass_rate = 100 percent`
- `fallback_count = 0` for known fixtures
- deterministic repeat count: same input returns same result 100 times
## Backend Work Required
Owned files:
- `lib/agent_routing.py`
- `lib/domains.py`
- `tests/test_agent_routing.py`
Implementation steps:
1. Move new identity routing into `lib/agent_routing.py`.
2. Keep `lib/domains.py` as compatibility for domain-oriented callers.
3. Define `AGENT_ORDER = ("Leo", "Theseus", "Rio", "Vida", "Clay", "Astra")`.
4. Define identity signals per agent.
5. Add path signal extraction for `domains`, `entities`, `core`, `foundations`, and `agents`.
6. Add branch prefix signal extraction.
7. Add capped keyword scoring from filenames and diff text.
8. Add top-2 selection rule.
9. Add fallback to Leo.
10. Add tests.
Forbidden files:
- `lib/evaluate.py`
- `lib/llm.py`
- deploy scripts
- secrets or runtime config outside route feature flag wiring
## Frontend Work Required
None.
## Expected Runtime And User-Visible Behavior
The router itself has no user-visible UI. Its behavior becomes visible through audit logs, PR comment reviewer selection, and proof artifacts.
Example:
```text
input: domains/internet-finance/x402-agent-payments.md
output: required_agents = ["Rio"]
```
Cross-domain example:
```text
input: ai systems claim plus x402 payment claim
output: required_agents = ["Theseus", "Rio"]
```
## Validation And Test Matrix
Commands:
```bash
python3 -m pytest tests/test_agent_routing.py
python3 -m ruff check lib/agent_routing.py lib/domains.py tests/test_agent_routing.py
git diff --check
```
Test classes:
- primary ownership routes
- broadened ownership routes
- branch fallback routes
- keyword routes
- top-2 cross-domain routes
- fallback routes
- deterministic tie-breaking
- compatibility wrapper behavior
## CI/CD, Release, And Pre-Push Gate Contract
Before PR:
- Route tests pass locally.
- No production config defaults change.
- No network dependency enters route tests.
Before staging:
- Eval integration spec consumes the route result without modifying route internals.
Before production:
- Route evidence appears in staging proof artifact.
## Independent CLI Audit Contract
Reviewer commands:
```bash
git diff -- lib/agent_routing.py lib/domains.py tests/test_agent_routing.py
python3 -m pytest tests/test_agent_routing.py
```
Reviewer checks:
- Route function is pure.
- Scores are explainable.
- Top-2 cap is enforced.
- Folder paths are not the only signal.
- Old callers still work or have a clear migration path.
## Outside-The-Box Fix Paths
If keyword scoring is noisy:
- Disable diff keyword scoring and use path plus branch only.
- Use LLM classifier in shadow mode only.
- Add explicit PR label or frontmatter hint later.
If identity boundaries are ambiguous:
- Prefer top-2 over fallback when two agents have meaningful scores.
- Log route evidence for later calibration.
## Maintenance Capture
Beneficial now:
- Keep route logic out of `lib/evaluate.py`.
- Keep compatibility wrappers narrow.
Avoid now:
- Large domain taxonomy rewrite.
- Dashboard UI changes.
- Paid classifier calls.
## Parallelization And Fanout
Classification: local_owner.
Do not fan out implementation. This module is a root contract consumed by eval integration.
Worker-ready prompt:
```text
implement the phase 1b agent identity router in teleo-infrastructure. own lib/agent_routing.py, lib/domains.py compatibility wrappers, and route tests only. make the route function pure, deterministic, evidence-bearing, and capped at top 2 required agents. do not touch eval integration or deploy code.
```
## Acceptance Criteria
- All required route fixtures pass.
- Route result includes primary agent, required agents, route kind, scores, evidence, and fallback status.
- Cross-domain route never requires more than two agents.
- No LLM, network, DB, or GitHub calls occur in the router.
## Readiness And Claim Boundaries
Allowed claim:
- "Agent identity routing is locally implemented and unit-tested."
Forbidden claim:
- "Phase 1b eval is complete."
## Spec Quality Self-Audit
Required headings present:
- Current Implementation Audit: present.
- Goal-Vs-Repo-Truth Diff: present.
- Completion Percent And Remaining Delta: present.
- Closure, Endpoint, And Deployment Truth: present.
- Critical Assumptions And Invalidators: present.
- State And Truth Contract: present.
- Measurement Contract: present.
- Backend Work Required: present.
- Frontend Work Required: present.
- Expected Runtime And User-Visible Behavior: present.
- Validation And Test Matrix: present.
- CI/CD, Release, And Pre-Push Gate Contract: present.
- Independent CLI Audit Contract: present.
- Outside-The-Box Fix Paths: present.
- Maintenance Capture: present.
- Parallelization And Fanout: present.
## Assistant-Added Caveats
This child spec intentionally keeps routing deterministic and no-spend. That may be less semantically smart than an LLM classifier, but it is the right first implementation for Phase 1b because it is testable, cheap, and auditable.

View file

@ -0,0 +1,343 @@
# Phase 1b Child Spec: Eval Pipeline Integration
Created: 2026-05-29
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Product Outcome Contract
Pipeline-v2 must use the Phase 1b route result to run the required Hermes agent reviews for a `decision-engine` PR. The old default shape where every non-LIGHT PR receives a domain review plus Leo review must be bypassed when Phase 1b routing is enabled.
## Goal
Integrate agent identity routing into `lib/evaluate.py` behind a feature flag, run one or two required reviewer agents, aggregate verdicts, and preserve existing merge or feedback behavior.
## Non-Goals
- Do not remove the old eval path until staging proof exists.
- Do not rewrite the full Forgejo/GitHub API abstraction.
- Do not redesign dashboards.
- Do not implement separate GitHub identities.
- Do not change extraction or validation behavior except as needed for eval tests.
## Current Implementation Audit
Current relevant code:
- `lib/evaluate.py::evaluate_pr` owns single PR evaluation.
- `lib/evaluate.py::evaluate_cycle` selects eligible PRs.
- `_build_domain_batches` groups STANDARD PRs by DB domain before fetching diffs.
- `_run_batch_domain_eval` runs batch domain reviews, then individual Leo reviews.
- `run_domain_review` in `lib/llm.py` prompts a domain expert through OpenRouter.
- `run_leo_review` in `lib/llm.py` prompts Leo through OpenRouter or Claude path depending on tier.
- `parse_verdict` in `lib/eval_parse.py` parses reviewer-specific verdict tags.
- `approve_pr`, `reopen_pr`, `close_pr`, and `start_review` handle state transitions.
Current behavior:
- Diff path detects a domain.
- `agent_for_domain(domain)` selects one domain agent.
- Domain review runs first.
- Leo review runs after domain approval for non-LIGHT PRs.
- `leo_verdict` and `domain_verdict` are the stored verdict fields.
- Contributor credit logic assumes Leo can be one evaluator and `domain_agent` can be the other.
## Existing-Spec Inventory
| Existing doc | Relevance | Decision |
| --- | --- | --- |
| `docs/phase1b-agent-routing-spec.md` | Parent route and eval contract. | Reuse. |
| `docs/ARCHITECTURE.md` | Existing pipeline stage model. | Reuse as baseline. |
| `docs/multi-model-eval-architecture.md` | Prior Leo-plus-second-model design. | Supersede for Phase 1b eval path only. |
| `handoff/deprecated/eval-scripts.md` | Confirms shell eval scripts are dead. | Reuse to avoid wrong surface. |
## Goal-Vs-Repo-Truth Diff
Goal:
- `evaluate_pr` calls the route scorer.
- Required agents are the only reviewer agents.
- One required agent means one review.
- Two required agents means two reviews and aggregate verdict.
- Default Leo second-review is removed when the feature flag is enabled.
- Old behavior remains available when the feature flag is disabled.
Branch truth:
- Legacy eval is still available when the feature flag is false.
- When the feature flag is true, eval invokes the identity route, runs required agents only, writes `review_records`, and projects aggregate state back into legacy `leo_verdict` and `domain_verdict` columns.
- Batch eval is disabled while the feature flag is true because stale DB-domain grouping is not route-aware.
- `run_agent_review` exists, but it uses prompt-level identity context rather than loading full KB identity/belief/reasoning files.
## Completion Percent And Remaining Delta
Current completion on this branch: 75 percent local implementation behind a default-off feature flag.
Remaining delta:
1. Decide direct GitHub `decision-engine` comment transport versus Forgejo-first cutover compatibility.
2. Prove with staging PRs and real daemon logs.
3. Update contributor/dashboard assumptions only where staging or tests prove breakage.
## Closure, Endpoint, And Deployment Truth
Local closure:
- Mocked eval tests prove route-to-review-to-aggregate behavior.
Staging closure:
- Staging sandbox PRs receive expected comments and DB state transitions.
Production closure:
- Live `decision-engine` PRs are handled by Phase 1b route path for 24 hours.
This spec cannot claim production closure without VPS proof.
## Critical Assumptions And Invalidators
Assumptions:
- Feature flag rollback is acceptable.
- Existing state fields can support Phase 1b initially by storing primary agent in `domain_agent` and aggregate details in audit rows.
- A DB schema migration is avoidable for the first PR.
- Master bot comments with `VERDICT:AGENT:*` are acceptable.
Invalidators:
- Downstream merge logic requires formal reviews from separate GitHub users.
- Dashboards or contributor credit fail hard when Leo is not present.
- Batch eval cannot be safely disabled and must be route-aware from day one.
- Production env cannot set feature flags.
## State And Truth Contract
Feature flag:
```text
PHASE1B_AGENT_ROUTING_ENABLED=false
```
When false:
- Existing eval behavior continues.
When true:
- Eval route is built for every non-bypass PR.
- Audit log records route JSON.
- Required agent reviews run.
- Aggregate verdict determines approval or feedback.
Minimal DB field use:
- `domain`: keep route primary domain or `multi`.
- `domain_agent`: keep primary agent.
- `domain_verdict`: keep aggregate non-Leo review verdict or aggregate verdict.
- `leo_verdict`: set `skipped` unless Leo is a required agent; if Leo is required, store Leo verdict.
- `review_records`: write one row per required reviewer attempt with reviewer agent, model, outcome, and notes.
- review comments include a `PHASE1B_REVIEW` marker and the current local helper suppresses duplicate posts for the same PR and agent.
- audit log: route and all per-agent verdicts.
This is a compatibility posture, not the ideal long-term schema.
## Measurement Contract
Required local assertions:
- Phase 1b flag disabled uses old runner calls.
- Phase 1b flag enabled calls `run_agent_review` once for single route.
- Phase 1b flag enabled calls `run_agent_review` twice for multi route.
- `run_leo_review` is not called unless Leo is in `required_agents`.
- all approve returns approved aggregate.
- one request changes returns feedback aggregate.
- transport failure reopens for retry.
- retry after a partial multi-agent success does not duplicate existing posted verdict comments.
## Backend Work Required
Owned files:
- `lib/evaluate.py`
- `lib/llm.py`
- `lib/config.py`
- `lib/eval_parse.py` only if parser compatibility needs explicit tests or normalization.
- `tests/test_evaluate_agent_routing.py`
- `tests/test_eval_parse.py`
Implementation steps:
1. Add `PHASE1B_AGENT_ROUTING_ENABLED` to `lib/config.py`.
2. Import route scorer.
3. Add `run_agent_review` in `lib/llm.py`.
4. Add helper to load agent context from KB worktree.
5. Add `aggregate_agent_verdicts`.
6. In `evaluate_pr`, after bypasses and diff filtering, branch into Phase 1b path when flag is true.
7. In Phase 1b path, run required reviews and post comments through the existing API helper.
8. Update DB fields conservatively.
9. Write `review_records` rows for each required reviewer attempt.
10. Preserve old logic under flag false.
11. Disable `_build_domain_batches` while flag is true or make it route-aware.
Forbidden files:
- Deprecated eval shell scripts.
- Deployment scripts unless needed for documenting the flag.
- Runtime secrets.
## Frontend Work Required
None.
## Expected Runtime And User-Visible Behavior
Single-agent example:
```text
PR touches internet finance.
route.required_agents = ["Rio"]
pipeline posts a Rio verdict.
merge proceeds if Rio approves.
```
Cross-agent example:
```text
PR touches AI systems and x402 payments.
route.required_agents = ["Theseus", "Rio"]
pipeline posts Theseus and Rio verdicts.
merge proceeds only if both approve.
```
Fallback example:
```text
PR cannot be confidently routed.
route.required_agents = ["Leo"]
pipeline posts Leo verdict.
route_kind = fallback is audited.
```
## Validation And Test Matrix
Commands:
```bash
python3 -m pytest tests/test_evaluate_agent_routing.py tests/test_eval_parse.py
python3 -m ruff check lib/evaluate.py lib/llm.py lib/config.py tests/test_evaluate_agent_routing.py
git diff --check
```
Test cases:
- flag-off old behavior smoke
- flag-on single reviewer approve
- flag-on single reviewer request changes
- flag-on two reviewer approve
- flag-on two reviewer one reject
- missing verdict
- transport failure
- Leo required route
- Leo not required route
- batch disabled or route-aware under flag
## CI/CD, Release, And Pre-Push Gate Contract
Before PR:
- Focused tests pass.
- Old behavior remains behind flag false.
- No production default flips to true.
Before staging:
- Operator can enable flag in staging env.
- Sandbox repo target is configured.
Before production:
- Staging proof artifact exists.
- Rollback command is known.
## Independent CLI Audit Contract
Reviewer commands:
```bash
git diff -- lib/evaluate.py lib/llm.py lib/config.py tests/test_evaluate_agent_routing.py
python3 -m pytest tests/test_evaluate_agent_routing.py
```
Reviewer checks:
- No deprecated scripts revived.
- No secrets introduced.
- Feature flag false preserves old path.
- Feature flag true bypasses default Leo second-review.
- Cross-domain aggregate requires all required reviewers to approve.
## Outside-The-Box Fix Paths
If compatibility fields become confusing:
- Add a narrow DB migration for `route_json` and `agent_verdicts_json`.
If batch eval blocks safe integration:
- Disable batch eval under Phase 1b flag for one release.
If LLM review prompts get too large:
- Load only identity plus beliefs first, then add reasoning/skills later.
## Maintenance Capture
Beneficial now:
- Isolate Phase 1b logic into helpers instead of expanding `evaluate_pr` deeply.
- Keep rollback path explicit.
Avoid now:
- Full eval architecture rewrite.
- Dashboard redesign.
- Broad DB migration unless tests require it.
## Parallelization And Fanout
Classification: local_owner.
Do not fan out before the router contract lands. Eval integration depends tightly on route result semantics.
Worker-ready prompt:
```text
wire phase 1b routing into teleo-infrastructure eval behind PHASE1B_AGENT_ROUTING_ENABLED. own lib/evaluate.py, lib/llm.py, lib/config.py, and mocked eval tests. run required agents from the route result, aggregate verdicts, preserve old behavior when the flag is false, and do not revive deprecated scripts.
```
## Acceptance Criteria
- Flag false path remains available.
- Flag true path runs required agents only.
- One or two verdicts aggregate correctly.
- Existing merge or feedback path is preserved.
- Focused mocked tests pass.
## Readiness And Claim Boundaries
Allowed claim:
- "Phase 1b eval integration is locally tested behind a feature flag."
Forbidden claim:
- "Phase 1b is live."
## Spec Quality Self-Audit
All required execution-grade headings are present. This spec intentionally defers exact production commands to the staging/proof child spec because they depend on VPS truth.
## Assistant-Added Caveats
The compatibility use of `domain_verdict` and `leo_verdict` is a pragmatic Phase 1b bridge. A cleaner route schema may be worth adding after staging proof, but a premature migration would widen the blast radius.

View file

@ -0,0 +1,296 @@
# Phase 1b Child Spec: GitHub Identity And Bot Posture
Created: 2026-05-29
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Product Outcome Contract
Phase 1b must post agent-specific verdicts for `decision-engine` PRs without requiring six separate GitHub accounts. Agent identity is represented in the comment content and verdict tags, while a single master bot account owns transport.
## Goal
Define and implement the minimum GitHub identity and comment transport posture for Phase 1b:
- canonical target is `living-ip/decision-engine`;
- one master bot token is acceptable;
- verdict comments preserve `VERDICT:AGENT:*`;
- duplicate comments are prevented;
- old Forgejo or mirror behavior remains rollback-safe until staging proof.
## Non-Goals
- Do not create separate GitHub users for all agents.
- Do not require GitHub branch protection to count separate formal reviewers in Phase 1b.
- Do not rewrite every Forgejo-named helper unless needed for Phase 1b comments.
- Do not redesign contributor credit.
- Do not revive deprecated eval shell scripts.
## Current Implementation Audit
Current truth:
- `pipeline-health-check.py` targets `https://api.github.com/repos/living-ip/decision-engine`.
- `research/research-session.sh` targets GitHub `living-ip/decision-engine` and `github-admin-token`.
- `handoff/phase1-step3-script-migration.md` documents Phase 1 single `livingIPbot` posture and defers per-agent identities.
- `lib/config.py` still defaults to Forgejo `teleo/teleo-codex`.
- `lib/github_feedback.py` hardcodes `living-ip/teleo-codex` and reads `github-pat`, not `decision-engine` and `github-admin-token`.
- `lib/evaluate.py` posts review comments through Forgejo helpers and per-agent Forgejo tokens.
- `lib/github_feedback.py` is a mirror feedback channel keyed by `prs.github_pr`, not the canonical review transport.
- `deploy/sync-mirror.sh` still references `living-ip/teleo-codex`.
- Fwaz confirmed separate GitHub identities are ideal and blocked on GitHub/PAT setup; Phase 1b implementation should not wait on six distinct accounts if the pipeline can post parseable `VERDICT:AGENT:*` comments through the pipeline bot.
## Existing-Spec Inventory
| Existing doc | Relevance | Decision |
| --- | --- | --- |
| `docs/phase1b-agent-routing-spec.md` | Parent identity posture. | Reuse. |
| `handoff/phase1-step3-script-migration.md` | Documents single bot token and GitHub `decision-engine` migration for scripts. | Reuse. |
| `handoff/deprecated/eval-scripts.md` | Confirms old eval scripts should not be revived. | Reuse. |
## Goal-Vs-Repo-Truth Diff
Goal:
- One canonical GitHub target for Phase 1b: `living-ip/decision-engine`.
- One master bot token for Phase 1b comments.
- Agent identity lives in verdict tags and comment headings.
- Comment posting supports idempotency by PR, head SHA, and agent.
Repo truth:
- GitHub target and token names are split across files.
- Eval comments still use Forgejo helpers.
- GitHub feedback is non-fatal mirror feedback, not agent review transport.
## Completion Percent And Remaining Delta
Current completion: 15 percent.
Remaining delta:
1. Add explicit GitHub target config with staging override.
2. Normalize token file selection or document compatibility.
3. Add Phase 1b comment posting helper for GitHub `decision-engine`.
4. Add idempotency marker.
5. Add tests for URL target, token path, missing token, and duplicate prevention.
6. Decide direct GitHub mode versus Forgejo-mirror mode before staging.
## Closure, Endpoint, And Deployment Truth
Local closure:
- Tests prove comments target `living-ip/decision-engine` and token material is not logged.
Staging closure:
- Sandbox PR comments are posted by master bot with agent verdict tags.
Production closure:
- Live `decision-engine` PR comments are posted by master bot without duplicates.
## Critical Assumptions And Invalidators
Assumptions:
- One bot account is enough for Phase 1b.
- Agent identity in verdict content satisfies acceptance.
- Formal GitHub reviews from distinct accounts are not required now.
- Per-agent PATs can be added later without changing the route contract.
Invalidators:
- Branch protection requires distinct GitHub reviewer identities.
- GitHub org disallows the selected PAT or bot account.
- Production daemon must remain Forgejo-first for the cutover window.
- Direct GitHub PRs lack the DB linkage used by existing `github_feedback`.
## State And Truth Contract
Comment idempotency marker:
```text
<!-- PHASE1B_REVIEW:PR=123:SHA=abc123:AGENT=RIO -->
```
Verdict marker remains:
```text
<!-- VERDICT:RIO:APPROVE -->
```
Required config:
```python
GITHUB_OWNER = "living-ip"
GITHUB_REPO = "decision-engine"
GITHUB_TOKEN_FILE = SECRETS_DIR / "github-admin-token"
```
Staging must override repo or owner without code changes.
## Measurement Contract
Minimum tests:
- URL builder targets `https://api.github.com/repos/living-ip/decision-engine`.
- Staging override changes target.
- Missing token returns non-fatal failure and audit detail.
- Token value is never logged.
- Duplicate marker prevents repeat comment for same PR, SHA, and agent.
- Six agent verdict tags remain parseable.
## Backend Work Required
Owned files:
- `lib/github_feedback.py` or a new `lib/github_reviews.py`.
- `lib/config.py`.
- `lib/evaluate.py` only where the eval integration calls the comment helper.
- `tests/test_github_identity.py` or equivalent.
Implementation steps:
1. Add canonical GitHub target config.
2. Add token lookup that prefers `github-admin-token` for Phase 1b and can fall back only if explicitly configured.
3. Add comment helper for agent verdict comments.
4. Add idempotency marker and readback check.
5. Add tests.
6. Wire eval integration to the helper under Phase 1b flag.
Forbidden files:
- Deprecated eval shell scripts.
- Production secrets.
- Broad deploy rewrite.
## Frontend Work Required
None.
## Expected Runtime And User-Visible Behavior
PR comment example:
```text
## Rio review
<review text>
<!-- PHASE1B_REVIEW:PR=123:SHA=abc123:AGENT=RIO -->
<!-- VERDICT:RIO:APPROVE -->
```
The GitHub account may be a master bot. The comment content must show which agent reviewed.
## Validation And Test Matrix
Commands:
```bash
python3 -m pytest tests/test_github_identity.py tests/test_eval_parse.py
python3 -m ruff check lib/github_feedback.py lib/config.py tests/test_github_identity.py
git diff --check
```
Test cases:
- canonical target
- staging override
- missing token
- no token logging
- idempotent comment marker
- all six verdict tags parse
## CI/CD, Release, And Pre-Push Gate Contract
Before PR:
- Local tests prove target and idempotency.
Before staging:
- Sandbox repo token exists.
- Production token is not used.
Before production:
- Bot account has comment permissions on `decision-engine`.
- Rollback path is old Forgejo or disabled Phase 1b flag.
## Independent CLI Audit Contract
Reviewer checks:
```bash
rg -n "teleo-codex|decision-engine|github-admin-token|github-pat|VERDICT|PHASE1B_REVIEW" lib tests pipeline-health-check.py research deploy
```
Audit questions:
- Which files still target `teleo-codex`?
- Are those files in the Phase 1b runtime path?
- Does any log path expose token values?
- Does idempotency prevent duplicate comments?
## Outside-The-Box Fix Paths
If direct GitHub comments are not safe in the first PR:
- Keep Forgejo review transport and post GitHub mirror feedback only in staging.
- Add a dry-run comment mode that writes the planned body into audit logs.
If GitHub PAT remains blocked:
- Use a GitHub App only for comment posting.
- Keep master bot for git push but app token for PR comments.
## Maintenance Capture
Beneficial now:
- Name GitHub target config clearly.
- Avoid proliferating `github-pat` versus `github-admin-token`.
Avoid now:
- Separate agent GitHub users.
- Full mirror rewrite.
- Contributor identity overhaul.
## Parallelization And Fanout
Classification: ready_now after the implementer explicitly chooses direct GitHub comments or Forgejo-mirror compatibility for the Phase 1b flag path.
Worker-ready prompt:
```text
implement phase 1b github review comment posture. use one master bot token, target living-ip/decision-engine with staging override support, add agent-specific verdict comment helper with idempotency marker, and prove no token leakage. do not create separate agent accounts or rewrite deploy/mirror broadly.
```
## Acceptance Criteria
- Phase 1b comment helper targets `decision-engine`.
- Master bot can post agent verdict tags.
- Duplicate comments are prevented.
- Missing token is non-fatal and auditable.
- Existing old transport remains rollback-safe.
## Readiness And Claim Boundaries
Allowed claim:
- "Master-bot GitHub verdict comment posture is locally specified/tested."
Forbidden claim:
- "Separate agent GitHub identities are solved."
## Spec Quality Self-Audit
All required execution-grade headings are present. The exact direct-GitHub versus Forgejo-mirror cutover remains a deliberate implementation decision because current daemon code is Forgejo-first.
## Assistant-Added Caveats
The repo has real target drift between `teleo-codex` and `decision-engine`. Do not hide that drift in the eval implementation. The Phase 1b PR should either fix the runtime path it uses or explicitly leave non-runtime references for a later migration.

View file

@ -0,0 +1,125 @@
# Phase 1b Local Review Guide
Status: local-only review artifact
Branch: `phase1b-agent-routing-local`
## What This Repo Is
`teleo-infrastructure` is the pipeline/runtime repo. For Phase 1b, it owns the evaluation daemon logic that watches PRs, fetches diffs, runs reviewers, posts verdict comments, and moves PR state toward merge or feedback.
Canonical split for this phase:
- KB repo: `decision-engine`
- implementation/runtime repo: `teleo-infrastructure`
- production runtime: VPS under `/opt/teleo-eval`, not currently accessible from this workspace
## What This Branch Changes
Local code changes:
- `lib/agent_routing.py`: new pure router that maps a PR diff to one or two Hermes agents.
- `lib/config.py`: adds `PHASE1B_AGENT_ROUTING_ENABLED`, default `false`.
- `lib/evaluate.py`: adds a feature-flagged Phase 1b eval path.
- `lib/llm.py`: adds `run_agent_review`.
- `tests/test_agent_routing.py`: router tests.
- `tests/test_evaluate_agent_routing.py`: mocked eval tests.
- `tests/test_eval_parse.py`: all six `VERDICT:AGENT:*` parser coverage.
Spec/docs changes:
- `docs/phase1b-agent-routing-spec.md`
- `docs/phase1b/README.md`
- child specs under `docs/phase1b/`
- `docs/phase1b/staging-blocker.json`
## What It Does Not Change
- It does not enable Phase 1b in production.
- It does not touch the VPS.
- It does not create or require six GitHub identities.
- It does not solve the Forgejo-vs-GitHub cutover.
- It does not fix unrelated full-suite failures.
## Current Safety Posture
The feature flag defaults off:
```text
PHASE1B_AGENT_ROUTING_ENABLED=false
```
With the flag off, the legacy eval path remains available. The Phase 1b path should only run in staging or a controlled daemon after explicit env config.
The local review hardening pass removed changes to `lib/domains.py` so the legacy domain map is not changed by this branch.
## Local Proof
Focused proof that currently passes:
```bash
.venv/bin/python -m pytest tests/test_agent_routing.py tests/test_evaluate_agent_routing.py tests/test_eval_parse.py
.venv/bin/ruff check lib/agent_routing.py lib/domains.py lib/evaluate.py lib/llm.py lib/config.py tests/test_agent_routing.py tests/test_evaluate_agent_routing.py
git diff --check
```
Latest focused result:
```text
61 passed
ruff: all checks passed
git diff --check: passed
```
Full-suite status:
```text
406 passed, 12 failed, 3 errors
```
Known full-suite failure groups:
- `db.migrate` fresh-fixture rebuild error: `prs_new has no column named auto_merge`
- contributor test fixture missing `submitted_by`
- date/frontmatter expectations in `test_post_extract.py`
- search threshold expectation in `test_search.py`
- missing `python-telegram-bot` imports for X content tests
Those failures mean this branch should not be called repo-green or PR-ready.
## How To Review Locally
Stay local:
```bash
git switch phase1b-agent-routing-local
git status --short --branch
git diff main...HEAD --stat
git diff main...HEAD -- lib/agent_routing.py lib/evaluate.py lib/llm.py lib/config.py
```
Review the behavior in this order:
1. `lib/agent_routing.py`
2. `tests/test_agent_routing.py`
3. `lib/evaluate.py`
4. `tests/test_evaluate_agent_routing.py`
5. `docs/phase1b/staging-blocker.json`
## Before Any PR
Do not open a PR until at least one of these is true:
- full-suite failures are triaged into accepted unrelated failures with issue links, or fixed;
- staging access is available and a sandbox proof path is ready;
- m3taversal/Fwaz explicitly accept a local-only draft review without staging proof.
## Before Production
Production requires:
- staging proof against sandbox `decision-engine`;
- exact reviewed SHA;
- Leo signoff;
- no direct VPS self-upgrades;
- `PHASE1B_AGENT_ROUTING_ENABLED` enabled only after cutover plan is written;
- rollback path to flag-off behavior.

View file

@ -0,0 +1,275 @@
# Phase 1b Child Spec: Reporting And Contributor Compatibility
Created: 2026-05-29
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Product Outcome Contract
Phase 1b must not make dashboards, health checks, or contributor credit lie about review state. Reporting may stay minimal, but it must not mark a cross-domain PR as ready before all required agents have reviewed.
## Goal
Update compatibility surfaces so Phase 1b required-agent reviews are represented accurately enough for operations, health, and contributor attribution without doing a dashboard redesign.
## Non-Goals
- Do not redesign the dashboard UI.
- Do not implement a new leaderboard model.
- Do not require a broad DB migration unless `review_records` is insufficient.
- Do not make production-readiness claims from health-check summaries alone.
## Current Implementation Audit
Current truth:
- `lib/db.py` already has `review_records` with `pr_number`, `domain`, `agent`, `reviewer`, `reviewer_model`, `outcome`, `rejection_reason`, and `notes`.
- `lib/contributor.py` assumes Leo reviews every PR and credits Leo plus one `domain_agent`.
- `lib/health.py` computes approval rates from `domain_verdict` and `leo_verdict`.
- `lib/health.py` builds reviewer strings only from `domain_verdict` and `leo_verdict`.
- `pipeline-health-check.py` can parse arbitrary `VERDICT:AGENT:*` tags, but it has no required-agent concept.
- A cross-domain PR with one approval and one missing required review could be misclassified if reporting only checks "any approve".
## Existing-Spec Inventory
| Existing doc | Relevance | Decision |
| --- | --- | --- |
| `docs/phase1b-agent-routing-spec.md` | Parent route/verdict state. | Reuse. |
| `docs/ARCHITECTURE.md` | Health/dashboard baseline. | Reuse as context. |
| `docs/DIAGNOSTICS-AGENT-SPEC.md` | Diagnostics philosophy. | Reuse as later direction, not immediate scope. |
## Goal-Vs-Repo-Truth Diff
Goal:
- Required-agent state is visible enough to avoid false readiness.
- Contributor evaluator credit follows actual approved reviewer agents.
- Health and pipeline checks can distinguish incomplete cross-domain review.
Repo truth:
- Legacy fields only represent `domain_verdict` plus `leo_verdict`.
- Contributor credit hardcodes Leo as universal reviewer.
- `pipeline-health-check.py` parses comments but does not know required reviewers.
## Completion Percent And Remaining Delta
Current completion: 10 percent because `review_records` already exists.
Remaining delta:
1. Ensure eval integration writes one `review_records` row per required reviewer.
2. Update contributor attribution to prefer approved `review_records`.
3. Keep legacy fields as projection only.
4. Add optional route marker parsing to `pipeline-health-check.py`.
5. Add tests proving no partial-review false readiness.
## Closure, Endpoint, And Deployment Truth
Local closure:
- Tests prove contributor credit and stage classification respect required reviewers.
Staging closure:
- Staging proof artifact and health readback agree on required-agent completion.
Production closure:
- Production health does not show PRs as ready before all required agents approve.
## Critical Assumptions And Invalidators
Assumptions:
- `review_records` is available in production DB schema.
- Eval integration can write `review_records` for each required reviewer.
- Dashboards can tolerate legacy projections during Phase 1b.
Invalidators:
- Production DB lacks `review_records`.
- Contributor code path cannot query `review_records` without performance issues.
- Branch protection or merge logic uses legacy fields directly for readiness.
## State And Truth Contract
`review_records` becomes the compatibility source for per-agent reviewer history.
Required eval write:
```text
one review_records row per required reviewer per PR attempt
```
Legacy projection:
- `domain_agent = primary_agent`
- `domain_verdict = aggregate_verdict`
- `leo_verdict = actual Leo verdict when Leo is required, else skipped`
Route/audit JSON remains the source for `required_agents`.
## Measurement Contract
Minimum compatibility metrics:
- `review_records_written_count`
- `required_reviews_missing_count`
- `partial_review_not_ready_count`
- `contributor_evaluator_credit_count_by_agent`
Minimum proof:
- A two-agent PR with one approval and one missing verdict is not classified as ready.
- A two-agent PR with two approvals is classified as ready.
- Contributor credit includes both approved reviewers.
## Backend Work Required
Owned files:
- `lib/contributor.py`
- `lib/health.py`
- `pipeline-health-check.py`
- `tests/test_contributor.py` or new focused test.
- `tests/test_pipeline_health_phase1b.py` if added.
Implementation steps:
1. Confirm `review_records` exists in local schema and migrations.
2. Update eval integration spec to write review records per required reviewer.
3. Update contributor credit to prefer approved `review_records.reviewer` rows.
4. Fall back to legacy `leo_verdict` and `domain_verdict` for old data.
5. Update health output to include review records or route audit fields where available.
6. Update pipeline health check to read required-agent markers if present.
7. Add tests.
Forbidden work:
- Dashboard redesign.
- New leaderboard model.
- Broad schema migration before proof requires it.
## Frontend Work Required
None.
## Expected Runtime And User-Visible Behavior
Operators should see:
- Per-agent reviewer outcomes when available.
- Cross-domain PRs not marked ready until all required reviewers approve.
- Contributor credit reflecting actual approved reviewer agents.
Existing dashboard layout can remain unchanged if data is honest.
## Validation And Test Matrix
Commands:
```bash
python3 -m pytest tests/test_contributor.py tests/test_pipeline_health_phase1b.py
python3 -m ruff check lib/contributor.py lib/health.py pipeline-health-check.py tests
git diff --check
```
Test cases:
- old data fallback credits Leo/domain reviewer.
- new `review_records` data credits all approved required reviewers.
- request-changes reviewer receives no evaluator credit.
- one missing required reviewer blocks ready classification.
- all required reviewers approve enables ready classification.
## CI/CD, Release, And Pre-Push Gate Contract
Before PR:
- Compatibility tests pass or are documented as not runnable due missing dev deps.
Before staging:
- Staging proof includes health and contributor-readback commands.
Before production:
- Operator verifies no partial-review false readiness in logs/health readback.
## Independent CLI Audit Contract
Reviewer commands:
```bash
rg -n "Leo reviews every PR|leo_verdict|domain_verdict|review_records|required_agents|VERDICT" lib pipeline-health-check.py tests
sqlite3 /path/to/pipeline.db ".schema review_records"
```
Reviewer checks:
- `review_records` is preferred for new evaluator credit.
- Legacy fallback remains for old rows.
- Health does not rely on any-approve for multi-review readiness.
## Outside-The-Box Fix Paths
If `review_records` is insufficient:
- Add additive `route_json` and `agent_verdicts_json` columns to `prs`.
If `pipeline-health-check.py` cannot read route markers:
- Treat cross-domain PRs as awaiting review unless all verdict tags expected by route artifact are present.
If contributor credit is too risky for Phase 1b:
- Defer credit mutation and emit review-record-only proof until after eval stability.
## Maintenance Capture
Beneficial now:
- Replace comments claiming "Leo reviews every PR."
- Add focused tests for the compatibility projection.
Avoid now:
- Dashboard UI rewrite.
- Historical backfill.
- Leaderboard redesign.
## Parallelization And Fanout
Classification: ready_now after eval integration establishes review record writes.
Worker-ready prompt:
```text
make reporting and contributor attribution phase 1b-compatible. prefer review_records for new evaluator credit, preserve legacy fallback, and prevent health/pipeline checks from marking cross-domain prs ready before all required agents approve. do not redesign dashboards or add broad schema migrations unless tests prove necessary.
```
## Acceptance Criteria
- No code path claims Leo reviews every new Phase 1b PR.
- Approved `review_records` can credit all required reviewer agents.
- Health/check logic avoids partial-review false readiness.
- Legacy data still renders.
## Readiness And Claim Boundaries
Allowed claim:
- "Reporting compatibility is updated to avoid false readiness and credit loss."
Forbidden claim:
- "Dashboards are redesigned for Phase 1b."
## Spec Quality Self-Audit
All required execution-grade headings are present. This spec is intentionally compatibility-scoped and does not attempt a full reporting product redesign.
## Assistant-Added Caveats
The safest first move is to write accurate `review_records` and route audit JSON. Rich dashboards should wait until production behavior proves stable.

View file

@ -0,0 +1,18 @@
{
"phase": "1b",
"blocked_area": "staging_and_production_proof",
"attempted_discovery": [
"audited teleo-infrastructure eval, config, deploy, systemd, github feedback, and health-check surfaces",
"implemented and tested local default-off phase1b routing path",
"opened draft pr for reviewed sha",
"recorded staging proof contract in docs/phase1b/staging-proof-spec.md"
],
"exact_blocker": "no usable staging vps clone, crabbox runner config, sandbox decision-engine repo token, or production read-only access is available in this workspace",
"why_it_cannot_be_solved_autonomously": "staging proof requires external infrastructure authority and non-production credentials; creating or using those without the project owner/runtime owner would risk mutating production or leaking production secrets",
"exact_next_action": "fwaz or m3taversal should provide either a scrubbed hetzner snapshot clone or crabbox config plus staging-only github/openrouter tokens and the sandbox decision-engine repo target",
"safe_until_unblocked": [
"keep PHASE1B_AGENT_ROUTING_ENABLED=false in production",
"review the draft pr locally and in ci",
"do not allow agents to self-edit production vps state for this change"
]
}

View file

@ -0,0 +1,356 @@
# Phase 1b Child Spec: Staging Proof
Created: 2026-05-29
Status: active draft
Parent spec: `docs/phase1b-agent-routing-spec.md`
## Product Outcome Contract
Phase 1b must be tested without mutating the production VPS or production `decision-engine` PRs. A staging clone or disposable remote test box must prove routing, verdict posting, and merge or feedback behavior against a sandbox target before production cutover.
## Goal
Define the staging proof path for Phase 1b: provision an isolated production-like runtime, disable production authority, run six single-domain PR cycles plus one cross-domain PR cycle, save a machine-readable proof artifact, then destroy or shut down the staging environment.
## Non-Goals
- Do not mutate production PRs.
- Do not use production GitHub tokens in staging.
- Do not prove 24-hour production stability.
- Do not promote a mutated staging server as production.
- Do not test payment, wallet, Twitter, or mainnet flows.
## Current Implementation Audit
Known repo truth:
- `systemd/teleo-pipeline.service` defines the production-style pipeline service.
- `deploy/` contains deployment and mirror scripts.
- `docs/ARCHITECTURE.md` documents VPS path assumptions and SQLite state.
- `docs/INFRASTRUCTURE.md` documents production as Hetzner `77.42.65.182`, root path `/opt/teleo-eval`, diagnostics on port `8081`, and health on port `8080`.
- `deploy/auto-deploy.sh` pulls from `/opt/teleo-eval/workspaces/deploy-infra`, syncs code into runtime paths, restarts changed Python services, and updates `/opt/teleo-eval/.last-deploy-sha` after smoke checks.
- `systemd/teleo-pipeline.service` expects `/opt/teleo-eval/pipeline/fix-ownership.sh`, while this repo stores that script under `deploy/fix-ownership.sh`; staging bootstrap must verify the live runtime path before assuming the unit works.
- `handoff/phase1-step3-script-migration.md` documents GitHub migration posture and `decision-engine` target for scripts.
- `handoff/deprecated/eval-scripts.md` confirms old eval scripts are dead.
- Fwaz described the current production update path as `pull -> services recognize pull -> edit on VPS -> PR to Leo`; staging proof must treat that as an unsafe legacy behavior to replace, not as a release gate.
- Fwaz approved Crabbox as the long-term disposable staging/test-box direction.
Unknown production truth:
- Exact current deployed SHA.
- Whether production service files match this repo.
- Whether production still points at Forgejo in the live daemon.
- Exact restart/deploy commands used by Fwaz or agents.
- Current secrets layout.
- Current `systemctl cat` output for `teleo-pipeline`, `teleo-diagnostics`, auto-deploy timers, cron-like research jobs, Telegram-related services, and any agent daemons.
- Whether production has uncommitted hotfixes, generated scripts, or local service patches under `/opt/teleo-eval`.
- Read-only live access is not available in this workspace; the infrastructure audit attempted SSH readback and hit authentication denial, so no production SHA or service state should be claimed from this spec.
## Existing-Spec Inventory
| Existing doc | Relevance | Decision |
| --- | --- | --- |
| `docs/phase1b-agent-routing-spec.md` | Parent proof requirements. | Reuse. |
| `docs/ARCHITECTURE.md` | VPS topology and service assumptions. | Reuse with current-readback requirement. |
| `systemd/teleo-pipeline.service` | Service command template. | Reuse as staging baseline. |
| `handoff/phase1-step3-script-migration.md` | GitHub `decision-engine` target context. | Reuse. |
## Goal-Vs-Repo-Truth Diff
Goal:
- Staging proof runs against sandbox `decision-engine`.
- Production services and secrets are disabled before test daemon starts.
- Proof artifact captures routes, verdicts, final PR states, SHAs, DB schema, feature flags, and logs.
Repo truth:
- Staging automation does not exist.
- No proof script exists for seven PR cases.
- No machine-readable Phase 1b proof schema exists outside the umbrella spec.
## Completion Percent And Remaining Delta
Current completion: 0 percent.
Remaining delta:
1. Choose staging substrate: Hetzner snapshot clone, Crabbox, or another disposable test box.
2. Define sandbox repo.
3. Define staging secrets.
4. Write or run proof sequence.
5. Retain proof artifact.
6. Confirm staging cannot mutate production.
## Closure, Endpoint, And Deployment Truth
Staging closure means:
- Staging environment is isolated.
- Sandbox PRs are created and processed.
- Required reviewer verdicts appear in PR comments.
- Pipeline state transitions match expected behavior.
- Proof artifact exists.
Production closure is separate and requires exact reviewed SHA deployment plus 24-hour readback.
## Critical Assumptions And Invalidators
Assumptions:
- A VPS snapshot or disposable equivalent can run the pipeline.
- Production secrets can be removed or replaced before daemon start.
- A sandbox GitHub repo can be used.
- The proof can run without real production inference spend, or spend is explicitly approved.
Invalidators:
- Clone boots production services before quarantine.
- Sandbox target cannot receive PRs/comments.
- No operator has cloud or VPS access.
- Secrets cannot be separated from production.
- Service paths on production are materially different from repo docs.
## State And Truth Contract
Proof artifact path should be under staging, then copied back into the PR or retained artifact location. Suggested filename:
```text
proof/phase1b-staging-proof-YYYYMMDD-HHMMSS.json
```
Required JSON fields:
```json
{
"phase": "1b",
"schema_version": 1,
"environment": {
"kind": "hetzner_snapshot|crabbox|disposable_remote",
"host": "...",
"snapshot_id": "...",
"created_from_prod_host": "77.42.65.182"
},
"teleo_infrastructure_sha": "...",
"decision_engine_target": "living-ip/decision-engine-sandbox",
"pipeline_db_schema": 26,
"feature_flags": {"PHASE1B_AGENT_ROUTING_ENABLED": "true"},
"safety": {
"prod_services_disabled": true,
"prod_timers_disabled": true,
"prod_crons_disabled": true,
"prod_secrets_removed": true,
"auto_merge_constrained": true
},
"test_cases": [],
"verification_outputs": {
"service_status_path": "...",
"journal_excerpt_path": "...",
"db_snapshot_path": "...",
"github_comments_path": "..."
},
"rollback": {
"production_sha_before": "...",
"candidate_sha": "...",
"rollback_command": "..."
},
"created_at": "..."
}
```
Each test case:
```json
{
"case": "internet-finance",
"pr": 12,
"required_agents": ["Rio"],
"posted_verdicts": {"Rio": "approve"},
"final_state": "approved",
"route_kind": "single"
}
```
## Measurement Contract
Minimum staging cases:
- grand strategy -> Leo
- ai systems or ai alignment -> Theseus
- internet finance -> Rio
- health -> Vida
- entertainment -> Clay
- space, robotics, energy, or advanced manufacturing -> Astra
- cross-domain ai plus x402 -> Theseus and Rio
Pass criteria:
- 7 of 7 route decisions match expected required agents.
- 7 of 7 PRs receive parseable verdict comments.
- No production repo receives comments.
- No production service remains enabled during staging run.
## Backend Work Required
Owned surfaces:
- Staging host.
- Sandbox repo.
- Staging env/config.
- Proof artifact generator or manual proof script.
Implementation steps:
1. Snapshot or provision staging environment.
2. Block public/prod access.
3. Disable production services.
4. Remove production secrets.
5. Set hostname to staging.
6. Configure sandbox target.
7. Deploy exact implementation SHA.
8. Enable Phase 1b feature flag.
9. Create seven sandbox PRs.
10. Run pipeline until verdicts and states are visible.
11. Save proof artifact.
12. Shut down or destroy staging.
## Frontend Work Required
None.
## Expected Runtime And User-Visible Behavior
Operator sees:
- Staging service status.
- Sandbox PR comments with agent verdict tags.
- SQLite rows or logs showing route decisions.
- Proof artifact summarizing pass/fail.
No production user-visible behavior should change during staging.
## Validation And Test Matrix
Commands will vary by staging substrate. Baseline readback:
```bash
hostname
git -C /opt/teleo-eval/workspaces/deploy-infra rev-parse HEAD
cat /opt/teleo-eval/.last-deploy-sha
systemctl is-active teleo-pipeline teleo-diagnostics teleo-auto-deploy.timer
systemctl list-timers | grep -E 'teleo|sync|extract|research' || true
curl -s localhost:8080/health | python3 -m json.tool
journalctl -u teleo-pipeline --since "1 hour ago" --no-pager
sqlite3 /opt/teleo-eval/pipeline/pipeline.db "select max(version) from schema_version;"
sqlite3 /opt/teleo-eval/pipeline/pipeline.db "select number,status,domain,domain_agent,leo_verdict,domain_verdict,auto_merge,github_pr from prs order by number desc limit 20;"
gh pr list --repo living-ip/decision-engine-sandbox --state all
gh pr view --repo living-ip/decision-engine-sandbox PR_NUMBER --comments
```
Safety checks:
```bash
systemctl is-enabled teleo-pipeline
systemctl cat teleo-pipeline
systemctl cat teleo-diagnostics
grep -R "github-admin-token" /opt/teleo-eval/secrets 2>/dev/null
git -C /opt/teleo-eval/workspaces/main remote -v
```
## CI/CD, Release, And Pre-Push Gate Contract
Before staging:
- Code PR has passed local tests.
- Sandbox target selected.
- Staging secrets prepared.
Before production:
- Staging proof artifact exists.
- Exact SHA to deploy is recorded.
- Rollback path is recorded.
- Leo approval/signoff for the exact reviewed SHA is recorded.
- The production cutover avoids direct agent self-edits on the VPS.
## Independent CLI Audit Contract
Auditor should verify:
- Staging host is not production.
- Production services were disabled before test daemon start.
- GitHub target is sandbox.
- Proof artifact PR IDs belong to sandbox repo.
- Logs show no production mutation.
## Outside-The-Box Fix Paths
If Hetzner snapshot clone is too risky:
- Use Crabbox with a synced checkout and fake/sandbox services.
- Use a fresh Hetzner server and repo checkout instead of disk clone.
- Use local fake GitHub/Forgejo API for pure pipeline proof.
Substrate guidance:
- Prefer a Hetzner snapshot clone for canonical staging proof because it exercises `/opt/teleo-eval`, systemd units, timers, runtime user ownership, SQLite path assumptions, and deploy scripts.
- Crabbox is acceptable and preferred long-term as `disposable_remote` proof for command/test execution, but it does not count as VPS-clone fidelity unless it recreates the same unit files, runtime paths, service user, database path, and deploy flow.
- A local fake GitHub/Forgejo API can prove parser and state logic, but it cannot close the staging acceptance gate for real GitHub comments.
If inference spend is a concern:
- Mock agent review responses in staging.
- Use a staging-specific cheap model.
- Run only one real model call after mocked proof passes.
## Maintenance Capture
Beneficial now:
- Add a reusable `proof/phase1b` script later if manual staging repeats.
- Record exact service and config readback.
Avoid now:
- Building a full deployment platform.
- Giving Crabbox or staging production secrets.
- Replacing production with staging server.
## Parallelization And Fanout
Classification: draft_gated.
This can be delegated to Fwaz or the infrastructure owner after code PR exists.
Worker-ready prompt:
```text
run phase 1b staging proof without mutating production. provision or clone a staging box, disable production services and secrets before starting the daemon, point the runtime at a sandbox decision-engine repo, enable phase 1b routing, run six single-domain prs plus one cross-domain pr, and save a machine-readable proof artifact. do not touch production prs or production secrets.
```
## Acceptance Criteria
- Staging is isolated.
- Seven sandbox PR cases run.
- Required agents match expected matrix.
- Verdicts are parseable.
- Proof artifact exists.
- Staging is stopped or destroyed after proof.
## Readiness And Claim Boundaries
Allowed claim:
- "Phase 1b passed staging proof."
Forbidden claim:
- "Production Phase 1b is complete."
## Spec Quality Self-Audit
All required execution-grade headings are present. Exact production commands remain unknown until VPS truth is read back.
## Assistant-Added Caveats
Crabbox is useful here only as a disposable staging/test substrate. It should not receive production secrets until there is a deliberate security review.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
# Approved Claim Bundle Isolation Canary
Status: `pass`
## Working Target
A Cory-approved strict bundle must move through a separate reviewer credential,
an immutable approval snapshot, and a narrow apply credential before exact
claim/source/evidence/edge/reasoning-tool rows become canonical. A stale payload,
replay, hash collision, or direct privilege bypass must leave no partial rows.
## Final Runtime Proof
- Generic bundle: `37/37` checks passed. Its exact payload-controlled projection
and exact table deltas were `2` claims, `2` sources, `2` evidence links, `1`
edge, and `1` reasoning tool.
- Real Helmer bundle: `37/37` checks passed. Its exact payload-controlled
projection and exact table deltas were `5` claims, `13` sources, `17`
evidence links, `7` edges, and `1` reasoning tool.
- Helmer source proposal: `a64df080-8502-42e2-98f4-9bbdecb8da73`; strict child
proposal in the final isolated run: `87344d36-5005-5d88-a04c-067aeceda3c3`.
- `kb_review` approved the exact pending type and full JSON payload as mapped
reviewer `m3ta`; the immutable approval row matched the proposal ledger.
- `kb_apply` could not directly update proposal status/payload, immutable
approval rows, or existing `claim_evidence` rows.
- Mutating the approved ledger after SQL generation made the stale apply fail
before canonical writes; restoring the ledger left the immutable approval
unchanged.
- Replay was refused. A different source ID reusing an existing source hash was
refused, and its proposal/approval remained intact with zero partial rows.
- Every payload-controlled persisted field, including weights, roles, owners,
tags, hashes, and relationships, matched the reviewed payload. Exact table
deltas also exclude unexpected extra rows under unrelated IDs.
- The migration readback proves one expected signature per gate function, no
legacy overload, no protected-role membership, no object owned by a login
role, and the exact reviewer/apply execute split.
## Isolation And Cleanup
- Both runs used a new unexposed `postgres:16` container built from a read-only
schema/reference copy of live `teleo`; no cluster-wide role was created in the
production `teleo-pg` instance.
- The wrapper issued read-only queries to live `teleo-pg`; endpoint counts were
identical before and after each final run, with zero deltas for all six tables:
`claims=1837`, `sources=4145`, `claim_evidence=4670`, `claim_edges=4916`,
`reasoning_tools=17`, `kb_proposals=26`. This is endpoint-count evidence, not
a claim that every live row byte was compared.
- `leoclean-gateway.service` stayed `active/running`, `MainPID=2999690`,
`NRestarts=0`.
- Inner clone databases, outer Postgres containers, temporary workdirs, and the
remote source bundle were removed. Independent name/path readbacks found no
residue after cleanup.
- Both receipts retain repo-relative SHA-256/size bindings for the apply,
approval, prerequisite SQL, inner canary, and isolation wrapper source files;
a local recomputation matched every retained hash.
- No Telegram message, production canonical apply, production permission
migration, service restart, or runtime-profile write occurred.
## Reproduce
Generic bundle:
```bash
scripts/run_approve_claim_isolated_container_canary.sh \
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
```
Real normalized Helmer bundle:
```bash
scripts/run_approve_claim_isolated_container_canary.sh \
--normalization-json docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json \
--output docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json
```
## Evidence And Claim Ceiling
- Generic receipt: `approve-claim-clone-canary-current.json`
- Helmer receipt: `helmer-approve-claim-clone-canary-current.json`
- Lossless normalization: `helmer-approve-claim-normalization-current.json`
- Inner harness: `scripts/run_approve_claim_clone_canary.py`
- Isolation wrapper: `scripts/run_approve_claim_isolated_container_canary.sh`
This proves the repo-owned review/apply lifecycle at isolated runtime tier. It
does not mean the new code, reviewer/apply roles, or permission migration are
deployed on VPS production, and it does not mean Helmer is canonical there.
`kb_apply` remains an operator-only trusted canonical writer; the proof covers
the supplied tool and privilege matrix, not a compromised apply credential.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
# Argus KB State Probe
Generated UTC: `2026-07-10T02:48:11.734733+00:00`
Mode: `argus_kb_state_readonly_http_probe`
Status: `pass`
Base URL: `http://77.42.65.182:8081`
## Fetch Sources
- `kb_proposals`: `supplied_base64_json`
- `metrics`: `supplied_base64_json`
- `vital_signs`: `supplied_base64_json`
## Checks
- `approved_need_apply_payload`: `True`
- `approved_proposals_visible`: `True`
- `http_readback_ok`: `True`
- `kb_proposals_endpoint_read_only`: `True`
- `no_worker_applyable_approved_proposals`: `True`
## Approved Proposal State
- Total: `3`
- Status counts: `{"approved": 3}`
- Review state counts: `{"approved_needs_apply_payload": 3}`
- Worker-applyable count: `0`
- Read-only endpoint: `True`
## Proposal IDs
- `14fa5ecc-ac7a-41c1-807d-a2e85b936617`
- `ac036c9d-20a0-4ffe-881f-57d6b7bacf22`
- `a64df080-8502-42e2-98f4-9bbdecb8da73`
## Claim Ceiling
This is public Argus HTTP diagnostics evidence only. It proves the diagnostics surface can see approved KB proposals and their applyability state; it does not prove Docker/Postgres row counts, gateway systemd state, Telegram-visible reply quality, or production apply execution.

View file

@ -0,0 +1,73 @@
# Claim/Source Contract Preview - 2026-07-09
## Scope
Read-only contract preview for live approved rich KB proposals fetched from the VPS leoclean profile. This does not write Postgres, does not restart Leo, and does not change runtime behavior.
## Result
### `14fa5ecc-ac7a-41c1-807d-a2e85b936617`
- Status: `approved`
- Proposal type: `attach_evidence`
- Direct apply ready: `false`
- Claim rows needing canonical IDs: `4`
- Source rows needing canonical IDs/deduplication: `5`
- Edge fragments blocked until IDs/mapping exist: `17`
- Evidence fragments blocked until claim/source IDs exist: `9`
- Next action: Do not apply directly. Review a claim/source creation contract in a clone, assign/deduplicate canonical IDs, then rerun strict normalization to produce apply_payload child proposals.
Contract gaps:
- `current_apply_role_cannot_insert_claims_or_sources`: A separate reviewed role/contract for public.claims and public.sources creation.
- `claim_candidates_need_canonical_ids`: Create or deduplicate claim rows, then rewrite edges/evidence using UUIDs.
- `public_claims_has_no_body_or_review_note_columns`: Store those details in public.sources excerpts or add reviewed schema support.
- `public_sources_has_limited_metadata_columns`: Deduplicate sources and decide where provenance metadata is retained.
- `old_claim_supersession_is_many_successors`: Use typed claim_edges for supersession components or introduce an aggregate successor node.
- `concept_map_dependency_has_no_first_class_storage`: Create first-class concept-map storage or keep the concept map as source-backed context.
Claim candidates:
- `capital_allocation_is_civilizational_steering` -> type `structural`, confidence `0.65`, needs canonical id
- `capital_allocation_depends_on_distributed_market_intelligence` -> type `structural`, confidence `0.7`, needs canonical id
- `internet_finance_structural_advantages_capital_allocation` -> type `structural`, confidence `0.45`, needs canonical id
- `internet_finance_is_mechanism_not_telos` -> type `normative/strategic-boundary`, confidence `0.8`, needs canonical id
Source candidates:
- `m3ta_telegram_approval_supersede_split_2026_07_05` -> source_type `dm`, needs dedupe/canonical id
- `kb_existing_hayek_claim_7da4851c` -> source_type `note`, needs dedupe/canonical id
- `hayek_use_of_knowledge_in_society_fee_snapshot` -> source_type `url`, needs dedupe/canonical id
- `rio_belief_capital_allocation_civilizational_infrastructure` -> source_type `note`, needs dedupe/canonical id
- `leo_active_strategy_v2_context` -> source_type `note`, needs dedupe/canonical id
### `ac036c9d-20a0-4ffe-881f-57d6b7bacf22`
- Status: `approved`
- Proposal type: `attach_evidence`
- Direct apply ready: `false`
- Claim rows needing canonical IDs: `3`
- Source rows needing canonical IDs/deduplication: `3`
- Edge fragments blocked until IDs/mapping exist: `6`
- Evidence fragments blocked until claim/source IDs exist: `5`
- Next action: Do not apply directly. Review a claim/source creation contract in a clone, assign/deduplicate canonical IDs, then rerun strict normalization to produce apply_payload child proposals.
Contract gaps:
- `current_apply_role_cannot_insert_claims_or_sources`: A separate reviewed role/contract for public.claims and public.sources creation.
- `claim_candidates_need_canonical_ids`: Create or deduplicate claim rows, then rewrite edges/evidence using UUIDs.
- `public_claims_has_no_body_or_review_note_columns`: Store those details in public.sources excerpts or add reviewed schema support.
- `public_sources_has_limited_metadata_columns`: Deduplicate sources and decide where provenance metadata is retained.
Claim candidates:
- `claim_nodes_need_bodies` -> type `governance/process`, confidence `0.85`, needs canonical id
- `internet_finance_structural_advantages_body_draft` -> type `structural/forecast-like thesis`, confidence `0.45`, needs canonical id
- `hayek_reframed_price_system_distributed_knowledge` -> type `structural/reframing`, confidence `0.8`, needs canonical id
Source candidates:
- `telegram_attachment_teleo_agent_strategy_kernels_2026_06_30` -> source_type `note`, needs dedupe/canonical id
- `telegram_feedback_m3ta_claim_shape_2026_07_05` -> source_type `note`, needs dedupe/canonical id
- `hayek_use_of_knowledge_in_society_fee_snapshot` -> source_type `url`, needs dedupe/canonical id
## Claim Ceiling
This proves the live approved packets can be converted into a row-level contract preview. It does not prove those new claim/source rows should be inserted into canonical production tables without a clone/staging apply rehearsal and a reviewed creation contract.
JSON proof: `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/claim-source-contract-preview-20260709/claim-source-contract-preview-current.json`
Fetched live proposal bundle: `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/claim-source-contract-preview-20260709/approved-rich-proposals-live.json`

View file

@ -0,0 +1,110 @@
# Cory Expected Working-Leo Outcomes - 2026-07-09
This is the current working target for restoring Leo. It intentionally scopes out non-Leo route work and keeps the focus on Cory/m3taversal's expected Leo behavior: Telegram-visible reasoning, memory, truthful KB state, and approved KB changes becoming canonical DB state with proof.
## Working Target
A working Leo is a Telegram-reachable agent that:
1. Remembers the ongoing conversation and can use prior turns in the same operator context.
2. Grounds KB answers in canonical Teleo Postgres state, not only in memory or proposal text.
3. Clearly distinguishes `proposed`, `approved`, `applied`, and `not applied`.
4. Does not overclaim that an approval changed the DB when only a proposal ledger changed.
5. When Cory/m3taversal approves a specific concrete KB change, moves that change through a narrow guarded review/apply path into canonical `public.*` rows.
6. Retains Cory's caveats verbatim when an approved change is applied.
7. Produces row-level before/after proof and leaves the live Leo runtime stable.
## What Cory Most Likely Means By "Working"
The best-supported reading is:
- "Able to manipulate the knowledge base" means approved, concrete KB changes can land as real canonical rows, not just text in staging tables.
- "Same state as last night" means Leo is stuck in proposal/approval state and the canonical DB is not moving forward.
- Partner-demo pressure means the path must be visible and reliable through Telegram, not only through local operator scripts.
- Cory may tolerate a more direct write posture than we would design from scratch, but the restored safe behavior should still be auditable: propose, review, apply, read back rows.
## Current Truth
Live behavior is now Cory-working for the restored VPS Telegram memory/KB-audit path, with a clear remaining DB-contract gap:
- The live hosted HTTP Leo chat route still fails the KB-state and memory benchmark.
- The live VPS Telegram-shaped GatewayRunner handler now passes same-session memory and canonical KB audit when launched in the same user/cwd context as the service.
- The handler confirmed proposal `00957f6c-9883-4015-95a4-6b09367efb0e` is `applied` and edge `c167933e-d513-4f43-9335-d5d8aeb259f2` exists in `public.claim_edges`.
- The authenticated Chrome/Telegram Web live group canary now proves the same behavior in the real `Leo` Telegram group: Leo remembered marker `WL-LIVE-TG-CORY-20260709`, preserved `approved is not the same as applied`, and read back the applied proposal plus canonical edge row.
- Old approved/freeform proposals are not safely applyable as-is because they lack strict `payload.apply_payload` and, for `14fa5ecc`, the approved successor claim/source rows do not already exist as canonical IDs.
- Telegram bot/group readiness is proven, and Chrome/Telegram Web is authenticated/read/write-proven through Computer Use for bot testing in the `Leo` group.
Validated lower-tier behavior:
- The VPS service is alive and stable under the live harness.
- Hermes CLI session memory passed a two-turn canary, but that is not the same as Telegram-visible proof.
- One strict KB apply canary mutated canonical DB state without changing Leo runtime.
- The live GatewayRunner handler canary proves Telegram-shaped authorization, same-session memory, and canonical KB audit without posting to Telegram or writing synthetic live-profile sessions.
- A guarded approval helper is installed on VPS and rejects re-approval of an already-applied proposal.
- Approved-proposal normalization confirmed that old freeform approvals need canonical IDs or strict rewrites before apply.
- Claim/source contract preview now converts live approved rich proposals into row-level requirements:
- `14fa5ecc`: 4 claim rows, 5 source rows, 17 edge fragments, 9 evidence fragments
- `ac036c9d`: 3 claim rows, 3 source rows, 6 edge fragments, 5 evidence fragments
- Rollback-only rich proposal creation plan now validates the supported subset against live VPS schema without permanent production mutation:
- `14fa5ecc`: 4 claims, 9 sources, 11 evidence rows, 3 supported edges; 14 blocked fragments remain
- `ac036c9d`: 3 claims, 6 sources, 6 evidence rows, 1 supported edge; 5 blocked fragments remain
- before/after generated-row counts remained zero for claims, sources, claim_evidence, and claim_edges
- Disposable staging DB commit proof now shows the supported subset can commit outside production:
- staging DB after commit: 7 claims, 15 sources, 17 claim_evidence rows, 4 claim_edges
- production after staging commit: 0 generated claims, 0 generated sources, 0 generated claim_evidence rows, 0 generated claim_edges
- staging DB was dropped and temp files were cleaned up
- An isolated local Leo chat candidate passes the KB-state and memory benchmark, but it is not deployed.
## Evidence Index
- Working definition and chat excerpts: `outputs/working-leo-definition-20260709.md`, `outputs/working-leo-chat-excerpts-20260709.md`
- Reviewer convergence: `outputs/fable-working-leo-convergence-20260709/fable-working-leo-convergence-report.md`
- VPS live harness: `outputs/leo-vps-live-harness-20260709/leo-vps-live-harness-current-20260709.md`
- Canonical apply canary: `outputs/kb-apply-canary-20260709/kb-apply-canary-execute-current.md`
- Telegram handler canary: `outputs/telegram-gateway-handler-canary-20260709/telegram-gateway-handler-canary-current.md`
- Live Telegram group canary: `outputs/telegram-live-canary-20260709/telegram-live-canary-current.md`
- Old approval normalization: `outputs/approved-proposal-normalization-20260709/approved-proposal-normalization-current.md`
- Claim/source contract preview: `outputs/claim-source-contract-preview-20260709/claim-source-contract-preview-current.md`
- Rich proposal creation plan: `outputs/rich-proposal-creation-plan-20260709/rich-proposal-creation-plan-current.md`
- Execution plan: `outputs/working-leo-execution-plan-20260709/working-leo-execution-plan-current.md`
- Live chat benchmark failure: `outputs/leo-http-chat-benchmark-20260709/leo-http-chat-benchmark-current.md`
- Local candidate chat benchmark pass: `outputs/leo-http-chat-benchmark-20260709/leo-http-chat-benchmark-http-localhost-3129-api-agents-leo-chat.md`
- Telegram readiness proof: `outputs/telegram-visible-readiness-20260709/telegram-visible-readiness-current.md`
- Telegram access supersession: `outputs/telegram-visible-readiness-20260709/telegram-access-supersession-20260709.md`
- Current state report: `outputs/working-leo-current-state-20260709.md`
- Current state image: `outputs/leo-db-state-06-current-proof.svg`
- Current live proof image: `outputs/leo-db-state-07-live-proof.svg`
- Rich proposal plan image: `outputs/leo-db-state-08-rich-plan.svg`
- Staging commit proof image: `outputs/leo-db-state-09-staging-commit.svg`
- Outcome image: `outputs/working-leo-cory-expected-outcomes.png`
## Benchmark Bar
The benchmark for Cory-working Leo should pass these checks:
1. Telegram visibility: passed in the live `Leo` group.
2. Memory: passed with marker `WL-LIVE-TG-CORY-20260709` and the correction from the prior turn.
3. KB audit: passed for proposal `00957f6c-9883-4015-95a4-6b09367efb0e` and edge `c167933e-d513-4f43-9335-d5d8aeb259f2`.
4. Truthfulness: passed for the tested applied canary and prior approved-vs-applied context.
5. Strict apply: passed for one approved concrete `add_edge` change with before/after row proof.
6. Caveat retention: passed for the strict canary approval note; old freeform caveat retention remains part of the claim/source creation contract.
7. Old approval handling: partially passed; old freeform approvals are now normalized into row-level requirements and explicitly not silently applied.
8. Runtime stability: passed; service stayed active/running with `NRestarts=0`. The latest direct-claim suite ran after the expected skill-change restart and stayed on `MainPID=1908033` throughout the harness.
9. Broad Cory-style outcomes and no-context direct claims: specified in the open-ended benchmark as nine disposable-clone/sandbox-first outcome scenarios plus six direct-claim/follow-up scenarios. The mixed-evidence sandbox score passes `20/20` using retained live Telegram evidence for `OE-01` through `OE-05`, fixture answers for `CS-01` through `CS-09`, and direct-claim expected-answer/follow-up fixtures for `DC-01` through `DC-06`; the CS rows are not live Telegram proof. A live VPS GatewayRunner handler run of exact `DC-01` through `DC-06` now returns replies with no DB/service side effects and strict-scores `6/6`, so no-context direct-claim behavior is handler-proven on VPS.
## Scope Boundaries
- Do not broaden this into parked route work before Leo meets the Cory-working bar.
- Do not introduce new naming tied to the parked lane in new artifacts or harnesses.
- Do not deploy the isolated local candidate until the Leo/Kb/memory behavior is reviewed and the Telegram-visible test path is ready.
- Keep the local chat-route candidate as evidence only for now; do not promote it as the Cory fix while its surrounding route surface is outside the current scope.
- Do not auto-apply old freeform approvals without canonical IDs, strict payloads, and explicit review.
- Do not give Leo broad autonomous DB write authority as part of "restoration"; that would be a feature change.
## Next Actions
1. Keep the live Telegram group canary as the top-level user-visible proof and the handler direct-claim suite as the non-posting regression harness.
2. Keep the direct-claim handler suite green: `DC-02` must recognize approved Helmer staging row `a64df080-8502-42e2-98f4-9bbdecb8da73` as approved/unapplied, and `DC-03` must not infer decision-matrix approval while decision-matrix tables are absent.
3. Do not auto-apply old freeform approvals.
4. For `14fa5ecc`, identify or create canonical claim/source rows before producing strict child proposals; otherwise treat claim/source creation support as a separate guarded contract expansion, preferably in a clone first.
5. Re-review and apply only strict proposals with row-level proof and runtime stability readback.

View file

@ -0,0 +1,31 @@
\set ON_ERROR_STOP on
begin;
do $$
declare
updated int;
begin
if not exists (select 1 from public.claims where id = 'e6ce4e36-15d2-53bc-8b74-dda0efc095c1'::uuid) then
raise exception 'cross-surface packet requires Claim D % to exist before anchor update', 'e6ce4e36-15d2-53bc-8b74-dda0efc095c1';
end if;
if not exists (select 1 from public.sources where id = '9a0d2429-b98b-585a-980c-0176db7f9fe7'::uuid) then
raise exception 'cross-surface packet requires approval source % to exist before anchor update', '9a0d2429-b98b-585a-980c-0176db7f9fe7';
end if;
update public.strategy_node_anchors
set claim_id = 'e6ce4e36-15d2-53bc-8b74-dda0efc095c1'::uuid,
weight = 0.85,
note = 'Mapped rich proposal 14fa5ecc applied: active Leo policy now anchors to Claim D (internet finance is a candidate mechanism, not the telos) instead of the retired compressed internet-finance claim.'
where id = '47f128a0-36c5-4b60-97d4-48c32fa48bc7'::uuid
and from_node_id = '52bc4264-330e-489d-920c-a493fd2d985a'::uuid
and anchor_role = 'justified_by'
and claim_id = 'd0000000-0000-0000-0000-000000000005'::uuid;
get diagnostics updated = row_count;
if updated <> 1 then
raise exception 'cross-surface anchor update affected % row(s), expected 1; anchor was not at old claim state', updated;
end if;
end $$;
commit;

View file

@ -0,0 +1,314 @@
cross-surface rehearsal started UTC 2026-07-09T21:35:41Z
copy packet files into teleo-pg container
drop old disposable DB if present
DROP DATABASE
create disposable DB teleo_cross_surface_rehearsal_20260709 from pg_dump of production teleo
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
clone base preflight before mapped packet commit
id | status | proposal_type | reviewed_by_handle | reviewed_at
--------------------------------------+----------+-----------------+--------------------+-------------------------------
14fa5ecc-ac7a-41c1-807d-a2e85b936617 | approved | attach_evidence | m3ta | 2026-07-05 19:30:48.707181+00
ac036c9d-20a0-4ffe-881f-57d6b7bacf22 | approved | attach_evidence | m3ta | 2026-07-05 19:11:40.673615+00
(2 rows)
check_name | id | reason
------------+----+--------
(0 rows)
expected_claims | expected_sources | expected_claim_edges | expected_claim_evidence
-----------------+------------------+----------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
0 | 0 | 0 | 0
(1 row)
clone base mapped packet authorized commit
BEGIN
SET
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
COMMIT
BEGIN
SET
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
COMMIT
clone base mapped packet postflight
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
id | type | confidence | text
--------------------------------------+------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | structural | 0.65 | Capital allocation is a form of civilizational steering because it determines which futures receive resources.
62e82ee8-b204-573b-9707-eb5d832b9d9e | structural | 0.45 | Internet finance has structural advantages that make it likely to become an increasingly important capital-allocation mechanism.
67003e10-e1ee-5905-8b51-1b0cb263ec11 | structural | 0.45 | Internet finance has structural advantages over traditional finance and is likely to become an increasingly important capital-allocation mechanism.
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | meta | 0.85 | KB claim nodes need explanatory bodies, not only one-sentence headlines.
cd8daac8-039c-5349-9c50-02f622e73b03 | structural | 0.8 | Hayeks most relevant contribution for Teleo is that prices coordinate dispersed, local, time-sensitive knowledge without central possession of that knowledge.
d009dfc4-9755-58a7-affb-80e3e842a775 | structural | 0.7 | Capital allocation depends on distributed knowledge, incentives, liquidity, and feedback loops.
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | normative | 0.8 | Internet finance is not Teleos telos; it is a candidate mechanism for pursuing the telos.
(7 rows)
id | source_type | excerpt
--------------------------------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
244a4c38-86e5-5910-91f4-6d71529e9b31 | other | {"body": "The existing KB Hayek claim says economic planning requires local and global information that are never simultaneously available to decision makers. That is directionally related, but the stronger Teleo-relevan
33fadb51-0bd8-5f2d-87fe-758a3dd7fac0 | article | {"excerpt": "There is beyond question a body of very important but unorganized knowledge... the knowledge of the particular circumstances of time and place.\n\nBy the price system... all the relevant information is conce
3a57238c-ca31-5fe1-bece-e5fb2f174e32 | other | {"excerpt": "Capital allocation is civilizational infrastructure, not a financial service.", "metadata": {"source_key": "rio_belief_capital_allocation_civilizational_infrastructure", "source_quality": "canonical agent be
4787c38e-74cb-5bf5-93e1-0cc9c290684c | other | {"excerpt": "Nodes should have a headline thats the claim belief or concept map etc, but also a body elaborating what the claim means.\n\nStandardize these nodes as: Headline, Body, Edges, Evidence.\n\nFor Hayek that is
4cc0c7ee-2166-5cae-9065-ea226fa2be5b | article | {"excerpt": "There is beyond question a body of very important but unorganized knowledge... the knowledge of the particular circumstances of time and place.\n\nBy the price system... all the relevant information is conce
6c3c26c1-39bc-58e5-8c76-28b5712420ce | other | {"body": "Internet finance may improve capital allocation by making ownership, capital formation, settlement, liquidity, governance, and market feedback more digitally native, programmable, global, and composable. The th
703fdfaf-22c9-5f5b-8d29-cf238d5e156f | other | {"excerpt": "# Teleo Collective Telos and Agent Strategy Kernels ... This is a working synthesis, not canonical KB state.\n\nCore Strategic Tools are named and then elaborated with short explanatory bodies: Inflection ma
87f49cf0-5516-5aef-8ed9-f69292917d32 | other | {"excerpt": "Hayek's knowledge problem reveals that economic planning requires both local and global information which are never simultaneously available to decision makers", "metadata": {"claim_id": "7da4851c-0aac-4084-
8f952541-5f12-5f24-88b3-21e7842163a1 | other | {"excerpt": "Humanity is in the early innings of a species-level inflection... changes in how society distributes value, forms capital, and coordinates around shared futures.", "metadata": {"source_key": "leo_active_stra
997e5e58-b20d-560a-a73a-eaa4bff1dc3e | other | {"body": "Teleos telos is emergent collective superintelligence oriented toward better futures, not internet finance for its own sake. Internet finance matters insofar as it can help coordinate capital, incentives, fore
9a0d2429-b98b-585a-980c-0176db7f9fe7 | dm | {"excerpt": "Yes lets do this. do the work for this, then come back to me with what you'd like me to approve\n\nOld claim: Internet finance is the coordination mechanism for capital allocation — capital allocation is ex
9fd61e7e-d5cb-5904-b5c2-a539946f7c96 | other | {"body": "Good capital allocation requires more than a central thesis about what matters. It needs distributed knowledge about local opportunities, credible incentives that reward truth-seeking and execution, liquidity o
a3cba49b-7016-5f42-9af6-041f12371a24 | other | {"body": "A one-sentence claim can be useful as a handle, but it often under-specifies scope, mechanism, caveats, and what evidence would actually support it. Bodies make claims reviewable by explaining intended meaning
eea07732-c65d-5db6-88d6-1ab08511eca6 | other | {"body": "This does not mean “crypto wins because crypto.” It means digitally native capital formation, programmable governance, global distribution, lower intermediation costs, faster liquidity, composable ownership, an
f80c01fe-63bb-5ccf-9bdd-744bab0e9163 | other | {"body": "Capital allocation is not merely financial plumbing. It decides which companies, technologies, infrastructures, institutions, and narratives get enough resources to become real. Because many possible futures co
(15 rows)
from_claim | to_claim | edge_type
--------------------------------------+--------------------------------------+--------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | d0000000-0000-0000-0000-000000000005 | supersedes
62e82ee8-b204-573b-9707-eb5d832b9d9e | c0000000-0000-0000-0000-00000000000c | relates
62e82ee8-b204-573b-9707-eb5d832b9d9e | cd8daac8-039c-5349-9c50-02f622e73b03 | derives_from
67003e10-e1ee-5905-8b51-1b0cb263ec11 | d0000000-0000-0000-0000-000000000005 | supersedes
67003e10-e1ee-5905-8b51-1b0cb263ec11 | d009dfc4-9755-58a7-affb-80e3e842a775 | requires
cd8daac8-039c-5349-9c50-02f622e73b03 | 62e82ee8-b204-573b-9707-eb5d832b9d9e | supports
cd8daac8-039c-5349-9c50-02f622e73b03 | 7da4851c-0aac-4084-a99f-baeeeb3aa23e | supersedes
d009dfc4-9755-58a7-affb-80e3e842a775 | 7da4851c-0aac-4084-a99f-baeeeb3aa23e | relates
d009dfc4-9755-58a7-affb-80e3e842a775 | d0000000-0000-0000-0000-000000000005 | supersedes
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 67003e10-e1ee-5905-8b51-1b0cb263ec11 | constrains
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | d0000000-0000-0000-0000-000000000005 | supersedes
(11 rows)
claim_id | source_id | role
--------------------------------------+--------------------------------------+-------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | 3a57238c-ca31-5fe1-bece-e5fb2f174e32 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | 8f952541-5f12-5f24-88b3-21e7842163a1 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | f80c01fe-63bb-5ccf-9bdd-744bab0e9163 | illustrates
62e82ee8-b204-573b-9707-eb5d832b9d9e | 4787c38e-74cb-5bf5-93e1-0cc9c290684c | grounds
62e82ee8-b204-573b-9707-eb5d832b9d9e | eea07732-c65d-5db6-88d6-1ab08511eca6 | illustrates
67003e10-e1ee-5905-8b51-1b0cb263ec11 | 6c3c26c1-39bc-58e5-8c76-28b5712420ce | illustrates
67003e10-e1ee-5905-8b51-1b0cb263ec11 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | 4787c38e-74cb-5bf5-93e1-0cc9c290684c | grounds
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | a3cba49b-7016-5f42-9af6-041f12371a24 | illustrates
cd8daac8-039c-5349-9c50-02f622e73b03 | 244a4c38-86e5-5910-91f4-6d71529e9b31 | illustrates
cd8daac8-039c-5349-9c50-02f622e73b03 | 4cc0c7ee-2166-5cae-9065-ea226fa2be5b | grounds
d009dfc4-9755-58a7-affb-80e3e842a775 | 33fadb51-0bd8-5f2d-87fe-758a3dd7fac0 | grounds
d009dfc4-9755-58a7-affb-80e3e842a775 | 9fd61e7e-d5cb-5904-b5c2-a539946f7c96 | illustrates
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 8f952541-5f12-5f24-88b3-21e7842163a1 | grounds
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 997e5e58-b20d-560a-a73a-eaa4bff1dc3e | illustrates
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
(17 rows)
id | superseded_by
--------------------------------------+--------------------------------------
7da4851c-0aac-4084-a99f-baeeeb3aa23e | cd8daac8-039c-5349-9c50-02f622e73b03
d0000000-0000-0000-0000-000000000005 | e6ce4e36-15d2-53bc-8b74-dda0efc095c1
(2 rows)
clone cross-surface preflight after base packet
check_name | id | from_node_id | anchor_role | claim_id | source_id | note
-----------------------+--------------------------------------+--------------------------------------+--------------+--------------------------------------+-----------+----------------------------------------------------------------------------------------------------------------------------------------
policy_anchor_current | 47f128a0-36c5-4b60-97d4-48c32fa48bc7 | 52bc4264-330e-489d-920c-a493fd2d985a | justified_by | d0000000-0000-0000-0000-000000000005 | | Telos/policy grounding approved by m3ta: capital allocation expresses priorities and must be coordinated with collective intelligence.
(1 row)
check_name | id | status | text
---------------------+--------------------------------------+--------+--------------------------------------------------------------------------------------------
base_claim_d_exists | e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | open | Internet finance is not Teleos telos; it is a candidate mechanism for pursuing the telos.
(1 row)
check_name | id | source_type | excerpt
------------------------+--------------------------------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
approval_source_exists | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | dm | {"excerpt": "Yes lets do this. do the work for this, then come back to me with what you'd like me to approve\n\nOld claim: Internet finance is the coordination mechanism for capital allocation — capital allocation is ex
(1 row)
check_name | id | from_node_id | anchor_role | to_shared_root_id | note
-----------------------------+--------------------------------------+--------------------------------------+-------------+--------------------------------------+---------------------------------------------------------------------------------------
telos_anchor_already_exists | c7dd984c-ac1b-43f9-9996-576ddf61c6d2 | 52bc4264-330e-489d-920c-a493fd2d985a | serves | 13100000-0000-0000-0000-000000000001 | Approved by m3ta: policy serves Teleo telos of emergent collective superintelligence.
(1 row)
check_name | id | name | criteria | evidence_bar
-----------------------+--------------------------------------+--------------+------------------------------------------------------------------------------------------------------------+-------------------------------------
governance_gate_known | 45695897-2a9c-4279-ac25-e59f166dec6a | evidence-bar | likely needs empirical data; experimental = coherent argument, limited validation; speculative = untested. | likely > experimental > speculative
governance_gate_known | fa88e31b-6fca-4801-bf72-f5db81950a11 | quality-gate | Specific enough to disagree with; evidence traceable; not a duplicate; right domain reviewers. |
(2 rows)
clone cross-surface authorized commit
BEGIN
DO
COMMIT
clone cross-surface postflight after anchor update
id | from_node_id | anchor_role | claim_id | source_id | weight | note
--------------------------------------+--------------------------------------+--------------+--------------------------------------+-----------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
47f128a0-36c5-4b60-97d4-48c32fa48bc7 | 52bc4264-330e-489d-920c-a493fd2d985a | justified_by | e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | | 0.85 | Mapped rich proposal 14fa5ecc applied: active Leo policy now anchors to Claim D (internet finance is a candidate mechanism, not the telos) instead of the retired compressed internet-finance claim.
(1 row)
id | status | superseded_by | text
--------------------------------------+--------+--------------------------------------+---------------------------------------------------------------------------------------------------------------------
d0000000-0000-0000-0000-000000000005 | open | e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | Internet finance is the coordination mechanism for capital allocation — capital allocation is expressed priorities.
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | open | | Internet finance is not Teleos telos; it is a candidate mechanism for pursuing the telos.
(2 rows)
id | anchor_role | to_shared_root_id | note
--------------------------------------+-------------+--------------------------------------+---------------------------------------------------------------------------------------
c7dd984c-ac1b-43f9-9996-576ddf61c6d2 | serves | 13100000-0000-0000-0000-000000000001 | Approved by m3ta: policy serves Teleo telos of emergent collective superintelligence.
(1 row)
clone direct anchor assertion after cross-surface commit
check_name | claim_id | source_id | weight | note
--------------------------+--------------------------------------+-----------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------
clone_anchor_after_cross | e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | | 0.85 | Mapped rich proposal 14fa5ecc applied: active Leo policy now anchors to Claim D (internet finance is a candidate mechanism, not the telos) instead of the retire
(1 row)
clone cross-surface delete rollback
BEGIN
DO
COMMIT
clone base mapped packet delete rollback
BEGIN
DELETE 17
DELETE 11
UPDATE 2
DELETE 15
DELETE 7
COMMIT
clone base preflight after both rollbacks
id | status | proposal_type | reviewed_by_handle | reviewed_at
--------------------------------------+----------+-----------------+--------------------+-------------------------------
14fa5ecc-ac7a-41c1-807d-a2e85b936617 | approved | attach_evidence | m3ta | 2026-07-05 19:30:48.707181+00
ac036c9d-20a0-4ffe-881f-57d6b7bacf22 | approved | attach_evidence | m3ta | 2026-07-05 19:11:40.673615+00
(2 rows)
check_name | id | reason
------------+----+--------
(0 rows)
expected_claims | expected_sources | expected_claim_edges | expected_claim_evidence
-----------------+------------------+----------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
0 | 0 | 0 | 0
(1 row)
clone direct anchor assertion after both rollbacks
check_name | claim_id | source_id | weight | note
-----------------------------+--------------------------------------+-----------+--------+----------------------------------------------------------------------------------------------------------------------------------------
clone_anchor_after_rollback | d0000000-0000-0000-0000-000000000005 | | 0.7 | Telos/policy grounding approved by m3ta: capital allocation expresses priorities and must be coordinated with collective intelligence.
(1 row)
production remains unchanged: base generated counts and anchor
id | status | proposal_type | reviewed_by_handle | reviewed_at
--------------------------------------+----------+-----------------+--------------------+-------------------------------
14fa5ecc-ac7a-41c1-807d-a2e85b936617 | approved | attach_evidence | m3ta | 2026-07-05 19:30:48.707181+00
ac036c9d-20a0-4ffe-881f-57d6b7bacf22 | approved | attach_evidence | m3ta | 2026-07-05 19:11:40.673615+00
(2 rows)
check_name | id | reason
------------+----+--------
(0 rows)
expected_claims | expected_sources | expected_claim_edges | expected_claim_evidence
-----------------+------------------+----------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
0 | 0 | 0 | 0
(1 row)
check_name | claim_id | source_id | weight | note
-------------------------+--------------------------------------+-----------+--------+----------------------------------------------------------------------------------------------------------------------------------------
production_anchor_state | d0000000-0000-0000-0000-000000000005 | | 0.7 | Telos/policy grounding approved by m3ta: capital allocation expresses priorities and must be coordinated with collective intelligence.
(1 row)
drop disposable DB and remove temp packet files
DROP DATABASE
cleanup readback
disposable_db_exists
----------------------
0
(1 row)
host_tmp_removed yes
container_tmp_removed yes
active
3252143
0
cross-surface rehearsal completed UTC 2026-07-09T21:35:46Z

View file

@ -0,0 +1,210 @@
cross-surface rehearsal started UTC 2026-07-09T21:28:54Z
copy packet files into teleo-pg container
drop old disposable DB if present
DROP DATABASE
create disposable DB teleo_cross_surface_rehearsal_20260709 from pg_dump of production teleo
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
clone base preflight before mapped packet commit
id | status | proposal_type | reviewed_by_handle | reviewed_at
--------------------------------------+----------+-----------------+--------------------+-------------------------------
14fa5ecc-ac7a-41c1-807d-a2e85b936617 | approved | attach_evidence | m3ta | 2026-07-05 19:30:48.707181+00
ac036c9d-20a0-4ffe-881f-57d6b7bacf22 | approved | attach_evidence | m3ta | 2026-07-05 19:11:40.673615+00
(2 rows)
check_name | id | reason
------------+----+--------
(0 rows)
expected_claims | expected_sources | expected_claim_edges | expected_claim_evidence
-----------------+------------------+----------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
0 | 0 | 0 | 0
(1 row)
clone base mapped packet authorized commit
BEGIN
SET
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
COMMIT
BEGIN
SET
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
COMMIT
clone base mapped packet postflight
generated_claim_rows | generated_source_rows | generated_edge_rows | generated_evidence_rows
----------------------+-----------------------+---------------------+-------------------------
7 | 15 | 11 | 17
(1 row)
id | type | confidence | text
--------------------------------------+------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | structural | 0.65 | Capital allocation is a form of civilizational steering because it determines which futures receive resources.
62e82ee8-b204-573b-9707-eb5d832b9d9e | structural | 0.45 | Internet finance has structural advantages that make it likely to become an increasingly important capital-allocation mechanism.
67003e10-e1ee-5905-8b51-1b0cb263ec11 | structural | 0.45 | Internet finance has structural advantages over traditional finance and is likely to become an increasingly important capital-allocation mechanism.
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | meta | 0.85 | KB claim nodes need explanatory bodies, not only one-sentence headlines.
cd8daac8-039c-5349-9c50-02f622e73b03 | structural | 0.8 | Hayeks most relevant contribution for Teleo is that prices coordinate dispersed, local, time-sensitive knowledge without central possession of that knowledge.
d009dfc4-9755-58a7-affb-80e3e842a775 | structural | 0.7 | Capital allocation depends on distributed knowledge, incentives, liquidity, and feedback loops.
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | normative | 0.8 | Internet finance is not Teleos telos; it is a candidate mechanism for pursuing the telos.
(7 rows)
id | source_type | excerpt
--------------------------------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
244a4c38-86e5-5910-91f4-6d71529e9b31 | other | {"body": "The existing KB Hayek claim says economic planning requires local and global information that are never simultaneously available to decision makers. That is directionally related, but the stronger Teleo-relevan
33fadb51-0bd8-5f2d-87fe-758a3dd7fac0 | article | {"excerpt": "There is beyond question a body of very important but unorganized knowledge... the knowledge of the particular circumstances of time and place.\n\nBy the price system... all the relevant information is conce
3a57238c-ca31-5fe1-bece-e5fb2f174e32 | other | {"excerpt": "Capital allocation is civilizational infrastructure, not a financial service.", "metadata": {"source_key": "rio_belief_capital_allocation_civilizational_infrastructure", "source_quality": "canonical agent be
4787c38e-74cb-5bf5-93e1-0cc9c290684c | other | {"excerpt": "Nodes should have a headline thats the claim belief or concept map etc, but also a body elaborating what the claim means.\n\nStandardize these nodes as: Headline, Body, Edges, Evidence.\n\nFor Hayek that is
4cc0c7ee-2166-5cae-9065-ea226fa2be5b | article | {"excerpt": "There is beyond question a body of very important but unorganized knowledge... the knowledge of the particular circumstances of time and place.\n\nBy the price system... all the relevant information is conce
6c3c26c1-39bc-58e5-8c76-28b5712420ce | other | {"body": "Internet finance may improve capital allocation by making ownership, capital formation, settlement, liquidity, governance, and market feedback more digitally native, programmable, global, and composable. The th
703fdfaf-22c9-5f5b-8d29-cf238d5e156f | other | {"excerpt": "# Teleo Collective Telos and Agent Strategy Kernels ... This is a working synthesis, not canonical KB state.\n\nCore Strategic Tools are named and then elaborated with short explanatory bodies: Inflection ma
87f49cf0-5516-5aef-8ed9-f69292917d32 | other | {"excerpt": "Hayek's knowledge problem reveals that economic planning requires both local and global information which are never simultaneously available to decision makers", "metadata": {"claim_id": "7da4851c-0aac-4084-
8f952541-5f12-5f24-88b3-21e7842163a1 | other | {"excerpt": "Humanity is in the early innings of a species-level inflection... changes in how society distributes value, forms capital, and coordinates around shared futures.", "metadata": {"source_key": "leo_active_stra
997e5e58-b20d-560a-a73a-eaa4bff1dc3e | other | {"body": "Teleos telos is emergent collective superintelligence oriented toward better futures, not internet finance for its own sake. Internet finance matters insofar as it can help coordinate capital, incentives, fore
9a0d2429-b98b-585a-980c-0176db7f9fe7 | dm | {"excerpt": "Yes lets do this. do the work for this, then come back to me with what you'd like me to approve\n\nOld claim: Internet finance is the coordination mechanism for capital allocation — capital allocation is ex
9fd61e7e-d5cb-5904-b5c2-a539946f7c96 | other | {"body": "Good capital allocation requires more than a central thesis about what matters. It needs distributed knowledge about local opportunities, credible incentives that reward truth-seeking and execution, liquidity o
a3cba49b-7016-5f42-9af6-041f12371a24 | other | {"body": "A one-sentence claim can be useful as a handle, but it often under-specifies scope, mechanism, caveats, and what evidence would actually support it. Bodies make claims reviewable by explaining intended meaning
eea07732-c65d-5db6-88d6-1ab08511eca6 | other | {"body": "This does not mean “crypto wins because crypto.” It means digitally native capital formation, programmable governance, global distribution, lower intermediation costs, faster liquidity, composable ownership, an
f80c01fe-63bb-5ccf-9bdd-744bab0e9163 | other | {"body": "Capital allocation is not merely financial plumbing. It decides which companies, technologies, infrastructures, institutions, and narratives get enough resources to become real. Because many possible futures co
(15 rows)
from_claim | to_claim | edge_type
--------------------------------------+--------------------------------------+--------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | d0000000-0000-0000-0000-000000000005 | supersedes
62e82ee8-b204-573b-9707-eb5d832b9d9e | c0000000-0000-0000-0000-00000000000c | relates
62e82ee8-b204-573b-9707-eb5d832b9d9e | cd8daac8-039c-5349-9c50-02f622e73b03 | derives_from
67003e10-e1ee-5905-8b51-1b0cb263ec11 | d0000000-0000-0000-0000-000000000005 | supersedes
67003e10-e1ee-5905-8b51-1b0cb263ec11 | d009dfc4-9755-58a7-affb-80e3e842a775 | requires
cd8daac8-039c-5349-9c50-02f622e73b03 | 62e82ee8-b204-573b-9707-eb5d832b9d9e | supports
cd8daac8-039c-5349-9c50-02f622e73b03 | 7da4851c-0aac-4084-a99f-baeeeb3aa23e | supersedes
d009dfc4-9755-58a7-affb-80e3e842a775 | 7da4851c-0aac-4084-a99f-baeeeb3aa23e | relates
d009dfc4-9755-58a7-affb-80e3e842a775 | d0000000-0000-0000-0000-000000000005 | supersedes
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 67003e10-e1ee-5905-8b51-1b0cb263ec11 | constrains
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | d0000000-0000-0000-0000-000000000005 | supersedes
(11 rows)
claim_id | source_id | role
--------------------------------------+--------------------------------------+-------------
004195cc-ab62-526c-90fb-51cb0f826ec0 | 3a57238c-ca31-5fe1-bece-e5fb2f174e32 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | 8f952541-5f12-5f24-88b3-21e7842163a1 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
004195cc-ab62-526c-90fb-51cb0f826ec0 | f80c01fe-63bb-5ccf-9bdd-744bab0e9163 | illustrates
62e82ee8-b204-573b-9707-eb5d832b9d9e | 4787c38e-74cb-5bf5-93e1-0cc9c290684c | grounds
62e82ee8-b204-573b-9707-eb5d832b9d9e | eea07732-c65d-5db6-88d6-1ab08511eca6 | illustrates
67003e10-e1ee-5905-8b51-1b0cb263ec11 | 6c3c26c1-39bc-58e5-8c76-28b5712420ce | illustrates
67003e10-e1ee-5905-8b51-1b0cb263ec11 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | 4787c38e-74cb-5bf5-93e1-0cc9c290684c | grounds
9bcf7e6b-4e98-52b1-91ef-96cfc15e3df0 | a3cba49b-7016-5f42-9af6-041f12371a24 | illustrates
cd8daac8-039c-5349-9c50-02f622e73b03 | 244a4c38-86e5-5910-91f4-6d71529e9b31 | illustrates
cd8daac8-039c-5349-9c50-02f622e73b03 | 4cc0c7ee-2166-5cae-9065-ea226fa2be5b | grounds
d009dfc4-9755-58a7-affb-80e3e842a775 | 33fadb51-0bd8-5f2d-87fe-758a3dd7fac0 | grounds
d009dfc4-9755-58a7-affb-80e3e842a775 | 9fd61e7e-d5cb-5904-b5c2-a539946f7c96 | illustrates
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 8f952541-5f12-5f24-88b3-21e7842163a1 | grounds
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 997e5e58-b20d-560a-a73a-eaa4bff1dc3e | illustrates
e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | grounds
(17 rows)
id | superseded_by
--------------------------------------+--------------------------------------
7da4851c-0aac-4084-a99f-baeeeb3aa23e | cd8daac8-039c-5349-9c50-02f622e73b03
d0000000-0000-0000-0000-000000000005 | e6ce4e36-15d2-53bc-8b74-dda0efc095c1
(2 rows)
clone cross-surface preflight after base packet
check_name | id | from_node_id | anchor_role | claim_id | source_id | note
-----------------------+--------------------------------------+--------------------------------------+--------------+--------------------------------------+-----------+----------------------------------------------------------------------------------------------------------------------------------------
policy_anchor_current | 47f128a0-36c5-4b60-97d4-48c32fa48bc7 | 52bc4264-330e-489d-920c-a493fd2d985a | justified_by | d0000000-0000-0000-0000-000000000005 | | Telos/policy grounding approved by m3ta: capital allocation expresses priorities and must be coordinated with collective intelligence.
(1 row)
check_name | id | status | text
---------------------+--------------------------------------+--------+--------------------------------------------------------------------------------------------
base_claim_d_exists | e6ce4e36-15d2-53bc-8b74-dda0efc095c1 | open | Internet finance is not Teleos telos; it is a candidate mechanism for pursuing the telos.
(1 row)
check_name | id | source_type | excerpt
------------------------+--------------------------------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
approval_source_exists | 9a0d2429-b98b-585a-980c-0176db7f9fe7 | dm | {"excerpt": "Yes lets do this. do the work for this, then come back to me with what you'd like me to approve\n\nOld claim: Internet finance is the coordination mechanism for capital allocation — capital allocation is ex
(1 row)
check_name | id | from_node_id | anchor_role | to_shared_root_id | note
-----------------------------+--------------------------------------+--------------------------------------+-------------+--------------------------------------+---------------------------------------------------------------------------------------
telos_anchor_already_exists | c7dd984c-ac1b-43f9-9996-576ddf61c6d2 | 52bc4264-330e-489d-920c-a493fd2d985a | serves | 13100000-0000-0000-0000-000000000001 | Approved by m3ta: policy serves Teleo telos of emergent collective superintelligence.
(1 row)
check_name | id | name | criteria | evidence_bar
-----------------------+--------------------------------------+--------------+------------------------------------------------------------------------------------------------------------+-------------------------------------
governance_gate_known | 45695897-2a9c-4279-ac25-e59f166dec6a | evidence-bar | likely needs empirical data; experimental = coherent argument, limited validation; speculative = untested. | likely > experimental > speculative
governance_gate_known | fa88e31b-6fca-4801-bf72-f5db81950a11 | quality-gate | Specific enough to disagree with; evidence traceable; not a duplicate; right domain reviewers. |
(2 rows)
clone cross-surface authorized commit
BEGIN

View file

@ -0,0 +1,24 @@
\set ON_ERROR_STOP on
begin;
do $$
declare
restored int;
begin
update public.strategy_node_anchors
set claim_id = 'd0000000-0000-0000-0000-000000000005'::uuid,
source_id = null,
weight = 0.7,
note = 'Telos/policy grounding approved by m3ta: capital allocation expresses priorities and must be coordinated with collective intelligence.'
where id = '47f128a0-36c5-4b60-97d4-48c32fa48bc7'::uuid
and from_node_id = '52bc4264-330e-489d-920c-a493fd2d985a'::uuid
and anchor_role = 'justified_by'
and claim_id = 'e6ce4e36-15d2-53bc-8b74-dda0efc095c1'::uuid;
get diagnostics restored = row_count;
if restored <> 1 then
raise exception 'cross-surface anchor rollback affected % row(s), expected 1; anchor was not at Claim D state', restored;
end if;
end $$;
commit;

View file

@ -0,0 +1,55 @@
{
"already_represented": [
{
"id": "c7dd984c-ac1b-43f9-9996-576ddf61c6d2",
"reason": "The active Leo policy already serves the Teleo telos shared root; the Claim D anchor update keeps internet finance subordinate to that telos.",
"table": "public.strategy_node_anchors",
"target": "teleo telos",
"to_shared_root_id": "13100000-0000-0000-0000-000000000001"
}
],
"base_packet_required_first": "production-apply-packet-mapped-20260709",
"known_non_writable_targets": [
{
"existing_row_id": "fa88e31b-6fca-4801-bf72-f5db81950a11",
"reason": "Governance gate exists, but current schema has no claim-to-governance-gate edge/anchor table.",
"target": "governance_gate:quality-gate"
},
{
"existing_row_id": "45695897-2a9c-4279-ac25-e59f166dec6a",
"reason": "Governance gate exists, but current schema has no claim-to-governance-gate edge/anchor table.",
"target": "governance_gate:evidence-bar"
},
{
"existing_row_id": null,
"reason": "No first-class concept-map table exists in the current live schema.",
"target": "concept_map:distributed_market_intelligence"
},
{
"existing_row_id": null,
"reason": "No exact Rio strategy node or claim target exists in the current live schema.",
"target": "rio:capital-deployment-layer"
},
{
"existing_row_id": null,
"reason": "No exact Rio active strategy node exists in the current live schema.",
"target": "rio:active strategy v1 / internet-finance capital product"
}
],
"packet_version": 1,
"production_apply_status": "not_executed",
"purpose": "Companion packet for mapped rich proposal non-claim surfaces.",
"safe_writes": [
{
"approval_source_id": "9a0d2429-b98b-585a-980c-0176db7f9fe7",
"from_node_id": "52bc4264-330e-489d-920c-a493fd2d985a",
"from_node_title": "Multi-agent public intelligence",
"id": "47f128a0-36c5-4b60-97d4-48c32fa48bc7",
"new_claim_id": "e6ce4e36-15d2-53bc-8b74-dda0efc095c1",
"old_claim_id": "d0000000-0000-0000-0000-000000000005",
"operation": "update",
"reason": "Replace the active Leo policy anchor that points at the retiring compressed internet-finance claim with the approved Claim D boundary claim.",
"table": "public.strategy_node_anchors"
}
]
}

View file

@ -0,0 +1,30 @@
\set ON_ERROR_STOP on
-- Cross-surface postflight row-level readback.
select id::text,
from_node_id::text,
anchor_role,
claim_id::text,
source_id::text,
weight,
left(note, 240) as note
from public.strategy_node_anchors
where id = '47f128a0-36c5-4b60-97d4-48c32fa48bc7'::uuid;
select c.id::text,
c.status,
c.superseded_by::text,
left(c.text, 240) as text
from public.claims c
where c.id in (select id from (values
('d0000000-0000-0000-0000-000000000005'::uuid),
('e6ce4e36-15d2-53bc-8b74-dda0efc095c1'::uuid)
) as v(id))
order by c.id;
select id::text,
anchor_role,
to_shared_root_id::text,
left(note, 240) as note
from public.strategy_node_anchors
where id = 'c7dd984c-ac1b-43f9-9996-576ddf61c6d2'::uuid;

View file

@ -0,0 +1,48 @@
\set ON_ERROR_STOP on
-- Cross-surface companion preflight. Read-only.
-- Run after the mapped rich proposal claim/source packet has been applied.
select 'policy_anchor_current' as check_name,
id::text,
from_node_id::text,
anchor_role,
claim_id::text,
source_id::text,
left(note, 220) as note
from public.strategy_node_anchors
where id = '47f128a0-36c5-4b60-97d4-48c32fa48bc7'::uuid;
select 'base_claim_d_exists' as check_name,
id::text,
status,
left(text, 220) as text
from public.claims
where id = 'e6ce4e36-15d2-53bc-8b74-dda0efc095c1'::uuid;
select 'approval_source_exists' as check_name,
id::text,
source_type,
left(coalesce(excerpt, ''), 220) as excerpt
from public.sources
where id = '9a0d2429-b98b-585a-980c-0176db7f9fe7'::uuid;
select 'telos_anchor_already_exists' as check_name,
id::text,
from_node_id::text,
anchor_role,
to_shared_root_id::text,
left(note, 220) as note
from public.strategy_node_anchors
where id = 'c7dd984c-ac1b-43f9-9996-576ddf61c6d2'::uuid;
select 'governance_gate_known' as check_name,
id::text,
name,
left(criteria, 180) as criteria,
left(evidence_bar, 180) as evidence_bar
from public.governance_gates
where id in (select id from (values
('fa88e31b-6fca-4801-bf72-f5db81950a11'::uuid),
('45695897-2a9c-4279-ac25-e59f166dec6a'::uuid)
) as v(id))
order by name;

View file

@ -0,0 +1,75 @@
# Cross-Surface Resolution Packet - 2026-07-09
Generated UTC: `2026-07-09T21:38:00Z`
## Verdict
Clone-proven, not production-applied.
The mapped rich proposal packet handles canonical claim/source/evidence/claim-edge rows. This companion packet handles the one currently safe non-claim write: updating strategy anchor `47f128a0-36c5-4b60-97d4-48c32fa48bc7` after Claim D exists.
## Safe Write
- Table: `public.strategy_node_anchors`
- Anchor: `47f128a0-36c5-4b60-97d4-48c32fa48bc7`
- From node: `52bc4264-330e-489d-920c-a493fd2d985a` (`Multi-agent public intelligence`)
- Old claim: `d0000000-0000-0000-0000-000000000005`
- New claim: `e6ce4e36-15d2-53bc-8b74-dda0efc095c1`
- New meaning: active Leo policy is justified by the boundary claim that internet finance is a candidate mechanism, not Teleo's telos.
## Clone Proof
Disposable database: `teleo_cross_surface_rehearsal_20260709`
Sequence:
1. Created disposable clone from live `teleo` using `pg_dump | psql`.
2. Applied mapped base packet in clone: `7` claims, `15` sources, `11` claim edges, `17` claim evidence rows.
3. Ran cross-surface preflight:
- anchor still pointed to old claim `d0000000-0000-0000-0000-000000000005`
- Claim D existed
- approval source existed
- telos anchor already existed
- governance gate rows existed
4. Ran cross-surface authorized commit in clone.
5. Postflight read back anchor pointing to Claim D with weight `0.85`.
6. Ran cross-surface delete rollback.
7. Ran mapped base packet delete rollback.
8. Clone returned to zero generated mapped rows and original anchor state.
9. Production stayed unchanged: generated rows remained `0/0/0/0`, and production anchor still points to old claim with weight `0.7`.
10. Dropped disposable DB and removed host/container temp packet files.
Cleanup readback:
- `disposable_db_exists`: `0`
- host tmp removed: yes
- container tmp removed: yes
- `leoclean-gateway.service`: `active`
- `MainPID`: `3252143`
- `NRestarts`: `0`
## Constraint Found And Fixed
The first clone attempt failed because `public.strategy_node_anchors` rejects a row with both `claim_id` and `source_id` set. The packet was corrected so the anchor points only to Claim D. The approval source remains required as provenance and is linked through Claim D's evidence rows from the mapped base packet.
## Explicit Deferrals
These approved fragments are not safely writable through the current schema:
- `governance_gate:quality-gate`: row exists, but there is no claim-to-governance-gate edge/anchor table.
- `governance_gate:evidence-bar`: row exists, but there is no claim-to-governance-gate edge/anchor table.
- `concept_map:distributed_market_intelligence`: no first-class concept-map table exists.
- `rio:capital-deployment-layer`: no exact Rio strategy node or claim target exists.
- `rio:active strategy v1 / internet-finance capital product`: no exact Rio active strategy node exists.
## Evidence
- Packet JSON: `docs/reports/leo-working-state-20260709/cross-surface-packet.json`
- Authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/cross-surface-apply-authorized-commit.sql`
- Delete rollback SQL: `docs/reports/leo-working-state-20260709/cross-surface-delete-rollback.sql`
- Successful clone rehearsal log: `docs/reports/leo-working-state-20260709/cross-surface-clone-rehearsal-current.log`
- Failed-source-id constraint log: `docs/reports/leo-working-state-20260709/cross-surface-clone-rehearsal-failed-source-id-constraint.log`
## Claim Ceiling
This proves the strategy-anchor companion write is schema-compatible, reversible, and safe in a disposable clone after the mapped base packet exists. It does not prove production has been updated, because production commit was intentionally not executed.

View file

@ -0,0 +1,190 @@
# Current Truth Index - Leo / Teleo - 2026-07-09
Use this file before making status claims. Prefer fresh VPS/GCP readbacks when cheap; this directory is the retained July 9 evidence snapshot committed to the Teleo infrastructure repo.
## Definition And Outcomes
- Working Leo definition: `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`
- Cory expected outcomes: `docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md`
- Current state: `docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md`
- Execution plan: `docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md`
- Open-ended benchmark spec: `docs/reports/leo-working-state-20260709/working-leo-open-ended-benchmark-spec.json` (5 live-safe open prompts, 9 broader Cory-style outcome scenarios, and 6 no-context direct-claim follow-up prompts for disposable clone/sandbox first)
- Open-ended live Telegram canary: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-cory-style-20260709.md`
- Open-ended live Telegram canary JSON: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-cory-style-20260709.json`
- Retained Telegram benchmark score for the open-ended canary: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-benchmark-score-current.md`
- Retained Telegram benchmark score JSON: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-benchmark-score-current.json`
- Full live-safe open-ended Telegram suite report: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-current.md`
- Full live-safe open-ended Telegram suite score: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-score-current.md`
- Full live-safe open-ended Telegram suite score JSON: `docs/reports/leo-working-state-20260709/telegram-live-open-ended-suite-score-current.json`
- Full Cory-style outcome/direct-claim sandbox results: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-results-current.md`
- Full Cory-style outcome/direct-claim sandbox score: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.md`
- Full Cory-style outcome sandbox score JSON: `docs/reports/leo-working-state-20260709/working-leo-cory-outcome-sandbox-score-current.json`
- Live VPS handler direct-claim suite report: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-current.json`
- Live VPS handler direct-claim suite score: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-score-current.md`
- Live VPS handler direct-claim suite review: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-review-current.md`
- Telegram-visible direct-claim authorization packet, ready-to-request but not sent: `docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-authorization-packet-current.md`
- Telegram-visible direct-claim capture receipt, currently awaiting capture: `docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-capture-receipt-current.md`
- Telegram-visible direct-claim pre-send preflight, passed from supplied top-level SSH readback payload: `docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-preflight-current.md`
- Telegram-visible direct-claim full-access remote readback command: `docs/reports/leo-working-state-20260709/telegram-visible-direct-claim-remote-readback-command-current.sh`
- Argus public KB state probe, read-only HTTP diagnostics: `docs/reports/leo-working-state-20260709/argus-kb-state-current.md`
- Live VPS handler direct-claim follow-up challenge report: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-followup-current.json`
- Fable onboarding prompt: `docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md`
- Operator surface map: `docs/reports/leo-working-state-20260709/operator-surface-map.md`
- VPS/Hermes/DB mapping: `docs/reports/leo-working-state-20260709/vps-hermes-db-mapping-20260709.md`
- DB provenance/load-path report: `docs/reports/leo-working-state-20260709/leo-db-provenance-report-20260709T214741Z.md`
- Restart-survival harness: `scripts/collect_leo_restart_survival_proof.py`
- Restart-survival proof output, passed after live execution: `docs/reports/leo-working-state-20260709/leo-restart-survival-proof-current.md`
- Restart-survival proof JSON, passed after live execution: `docs/reports/leo-working-state-20260709/leo-restart-survival-proof-current.json`
- Restart-survival no-post handler report: `docs/reports/leo-working-state-20260709/leo-restart-survival-direct-claim-current.json`
- Restart-survival no-post handler score for the pre-repair restart run, `5/6`: `docs/reports/leo-working-state-20260709/leo-restart-survival-direct-claim-score-current.md`
- Final post-repair no-post direct-claim score, `6/6`: `docs/reports/leo-working-state-20260709/leo-post-skill-repair-direct-claim-score-current.md`
- Final post-repair no-post direct-claim summary: `docs/reports/leo-working-state-20260709/leo-post-skill-repair-direct-claim-summary-current.md`
- Final post-repair no-post direct-claim summary JSON: `docs/reports/leo-working-state-20260709/leo-post-skill-repair-direct-claim-summary-current.json`
## Telegram And Live Leo Proof
- Live Telegram canary: `docs/reports/leo-working-state-20260709/telegram-live-canary-current.md`
- Live Telegram canary JSON: `docs/reports/leo-working-state-20260709/telegram-live-canary-current.json`
- Live Telegram DB-write canary: `docs/reports/leo-working-state-20260709/telegram-live-db-write-canary-20260709.md`
- Live Telegram DB-write canary JSON: `docs/reports/leo-working-state-20260709/telegram-live-db-write-canary-20260709.json`
- Handler canary: `docs/reports/leo-working-state-20260709/telegram-gateway-handler-canary-current.md`
- Handler canary JSON: `docs/reports/leo-working-state-20260709/telegram-gateway-handler-canary-current.json`
- Handler direct-claim suite: `docs/reports/leo-working-state-20260709/telegram-handler-direct-claim-suite-review-current.md`
## KB DB Apply Proof
- Strict apply plan: `docs/reports/leo-working-state-20260709/kb-apply-canary-plan-current.md`
- Strict apply execute: `docs/reports/leo-working-state-20260709/kb-apply-canary-execute-current.md`
- Approved claim-bundle isolation canary: `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
- Generic approved claim-bundle isolation receipt: `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json`
- Helmer approved claim-bundle isolation receipt: `docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json`
- Lossless Helmer strict normalization: `docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json`
- Approved claim-bundle lifecycle harness: `scripts/run_approve_claim_clone_canary.py`
- Approved claim-bundle isolated-container wrapper: `scripts/run_approve_claim_isolated_container_canary.sh`
- Live VPS source-composition clone checkpoint summary: `docs/reports/leo-working-state-20260709/leo-source-composition-clone-checkpoint-current.md`
- Live VPS source-composition clone checkpoint full JSON: `docs/reports/leo-working-state-20260709/leo-source-composition-clone-checkpoint-current.json`
- Claim/source preview: `docs/reports/leo-working-state-20260709/claim-source-contract-preview-current.md`
- Mapped rich proposal plan: `docs/reports/leo-working-state-20260709/rich-proposal-creation-plan-mapped-current.md`
- Mapped production apply packet, not executed in production: `docs/reports/leo-working-state-20260709/production-apply-packet-current.md`
- Mapped production apply packet JSON: `docs/reports/leo-working-state-20260709/production-apply-packet.json`
- Mapped production apply authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/production-apply-authorized-commit.sql`
- Mapped production apply delete rollback SQL: `docs/reports/leo-working-state-20260709/production-delete-rollback.sql`
- Cross-surface strategy anchor companion packet, not executed in production: `docs/reports/leo-working-state-20260709/cross-surface-resolution-current.md`
- Cross-surface packet JSON: `docs/reports/leo-working-state-20260709/cross-surface-packet.json`
- Cross-surface authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/cross-surface-apply-authorized-commit.sql`
- Cross-surface delete rollback SQL: `docs/reports/leo-working-state-20260709/cross-surface-delete-rollback.sql`
- Cross-surface clone commit/delete rollback log: `docs/reports/leo-working-state-20260709/cross-surface-clone-rehearsal-current.log`
- Rio strategy-context companion packet, not executed in production: `docs/reports/leo-working-state-20260709/rio-strategy-context-current.md`
- Rio strategy-context packet JSON: `docs/reports/leo-working-state-20260709/rio-strategy-packet.json`
- Rio strategy-context authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/rio-strategy-apply-authorized-commit.sql`
- Rio strategy-context delete rollback SQL: `docs/reports/leo-working-state-20260709/rio-strategy-delete-rollback.sql`
- Rio strategy-context clone commit/delete rollback log: `docs/reports/leo-working-state-20260709/rio-strategy-clone-rehearsal-current.log`
- Governance/concept-map schema companion packet, not executed in production: `docs/reports/leo-working-state-20260709/governance-concept-current.md`
- Governance/concept-map packet JSON: `docs/reports/leo-working-state-20260709/governance-concept-packet.json`
- Governance/concept-map authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/governance-concept-apply-authorized-commit.sql`
- Governance/concept-map delete rollback SQL: `docs/reports/leo-working-state-20260709/governance-concept-delete-rollback.sql`
- Governance/concept-map clone commit/delete rollback log: `docs/reports/leo-working-state-20260709/governance-concept-clone-rehearsal-current.log`
- Helmer 7 Powers proposal plan: `docs/reports/leo-working-state-20260709/helmer-7powers-creation-plan.json`
- Helmer 7 Powers apply packet, not executed in production: `docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-packet-current.md`
- Helmer 7 Powers apply packet JSON: `docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-packet.json`
- Helmer 7 Powers authorized commit SQL, not executed in production: `docs/reports/leo-working-state-20260709/helmer-7powers-production-apply-authorized-commit.sql`
- Helmer 7 Powers delete rollback SQL: `docs/reports/leo-working-state-20260709/helmer-7powers-production-delete-rollback.sql`
- Working Leo integrated packet manifest, not executed in production: `docs/reports/leo-working-state-20260709/working-leo-integrated-packet.md`
- Working Leo integrated packet JSON: `docs/reports/leo-working-state-20260709/working-leo-integrated-packet.json`
- Working Leo integrated clone rehearsal report: `docs/reports/leo-working-state-20260709/working-leo-integrated-packet-current.md`
- Working Leo integrated clone rehearsal log: `docs/reports/leo-working-state-20260709/working-leo-integrated-clone-rehearsal-current.log`
- Working Leo authorized-apply readiness verifier: `docs/reports/leo-working-state-20260709/working-leo-apply-readiness-current.md`
- Working Leo authorized-apply readiness verifier JSON: `docs/reports/leo-working-state-20260709/working-leo-apply-readiness-current.json`
- Working Leo authorized apply operator runbook, not executed: `docs/reports/leo-working-state-20260709/working-leo-authorized-apply-runbook-current.md`
- Working Leo authorized apply operator runbook JSON: `docs/reports/leo-working-state-20260709/working-leo-authorized-apply-runbook-current.json`
- Working Leo authorized apply session plan, not executed: `docs/reports/leo-working-state-20260709/working-leo-authorized-apply-session-plan-current.md`
- Working Leo authorized apply session plan JSON: `docs/reports/leo-working-state-20260709/working-leo-authorized-apply-session-plan-current.json`
- Working Leo production apply authorization packet, not executed: `docs/reports/leo-working-state-20260709/working-leo-production-apply-authorization-packet-current.md`
- Working Leo production apply authorization packet JSON: `docs/reports/leo-working-state-20260709/working-leo-production-apply-authorization-packet-current.json`
- Working Leo production apply authorization verification, currently refused/missing text: `docs/reports/leo-working-state-20260709/working-leo-production-apply-authorization-verification-current.md`
- Working Leo production apply authorization verification JSON: `docs/reports/leo-working-state-20260709/working-leo-production-apply-authorization-verification-current.json`
- Working Leo live production preflight baseline, read-only: `docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md`
- Working Leo live production preflight baseline JSON: `docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json`
- Working Leo production apply receipt template, not executed: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-template-current.md`
- Working Leo production apply receipt template JSON: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-template-current.json`
- Working Leo production apply receipt assembly, currently not ready: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-assembly-current.md`
- Working Leo production apply receipt assembly JSON: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-assembly-current.json`
- Working Leo production apply receipt validation, currently missing receipt: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-validation-current.md`
- Working Leo production apply receipt validation JSON: `docs/reports/leo-working-state-20260709/working-leo-production-apply-receipt-validation-current.json`
- Working Leo cleanup readback: `docs/reports/leo-working-state-20260709/working-leo-cleanup-readback-current.md`
- Working Leo cleanup readback JSON: `docs/reports/leo-working-state-20260709/working-leo-cleanup-readback-current.json`
- Helmer live rollback log: `docs/reports/leo-working-state-20260709/helmer-7powers-live-preflight-rollback-current.log`
- Helmer clone commit/delete rollback log: `docs/reports/leo-working-state-20260709/helmer-7powers-clone-commit-delete-rollback-current.log`
- Helmer ledger clone commit/delete rollback log: `docs/reports/leo-working-state-20260709/helmer-7powers-ledger-clone-commit-delete-rollback-current.log`
## GCP
- Current GCP parity probe: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.md`
- Current GCP parity probe JSON: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.json`
- Current GCP Cloud SQL live inventory: `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
- Current GCP Cloud SQL live inventory JSON: `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
- Current GCP Cloud SQL Studio parity gate: `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
- Current GCP Cloud SQL Studio parity gate JSON: `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
- Current GCP Leo runtime observability: `docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.md`
- Current GCP Leo runtime observability JSON: `docs/reports/leo-working-state-20260709/gcp-leo-runtime-observability-current.json`
- Prior auth-capable GCP blocker: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-blocker-20260709.json`
## Images
- Skill-pack map: `docs/reports/leo-working-state-20260709/leo-db-state-13-skill-pack.svg`
- Helmer packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-14-helmer-packet.svg`
- Open-ended Cory-style canary map: `docs/reports/leo-working-state-20260709/leo-db-state-15-open-ended-canary.svg`
- Helmer ledger packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-16-helmer-ledger.svg`
- Cory-style outcome benchmark map: `docs/reports/leo-working-state-20260709/leo-db-state-17-cory-outcome-benchmark.svg`
- Leoclean DB versus workspace map: `docs/reports/leo-working-state-20260709/leo-db-state-18-db-vs-workspace.svg`
- Claim/body/metadata schema map: `docs/reports/leo-working-state-20260709/leo-db-state-19-claims-body-metadata.svg`
- Cross-surface strategy anchor proof: `docs/reports/leo-working-state-20260709/leo-db-state-20-cross-surface-anchor.svg`
- Forgejo/load/agent update loop map: `docs/reports/leo-working-state-20260709/leo-db-state-21-forgejo-load-agent-loop.svg`
- Identity render / decision matrix / document artifact map: `docs/reports/leo-working-state-20260709/leo-db-state-22-identity-render-matrix-artifacts.svg`
- Rio strategy context packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-23-rio-strategy-context.svg`
- Governance/concept-map schema packet proof: `docs/reports/leo-working-state-20260709/leo-db-state-24-governance-concept-schema.svg`
- Integrated apply rehearsal proof: `docs/reports/leo-working-state-20260709/leo-db-state-25-integrated-apply-rehearsal.svg`
- Direct-claim handler state: `docs/reports/leo-working-state-20260709/leo-db-state-26-direct-claim-handler.svg`
- Direct-claim handler pass state: `docs/reports/leo-working-state-20260709/leo-db-state-27-direct-claim-handler-pass.svg`
- DB loading diagram SVG: `docs/reports/leo-working-state-20260709/leo-db-loading-diagram-20260709T214741Z.svg`
- DB loading diagram Mermaid source: `docs/reports/leo-working-state-20260709/leo-db-loading-diagram-20260709T214741Z.mmd`
## Current Claim Ceiling
- VPS Leo Telegram memory, KB audit, and staged-write canaries are live-proven.
- VPS Leo live-safe open-ended Telegram suite is live-proven and retained-benchmark-scored for `OE-01` through `OE-05`: full selected coverage, `5/5` scored prompts passing, no overclaim, and no marker/staging/canonical KB rows created by the suite.
- The broader sandbox-first Cory-style benchmark has a full-catalog mixed-evidence score: real retained live Telegram evidence for `OE-01` through `OE-05`, sandbox fixture replies for `CS-01` through `CS-09`, and no-context direct-claim expected-answer/follow-up fixtures for `DC-01` through `DC-06`, `20/20` passing. This is not a live Telegram run for the CS/DC prompts.
- The live VPS GatewayRunner direct-claim handler suite for exact `DC-01` through `DC-06` passed again after deploy SHA `7ab563e1aebe08913e72781e4e16a2b2883c5230`: `6/6` runtime replies, strict score `6/6`, no Telegram post, no live profile write, no production DB apply, unchanged DB counts (`public.claims=1837`, `public.sources=4145`, `public.claim_edges=4916`, `public.claim_evidence=4670`, `kb_stage.kb_proposals=26` before and after), temp profile removed, and `leoclean-gateway.service` stayed active/running during the harness with `MainPID=1908033`, `NRestarts=0`, `ExecMainStartTimestamp=Fri 2026-07-10 01:04:22 UTC`.
- Intentional restart survival is now live-proven as a separate required proof row, not implied by `NRestarts=0`: `systemctl restart leoclean-gateway.service` returned `0`, service moved from `MainPID=1908033` to `MainPID=2845730`, stayed active/running after restart and after the no-post handler smoke, canonical DB counts stayed unchanged (`1837/4145/4670/4916/26`), no Telegram post or production DB apply ran, no live profile change was recorded, temp profile cleanup succeeded, and the post-restart no-post `DC-01` through `DC-06` handler smoke returned six runtime replies. The retained pre-repair post-restart strict score was `5/6`; after deploying repair SHA `090becae1621436467610e529d6328d70964d11f`, the same no-post direct-claim suite strict-scored `6/6` with `DC-01` through `DC-06` all passing, DB counts unchanged, no Telegram post, no production DB apply, no live profile change, and cleanup confirmed. The raw handler JSON is retained on the VPS at `/tmp/leo-post-skill-repair-direct-claim-current.json` with SHA256 `6fffdbafc18913c4fc62feb00906988dee2e696d279f22dd284951a79451f39c`.
- The Telegram-visible direct-claim benchmark is now executable as an exact authorization packet, not yet run: target `Leo` group `-5146042086`, raw no-context messages `DC-01` through `DC-06`, expected proof paths, before/after DB/service readbacks, scorer output, and final screenshot/accessibility proof are retained in `telegram-visible-direct-claim-authorization-packet-current.md`; `telegram_visible_messages_sent=false`.
- The Telegram-visible direct-claim capture receipt assembler is retained and currently `status=awaiting_capture`; it will not emit a passing receipt until the exact operator authorization text, exact six DC messages, nonempty Telegram-visible Leo replies, DB before/after counts, service readback, final screenshot/accessibility proof, and `6/6` direct-claim score are present.
- The Telegram-visible direct-claim pre-send preflight now passes from supplied top-level SSH readback payload: deploy head and `.last-deploy-sha` both equal `63278b6354d3f5f15ed68897be95d484530db59f`, the remote packet is present/nonempty, `leoclean-gateway.service` is active/running with `MainPID=2999690`, `NRestarts=0`, and `ExecMainStartTimestamp=Fri 2026-07-10 03:47:11 UTC`, and DB counts read as `public.claims=1837`, `public.sources=4145`, `public.claim_edges=4916`, `public.claim_evidence=4670`, `kb_stage.kb_proposals=26`. The retained remote readback command SHA256 is `9bbfbe1de6e3f63e7aa9f73a7f412aa16f8f2f60b3c46f87ee93472290047c89`; no Telegram messages were sent and no DB apply ran.
- The public Argus KB diagnostics HTTP probe passes from live `curl` payloads captured at `2026-07-10T02:47:53Z`: `3` approved proposals are visible (`14fa5ecc`, `ac036c9d`, `a64df080`), all are `approved_needs_apply_payload`, `worker_applyable_count=0`, and the endpoint is read-only. This supports Leo's expected answer that the approved KB work is not yet directly applyable/canonical, but it is not Docker/Postgres row-count or gateway-service proof.
- The highest-risk direct-claim cases are now green at handler level: `DC-02` recognizes approved Helmer staging row `a64df080-8502-42e2-98f4-9bbdecb8da73` as approved/unapplied, `DC-03` says the decision-matrix path is not shipped while matrix tables are absent, `DC-04` avoids a single-cause document pointer diagnosis, `DC-05` separates staging demo from authorized canonical apply, and `DC-06` separates `SOUL.md` runtime edits from canonical DB identity.
- A strict canonical `add_edge` apply canary is live-proven.
- The repo-owned normal apply path now supports an atomic, insert-only `approve_claim` bundle behind separate `kb_review` and `kb_apply` credentials, a DB-role-to-reviewer principal mapping, and an immutable approval snapshot. Final source-bound isolated-container receipts pass `37/37` for both the generic bundle (`2` claims, `2` sources, `2` evidence rows, `1` edge, `1` reasoning tool) and the real losslessly normalized Helmer bundle (`5/13/17/7/1`). They prove exact payload-controlled projections and table deltas, reviewer/apply separation, denied direct proposal/evidence mutation, stale-payload refusal before canonical writes, replay refusal, source-hash rollback, exact agent/external-claim seeding, hardened role/function/ACL state, and independent inner/outer cleanup. PR #72 source later auto-synchronized to the VPS checkout and deploy stamp at merge commit `771679061f0a0b6463fb7d9cf530eb9d2d4e5440`; the timer journal says no Python changes and no service restart, the commit had no delta under the Leo profile source paths, the gateway stayed PID `2999690`/`NRestarts=0`, the apply worker stayed disabled/inactive, and read-only count endpoints remained `1837/4145/4670/4916/17/26`. This is a source checkout sync, not proof that the production permission migration ran, the worker was enabled, canonical rows changed, or byte-for-byte live content stayed equal. Helmer remains unapplied on production. See `pr72-vps-auto-deploy-runtime-nonchange-current.md`.
- The live VPS source-composition checkpoint now passes `34/34` with zero errors on a fresh no-send full-data clone: Leo searched the current KB, extracted two atomic conflicting claims from a new hash-bound document and post, retained exact evidence/source links, staged a strict proposal, passed separated review and guarded apply, then reopened in a new handler process and discovered/reasoned over the exact canonical proposal, claims, source UUIDs, and conflict edge without supplied IDs while recalling the prior conversation marker. Production counts and row-content fingerprints, gate schema, gateway PID/restarts, and live bridge hashes stayed unchanged. All three experiment containers, anonymous volumes, private credentials, run directories, temporary profiles, and child processes were removed. This proves current-data clone composition and restarted reasoning, not Telegram-visible delivery, production apply, scheduled DB-to-identity recomposition, arbitrary-source breadth, or GCP parity. See `leo-source-composition-clone-checkpoint-current.md`.
- Helmer 7 Powers is full-coverage packet and ledger clone-proven, not production-applied.
- Mapped rich approved proposal packet is clone/rehearsal-proven for the supported subset, not production-applied.
- The strategy-anchor companion write for mapped rich proposal `14fa5ecc` is clone-proven and reversible, not production-applied.
- The Rio strategy-context companion write for mapped rich proposal `14fa5ecc` is clone-proven and reversible: after the mapped base packet exists, it creates `2` Rio strategy nodes and `5` strategy anchors in a disposable clone; production stayed unchanged.
- The governance/concept-map schema companion write is clone-proven and reversible: after the mapped base packet exists, it creates minimal concept-map and claim-to-governance link tables, then inserts `1` concept map, `1` concept link, and `2` governance links in a disposable clone; production stayed unchanged.
- The full integrated packet set is now clone-proven in production dependency order: mapped base, cross-surface anchor, Rio strategy context, governance/concept schema, and Helmer 7 Powers applied together in `teleo_integrated_rehearsal_20260710`, then rolled back in reverse order; production stayed unchanged and the disposable clone/temp files were removed.
- The authorized-apply readiness verifier now passes: packet files exist and hash, apply/rollback order matches the manifest, expected counts match, clone apply/rollback markers are ordered, clone cleanup is proven, service stayed stable, and `production_apply_executed=false`.
- The authorized apply operator runbook is generated and ready-not-executed: it gives preflight/apply/postflight/rollback command templates in manifest order, requires `TELEO_DATABASE_URL`, and now also includes a post-apply regression command bundle plus required production-apply receipt fields and an offline receipt validator. It still does not authorize or execute production SQL.
- The authorized apply session plan is generated and ready-not-executed: it maps concrete output log paths for pre-apply validation, preflight, apply, postflight, post-apply regression, service readback, cleanup readback, receipt validation, and rollback. Apply and rollback steps require explicit authorization; non-apply steps are marked non-mutating.
- The production apply authorization packet is generated and ready-to-request, not authorized/executed: all evidence gates pass, it names the exact integrated packet order and the operator authorization template, requires a fresh deployed-SHA readback at apply time, and keeps `production_apply_executed=false` plus `production_apply_authorization_present=false`.
- The production apply authorization verifier currently refuses the apply window in the committed artifact with fresh deployed SHA `a717965b4527819e6d06e3a779e4855629058318`: `deployed_sha_shape=true`, `fresh_deployed_sha_required=false`, `authorization_text_present=false`, `operator_identity_present=false`, `authorization_text_matches_expected=false`, `safe_to_enter_apply_window=false`, and `production_apply_executed=false`. It now fails only on missing exact authorization text/operator identity, not stale deploy ambiguity.
- The live production preflight baseline now passes through the read-only collector: `5/5` preflight files executed on the VPS with `begin transaction read only`, `mutates_production_db=false`, `applies_production_packet=false`, and `production_apply_executed=false`. Current production still has generated base rows `0/0/0/0`, Helmer rows `0/0/0/0/0`, no generated governance/concept tables, and dependent Rio/cross-surface/governance base rows absent before any apply, as expected.
- The production apply receipt template is generated but intentionally invalid as a final receipt: `production_apply_executed=false`, `valid_production_receipt=false`, and it names the fields that must only be filled after authorized apply execution (`explicit_authorization_record`, `operator_identity`, `apply_output_paths`, `postflight_output_paths`, `actual_after_apply_counts`, `telegram_or_handler_regression_paths`, `service_readback`, `cleanup_readback`).
- The production apply receipt assembler currently refuses final receipt emission: `receipt_ready=false`, `authorization_verified=false`, `all_session_artifacts_exist=false`, `actual_counts_match_expected=false`, no apply/postflight/regression/service/cleanup logs exist, and `production_apply_executed=false`.
- The production-apply receipt validator currently reports `missing_receipt`, which is correct because no production apply has been authorized or executed.
- DB-first identity rendering is now documented from the July 9 PDFs plus VPS readback: canonical identity rows feed rendered `SOUL.md`, direct SOUL edits are not canonical DB updates, and no active renderer/timer/hook is proven in the current VPS readback.
- The decision-matrix approval schema is still a target design on the current VPS: `kb_stage.matrix_voters`, `kb_stage.proposal_votes`, and `kb_stage.proposal_decisions` were not present in live schema readback.
- Pending/approved document proposals are real `kb_stage.kb_proposals` rows with `source_ref`s, but inspected `source_ref`s did not directly match `public.sources`; they still need review/apply mapping into canonical sources/evidence/edges.
- Governance-gate and concept-map targets are now schema-packet-proven, but not production-applied; the live VPS production schema still has no generated governance/concept tables.
- The live VPS runtime behavior should not be changed while the DB apply path is stabilized.
- Latest cleanup readback found no leftover rehearsal/clone/staging database names on the VPS Postgres container and did not mutate production DB or post to Telegram.
- A live Telegram regression canary should be rerun after any production apply; no production apply has been authorized or executed.
- GCP control-plane Chrome readback now proves project `teleo-501523`, running VM `teleo-prod-1` in `europe-west6-a`, and one available private-only PostgreSQL `16.14` Cloud SQL instance, `teleo-pgvector-standby`, in `europe-west6-c`. Cloud SQL Studio is reachable but exposes no enabled existing IAM database principal and requires built-in password authentication; no credential was submitted and no SQL ran. Database-row/runtime/DC-01-through-DC-06 parity therefore remains unproven. The exact next gate is an already-existing authenticated Studio principal or a separately authorized IAM/database-user enablement window.
- GCP Console observability now separately proves `teleo-prod-1` is running and that serial output through `2026-07-10T15:57:54Z` contains recurring successful `leoclean-cloudsql-memory-sync.service` cycles. The latest exact `leoclean-gcp-prod-parallel.service` lifecycle event in the retained serial buffer is a Hermes start on July 9 with no later exact stop/failure. Cloud Logging has `92` guest/platform records over seven days but zero matches for `leoclean`, `hermes`, `gateway`, `postgres`, or `teleo-kb`, and Ops Agent is absent. This is bounded unit-activity evidence, not current PID/restart/SHA/profile/model/auth/Telegram/DB-row parity; no-post Cory execution remains unsafe and was not attempted. The agent-created Console tab was closed and no GCP/DB/service/IAM/deploy/credential/Telegram mutation occurred.

View file

@ -0,0 +1,32 @@
# Fable / Worker Onboarding Prompt - Leo / Teleo
current_canary: read the retained evidence index, then produce a repo/VPS/GCP/Telegram working-state map with exact claim ceilings and next executable action.
Use the Teleo repo skill pack at:
`.agents/skills/`
Load these draft skills first:
1. `skills/teleo-leo-onboarding/SKILL.md`
2. `skills/working-leo-cory-outcomes/SKILL.md`
3. `skills/teleo-kb-db-change-workflow/SKILL.md`
4. `skills/teleo-vps-runtime-ops/SKILL.md`
5. `skills/teleo-proof-handoff/SKILL.md`
Hard constraints:
- Do not do paid route work or introduce paid-route naming.
- Do not change live VPS Leo runtime behavior unless explicitly told.
- Do not production-apply DB packets unless explicitly authorized.
- Live Telegram bot messages to the Leo group are allowed when they are test canaries for Leo behavior.
- Use Computer Use/Chrome for Telegram UI. Do not use AppleScript, OSA, System Events, or focus hijacking.
- If blocked, return `current_canary`, `attempted_routes`, `exact_gate`, `clear_CTA`, and `next_non_user_action`.
Expected output:
1. One-page understanding of the company/product/Leo role.
2. Current status split: VPS, Telegram, DB apply, GCP.
3. Exact list of evidence files read.
4. What is proven, not proven, and unsafe to claim.
5. Next action that can be run now without production mutation.

View file

@ -0,0 +1,127 @@
{
"schema": "teleo.gcpCloudSqlLiveReadonly.v1",
"captured_at_utc": "2026-07-10T06:52:27Z",
"source_thread_id": "019f47af-f2cc-7c10-b928-a9283afa2621",
"current_canary": "Open the authenticated Teleo Cloud SQL instances page with authuser=5 and retain the complete sanitized current inventory plus instance routing metadata.",
"required_tier": "T3_live_readonly",
"current_tier": "T3_live_readonly",
"status": "done_verified",
"strongest_claim_allowed": "Project teleo-501523 currently exposes exactly one Cloud SQL instance in the authenticated Console: teleo-pgvector-standby. It is runnable/available, PostgreSQL 16.14 Enterprise, single-zone in europe-west6-c, and reachable only through private connectivity on teleo-staging-net. This does not prove database contents, schema parity, row counts, application connectivity, or production cutover.",
"capture": {
"surface": "authenticated Google Cloud Console DOM readback",
"account": "b***@livingip.xyz",
"project_id": "teleo-501523",
"project_display_name": "Teleo",
"list_url": "https://console.cloud.google.com/sql/instances?authuser=5&project=teleo-501523",
"overview_url": "https://console.cloud.google.com/sql/instances/teleo-pgvector-standby/overview?authuser=5&project=teleo-501523",
"cli_authentication_used": false,
"secret_values_captured": false
},
"inventory": {
"instance_count": 1,
"zero_instances": false,
"instances": [
{
"instance_id": "teleo-pgvector-standby",
"status_ui": "Available; Runnable",
"engine": "PostgreSQL",
"major_version": "16",
"database_version": "PostgreSQL 16.14",
"cloud_sql_edition": "Enterprise",
"instance_role": "Primary instance",
"region": "europe-west6",
"zone": "europe-west6-c",
"availability": "Single zone",
"high_availability": false,
"machine": {
"vcpus": 1,
"memory_mb_ui": 628.74
},
"storage": {
"type": "SSD",
"provisioned_gb": 10,
"used_gb_ui": 2,
"automatic_storage_increase": true
},
"connectivity": {
"connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
"private_ip_connectivity": true,
"private_service_access": true,
"associated_network": "projects/teleo-501523/global/networks/teleo-staging-net",
"vpc_network": "teleo-staging-net",
"allocated_ip_range": "Automatically assigned IP range",
"internal_ip": "10.61.0.3",
"public_ip_connectivity": false,
"public_ip": null,
"default_tcp_port": 5432,
"write_endpoint": null
},
"database_authentication": {
"cloudsql_iam_authentication": true,
"application_or_operator_principal_verified": false
},
"resilience_and_backup": {
"automated_backups": true,
"backup_tier": "Standard",
"point_in_time_recovery": true,
"deletion_prevention": true,
"latest_visible_backup_status": "Backup finished",
"latest_visible_backup_date_local": "2026-07-10"
},
"recent_visible_operations_sanitized": [
"Backup finished on 2026-07-10",
"Backup finished on 2026-07-09",
"Two imports succeeded on 2026-07-08",
"Database creation completed on 2026-07-08"
],
"observed_health_issues": [
"Auditing not enabled",
"No password policy",
"No user password policy"
],
"labels": [],
"maintenance": {
"window": "Sunday 06:00-07:00 CEST",
"scheduled_now": false,
"version_current": true
}
}
]
},
"db_parity_route": {
"target_instance": "teleo-pgvector-standby",
"required_network_surface": "A runtime or connector with private reachability to teleo-staging-net",
"private_endpoint": "10.61.0.3:5432",
"cloud_sql_connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
"public_route_available": false,
"iam_database_authentication_enabled": true,
"database_name_proven_by_this_pass": false,
"next_readonly_owner_action": "From a verified teleo-staging-net-connected runtime, authenticate with an authorized database principal and run read-only database/schema/count parity queries against this instance. Do not attempt a public-IP connection because public connectivity is disabled."
},
"not_proven": [
"database names",
"database contents or row counts",
"schema parity with the VPS database",
"successful application-level connection from teleo-prod-1",
"production traffic cutover",
"high availability or cross-zone failover"
],
"actions": [
"Opened the exact Cloud SQL instances URL in the authorized authuser=5 Chrome session",
"Read the complete live instance treegrid and verified one and only one instance row",
"Opened the instance Overview through read-only navigation",
"Read status, engine/version, region/zone, machine, storage, connectivity, backup, IAM-authentication, and maintenance metadata",
"Did not activate Edit, Connect, Restart, Stop, Import, Export, Migrate, Clone, Delete, or failover controls"
],
"cleanup_readback": {
"browser_tab_finalized": true,
"browser_tab_disposition": "agent-created evidence tab closed; no handoff tab kept",
"gcp_writes": 0,
"database_writes": 0,
"service_restarts": 0,
"deploys": 0,
"iam_changes": 0,
"credential_or_key_creation": 0,
"telegram_sends": 0
}
}

View file

@ -0,0 +1,56 @@
# Teleo Cloud SQL T3 live-readonly inventory
Captured: `2026-07-10T06:52:27Z`
## Outcome
The authenticated `authuser=5` Cloud Console session proves that project `teleo-501523` currently has exactly one Cloud SQL instance.
- Required tier: `T3_live_readonly`
- Current tier: `T3_live_readonly`
- Status: `done_verified`
- Account: `b***@livingip.xyz`
- Project: `Teleo` / `teleo-501523`
## Current inventory
| Instance | Status | Engine | Region / zone | Availability | Connectivity |
|---|---|---|---|---|---|
| `teleo-pgvector-standby` | Available / runnable | PostgreSQL `16.14`, Enterprise | `europe-west6` / `europe-west6-c` | Single-zone; not highly available | Private IP and PSA enabled; public IP disabled |
Instance count: `1`.
## DB parity routing metadata
- Cloud SQL connection name: `teleo-501523:europe-west6:teleo-pgvector-standby`
- VPC: `projects/teleo-501523/global/networks/teleo-staging-net`
- Private endpoint: `10.61.0.3:5432`
- Private Service Access: enabled
- Public IP connectivity: disabled
- Cloud SQL IAM database authentication: enabled
- Write endpoint: none shown
The DB parity worker must run from a host or connector with private reachability to `teleo-staging-net`, authenticate with an authorized database principal, and issue read-only schema/count queries. A public-IP route is unavailable.
## Other current facts
- Machine: `1 vCPU`, `628.74 MB`
- Storage: `10 GB` SSD, `2 GB` shown used; automatic storage increase enabled
- Automated backups and point-in-time recovery: enabled
- Deletion prevention: enabled
- Latest visible backup: finished on `2026-07-10`
- Two sanitized import operations are shown as successful on `2026-07-08`
- Maintenance: Sunday `06:00-07:00 CEST`; no maintenance currently scheduled
- Observed health issues: auditing not enabled, no password policy, no user password policy
## Claim ceiling
This proves the current Cloud SQL control-plane inventory and private routing surface. It does not prove database names, contents, row counts, schema parity, an application connection from `teleo-prod-1`, production traffic cutover, or failover capability.
## Actions and mutation boundary
The exact live list and instance Overview URLs were read through Chrome. No Edit, Connect, Restart, Stop, Import, Export, Migrate, Clone, Delete, or failover control was activated. GCP writes, DB writes, IAM changes, key creation, service restarts, deploys, and Telegram sends were all zero.
Cleanup readback: the agent-created Cloud SQL evidence tab was closed and no browser handoff tab was retained.
Machine-readable evidence: `gcp-cloud-sql-t3-live-readonly-current.json`.

View file

@ -0,0 +1,85 @@
{
"schema": "teleo.gcpCloudSqlStudioParityGate.v1",
"captured_at_utc": "2026-07-10T06:57:22.085Z",
"source_thread_id": "019f47af-f2cc-7c10-b928-a9283afa2621",
"current_canary": "Open Cloud SQL Studio for teleo-pgvector-standby with authuser=5, authenticate only with an already-authorized principal, execute a transaction-level read-only schema/count parity query, and roll it back.",
"required_tier": "T3_live_readonly",
"current_tier": "T0_spec",
"supporting_gate_evidence_tier": "T3_live_readonly",
"status": "blocked_with_artifact",
"strongest_claim_allowed": "The authenticated Cloud SQL Studio surface is live and reachable for teleo-pgvector-standby, but it did not expose an existing IAM database principal. The visible login flow required built-in database authentication with a password credential. No database session or SQL readback was established, so database contents and VPS parity remain unproven.",
"control_plane_evidence_source": "/Users/user/Documents/Codex/2026-07-10/leo-gcp-control-plane/outputs/gcp-cloud-sql-t3-live-readonly-current.json",
"target": {
"project": "teleo-501523",
"instance": "teleo-pgvector-standby",
"connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
"engine": "PostgreSQL 16.14",
"private_endpoint": "10.61.0.3:5432",
"network": "teleo-staging-net",
"instance_iam_db_authentication_flag": "on",
"console_account": "b***@livingip.xyz"
},
"live_studio_readback": {
"studio_url_sanitized": "https://console.cloud.google.com/sql/instances/teleo-pgvector-standby/studio?authuser=5&project=teleo-501523",
"studio_loaded": true,
"login_dialog_visible": true,
"database_selector_visible": true,
"user_selector_visible": true,
"iam_database_authentication_option_visible": true,
"iam_database_authentication_option_enabled": false,
"built_in_database_authentication_selected": true,
"password_credential_required": true,
"password_value_retained": false,
"authenticate_clicked": false
},
"sql_execution": {
"database_session_established": false,
"begin_transaction_read_only_executed": false,
"current_database_read": false,
"current_user_read": false,
"transaction_read_only_read": false,
"catalog_or_table_selects_executed": false,
"rollback_executed": false,
"query_timestamp_utc": null
},
"parity_fields": {
"database_name": null,
"schemas": null,
"public.claims": null,
"public.sources": null,
"public.claim_evidence": null,
"public.claim_edges": null,
"kb_stage.kb_proposals": null,
"proposal_ids_and_statuses": null,
"selected_row_identities": null,
"gcp_vs_vps": null,
"ahead_behind_equal": null
},
"actions": [
"Opened the authenticated instance Overview URL with authuser=5 in an agent-created Chrome tab.",
"Discovered the exact Cloud SQL Studio href from the live instance navigation.",
"Opened Cloud SQL Studio and read the visible database-login state.",
"Stopped before authentication because the IAM option was disabled and the selected built-in flow required a password credential.",
"Did not open browser SSH because that path may create an ephemeral key.",
"Finalized Chrome and closed the agent-created tab."
],
"exact_gate": "Cloud SQL Studio exposes no enabled existing IAM database principal for this console session. Its selected built-in authentication path requires a raw database password, which this lane is forbidden to inspect, submit, recover, or replace.",
"why_autonomous_repair_stops": "Authenticating would require handling a raw password, or creating/granting a database or IAM principal. Both are explicit stop conditions, and the latter would mutate GCP or the database.",
"clear_CTA": "Using an already-existing authorized database principal, have an operator complete the Cloud SQL Studio database login without sharing credentials with Codex, then tell Codex the Studio editor is authenticated. Otherwise route IAM/user enablement to a separately authorized write lane.",
"next_non_user_action": "Once Studio is already authenticated with an existing principal, run only BEGIN TRANSACTION READ ONLY; identity/read-only checks; catalog/table/count SELECTs; and ROLLBACK, then retain the sanitized results.",
"cleanup_readback": {
"browser_finalized": true,
"agent_created_tab_closed": true,
"database_session_open": false,
"transaction_open": false,
"gcp_writes": 0,
"database_writes": 0,
"iam_changes": 0,
"credential_or_key_creation": 0,
"browser_ssh_opened": false,
"service_restarts": 0,
"deploys": 0,
"telegram_sends": 0
},
"secret_values_included": false
}

Some files were not shown because too many files have changed in this diff Show more