From 01ec9877e6b1b0e7170e8a8d5df781b43b978306 Mon Sep 17 00:00:00 2001 From: Vijay Raghav Varada Date: Mon, 10 Aug 2026 02:04:11 +0530 Subject: [PATCH] docs(D26): ai-company-brain -> project-docs; INDEX.md classification of record; 19 deferred/historical banners Owner-directed consolidation: the folder renamed with a 261-file reference sweep (CI path filters included); project-docs/INDEX.md is the entry point - ACTIVE (dispatch) / CONTRACTS / REFERENCE / DEFERRED-HISTORICAL; banners applied so nothing dispatches from parked, research, or record docs; #399's four new docs classified; WS-26 D5 and WS-27 o-t rows updated to merged; standing rule: every new spec enters INDEX in its creating PR. Co-Authored-By: Claude Fable 5 --- .env.example | 2 +- .github/agents/commandcenter.agent.md | 2 +- .github/prompts/system.md | 4 +- .github/workflows/codebase-health.yml | 4 +- .github/workflows/deploy.yml | 4 +- .github/workflows/pr-check.yml | 4 +- AGENTS.md | 16 +-- FOUNDATION_AUDIT_REPORT.md | 2 +- FOUNDATION_BUILDOUT_CHECKLIST.md | 16 +-- FOUNDATION_CONTINUATION.md | 2 +- README.md | 12 +-- apps/agents/agent-task-manager/agents.py | 2 +- apps/services/gateway/AGENTS.md | 20 ++-- apps/services/gateway/gateway/main.py | 12 +-- apps/services/gateway/gateway/rooms.py | 2 +- .../gateway/gateway/routes/admin/__init__.py | 2 +- .../gateway/gateway/routes/admin/_common.py | 2 +- .../gateway/routes/admin/access_requests.py | 2 +- .../gateway/gateway/routes/admin/groups.py | 2 +- .../gateway/gateway/routes/admin/members.py | 2 +- .../gateway/gateway/routes/admin/roles.py | 2 +- .../gateway/gateway/routes/crm/__init__.py | 2 +- .../gateway/gateway/routes/crm/activities.py | 2 +- .../gateway/gateway/routes/crm/admin.py | 2 +- .../gateway/gateway/routes/crm/auto_lead.py | 2 +- .../gateway/routes/crm/broker_handlers.py | 2 +- .../gateway/gateway/routes/crm/core.py | 2 +- .../gateway/routes/crm/deal_contacts.py | 2 +- .../gateway/gateway/routes/crm/import_zoho.py | 2 +- .../gateway/gateway/routes/crm/pipeline.py | 2 +- .../gateway/gateway/routes/crm/records.py | 2 +- .../gateway/gateway/routes/crm/reports.py | 2 +- .../gateway/routes/crm/stage_metadata.py | 2 +- .../gateway/gateway/routes/crm/sync_zoho.py | 2 +- .../gateway/gateway/routes/notes/__init__.py | 2 +- .../gateway/gateway/routes/notes/core.py | 2 +- .../gateway/gateway/routes/people/__init__.py | 2 +- .../gateway/gateway/routes/people/core.py | 2 +- .../gateway/routes/people/directory.py | 2 +- .../gateway/routes/projects/__init__.py | 2 +- .../gateway/routes/projects/activities.py | 2 +- .../gateway/gateway/routes/projects/admin.py | 2 +- .../gateway/routes/projects/agent_dispatch.py | 2 +- .../gateway/gateway/routes/projects/bulk.py | 2 +- .../gateway/routes/projects/calendar.py | 2 +- .../gateway/gateway/routes/projects/core.py | 2 +- .../gateway/routes/projects/custom_fields.py | 2 +- .../gateway/routes/projects/filters.py | 2 +- .../gateway/routes/projects/import_clickup.py | 2 +- .../gateway/routes/projects/import_tasks.py | 2 +- .../gateway/routes/projects/mapping.py | 2 +- .../gateway/gateway/routes/projects/me.py | 2 +- .../gateway/routes/projects/notifications.py | 2 +- .../gateway/routes/projects/personal.py | 2 +- .../gateway/routes/projects/recurrence.py | 2 +- .../gateway/routes/projects/relations.py | 2 +- .../gateway/gateway/routes/projects/search.py | 2 +- .../gateway/gateway/routes/projects/tags.py | 2 +- .../gateway/gateway/routes/projects/tasks.py | 2 +- .../gateway/gateway/routes/projects/tree.py | 2 +- .../gateway/gateway/routes/projects/views.py | 2 +- apps/services/gateway/gateway/routes/rooms.py | 2 +- .../gateway/gateway/routes/tasks/core.py | 4 +- .../routes/whatsapp/transport/calls.py | 2 +- .../gateway/routes/workflows/__init__.py | 2 +- .../gateway/gateway/routes/workflows/core.py | 2 +- .../ingestion/sources/zoho/writer.py | 2 +- apps/services/meeting_bot/README.md | 2 +- apps/services/orchestrator/AGENTS.md | 6 +- .../orchestrator/orchestrator/declarative.py | 2 +- .../orchestrator/orchestrator/mutation.py | 2 +- .../orchestrator/orchestrator/steer.py | 2 +- codegraph.json | 4 +- deploy/hostinger/acb-backup.timer | 2 +- docs/DESIGN_LIMITATION_native_maf_mutation.md | 2 +- docs/app-workshop/README.md | 2 +- docs/multiplayer/README.md | 42 ++++---- docs/multiplayer/agent-kinds.md | 8 +- docs/multiplayer/memory-clearance.md | 12 +-- docs/workflow-editor/README.md | 2 +- infra/AGENTS.md | 4 +- infra/docker-compose.yml | 2 +- infra/postgres/01_schema.sql | 2 +- infra/postgres/102_whatsapp.sql | 2 +- infra/postgres/120_live_session.sql | 2 +- infra/postgres/121_copilot_event.sql | 2 +- infra/postgres/124_meeting_brief.sql | 2 +- infra/postgres/125_meeting_agenda.sql | 2 +- infra/postgres/130_org_access_control.sql | 2 +- .../131_integration_memory_permissions.sql | 2 +- infra/postgres/132_workflows.sql | 2 +- .../133_workflows_publish_permission.sql | 2 +- .../134_workflows_automation_health.sql | 2 +- infra/postgres/136_agent_blob_instance.sql | 4 +- .../137_quarantine_commingled_agent_data.sql | 4 +- .../138_groups_and_session_participants.sql | 2 +- .../139_room_authorship_and_agents.sql | 2 +- infra/postgres/141_seed_center_groups.sql | 2 +- infra/postgres/142_agent_skill_settings.sql | 2 +- infra/postgres/143_access_request.sql | 2 +- infra/postgres/144_crm.sql | 2 +- infra/postgres/145_crm_zoho_sync.sql | 2 +- infra/postgres/146_projects.sql | 2 +- infra/postgres/147_projects_personal.sql | 2 +- infra/postgres/148_people_key_shape.sql | 2 +- infra/postgres/149_people.sql | 2 +- infra/postgres/150_projects_attachments.sql | 2 +- infra/postgres/152_projects_notifications.sql | 2 +- infra/postgres/155_projects_custom_fields.sql | 2 +- infra/postgres/156_projects_tags.sql | 2 +- infra/postgres/157_org_first_party.sql | 2 +- infra/postgres/158_per_org_credentials.sql | 2 +- infra/postgres/159_control_plane.sql | 2 +- infra/postgres/160_projects_recurrence.sql | 2 +- infra/postgres/161_projects_tenancy.sql | 2 +- infra/postgres/163_crm_auto_lead_cursor.sql | 2 +- infra/postgres/48_task_manager_gtd.sql | 2 +- infra/postgres/95_note_taker.sql | 2 +- infra/postgres/generated/01_add_columns.sql | 2 +- infra/postgres/generated/02_backfill.sql | 2 +- infra/postgres/generated/03_constraints.sql | 2 +- infra/postgres/generated/04_policies.sql | 2 +- learning-resources/04-cicd.md | 2 +- packages/AGENTS.md | 4 +- packages/acb_auth/acb_auth/__init__.py | 2 +- packages/acb_auth/acb_auth/access.py | 4 +- packages/acb_auth/acb_auth/permissions.py | 2 +- packages/acb_auth/acb_auth/roles.py | 2 +- packages/acb_common/acb_common/placement.py | 2 +- packages/acb_common/acb_common/settings.py | 2 +- packages/acb_skills/AGENTS.md | 2 +- packages/acb_skills/acb_skills/agent_paths.py | 4 +- .../acb_skills/acb_skills/integrations.py | 2 +- packages/acb_skills/acb_skills/manifest.py | 2 +- packages/acb_stt/acb_stt/__init__.py | 2 +- .../acb_stt/acb_stt/assemblyai_provider.py | 2 +- {ai-company-brain => project-docs}/AGENTS.md | 0 .../HANDOVER.md | 5 + project-docs/INDEX.md | 98 +++++++++++++++++++ .../agent_repo_compatibility.md | 0 .../agents-workspaces-artifacts.md | 0 .../project_plan.md | 0 .../reference.md | 0 .../specs/agent_architecture.md | 2 +- .../specs/agent_coding_skill.md | 0 .../specs/agent_file_and_memory_framework.md | 0 .../specs/agent_persistence_implementation.md | 0 .../specs/agent_platform_hardening_2026-07.md | 5 + .../specs/archive/README.md | 0 .../specs/archive/artifact_viewer.md | 0 .../chat_implementation_review_2026-07.md | 0 .../chat_ui_agui_hitl_review_2026-07.md | 0 .../specs/archive/context_assembly_c2.md | 0 .../specs/archive/core_loop_unification.md | 0 .../archive/dev_velocity_tooling_2026-07.md | 2 +- .../specs/archive/email_ai_assistant.md | 0 .../specs/archive/email_app_review.md | 0 .../archive/email_feature_review_2026-07.md | 0 .../archive/email_inbox_zero_parity_plan.md | 0 .../specs/archive/email_tool_consolidation.md | 0 .../archive/pixel_art_office_pipeline.md | 0 .../runtime_agent_effectiveness_2026-07.md | 0 .../specs/archive/stream_reconnection.md | 0 .../specs/archive/vscode_tool_integration.md | 0 .../specs/backup_and_restore.md | 2 +- .../specs/calendar_ai_review.md | 5 + .../specs/calendar_focus_os.md | 6 +- .../specs/calendar_timeboxing.md | 0 .../specs/calendar_ux_review.md | 5 + .../chat_agent_framework_review_2026-07.md | 7 +- .../specs/chat_ux.md | 5 + .../specs/colleague_onboarding.md | 0 .../specs/competitive_hardening_2026-07.md | 5 + .../specs/core_module_map.md | 0 .../specs/crm_app.md | 0 .../specs/department_centers.md | 0 .../specs/deploy_delivery_path.md | 2 +- .../specs/drawio_diagram_svc_contract.md | 5 + .../specs/drawio_integration.md | 5 + .../specs/email_app_master_plan.md | 0 .../specs/future_modules_roadmap.md | 0 .../specs/generative_ui_2.md | 0 .../specs/groups_sessions_authority.md | 0 .../specs/harness_hardening_2026-07.md | 5 + .../specs/live_meeting_copilot.md | 5 + .../specs/llm_caching_memory.md | 4 +- .../specs/mcp_plugin_integration.md | 4 +- .../specs/meeting_bot_platform_plan.md | 0 .../specs/memory_architecture.md | 0 .../specs/mockups/calendar_focus_os.html | 0 .../mockups/whatsapp_message_manager.html | 0 .../specs/multi_agent_orchestration.md | 0 .../specs/multi_tenancy.md | 2 +- .../specs/multi_tenancy_leak_audit.md | 0 .../specs/multi_user_organization_research.md | 5 + .../specs/multiplayer_prior_art_qm_2026-08.md | 5 + .../specs/note_taker_app.md | 0 .../specs/note_taker_research_2026-07.md | 5 + .../specs/observability_e2.md | 2 +- .../specs/org_access_control.md | 0 .../specs/paca_pm_research_2026-08.md | 5 + .../specs/people_center_app.md | 0 .../specs/permissions_sandbox_b6.md | 0 .../specs/plane_pm_research_2026-08.md | 5 + .../specs/project_management_app.md | 0 .../specs/saas_multitenancy.md | 0 .../specs/saas_multitenancy_handover.md | 6 +- .../specs/saas_multitenancy_implementation.md | 0 .../single_agent_chat_bug_audit_2026-07.md | 5 + .../specs/skills_registry.md | 0 .../specs/skills_scope_out.md | 0 .../specs/subscription_console.md | 0 .../specs/task_manager_app.md | 2 +- .../specs/task_manager_harness_2026-07.md | 5 + .../task_manager_hr_planning_and_memory.md | 0 .../specs/tenancy_and_visibility.md | 0 .../specs/user_management_contract.md | 0 .../specs/whatsapp_calls_note_taker.md | 5 + .../specs/whatsapp_message_manager.md | 0 .../specs/workflows_app.md | 2 +- .../system_architecture.md | 0 .../work_plan.md | 34 +++++-- pyproject.toml | 2 +- scripts/README.md | 2 +- scripts/check_whatsapp_calling.py | 2 +- scripts/codebase_health.py | 2 +- scripts/gen_tenant_migration.py | 4 +- scripts/onboarding_preflight.py | 8 +- skills/AGENTS.md | 2 +- skills/README.md | 2 +- tests/README.md | 2 +- tests/unit/test_admin_groups.py | 2 +- tests/unit/test_admin_member_offboarding.py | 2 +- tests/unit/test_admin_member_purge.py | 2 +- tests/unit/test_admin_tenancy.py | 2 +- tests/unit/test_agent_manifest.py | 2 +- tests/unit/test_agent_paths.py | 2 +- tests/unit/test_agent_run_identity.py | 2 +- tests/unit/test_blob_store_instance.py | 2 +- tests/unit/test_chat_hardening.py | 2 +- tests/unit/test_crm_agent.py | 2 +- tests/unit/test_crm_agent_write.py | 2 +- tests/unit/test_crm_auto_lead.py | 2 +- tests/unit/test_crm_convert.py | 2 +- tests/unit/test_crm_email_timeline.py | 2 +- tests/unit/test_crm_migration.py | 2 +- tests/unit/test_crm_pipeline.py | 2 +- tests/unit/test_crm_reports.py | 2 +- tests/unit/test_crm_routes.py | 2 +- tests/unit/test_crm_stage_metadata.py | 2 +- tests/unit/test_crm_zoho_import.py | 2 +- tests/unit/test_crm_zoho_sync.py | 2 +- tests/unit/test_declarative_builder.py | 2 +- tests/unit/test_email_assistant_settings.py | 2 +- .../unit/test_groups_session_participants.py | 2 +- tests/unit/test_instance_wiring.py | 4 +- tests/unit/test_integration_env_scoping.py | 2 +- .../test_mt0b_self_mutation_containment.py | 2 +- tests/unit/test_mt0d_per_org_credentials.py | 2 +- tests/unit/test_notes_owner_scoping.py | 2 +- tests/unit/test_orchestrator_registration.py | 2 +- tests/unit/test_org_access_control.py | 2 +- tests/unit/test_org_access_credentials.py | 2 +- tests/unit/test_org_access_enforcement.py | 2 +- tests/unit/test_people_directory.py | 2 +- tests/unit/test_people_key_shape.py | 2 +- tests/unit/test_people_write.py | 2 +- tests/unit/test_projects_automation.py | 4 +- tests/unit/test_projects_bulk.py | 2 +- tests/unit/test_projects_calendar.py | 2 +- tests/unit/test_projects_cards.py | 2 +- tests/unit/test_projects_custom_fields.py | 2 +- tests/unit/test_projects_filters.py | 2 +- tests/unit/test_projects_grants.py | 2 +- tests/unit/test_projects_import_mapping.py | 2 +- tests/unit/test_projects_migration.py | 4 +- tests/unit/test_projects_notifications.py | 2 +- tests/unit/test_projects_personal.py | 2 +- tests/unit/test_projects_recurrence.py | 2 +- tests/unit/test_projects_relations.py | 2 +- tests/unit/test_projects_routes.py | 2 +- tests/unit/test_projects_search.py | 2 +- tests/unit/test_projects_tags.py | 2 +- tests/unit/test_projects_tenancy.py | 2 +- tests/unit/test_rooms.py | 2 +- tests/unit/test_session_authority.py | 2 +- tests/unit/test_signin_requests.py | 2 +- tests/unit/test_steer_routing.py | 2 +- tests/unit/test_tenant_coverage.py | 2 +- tests/unit/test_tenant_placement.py | 2 +- tests/unit/test_tenant_session.py | 2 +- tests/unit/test_think_mode.py | 2 +- uv.lock | 4 +- workbench/AGENTS.md | 10 +- workbench/README.md | 2 +- workbench/control_plane/README.md | 2 +- .../src/app/centers/[slug]/page.tsx | 2 +- .../control_plane/src/app/crm/lib/board.ts | 2 +- .../control_plane/src/app/crm/lib/reports.ts | 2 +- .../control_plane/src/app/crm/lib/types.ts | 2 +- workbench/control_plane/src/app/crm/page.tsx | 2 +- .../src/app/people/lib/directory.ts | 2 +- .../src/app/people/lib/registration.test.ts | 2 +- .../control_plane/src/app/people/page.tsx | 2 +- .../src/app/projects/components/FilterBar.tsx | 2 +- .../src/app/projects/components/MyWork.tsx | 2 +- .../src/app/projects/lib/assignees.ts | 2 +- .../src/app/projects/lib/board.test.ts | 2 +- .../src/app/projects/lib/board.ts | 2 +- .../src/app/projects/lib/mywork.ts | 2 +- .../src/app/projects/lib/registration.test.ts | 2 +- .../src/app/projects/lib/tree.test.ts | 2 +- .../src/app/projects/lib/tree.ts | 2 +- .../control_plane/src/app/projects/page.tsx | 2 +- .../src/app/settings/groups/page.tsx | 2 +- .../src/app/settings/members/[email]/page.tsx | 2 +- .../src/app/settings/members/confirmPurge.ts | 2 +- .../src/app/settings/members/page.tsx | 4 +- .../src/app/settings/members/selfGuard.ts | 2 +- .../src/app/settings/members/types.ts | 2 +- .../src/app/settings/roles/page.tsx | 2 +- .../control_plane/src/app/tasks/lib/types.ts | 2 +- .../src/app/tasks/lib/waiting.ts | 2 +- .../control_plane/src/app/tasks/page.tsx | 2 +- .../src/app/whatsapp/calls/page.tsx | 2 +- .../control_plane/src/app/whatsapp/page.tsx | 2 +- .../src/app/workflows/lib/types.ts | 2 +- .../src/components/AccessProvider.tsx | 2 +- .../src/components/ThinkingContainer.tsx | 2 +- workbench/control_plane/src/lib/access.ts | 2 +- workbench/control_plane/src/lib/centers.ts | 2 +- workbench/control_plane/src/lib/nav.ts | 2 +- workbench/control_plane/src/lib/providers.ts | 2 +- 333 files changed, 571 insertions(+), 364 deletions(-) rename {ai-company-brain => project-docs}/AGENTS.md (100%) rename {ai-company-brain => project-docs}/HANDOVER.md (98%) create mode 100644 project-docs/INDEX.md rename {ai-company-brain => project-docs}/agent_repo_compatibility.md (100%) rename {ai-company-brain => project-docs}/agents-workspaces-artifacts.md (100%) rename {ai-company-brain => project-docs}/project_plan.md (100%) rename {ai-company-brain => project-docs}/reference.md (100%) rename {ai-company-brain => project-docs}/specs/agent_architecture.md (99%) rename {ai-company-brain => project-docs}/specs/agent_coding_skill.md (100%) rename {ai-company-brain => project-docs}/specs/agent_file_and_memory_framework.md (100%) rename {ai-company-brain => project-docs}/specs/agent_persistence_implementation.md (100%) rename {ai-company-brain => project-docs}/specs/agent_platform_hardening_2026-07.md (99%) rename {ai-company-brain => project-docs}/specs/archive/README.md (100%) rename {ai-company-brain => project-docs}/specs/archive/artifact_viewer.md (100%) rename {ai-company-brain => project-docs}/specs/archive/chat_implementation_review_2026-07.md (100%) rename {ai-company-brain => project-docs}/specs/archive/chat_ui_agui_hitl_review_2026-07.md (100%) rename {ai-company-brain => project-docs}/specs/archive/context_assembly_c2.md (100%) rename {ai-company-brain => project-docs}/specs/archive/core_loop_unification.md (100%) rename {ai-company-brain => project-docs}/specs/archive/dev_velocity_tooling_2026-07.md (99%) rename {ai-company-brain => project-docs}/specs/archive/email_ai_assistant.md (100%) rename {ai-company-brain => project-docs}/specs/archive/email_app_review.md (100%) rename {ai-company-brain => project-docs}/specs/archive/email_feature_review_2026-07.md (100%) rename {ai-company-brain => project-docs}/specs/archive/email_inbox_zero_parity_plan.md (100%) rename {ai-company-brain => project-docs}/specs/archive/email_tool_consolidation.md (100%) rename {ai-company-brain => project-docs}/specs/archive/pixel_art_office_pipeline.md (100%) rename {ai-company-brain => project-docs}/specs/archive/runtime_agent_effectiveness_2026-07.md (100%) rename {ai-company-brain => project-docs}/specs/archive/stream_reconnection.md (100%) rename {ai-company-brain => project-docs}/specs/archive/vscode_tool_integration.md (100%) rename {ai-company-brain => project-docs}/specs/backup_and_restore.md (99%) rename {ai-company-brain => project-docs}/specs/calendar_ai_review.md (97%) rename {ai-company-brain => project-docs}/specs/calendar_focus_os.md (99%) rename {ai-company-brain => project-docs}/specs/calendar_timeboxing.md (100%) rename {ai-company-brain => project-docs}/specs/calendar_ux_review.md (97%) rename {ai-company-brain => project-docs}/specs/chat_agent_framework_review_2026-07.md (98%) rename {ai-company-brain => project-docs}/specs/chat_ux.md (99%) rename {ai-company-brain => project-docs}/specs/colleague_onboarding.md (100%) rename {ai-company-brain => project-docs}/specs/competitive_hardening_2026-07.md (98%) rename {ai-company-brain => project-docs}/specs/core_module_map.md (100%) rename {ai-company-brain => project-docs}/specs/crm_app.md (100%) rename {ai-company-brain => project-docs}/specs/department_centers.md (100%) rename {ai-company-brain => project-docs}/specs/deploy_delivery_path.md (99%) rename {ai-company-brain => project-docs}/specs/drawio_diagram_svc_contract.md (98%) rename {ai-company-brain => project-docs}/specs/drawio_integration.md (98%) rename {ai-company-brain => project-docs}/specs/email_app_master_plan.md (100%) rename {ai-company-brain => project-docs}/specs/future_modules_roadmap.md (100%) rename {ai-company-brain => project-docs}/specs/generative_ui_2.md (100%) rename {ai-company-brain => project-docs}/specs/groups_sessions_authority.md (100%) rename {ai-company-brain => project-docs}/specs/harness_hardening_2026-07.md (97%) rename {ai-company-brain => project-docs}/specs/live_meeting_copilot.md (98%) rename {ai-company-brain => project-docs}/specs/llm_caching_memory.md (99%) rename {ai-company-brain => project-docs}/specs/mcp_plugin_integration.md (99%) rename {ai-company-brain => project-docs}/specs/meeting_bot_platform_plan.md (100%) rename {ai-company-brain => project-docs}/specs/memory_architecture.md (100%) rename {ai-company-brain => project-docs}/specs/mockups/calendar_focus_os.html (100%) rename {ai-company-brain => project-docs}/specs/mockups/whatsapp_message_manager.html (100%) rename {ai-company-brain => project-docs}/specs/multi_agent_orchestration.md (100%) rename {ai-company-brain => project-docs}/specs/multi_tenancy.md (99%) rename {ai-company-brain => project-docs}/specs/multi_tenancy_leak_audit.md (100%) rename {ai-company-brain => project-docs}/specs/multi_user_organization_research.md (99%) rename {ai-company-brain => project-docs}/specs/multiplayer_prior_art_qm_2026-08.md (98%) rename {ai-company-brain => project-docs}/specs/note_taker_app.md (100%) rename {ai-company-brain => project-docs}/specs/note_taker_research_2026-07.md (99%) rename {ai-company-brain => project-docs}/specs/observability_e2.md (99%) rename {ai-company-brain => project-docs}/specs/org_access_control.md (100%) rename {ai-company-brain => project-docs}/specs/paca_pm_research_2026-08.md (98%) rename {ai-company-brain => project-docs}/specs/people_center_app.md (100%) rename {ai-company-brain => project-docs}/specs/permissions_sandbox_b6.md (100%) rename {ai-company-brain => project-docs}/specs/plane_pm_research_2026-08.md (98%) rename {ai-company-brain => project-docs}/specs/project_management_app.md (100%) rename {ai-company-brain => project-docs}/specs/saas_multitenancy.md (100%) rename {ai-company-brain => project-docs}/specs/saas_multitenancy_handover.md (98%) rename {ai-company-brain => project-docs}/specs/saas_multitenancy_implementation.md (100%) rename {ai-company-brain => project-docs}/specs/single_agent_chat_bug_audit_2026-07.md (98%) rename {ai-company-brain => project-docs}/specs/skills_registry.md (100%) rename {ai-company-brain => project-docs}/specs/skills_scope_out.md (100%) rename {ai-company-brain => project-docs}/specs/subscription_console.md (100%) rename {ai-company-brain => project-docs}/specs/task_manager_app.md (99%) rename {ai-company-brain => project-docs}/specs/task_manager_harness_2026-07.md (96%) rename {ai-company-brain => project-docs}/specs/task_manager_hr_planning_and_memory.md (100%) rename {ai-company-brain => project-docs}/specs/tenancy_and_visibility.md (100%) rename {ai-company-brain => project-docs}/specs/user_management_contract.md (100%) rename {ai-company-brain => project-docs}/specs/whatsapp_calls_note_taker.md (99%) rename {ai-company-brain => project-docs}/specs/whatsapp_message_manager.md (100%) rename {ai-company-brain => project-docs}/specs/workflows_app.md (99%) rename {ai-company-brain => project-docs}/system_architecture.md (100%) rename {ai-company-brain => project-docs}/work_plan.md (95%) diff --git a/.env.example b/.env.example index c9e2c77bf..f63365e1d 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ OPENROUTER_API_KEY= GROQ_API_KEY= GITHUB_TOKEN= -# --- Note Taker STT (spec: ai-company-brain/specs/note_taker_app.md §3.4) --- +# --- Note Taker STT (spec: project-docs/specs/note_taker_app.md §3.4) --- # Transcription routes through the platform's LiteLLM plumbing: the model is the # `tier-stt` tier (infra/litellm/config.yaml, editable in Settings → Models), # resolved and keyed exactly like the chat tiers. Default: groq/whisper-large-v3-turbo. diff --git a/.github/agents/commandcenter.agent.md b/.github/agents/commandcenter.agent.md index 0f3f033dd..e030956f2 100644 --- a/.github/agents/commandcenter.agent.md +++ b/.github/agents/commandcenter.agent.md @@ -49,7 +49,7 @@ Read root `AGENTS.md` for the full list. Key ones: - Agent loader: `packages/acb_skills/acb_skills/loader.py` - Gateway routes: `apps/gateway/gateway/routes/` - Chat frontend: `workbench/control_plane/src/` -- System design: `ai-company-brain/` +- System design: `project-docs/` ## Available Dev Skills (`.github/skills/`) diff --git a/.github/prompts/system.md b/.github/prompts/system.md index b58480296..7cfece914 100644 --- a/.github/prompts/system.md +++ b/.github/prompts/system.md @@ -66,8 +66,8 @@ You can read, edit, and test any part of the codebase. - **Postgres schema**: `infra/postgres/` - **Chat frontend (Next.js)**: `workbench/control_plane/src/` - **Stream relay**: `apps/orchestrator/orchestrator/stream_relay.py` -- **System design docs**: `ai-company-brain/` -- **Agent builder guide**: `ai-company-brain/agent_repo_compatibility.md` +- **System design docs**: `project-docs/` +- **Agent builder guide**: `project-docs/agent_repo_compatibility.md` ## Development Commands diff --git a/.github/workflows/codebase-health.yml b/.github/workflows/codebase-health.yml index c202f7cf4..5b2cc7120 100644 --- a/.github/workflows/codebase-health.yml +++ b/.github/workflows/codebase-health.yml @@ -3,7 +3,7 @@ # rolling tracking issue with the complexity + size + maintainability report. # It FLAGS and PROPOSES — it never edits code. Acting on the report is done # by a human or a coding agent via the `codebase-health-audit` skill. -# Spec: ai-company-brain/specs/dev_velocity_tooling_2026-07.md +# Spec: project-docs/specs/dev_velocity_tooling_2026-07.md name: codebase-health on: @@ -67,7 +67,7 @@ jobs: cat health.txt echo '```' echo "" - echo "Ratchet plan: [\`dev_velocity_tooling_2026-07.md\`](../blob/main/ai-company-brain/specs/dev_velocity_tooling_2026-07.md)" + echo "Ratchet plan: [\`dev_velocity_tooling_2026-07.md\`](../blob/main/project-docs/specs/dev_velocity_tooling_2026-07.md)" } > "$body_file" # Find an existing open tracking issue by its label; update it, else create. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c4873020f..da1c52124 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,14 +32,14 @@ on: # WS-25: `paths-ignore` was REMOVED here, deliberately, and it cost a few # minutes of CI per docs commit to do it. # - # It used to skip this whole workflow for `**.md`, `ai-company-brain/**`, + # It used to skip this whole workflow for `**.md`, `project-docs/**`, # `skills/**` and `workbench/e2e/**`. That was a sound optimisation while # delivery was push-based and documentation did not need to reach the box. # It is wrong now, for two reasons: # 1. `publish-release` lives in this workflow, and a workflow-level path # filter skips the JOBS too — so a docs-only merge would never move # `release`, and the box would never converge on it. Agents running ON - # the box read AGENTS.md and ai-company-brain/specs; stale copies there + # the box read AGENTS.md and project-docs/specs; stale copies there # are the exact failure this workstream exists to remove. # 2. It made the stranding invisible. #357 was documentation-only, so no # run was ever queued for it — "no failed run" read as "nothing to do" diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index f599a869a..148ebb22e 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -13,7 +13,7 @@ on: # code could look green all the way up the stack. Run on every PR instead. paths-ignore: - "**.md" - - "ai-company-brain/**" + - "project-docs/**" permissions: contents: read @@ -54,7 +54,7 @@ jobs: # ~1.3k-finding style backlog + C901 complexity hotspots). This is the # ratchet dashboard — the number should trend down; rules graduate to the # blocking gate above as each class is cleaned. Ratchet plan + backlog: - # ai-company-brain/specs/dev_velocity_tooling_2026-07.md + # project-docs/specs/dev_velocity_tooling_2026-07.md - name: Ruff full report (style backlog dashboard, non-blocking) continue-on-error: true run: uv run ruff check . || echo "style backlog above (non-blocking — see ratchet plan)" diff --git a/AGENTS.md b/AGENTS.md index 28bea6d9a..92295b8a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ Copilot SDK sandboxes. ## Global Constraints (Non-Negotiable) -1. No in-app agent/skill *code* editing -- all code authoring is VS Code + Git. The Workflows app (`/workflows`) is the sanctioned exception-by-design: workflows are DB-persisted configuration orchestrating code-authored agents, compiled to MAF Workflows (ADR-028; spec ai-company-brain/specs/workflows_app.md) -- not generated agent code, not a second runtime +1. No in-app agent/skill *code* editing -- all code authoring is VS Code + Git. The Workflows app (`/workflows`) is the sanctioned exception-by-design: workflows are DB-persisted configuration orchestrating code-authored agents, compiled to MAF Workflows (ADR-028; spec project-docs/specs/workflows_app.md) -- not generated agent code, not a second runtime 2. No credentials in agent or skill repos -- Integration Registry holds all secrets 3. Self-mutation max_mutation_attempts = 1 per failure event - ⚠️ **DEV-ONLY / must be replaced before production:** native MAF agents (local_path, no own remote) currently land approved self-mutations by opening a PR against THIS Command Center monorepo. This is fine only while all agents are first-party and Command Center is WIP. It MUST be swapped for a tenant-isolated mechanism before any multi-tenant/customer deployment — third parties must never push to the shared monorepo. See `docs/DESIGN_LIMITATION_native_maf_mutation.md`. **This is now ticketed as `saas_multitenancy.md` MT-0b (WS-29) and is a HARD BLOCKER before customer #2** — the cheapest sufficient fix is a config gate defaulting to disabled, not a redesign. @@ -100,10 +100,10 @@ Copilot SDK sandboxes. 7. No Theia / browser IDE 8. Source systems are authoritative -- CommandCenter is a read-mostly mirror 9. New event-driven / specialist-agent execution features default to MAF paths; the Copilot-SDK runtime is reserved for interactive chat + mutation (both gateway-routed), not new autonomous execution entrypoints -10. **All gateway endpoints require auth, by construction rather than by opting in.** `require_authenticated` is attached app-wide at the `FastAPI(dependencies=[…])` level, so a route added tomorrow is covered without anyone remembering; `PUBLIC_ROUTES` is the exemption list and every entry authenticates itself another way. **Before building or modifying ANY app, read `ai-company-brain/specs/user_management_contract.md`** — the ten binding rules for identity, membership and authorization, each one learned by breaking it. In particular: never navigate the browser directly at the gateway (it carries no credentials), never add a route to `PUBLIC_ROUTES` to make it reachable, and never take the acting identity from a query parameter or request body — **nor the acting TENANT, which is R11, added 2026-08-08 with D15; the tenant comes from the authenticated session or a tenant-scoped API key and from nowhere else.** The contract carries **eleven** rules, not ten. +10. **All gateway endpoints require auth, by construction rather than by opting in.** `require_authenticated` is attached app-wide at the `FastAPI(dependencies=[…])` level, so a route added tomorrow is covered without anyone remembering; `PUBLIC_ROUTES` is the exemption list and every entry authenticates itself another way. **Before building or modifying ANY app, read `project-docs/specs/user_management_contract.md`** — the ten binding rules for identity, membership and authorization, each one learned by breaking it. In particular: never navigate the browser directly at the gateway (it carries no credentials), never add a route to `PUBLIC_ROUTES` to make it reachable, and never take the acting identity from a query parameter or request body — **nor the acting TENANT, which is R11, added 2026-08-08 with D15; the tenant comes from the authenticated session or a tenant-scoped API key and from nowhere else.** The contract carries **eleven** rules, not ten. 11. **Multi-tenancy is `organization_id` + Postgres RLS, and it is NOT built yet.** The tenant boundary was re-taken on 2026-08-08 (**D15**, board **WS-29**, spec - `ai-company-brain/specs/saas_multitenancy.md`): a tenant is a **row** isolated by + `project-docs/specs/saas_multitenancy.md`): a tenant is a **row** isolated by `FORCE ROW LEVEL SECURITY` bound at the `get_db()` seam; a deployment is a *placement*, not a boundary. This **supersedes `tenancy_and_visibility.md` §1 and §6** (one-deployment-per-tenant) — **§2–§5 of that document, the private → Center → org @@ -114,7 +114,7 @@ Copilot SDK sandboxes. `organization_id`; visibility inside a tenant stays `email | group: | org`), and **never give an agent a raw-SQL tool or a database connection** — §0.9.3 makes that a condition on the whole tenancy decision, not a nicety. Board rule **R5** - (`ai-company-brain/work_plan.md` §1, owner-directed 2026-08-09) binds every PR + (`project-docs/work_plan.md` §1, owner-directed 2026-08-09) binds every PR tenant-ready by construction while WS-29 is in flight: new persisted tables satisfy the tenant-coverage gate (or are exempted with a reason), no new database-connection or Redis sites outside the seam/wrapper, and session acquisition stays on the seam @@ -175,16 +175,16 @@ best practice: eval harnesses, observability, sandboxing). Consult it when designing or reviewing harness features; cite the practice you're applying or deliberately rejecting in the spec/PR. -- Our gap analysis + work queue: ai-company-brain/specs/harness_hardening_2026-07.md +- Our gap analysis + work queue: project-docs/specs/harness_hardening_2026-07.md (HH-1..8). Check it before starting harness work — the gap may already be queued, in-progress, or explicitly deferred with rationale. -- Competitor reference implementations: ai-company-brain/specs/competitive_hardening_2026-07.md +- Competitor reference implementations: project-docs/specs/competitive_hardening_2026-07.md (CH-1..9, sourced from COMPETITIVE_COMPARISON.md) — proven patterns from Hermes Agent (fail-closed approval, container sandbox flags, self-improving Curator, typed sub-agent messaging) and OpenClaw (durable job queue, hub-and-spoke channels; and its CVEs as the cautionary case). Consult it for "what good looks like" when hardening security, plumbing, or multi-agent coordination. -- Multiplayer prior art: ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md +- Multiplayer prior art: project-docs/specs/multiplayer_prior_art_qm_2026-08.md (QM-0..7, sourced from github.com/yc-software/qm) — a shipped multiplayer agent harness that independently reproduced our least-cleared-viewer rule. Consult it before building room concurrency (it folds a second turn into the live run as a @@ -236,5 +236,5 @@ Check with `uv tree` / `uv pip list`. Key runtime pins: `agent-framework-*` | Skills | skills/AGENTS.md | Skill definitions and SKILL.md patterns | | Infrastructure | infra/AGENTS.md | Docker Compose, Postgres, LLM tier config, Redis | | Deployment | deploy/AGENTS.md | Hostinger VPS deployment | -| Planning docs | ai-company-brain/AGENTS.md | Product requirements, project plan, architecture | +| Planning docs | project-docs/AGENTS.md | Product requirements, project plan, architecture | | Workbench UI | workbench/AGENTS.md | Control Plane (Next.js) and local dev tools | diff --git a/FOUNDATION_AUDIT_REPORT.md b/FOUNDATION_AUDIT_REPORT.md index 6638c7afa..3ca213261 100644 --- a/FOUNDATION_AUDIT_REPORT.md +++ b/FOUNDATION_AUDIT_REPORT.md @@ -6,7 +6,7 @@ > **Companion document:** `FOUNDATION_BUILDOUT_CHECKLIST.md` lists every missing / partial / unwired capability with rationale, dependencies, approach, and priority. -> **Update 2026-08-01 (doc-truth pass):** the findings below are a **2026-07-11 snapshot** and are preserved as the audit record — do not read them as current state. File paths predate the `apps/` → `apps/services/` restructure (e.g. `apps/gateway/` is now `apps/services/gateway/`). Current execution state is tracked in `FOUNDATION_BUILDOUT_CHECKLIST.md` and `ai-company-brain/work_plan.md`. Note: "BO-4" in §5 corresponds to what the checklist tracks as **BO-20** (event-bus consumer + durable job queue). +> **Update 2026-08-01 (doc-truth pass):** the findings below are a **2026-07-11 snapshot** and are preserved as the audit record — do not read them as current state. File paths predate the `apps/` → `apps/services/` restructure (e.g. `apps/gateway/` is now `apps/services/gateway/`). Current execution state is tracked in `FOUNDATION_BUILDOUT_CHECKLIST.md` and `project-docs/work_plan.md`. Note: "BO-4" in §5 corresponds to what the checklist tracks as **BO-20** (event-bus consumer + durable job queue). --- diff --git a/FOUNDATION_BUILDOUT_CHECKLIST.md b/FOUNDATION_BUILDOUT_CHECKLIST.md index 62ecf1e27..00094430d 100644 --- a/FOUNDATION_BUILDOUT_CHECKLIST.md +++ b/FOUNDATION_BUILDOUT_CHECKLIST.md @@ -2,8 +2,8 @@ **Date:** 2026-07-11 · **Deploy status updated:** 2026-07-13 · **Competitive refs added:** 2026-07-13 **§BO‑20 rewritten and verified against code: 2026-08-02** (WS‑4 audit remediation). Verified this pass: the ingestion package contents (no `worker.py`), the ClickUp → `event_hooks.emit_event` → `workflows.triggers.dispatch_event` → `start_run` fan-out, the Gmail/Zoho `TODO` stubs, the repo-wide absence of `xreadgroup`/`xgroup`/`xack`, the four checked-in systemd units, the already-provisioned Redis compose service, `uv.lock`'s lack of any job-queue library, and the gateway lifespan's supervised loops (five wired, **all five** stopped on shutdown, four actually started in the default config — WhatsApp enrichment is flag-gated off). §BO‑20 now carries acceptance criteria, verification commands, gate labels, and one named owner decision (§BO‑20.0). **That decision was answered on 2026-08-02 — `BO‑20 = Option A (in‑process)`** — so nothing in §BO‑20 is blocked on a decision any more. **BO‑20f and BO‑20a are BUILT (both 2026-08-02) and BO‑20b slice 1 (`emit_event` strict mode) 2026-08-03, moving §BO‑20 ☐ → ◑; BO‑20b slice 2 and BO‑20c–e are open and dispatchable.** ⚠️ **Slice 1 is necessary but NOT sufficient** (adversarial review 2026-08-03): the only sink production registers, `workflows.triggers.dispatch_event`, swallows every exception, so `raise_on_error=True` is a no‑op on the real registry — **slice 2's scope grew** to include a matching strict path in `dispatch_event`, and the §BO‑20 non‑goal "Not a change to `dispatch_event`" is struck and qualified. Two claims in the stamp above were made false by BO‑20a and are corrected in place below: `xreadgroup`/`xgroup`/`xack` now exist (in `ingestion/consumer.py` only), and the lifespan's supervised loops are **six** wired / **all six** stopped on shutdown (how many actually *start* is data‑dependent — see §BO‑20 "What is true today" §7, which counts it honestly; the new ingestion consumer joins WhatsApp enrichment as flag‑gated **off**, so it starts nowhere today). `INGESTION_CONSUMER` is registered in `work_plan.md` §6. **Other sections carry no such stamp** — BO‑1/BO‑19 were stamped by the 2026-08-01 doc-truth pass; the rest are as-authored. -**§BO‑10 / §BO‑13 / §BO‑14 / §BO‑15 / §BO‑19 re-measured and corrected, §BO‑23 added, and the "can we go app by app?" verdict block added: 2026-08-03** (WS‑0 truth pass, second commit on PR #344). Verified this pass by measuring, not by reading the prior doc: the **12** `create_async_engine` call sites across 10 modules and the fact that the 8 cached singletons are never disposed (BO‑10 said "three+"); `executor.py` at **5,010** lines and `run_agent_stream` at **~1,942** (BO‑13 claimed 4,069 / ~1,600 — both July numbers the file has since grown past); `permission_policy.decide`'s two hard‑veto return paths and `install_dependency`'s `destructive: True` annotation (BO‑14 claimed the gate "can never deny" and the registry was "empty" — **both false**); `model_limits.py` as the retired‑five‑sources context‑window SoT versus `_TIER_DEFAULTS` + three still‑on‑disk config files (BO‑15 is **half** closed, not closed and not untouched); the root `AGENTS.md`'s deleted version table and `infra/AGENTS.md`'s corrected proxy/Langfuse lines (BO‑19 → ✅); `dump_schema.sh`'s `--schema-only`, `apply_migrations.sh`'s 140‑file `ON_ERROR_STOP=1` replay, the absence of any WAL/pgbackrest/wal‑g config, and `deploy/hostinger/README.md:115` (→ **BO‑23**); and, live against GitHub, `branches/main/protection` → 404 with `rulesets` → `[]`. **Two audit claims handed to this pass were wrong and are not transcribed:** BO‑14/BO‑15 were described as fully closed (BO‑14's defects are closed but its *residual* is real and different; BO‑15 is half). Tenancy/visibility architecture from the same day lives in `ai-company-brain/specs/tenancy_and_visibility.md`, not here. *(Amended 2026-08-09: the tenancy half of that architecture now lives in `ai-company-brain/specs/saas_multitenancy.md` — D15, 2026-08-08; visibility stays with `tenancy_and_visibility.md` §2–§5.)* -**Companion to:** `FOUNDATION_AUDIT_REPORT.md` · handoff details in `FOUNDATION_CONTINUATION.md` (see its "LATEST STATUS" block) · competitive learnings (proven reference implementations from Hermes Agent & OpenClaw) in `ai-company-brain/specs/competitive_hardening_2026-07.md` (`CH-*`) and `COMPETITIVE_COMPARISON.md` · tenancy architecture of record in `ai-company-brain/specs/saas_multitenancy.md` (D15, 2026-08-08) · visibility architecture of record in `ai-company-brain/specs/tenancy_and_visibility.md` §2–§5. +**§BO‑10 / §BO‑13 / §BO‑14 / §BO‑15 / §BO‑19 re-measured and corrected, §BO‑23 added, and the "can we go app by app?" verdict block added: 2026-08-03** (WS‑0 truth pass, second commit on PR #344). Verified this pass by measuring, not by reading the prior doc: the **12** `create_async_engine` call sites across 10 modules and the fact that the 8 cached singletons are never disposed (BO‑10 said "three+"); `executor.py` at **5,010** lines and `run_agent_stream` at **~1,942** (BO‑13 claimed 4,069 / ~1,600 — both July numbers the file has since grown past); `permission_policy.decide`'s two hard‑veto return paths and `install_dependency`'s `destructive: True` annotation (BO‑14 claimed the gate "can never deny" and the registry was "empty" — **both false**); `model_limits.py` as the retired‑five‑sources context‑window SoT versus `_TIER_DEFAULTS` + three still‑on‑disk config files (BO‑15 is **half** closed, not closed and not untouched); the root `AGENTS.md`'s deleted version table and `infra/AGENTS.md`'s corrected proxy/Langfuse lines (BO‑19 → ✅); `dump_schema.sh`'s `--schema-only`, `apply_migrations.sh`'s 140‑file `ON_ERROR_STOP=1` replay, the absence of any WAL/pgbackrest/wal‑g config, and `deploy/hostinger/README.md:115` (→ **BO‑23**); and, live against GitHub, `branches/main/protection` → 404 with `rulesets` → `[]`. **Two audit claims handed to this pass were wrong and are not transcribed:** BO‑14/BO‑15 were described as fully closed (BO‑14's defects are closed but its *residual* is real and different; BO‑15 is half). Tenancy/visibility architecture from the same day lives in `project-docs/specs/tenancy_and_visibility.md`, not here. *(Amended 2026-08-09: the tenancy half of that architecture now lives in `project-docs/specs/saas_multitenancy.md` — D15, 2026-08-08; visibility stays with `tenancy_and_visibility.md` §2–§5.)* +**Companion to:** `FOUNDATION_AUDIT_REPORT.md` · handoff details in `FOUNDATION_CONTINUATION.md` (see its "LATEST STATUS" block) · competitive learnings (proven reference implementations from Hermes Agent & OpenClaw) in `project-docs/specs/competitive_hardening_2026-07.md` (`CH-*`) and `COMPETITIVE_COMPARISON.md` · tenancy architecture of record in `project-docs/specs/saas_multitenancy.md` (D15, 2026-08-08) · visibility architecture of record in `project-docs/specs/tenancy_and_visibility.md` §2–§5. > **🚀 Deploy status:** read live deploy state from `gh run list` and `git log origin/main` — not from this doc. Next recommended P0: **BO‑8** (secret rotation + history purge — owner‑gated); BO‑1's approval loop has since shipped (see §BO‑1). > **Update 2026-08-01 (doc-truth pass):** the previous pinned‑commit claim here (`origin/main = ccccdc8`, unpushed `1684e1a`) was a 2026‑07‑13 snapshot and went stale; this doc no longer tracks deploy state. @@ -32,7 +32,7 @@ added**: | # | Exception | Item | The one‑line reason | |---|---|---|---| -| 1 | ~~**`main` has no branch protection**~~ **CLOSED 2026‑08‑03** | §BO‑17 / `work_plan.md` WS‑5 | Was 404 / rulesets `[]` for months. **Now enabled:** PRs required (`required_approving_review_count: 0` — a sole maintainer must still be able to land work), `enforce_admins: true` (without it the protection is decorative for the only person who pushes), force‑pushes and deletions **blocked**. ⚠️ **`required_status_checks` is deliberately `null`** — `pr-check.yml` carries `paths-ignore: ["**.md", "ai-company-brain/**"]`, so a docs‑only PR runs **no** checks at all; requiring those contexts would leave every docs PR permanently unmergeable. To require them, first give `pr-check` an always‑runs sentinel job, then add that job as the only required context. | +| 1 | ~~**`main` has no branch protection**~~ **CLOSED 2026‑08‑03** | §BO‑17 / `work_plan.md` WS‑5 | Was 404 / rulesets `[]` for months. **Now enabled:** PRs required (`required_approving_review_count: 0` — a sole maintainer must still be able to land work), `enforce_admins: true` (without it the protection is decorative for the only person who pushes), force‑pushes and deletions **blocked**. ⚠️ **`required_status_checks` is deliberately `null`** — `pr-check.yml` carries `paths-ignore: ["**.md", "project-docs/**"]`, so a docs‑only PR runs **no** checks at all; requiring those contexts would leave every docs PR permanently unmergeable. To require them, first give `pr-check` an always‑runs sentinel job, then add that job as the only required context. | | 2 | **No *scheduled* backup; no restore ever exercised** — ◑ **both halves now addressed in-repo 2026‑08‑07; one owner step left** | **§BO‑23** (below) | The `acb-backup.service`/`.timer` units now exist under `deploy/hostinger/`, and `deploy.sh` installs and enables every unit in that directory on each deploy — so the schedule arrives with the code instead of waiting for someone to remember. **A restore has now been executed**, and keeps being executed: `scripts/rehearse_restore.sh` does a real round trip (seed → dump → DROP → restore → md5 compare) and runs on every PR against a real Postgres. Verified to fail when the restore does not restore, not only to pass when it does. **Left for the owner:** run one restore against a *production* backup — the rehearsal proves the tooling, not that this box's data and volume are what you think. Recovery position otherwise unchanged: Hostinger VM images, weekly, 2 retained, ~58 min, whole-machine. | | 3 | ~~**DB engine sprawl**~~ **CLOSED 2026‑08‑06** | §BO‑10 | Was **12 `create_async_engine` call sites across 10 modules** (+ one sync engine), 8 of them undisposed process‑lifetime singletons, one arriving per app — the only item whose cost compounded per app. **Now one engine and one pool for every async caller**, in `acb_common/db.py` (not the gateway: `acb_auth.access` runs in the gateway process and cannot import it). `acb_audit.record()` is non-blocking on the loop, drained at shutdown. A new engine now fails `tests/unit/test_db_engine_seam.py`. Remaining by design: `acb_graph`'s **sync** engine and `email_ingestion`'s per‑run engines. | @@ -124,13 +124,13 @@ Measured on this branch, 2026-08-03: **`32 passed in 1.95s`** — hermetic, no l - **Competitive ref (CH‑2):** Hermes Agent routes every risky action through a **single fail‑closed approval gate** — the pattern to copy is "one choke point that a write physically cannot bypass," which is exactly what `execute()`‑only‑writes enforces. See `specs/competitive_hardening_2026-07.md`. ### BO‑2 — Enforceable authentication + authorization *(P0)* ✅ -- **Authorization (2026‑07‑29):** org access control shipped — DB‑backed roles + per‑user allow/deny overrides (`acb_auth.permissions`/`access`), `require_permission` on every feature router, per‑agent run gating, and per‑member integration credentials. Spec: `ai-company-brain/specs/org_access_control.md`. The remaining BO‑2 work is the *authentication* posture (residual 1), not the permission model. +- **Authorization (2026‑07‑29):** org access control shipped — DB‑backed roles + per‑user allow/deny overrides (`acb_auth.permissions`/`access`), `require_permission` on every feature router, per‑agent run gating, and per‑member integration credentials. Spec: `project-docs/specs/org_access_control.md`. The remaining BO‑2 work is the *authentication* posture (residual 1), not the permission model. - **Missing (historical — resolved):** `get_current_user` never rejects (`acb_auth/deps.py:76`); it only labels. So mutation‑approve (`agent.py:1852`, `git push`), the memory API (`memory.py`, IDOR), and `/agent/webhook/{source}` (`agent.py:3428`; the long‑standing `:2522` here was stale — corrected 2026-08-02 to match §BO‑20) were anonymous‑reachable. `/v1` had no auth at all. **Update 2026-08-01 (doc-truth pass):** this paragraph is resolved history — every systemic item in the Residual list below is ✅; the honest remaining residual is that in‑process agents can read the identity token until BO‑7. - **Done this pass:** **✅ F1** authenticates `/v1`; **✅ F7** adds `acb_auth.require_internal_auth` and gates the state‑changing mutation routes + the whole `/memory` router (401 anonymous). This closes C1/C2/C6 for the specific dangerous endpoints. - **Why needed:** Prevents anonymous code‑push, cross‑tenant memory read/delete, and unauthenticated agent triggering. - **Dependencies:** Confirm each protected endpoint's caller sends the internal token or a real user session (the Next.js server routes and `memory.ts` already send `Bearer LITELLM_MASTER_KEY` — verified). -- **Residual (the systemic fix):** (1) Add `acb_auth.require_authenticated` and make it the DEFAULT posture rather than opt‑in per route — **✅ done**: attached once at `FastAPI(dependencies=[...])`, so every route is covered by construction and a new one needs no opt‑in. `gateway.main.PUBLIC_ROUTES` is the complete anonymous‑reachable list (health, provider webhooks, OAuth callbacks, bridge/bot callbacks — each self‑authenticating). Closed two live holes: `/agent/workspace/{id}/history` (anonymous read of an agent's file history) and `/promote` (anonymous write). Swagger/ReDoc cannot carry the guard structurally, so they are dev‑only now. A coverage test fails if any route ever bypasses the guard. (2) Cover the remaining `agent.py` routes and `oauth.py` — **✅ done**: agent registry writes now need `agents:manage`, all three run endpoints call `assert_can_run_agent`, and `oauth.py` authorize/refresh need `feature:integrations` (the callback stays open by design — HMAC‑signed `state`). (3) Sign/verify `/agent/webhook/{source}` — **✅ done**: HMAC‑SHA256 over the raw body in `X-CC-Signature`, per‑source secrets override a global one, and it FAILS CLOSED (503) when unconfigured. (4) Split the service‑identity token from `LITELLM_MASTER_KEY` — **✅ done**: `GATEWAY_INTERNAL_TOKEN` is identity‑only and `LITELLM_MASTER_KEY` is the `/v1` key checked by the new `require_llm_api_auth`; every `/v1` client reads `settings.llm_api_key`, with a test asserting none resolves the identity token. Residual: in‑process agents can still read it from `get_settings()` until **BO‑7**. See `ai-company-brain/specs/org_access_control.md` §8b. +- **Residual (the systemic fix):** (1) Add `acb_auth.require_authenticated` and make it the DEFAULT posture rather than opt‑in per route — **✅ done**: attached once at `FastAPI(dependencies=[...])`, so every route is covered by construction and a new one needs no opt‑in. `gateway.main.PUBLIC_ROUTES` is the complete anonymous‑reachable list (health, provider webhooks, OAuth callbacks, bridge/bot callbacks — each self‑authenticating). Closed two live holes: `/agent/workspace/{id}/history` (anonymous read of an agent's file history) and `/promote` (anonymous write). Swagger/ReDoc cannot carry the guard structurally, so they are dev‑only now. A coverage test fails if any route ever bypasses the guard. (2) Cover the remaining `agent.py` routes and `oauth.py` — **✅ done**: agent registry writes now need `agents:manage`, all three run endpoints call `assert_can_run_agent`, and `oauth.py` authorize/refresh need `feature:integrations` (the callback stays open by design — HMAC‑signed `state`). (3) Sign/verify `/agent/webhook/{source}` — **✅ done**: HMAC‑SHA256 over the raw body in `X-CC-Signature`, per‑source secrets override a global one, and it FAILS CLOSED (503) when unconfigured. (4) Split the service‑identity token from `LITELLM_MASTER_KEY` — **✅ done**: `GATEWAY_INTERNAL_TOKEN` is identity‑only and `LITELLM_MASTER_KEY` is the `/v1` key checked by the new `require_llm_api_auth`; every `/v1` client reads `settings.llm_api_key`, with a test asserting none resolves the identity token. Residual: in‑process agents can still read it from `get_settings()` until **BO‑7**. See `project-docs/specs/org_access_control.md` §8b. ### BO‑3 — Self‑mutation governance: human gate + real test gate + attempt counter *(P0)* ◑ - **Done this pass:** **✅ F8** — auto‑push is now opt‑in (`MUTATION_AUTO_PUSH`, default off) so a green commit stages in the approval inbox by default; `_tests_passed("")`/"no tests" now returns False (closes H3). **✅ H4** — `max_mutation_attempts` is now a REAL enforced counter: `mutation._register_mutation_attempt(run_id)` keeps a per‑run tally and refuses a second attempt for the same run (previously both call sites passed 0, so the `0 >= 1` guard was dead). 5 unit tests added (helper + the real entry point's early‑skip path). @@ -265,10 +265,10 @@ was stale — corrected 2026-08-02 to match §BO‑20) were anonymous‑reachabl > **This is the largest uncovered risk on the platform, and it scales with app count.** Every app that ships adds tables whose only copy is one Postgres volume on one VPS. It is filed P0 rather than P2 because unlike every other item here, the failure mode is *unrecoverable* — there is nothing to fix afterwards. **Update 2026‑08‑03 — tooling shipped, nothing is scheduled yet.** Runbook and -full rationale: `ai-company-brain/specs/backup_and_restore.md`. +full rationale: `project-docs/specs/backup_and_restore.md`. Done‑when 1 ✅ (`scripts/backup_db.sh`), 2 ✅ (`scripts/restore_db.sh`), -3 ✅ *(runbook is at `ai-company-brain/specs/backup_and_restore.md`, **not** +3 ✅ *(runbook is at `project-docs/specs/backup_and_restore.md`, **not** `deploy/hostinger/RESTORE.md` — `plan-guard` blocks agent writes under `deploy/`; the clause's intent, a verification step rather than only commands, is met)*, 4 ✅ (`apply_migrations.sh` now takes a dump before replaying the @@ -1747,7 +1747,7 @@ non‑blocking style backlog. 2. **P1 sprint:** BO‑7 (sandbox), BO‑5 (observability+cost), BO‑6 (migrations), BO‑12/BO‑14 (runtime + permission model), BO‑15 (LLM config SoT — **tier→model half only**), BO‑17/BO‑18 (gates), **BO‑20 (event‑bus consumer + job queue)**. *(BO‑19 closed 2026‑08‑03.)* 3. **P2/P3:** BO‑9, ~~BO‑10~~ **(✅ closed 2026‑08‑06 — one engine/pool + non-blocking audit; it was the one item that compounded per app)**, BO‑11, BO‑13, BO‑16, **BO‑21 (memory activation)**. -**Competitive‑informed items** (proven reference implementations from Hermes Agent / OpenClaw — full mapping in `ai-company-brain/specs/competitive_hardening_2026-07.md`): CH‑1→BO‑7/BO‑14, CH‑2→BO‑1, CH‑3→BO‑20, CH‑4→WBS 3.3, CH‑5→BO‑12, CH‑6→BO‑21, CH‑7→Phase‑5 Annealer, CH‑8→BO‑5. These do not change the sequencing above — they attach a "what good looks like" reference to items we already have, plus the two new items (BO‑20/BO‑21) the comparison surfaced. +**Competitive‑informed items** (proven reference implementations from Hermes Agent / OpenClaw — full mapping in `project-docs/specs/competitive_hardening_2026-07.md`): CH‑1→BO‑7/BO‑14, CH‑2→BO‑1, CH‑3→BO‑20, CH‑4→WBS 3.3, CH‑5→BO‑12, CH‑6→BO‑21, CH‑7→Phase‑5 Annealer, CH‑8→BO‑5. These do not change the sequencing above — they attach a "what good looks like" reference to items we already have, plus the two new items (BO‑20/BO‑21) the comparison surfaced. The review pass already delivered F1–F6 (see report §6), which knock out the open LLM proxy, the on‑disk secret/junk exposure, the false‑$0 cost bug, the migration‑number collision, and the worst doc drift — clearing the cheapest Critical/High items so the P0 sprint can focus on the architectural ones. diff --git a/FOUNDATION_CONTINUATION.md b/FOUNDATION_CONTINUATION.md index 81de3dea2..9cd95979b 100644 --- a/FOUNDATION_CONTINUATION.md +++ b/FOUNDATION_CONTINUATION.md @@ -1,6 +1,6 @@ # Foundation Audit — Continuation & Handoff Guide -> ⚠️ **Historical session log.** 'LATEST STATUS' below is the state as of **2026-07-13** and is NOT maintained; its `origin/main` pin is weeks stale. For current state read `ai-company-brain/work_plan.md` §2; for foundation items `FOUNDATION_BUILDOUT_CHECKLIST.md`. *(Banner added 2026-08-09.)* +> ⚠️ **Historical session log.** 'LATEST STATUS' below is the state as of **2026-07-13** and is NOT maintained; its `origin/main` pin is weeks stale. For current state read `project-docs/work_plan.md` §2; for foundation items `FOUNDATION_BUILDOUT_CHECKLIST.md`. *(Banner added 2026-08-09.)* **Purpose:** everything still needed to finish the foundational audit + fixing, written so it can be picked up on a machine **with Postgres access**. Read this alongside `FOUNDATION_AUDIT_REPORT.md` (findings) and `FOUNDATION_BUILDOUT_CHECKLIST.md` (item tracker). This doc is the *executable* plan — concrete files, DDL, code sketches, test approach, and verification commands. diff --git a/README.md b/README.md index 6c3f016e5..70fec2153 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Each **skill repo** is a Python package — pip‑installable, well‑typed entr ## This repo layout (CommandCenter Core) ``` -ai-company-brain/ # Planning docs — start with AGENTS.md +project-docs/ # Planning docs — start with AGENTS.md project_plan.md # Requirements + roadmap + WBS system_architecture.md # C4 diagrams, data model, ADRs reference.md # MAF / Copilot SDK / memory notes @@ -231,14 +231,14 @@ See [`Makefile`](Makefile) for convenience targets. ## Planning docs -All planning artefacts live in [`ai-company-brain/`](ai-company-brain/). Start with `AGENTS.md`. +All planning artefacts live in [`project-docs/`](project-docs/). Start with `AGENTS.md`. | Doc | Purpose | |---|---| | [`AGENTS.md`](AGENTS.md) | Root project contract + constraints + conventions (read first) | -| [`ai-company-brain/project_plan.md`](ai-company-brain/project_plan.md) | Requirements + milestones + WBS + risks | -| [`ai-company-brain/system_architecture.md`](ai-company-brain/system_architecture.md) | C4 diagrams, data model, ADRs | -| [`ai-company-brain/reference.md`](ai-company-brain/reference.md) | MAF / Copilot SDK / memory library notes | +| [`project-docs/project_plan.md`](project-docs/project_plan.md) | Requirements + milestones + WBS + risks | +| [`project-docs/system_architecture.md`](project-docs/system_architecture.md) | C4 diagrams, data model, ADRs | +| [`project-docs/reference.md`](project-docs/reference.md) | MAF / Copilot SDK / memory library notes | | [`FOUNDATION_AUDIT_REPORT.md`](FOUNDATION_AUDIT_REPORT.md) | Foundational architecture audit (findings by severity) | | [`FOUNDATION_BUILDOUT_CHECKLIST.md`](FOUNDATION_BUILDOUT_CHECKLIST.md) | Missing/partial foundational capabilities + priorities (incl. `CH-*` competitive refs, BO‑20/BO‑21) | -| [`COMPETITIVE_COMPARISON.md`](COMPETITIVE_COMPARISON.md) | Three‑way comparison vs Hermes Agent & OpenClaw — where we stand + what to learn (annealed into `ai-company-brain/specs/competitive_hardening_2026-07.md`) | +| [`COMPETITIVE_COMPARISON.md`](COMPETITIVE_COMPARISON.md) | Three‑way comparison vs Hermes Agent & OpenClaw — where we stand + what to learn (annealed into `project-docs/specs/competitive_hardening_2026-07.md`) | diff --git a/apps/agents/agent-task-manager/agents.py b/apps/agents/agent-task-manager/agents.py index 2d1a1ab2a..5654e06cb 100644 --- a/apps/agents/agent-task-manager/agents.py +++ b/apps/agents/agent-task-manager/agents.py @@ -1,6 +1,6 @@ """agent-task-manager — the GTD Task Manager agent. -The agent behind the /tasks app (spec: ai-company-brain/specs/ +The agent behind the /tasks app (spec: project-docs/specs/ task_manager_app.md §3.1): captures thoughts, clarifies the inbox through the GTD decision tree, organizes items toward LOCAL or a connected PM workspace (ClickUp first), and answers status/progress/workload questions. diff --git a/apps/services/gateway/AGENTS.md b/apps/services/gateway/AGENTS.md index 968c59f5b..18d44cff6 100644 --- a/apps/services/gateway/AGENTS.md +++ b/apps/services/gateway/AGENTS.md @@ -17,21 +17,21 @@ webhook receivers, OAuth callbacks, and the Control Plane API. 1. main.py -- FastAPI app factory, lifespan (key loading, model cache warmup, aiosmtpd inbound SMTP startup, background email sync scheduler, and the seven supervised loops — email sync, WhatsApp enrichment, tasks provider-sync, calendar auto-rollover, workflow schedule scanner, the BO-20a ingestion event-bus consumer `ingestion.consumer.start_ingestion_consumer` (flag-gated OFF on `INGESTION_CONSUMER`), and the CRM⟷Zoho two-way sync `routes.crm.sync_zoho.start_crm_zoho_sync` (flag-gated OFF on `CRM_ZOHO_SYNC`, and the gate lives inside that function, not here); **every one of the seven is stopped unconditionally on shutdown**, including the flag-gated ones whose loop may never have started — for the ingestion consumer that contract is pinned by `tests/unit/test_ingestion_consumer.py::test_gateway_lifespan_starts_the_consumer_and_stops_it_after_yield`, which reads `main.py` as text and asserts start-before-`yield`, stop-after, and no flag guard on the stop), /copilot/chat AG-UI endpoint (relayed through stream_relay.run_detached when thread_id present) 2. routes/agent.py -- /agent/run, /agent/run/stream (detached: agent survives client disconnect), /agent/run/{thread_id}/reconnect (replay + live follow), /agent/webhook, agent CRUD, mutation inbox (approve/reject). Org access control: `GET /agent` filters the registry to agents the caller may run (unless they hold `agents:manage`, who see everything); all THREE run endpoints (`/run`, `/run/stream`, `/run/async`) call `assert_can_run_agent` — the filtered picker is UX, the endpoint check is the boundary of record. Registry writes (register/patch/delete/pull) need `agents:manage`. `/agent/webhook/{source}` is the fourth run path and is authenticated by HMAC-SHA256 over the raw body (`X-CC-Signature`; per-source `AGENT_WEBHOOK_SECRET_` overrides the global one) — it FAILS CLOSED with 503 when no secret is configured, because it starts an agent run and is internet-reachable. The same events also fan out to workflow event triggers (`routes/workflows/triggers.dispatch_event`, best-effort) — an event can route to an agent, to workflows, or both 3. routes/chat.py -- Chat history CRUD (Postgres-backed sessions and messages) + GET /chat/active-sessions (Redis cc:active:* scan for running agents). Authorization is MEMBERSHIP, not ownership: every predicate that used to be `WHERE user_id = :uid` now goes through `gateway/rooms.py` (`SESSION_VISIBLE_SQL` for lists, `resolve_room_access` for a single session), which is the same answer when a session has one participant. `_get_messages` also applies two room rules that are no-ops outside a room — the late-joiner waterline (`chat_session_participant.join_message_ts`) and the clearance filter (`_render_message` redacts a turn produced by a run acting on capabilities the reader does not hold). `_upsert_messages` stamps authorship SET-ONCE: the three racing writers cannot rename an author, and a human turn is always attributed to the authenticated caller, never to a client-supplied identity. -3a. routes/rooms.py + rooms.py + room_stream.py -- The room layer (spec: ai-company-brain/specs/groups_sessions_authority.md, docs/multiplayer/README.md). `resolve_room_access(session_id, email) -> RoomAccess` is the single source of room authorization: owner/member/viewer, graded so "may read but not send" is expressible; group subjects expand at read time; the most capable matching subject wins. The two "we don't know" cases resolve in OPPOSITE directions, deliberately: a **missing session row** is permissive (a thread with no row belongs to nobody, so it belongs to whoever is asking — every new conversation is in that state until its first turn persists), while a **failed lookup** now resolves to NO access and sets `resolve_failed` (it used to return full owner access, which handed the caller read/send/cancel on a room that may be somebody else's at the one moment we cannot tell). `denied()` reads `resolve_failed` to say "try again" rather than "you are not a participant". `cc:room:{tid}` is a SECOND stream alongside `cc:stream:{tid}` because the latter is deleted at every run boundary and a room's memory of itself must outlive runs. `GET /chat/sessions/{id}/room` answers the whole header in one call INCLUDING the capability cap — what the room cannot reach and who is the reason — because the intersection rule's failure mode is mystery. Agents in a room live in `chat_session_agent`, deliberately NOT in `chat_session_participant`: participants are access principals the intersection folds, and an agent has no access of its own. Tables: infra/postgres/138, 139. +3a. routes/rooms.py + rooms.py + room_stream.py -- The room layer (spec: project-docs/specs/groups_sessions_authority.md, docs/multiplayer/README.md). `resolve_room_access(session_id, email) -> RoomAccess` is the single source of room authorization: owner/member/viewer, graded so "may read but not send" is expressible; group subjects expand at read time; the most capable matching subject wins. The two "we don't know" cases resolve in OPPOSITE directions, deliberately: a **missing session row** is permissive (a thread with no row belongs to nobody, so it belongs to whoever is asking — every new conversation is in that state until its first turn persists), while a **failed lookup** now resolves to NO access and sets `resolve_failed` (it used to return full owner access, which handed the caller read/send/cancel on a room that may be somebody else's at the one moment we cannot tell). `denied()` reads `resolve_failed` to say "try again" rather than "you are not a participant". `cc:room:{tid}` is a SECOND stream alongside `cc:stream:{tid}` because the latter is deleted at every run boundary and a room's memory of itself must outlive runs. `GET /chat/sessions/{id}/room` answers the whole header in one call INCLUDING the capability cap — what the room cannot reach and who is the reason — because the intersection rule's failure mode is mystery. Agents in a room live in `chat_session_agent`, deliberately NOT in `chat_session_participant`: participants are access principals the intersection folds, and an agent has no access of its own. Tables: infra/postgres/138, 139. 4. routes/oauth.py -- OAuth authorize->callback->refresh for Zoho/ClickUp/Google 5. routes/integrations.py — Integration Registry management, MCP server CRUD, Plugin install/remove -5a. routes/integrations_skills.py — WS-23 S1 read-only skills catalog: GET /integrations/skills returns the skill-family registry (acb_skills.skill_families) with MEASURED per-family token costs (real addendum render + tool JSON schemas, counted with the assemble_run_context tokenizer) plus a per-agent matrix derived from each agent's declared config.json tool_scope (no scope ⇒ all families, injection is fail-open) INTERSECTED with stored S2 skill settings (`disabled_families` per row — the matrix shows the effective surface). Same feature gate as routes/integrations.py (spec: ai-company-brain/specs/skills_registry.md) +5a. routes/integrations_skills.py — WS-23 S1 read-only skills catalog: GET /integrations/skills returns the skill-family registry (acb_skills.skill_families) with MEASURED per-family token costs (real addendum render + tool JSON schemas, counted with the assemble_run_context tokenizer) plus a per-agent matrix derived from each agent's declared config.json tool_scope (no scope ⇒ all families, injection is fail-open) INTERSECTED with stored S2 skill settings (`disabled_families` per row — the matrix shows the effective surface). Same feature gate as routes/integrations.py (spec: project-docs/specs/skills_registry.md) 5b. routes/agent_skills.py — WS-23 S2 per-agent skill toggles: GET/PUT /agent/{name}/skills over the `agent_skill_setting` table (org-access override shape — reason/set_by provenance; replace-wholesale PUT gated `admin:access:manage`). Rules enforced at the API: family must exist in SKILL_FAMILIES; `core` and `apps` are un-toggleable → 422 (core = floor, apps = managed via app_grants). Enforcement of stored rows lives in orchestrator/_tool_injection.py::_resolve_injected_scope (intersection only; no rows ⇒ byte-identical to pre-S2) 6. routes/memory.py -- Memory search and management endpoints. The path parameter is a SCOPE KEY (`acb_memory.scope_key`: an email, `agent:`, or `org:global`), not a user id, and `_authorize_scope` answers one rule per shape — your own email only (not admins': administering members is not reading their private context), agent memory on the same `agents:run:` the run path enforces, org memory on `memory:read_org`/`memory:write_org`, `room:` on room membership (its audience IS the room), `prefs:` on being that person, unknown shapes refused so a growing vocabulary fails closed. `require_internal_auth` alone used to be the whole gate, which was the wrong gate: it proves the caller IS the platform, and the Next BFF is the platform on behalf of every signed-in user. A service principal reaches shared scopes but must assert `X-User-Email` to touch a person's — omitting an identity must fail closed, since omission is how the original hole happened. `delete` also verifies the memory is IN the scope, or naming your own scope with someone else's memory id deletes theirs. 7. routes/settings.py -- LLM settings, model config -8. routes/email.py -- Email account CRUD, message listing/search, send, sync, AI chat, OAuth flow for Gmail/Microsoft/IMAP. Background sync scheduler hooks (refresh/remove) on account PATCH/DELETE. ⚠️ **`scheduler_hooks.py::process_new_mail` is the shared new-mail pipeline and the one seam the scheduler, the manual-sync route and the Graph webhook all funnel through** — rules → sweep → categorize senders → classify threads → auto-archive → **CRM auto-lead**. The last step is NOT this package's: it lives in `routes/crm/auto_lead.py` (WS-26d-autolead), runs after auto-archive so it only ever sees what is still in the inbox once the account's own automation has finished, and is called from inside `if auto_lead_enabled():` so the OFF state — which is the shipped state — enters no CRM code at all. Every step stays try/except-isolated on `sync.*_failed`: a failure in any of them, the CRM one included, must never break mail sync. ⚠️ This hook is **also reached by deep resyncs and first syncs**, which classify up to a year of mail without stamping `rules_held_back_at`; anything added here that acts per-message needs its own history discriminator (the CRM step's is a two-timestamp cursor). **`transport/oauth.py` — the connect flow**: the authorize leg is GATED and stays gated (the browser reaches it through the Next BFF; see "Authentication posture" below), and its `user_email` query parameter is a **fallback only** — `user.email` wins, or a crafted parameter still decides whose account the mailbox attaches to. ⚠️ **Known defect, filed not fixed** (`ai-company-brain/specs/email_app_master_plan.md` §7 Tier 1 item 1): `_oauth_states` is a module-level in-process dict. Every deploy restarts the gateway, so any OAuth flow in flight when a deploy lands loses its state and the callback fails `state not in _oauth_states` → the user is bounced to `/email/oauth/callback?error=invalid_state`. It is also not shared across workers, and entries are never expired so abandoned flows leak forever. Fix is Redis + TTL alongside signing the state; do not paper over it by widening the state check. transport/contacts.py serves GET /email/contacts/card — the people card behind a sender's name/avatar (identity, correspondence stats, last N messages, plus phone/title/company parsed out of the sender's own signature). Read side is derived entirely from mail already in the caller's own accounts (every query goes through core._account_scope); no directory lookup, no provider call. Write side: each open upserts what the parse learned into `email_contacts` (mig 119) via `_remember_contact`, so the mailbox accumulates a people directory for the planned Contacts view — see ai-company-brain/specs/email_app_master_plan.md §3.14 before extending it. Invariants that must not be weakened: `manual_fields[]` columns are never overwritten by a parse; an empty parse never blanks a stored value; derived facts (counts, last-seen) are never stored; the domain→company guess is display-only and applied AFTER the write. +8. routes/email.py -- Email account CRUD, message listing/search, send, sync, AI chat, OAuth flow for Gmail/Microsoft/IMAP. Background sync scheduler hooks (refresh/remove) on account PATCH/DELETE. ⚠️ **`scheduler_hooks.py::process_new_mail` is the shared new-mail pipeline and the one seam the scheduler, the manual-sync route and the Graph webhook all funnel through** — rules → sweep → categorize senders → classify threads → auto-archive → **CRM auto-lead**. The last step is NOT this package's: it lives in `routes/crm/auto_lead.py` (WS-26d-autolead), runs after auto-archive so it only ever sees what is still in the inbox once the account's own automation has finished, and is called from inside `if auto_lead_enabled():` so the OFF state — which is the shipped state — enters no CRM code at all. Every step stays try/except-isolated on `sync.*_failed`: a failure in any of them, the CRM one included, must never break mail sync. ⚠️ This hook is **also reached by deep resyncs and first syncs**, which classify up to a year of mail without stamping `rules_held_back_at`; anything added here that acts per-message needs its own history discriminator (the CRM step's is a two-timestamp cursor). **`transport/oauth.py` — the connect flow**: the authorize leg is GATED and stays gated (the browser reaches it through the Next BFF; see "Authentication posture" below), and its `user_email` query parameter is a **fallback only** — `user.email` wins, or a crafted parameter still decides whose account the mailbox attaches to. ⚠️ **Known defect, filed not fixed** (`project-docs/specs/email_app_master_plan.md` §7 Tier 1 item 1): `_oauth_states` is a module-level in-process dict. Every deploy restarts the gateway, so any OAuth flow in flight when a deploy lands loses its state and the callback fails `state not in _oauth_states` → the user is bounced to `/email/oauth/callback?error=invalid_state`. It is also not shared across workers, and entries are never expired so abandoned flows leak forever. Fix is Redis + TTL alongside signing the state; do not paper over it by widening the state check. transport/contacts.py serves GET /email/contacts/card — the people card behind a sender's name/avatar (identity, correspondence stats, last N messages, plus phone/title/company parsed out of the sender's own signature). Read side is derived entirely from mail already in the caller's own accounts (every query goes through core._account_scope); no directory lookup, no provider call. Write side: each open upserts what the parse learned into `email_contacts` (mig 119) via `_remember_contact`, so the mailbox accumulates a people directory for the planned Contacts view — see project-docs/specs/email_app_master_plan.md §3.14 before extending it. Invariants that must not be weakened: `manual_fields[]` columns are never overwritten by a parse; an empty parse never blanks a stored value; derived facts (counts, last-seen) are never stored; the domain→company guess is display-only and applied AFTER the write. 9. routes/v1_compat.py -- OpenAI-compatible /v1/chat/completions endpoint (used by Copilot SDK BYOK provider and MAF OpenAIChatCompletionClient). Includes message sanitization for providers with strict validation (e.g. DeepSeek rejects assistant messages with neither content nor tool_calls). 10. routes/debug.py -- E2 post-hoc diagnostics over the agent_run trace store (GET /debug/runs, /debug/runs/{id}, POST .../flag). EXECUTIVE/AGENT-gated. 11. routes/observability.py -- E2 LIVE observability over the global activity bus (cc:activity): GET /observability/activity/recent (backfill), /observability/activity/stream (SSE, agent+model activations across chat and ALL apps), /observability/active (runs in flight), /observability/roster (all agents + working/idle status for the office view), /observability/cost (daily LLM $ rollup by model/app). EXECUTIVE/AGENT-gated. Publish side: acb_common.activity + the executor run boundary + acb_llm._emit_usage (which also prices each call via litellm). App attribution is automatic — acb_llm.context._infer_app_source() reads the caller's gateway.routes. module, so any new app is observable with zero wiring. -12. routes/notes/ -- AI Note Taker /notes API (spec: ai-company-brain/specs/note_taker_app.md): meeting CRUD + library search, recording upload (multipart -> NOTES_MEDIA_DIR) + audio playback, background transcription pipeline over the pluggable acb_stt provider layer (BYOK Groq/OpenAI/Deepgram; summary_run rows carry per-stage status/errors), notes generation on acb_llm (templates.py prompt compiler + summaries.py grounded map-reduce -> meeting_note + draft action_item; auto-chained after transcription), and a per-meeting SSE progress stream (events.py). Same core/feature-module layout as routes/tasks. **A meeting is private to its `owner_email`**: `feature:notes` is permission to keep your own meetings here, never to read the company's — the transcripts are verbatim recordings of conversations nobody chose to publish. The owner predicate is written ONCE, in `core.OWNED_MEETING_PREDICATE` / `core.load_owned_meeting`, and is **case-insensitive** (`owner_email` is stamped verbatim from `X-User-Email`; an IdP that returns the same UPN cased differently between sessions would otherwise silently empty a member's own library). `meetings.py` binds it in the library SQL and in `_load_meeting` (404, never 403, so an id is never confirmed); `summaries.summarize` and `recordings.retranscribe` call `load_owned_meeting` directly. Rows with a NULL `owner_email` stay visible to everyone: pre-migration-95 legacy, and both insert paths stamp an owner, so the set cannot grow. Dispatch acts AS the owner (sends from their mailbox, files tasks under their name), so `dispatch.cross_owner_refusal` gates BOTH `_dispatch` and `_dispatch_email` — the seam, not the endpoints. `_dispatch` has three call sites but **seven** request paths reach it: `/actions/{id}/dispatch` and `/meetings/{id}/actions/approve-all` directly; `/meetings/{id}/summarize` via notes generation → `auto_dispatch`; and `/meetings/{id}/upload`, `/meetings/{id}/recordings/{rid}/complete`, `/meetings/{id}/retranscribe` and `/meetings/bot-join` via the transcription pipeline → generation → `auto_dispatch`. Count the request paths, not the call sites — that undercount is what hid the hole. There is **no sentinel actor**: `auto_dispatch(meeting_id, triggered_by)` takes the identity of the member whose request started the generation, threaded down as a REQUIRED parameter through `enqueue_summary` → `generate_notes` and `run_transcription`, and judged by `cross_owner_refusal` like any other. A previous `AUTOMATIC_ACTOR = "auto"` that the check waved through was a request-triggerable bypass — every "background" job here is started by somebody's HTTP call. For the meeting-bot ingest (nobody clicks it) the identity is `meeting_bot.requested_by`, the member who sent the notetaker — NOT the meeting's owner, because `bot_join` will attach a bot to a meeting the caller does not own. The N1-N3 remainder is CLOSED (2026-08-04, `colleague_onboarding.md` §4): `recordings.upload_recording` / `start_recording` / `get_audio` call `load_owned_meeting`; `_recording_path` — the loader `/chunk` and `/complete` share — joins `meeting` and binds the predicate, so neither route can acquire the hole separately and the per-chunk path pays no extra round trip; `qa.ask_meeting` loads the meeting BEFORE the transcript, so the 409 "no transcript yet" is not an oracle; `copilot`'s stream checks BEFORE the `StreamingResponse` starts (a 404 inside a started stream is a broken connection, not a refusal); `share.draft_followup_email` is a *read* — there is no grant, token or redemption path in that module, the send is a separate `/email/send`; `live.live_token` is scoped only when it names a meeting; `actions._load_action` joins `meeting` so both single-item routes inherit the scope, and `approve-all` was aligned to 404 rather than answering 200-with-an-empty-list; `meeting_bot.bot_join`'s attach branch binds the predicate INTO the `UPDATE` (no load-then-write window) and judges the CALLER, since checking the row against its own `owner_email` would compare the meeting to itself. **`live.py`'s `/live/wanted` and `/stt/bot-live-token` stay machine-authed on purpose** — the bot worker holds `MEETING_BOT_TOKEN` and no member identity, so an owner predicate has no owner and every way to invent one turns the bot token into a way to *assert* an identity. ⚠️ **Still NOT owner-scoped — spec §4's new N5, nine modules N1's table never enumerated:** `summaries.py`'s `GET`/`PUT /meetings/{id}/note` + `GET .../actions`, `copilot_context.py`, `copilot_agenda.py`, `meeting_bot.py`'s `/bot`, `/bot/diagnostics`, `/bot/screenshot`, `/bot/stop`, `live_transcript.py` (incl. `POST /meetings/{id}/say`, which speaks into the call), `live_session.py`, `speaker_id.py`, `agenda_progress.py`, `events.py`. ⚠️ **`/notes/meetings/{meeting_id}/live/wanted` is in neither `main.PUBLIC_ROUTES` nor this router's `exempt` list** while both its siblings are in both, so the gate 401s the worker before `_check_bot_auth` runs — filed, not fixed (the repair opens a route). `pipeline.py` and `summaries.generate_notes` are background jobs with no caller identity of their own — they are safe only because every entry point into them now carries `triggered_by`. -12a. routes/tasks/ -- Task-manager `/tasks` API (spec: ai-company-brain/specs/task_manager_app.md). Same core-is-the-leaf layout as routes/notes and routes/apps. **Two different scoping rules live here and must not be conflated.** Items and accounts are OWNER-scoped: per-user `user_id = :uid` predicates on items, and `core._assert_account_owner` bound at every account route. The **people directory** (`people.py` + `capability.py`'s `/people/embed`) is NOT owner-scoped and must never be made so — `gtd_people` is an *org* roster, not per-user rows. Its rule is `colleague_onboarding.md` §4 N4, owner-answered 2026-08-04: **directory open, HR fields restricted.** Both halves are defined once in `core.py` — `can_read_hr_fields` (`PEOPLE_HR_READ_PERMISSION = "admin:members:read"`) and `require_people_write()` (`PEOPLE_WRITE_PERMISSION = "admin:members:manage"`), both **existing** capabilities, never new slugs (a new slug is nobody's grant until an admin creates it, which would switch HR features off for the owner too). `GET /people` serves the basic org chart to any `feature:tasks` holder and projects `people.HR_FIELDS` (skills, skills_source, résumé summary, years of experience, capacity/current-load/available) to null/empty for everyone else; its `?q=` drops the `unnest(skills)` clause for the same caller, because matching on a column that is then stripped turns the search box into an oracle for it. **All FOUR write routes** — `POST /people`, `PATCH /people/{person_id}`, `POST /people/{person_id}/resume` and `capability.py`'s `POST /people/embed` — carry `require_people_write()` as a route dependency; count routes, not call sites. ⚠️ `_row_to_person(row, *, include_hr)` is keyword-only with **no default** on purpose: a route added later must state which audience it serves rather than inherit the permissive answer. ⚠️ The projection is at the **serialization** layer, never in the SQL, because `fetch_people_for_clarify` runs its own query and MUST keep returning full rows — it is the in-process delegation feed for `ai.py`, `capture_email.py` and `planning.py`, takes `db` and no user, and is never reached through the router. Narrowing it degrades agent delegation silently and protects nobody. Pinned by `tests/unit/test_tasks_people_scoping.py`. +12. routes/notes/ -- AI Note Taker /notes API (spec: project-docs/specs/note_taker_app.md): meeting CRUD + library search, recording upload (multipart -> NOTES_MEDIA_DIR) + audio playback, background transcription pipeline over the pluggable acb_stt provider layer (BYOK Groq/OpenAI/Deepgram; summary_run rows carry per-stage status/errors), notes generation on acb_llm (templates.py prompt compiler + summaries.py grounded map-reduce -> meeting_note + draft action_item; auto-chained after transcription), and a per-meeting SSE progress stream (events.py). Same core/feature-module layout as routes/tasks. **A meeting is private to its `owner_email`**: `feature:notes` is permission to keep your own meetings here, never to read the company's — the transcripts are verbatim recordings of conversations nobody chose to publish. The owner predicate is written ONCE, in `core.OWNED_MEETING_PREDICATE` / `core.load_owned_meeting`, and is **case-insensitive** (`owner_email` is stamped verbatim from `X-User-Email`; an IdP that returns the same UPN cased differently between sessions would otherwise silently empty a member's own library). `meetings.py` binds it in the library SQL and in `_load_meeting` (404, never 403, so an id is never confirmed); `summaries.summarize` and `recordings.retranscribe` call `load_owned_meeting` directly. Rows with a NULL `owner_email` stay visible to everyone: pre-migration-95 legacy, and both insert paths stamp an owner, so the set cannot grow. Dispatch acts AS the owner (sends from their mailbox, files tasks under their name), so `dispatch.cross_owner_refusal` gates BOTH `_dispatch` and `_dispatch_email` — the seam, not the endpoints. `_dispatch` has three call sites but **seven** request paths reach it: `/actions/{id}/dispatch` and `/meetings/{id}/actions/approve-all` directly; `/meetings/{id}/summarize` via notes generation → `auto_dispatch`; and `/meetings/{id}/upload`, `/meetings/{id}/recordings/{rid}/complete`, `/meetings/{id}/retranscribe` and `/meetings/bot-join` via the transcription pipeline → generation → `auto_dispatch`. Count the request paths, not the call sites — that undercount is what hid the hole. There is **no sentinel actor**: `auto_dispatch(meeting_id, triggered_by)` takes the identity of the member whose request started the generation, threaded down as a REQUIRED parameter through `enqueue_summary` → `generate_notes` and `run_transcription`, and judged by `cross_owner_refusal` like any other. A previous `AUTOMATIC_ACTOR = "auto"` that the check waved through was a request-triggerable bypass — every "background" job here is started by somebody's HTTP call. For the meeting-bot ingest (nobody clicks it) the identity is `meeting_bot.requested_by`, the member who sent the notetaker — NOT the meeting's owner, because `bot_join` will attach a bot to a meeting the caller does not own. The N1-N3 remainder is CLOSED (2026-08-04, `colleague_onboarding.md` §4): `recordings.upload_recording` / `start_recording` / `get_audio` call `load_owned_meeting`; `_recording_path` — the loader `/chunk` and `/complete` share — joins `meeting` and binds the predicate, so neither route can acquire the hole separately and the per-chunk path pays no extra round trip; `qa.ask_meeting` loads the meeting BEFORE the transcript, so the 409 "no transcript yet" is not an oracle; `copilot`'s stream checks BEFORE the `StreamingResponse` starts (a 404 inside a started stream is a broken connection, not a refusal); `share.draft_followup_email` is a *read* — there is no grant, token or redemption path in that module, the send is a separate `/email/send`; `live.live_token` is scoped only when it names a meeting; `actions._load_action` joins `meeting` so both single-item routes inherit the scope, and `approve-all` was aligned to 404 rather than answering 200-with-an-empty-list; `meeting_bot.bot_join`'s attach branch binds the predicate INTO the `UPDATE` (no load-then-write window) and judges the CALLER, since checking the row against its own `owner_email` would compare the meeting to itself. **`live.py`'s `/live/wanted` and `/stt/bot-live-token` stay machine-authed on purpose** — the bot worker holds `MEETING_BOT_TOKEN` and no member identity, so an owner predicate has no owner and every way to invent one turns the bot token into a way to *assert* an identity. ⚠️ **Still NOT owner-scoped — spec §4's new N5, nine modules N1's table never enumerated:** `summaries.py`'s `GET`/`PUT /meetings/{id}/note` + `GET .../actions`, `copilot_context.py`, `copilot_agenda.py`, `meeting_bot.py`'s `/bot`, `/bot/diagnostics`, `/bot/screenshot`, `/bot/stop`, `live_transcript.py` (incl. `POST /meetings/{id}/say`, which speaks into the call), `live_session.py`, `speaker_id.py`, `agenda_progress.py`, `events.py`. ⚠️ **`/notes/meetings/{meeting_id}/live/wanted` is in neither `main.PUBLIC_ROUTES` nor this router's `exempt` list** while both its siblings are in both, so the gate 401s the worker before `_check_bot_auth` runs — filed, not fixed (the repair opens a route). `pipeline.py` and `summaries.generate_notes` are background jobs with no caller identity of their own — they are safe only because every entry point into them now carries `triggered_by`. +12a. routes/tasks/ -- Task-manager `/tasks` API (spec: project-docs/specs/task_manager_app.md). Same core-is-the-leaf layout as routes/notes and routes/apps. **Two different scoping rules live here and must not be conflated.** Items and accounts are OWNER-scoped: per-user `user_id = :uid` predicates on items, and `core._assert_account_owner` bound at every account route. The **people directory** (`people.py` + `capability.py`'s `/people/embed`) is NOT owner-scoped and must never be made so — `gtd_people` is an *org* roster, not per-user rows. Its rule is `colleague_onboarding.md` §4 N4, owner-answered 2026-08-04: **directory open, HR fields restricted.** Both halves are defined once in `core.py` — `can_read_hr_fields` (`PEOPLE_HR_READ_PERMISSION = "admin:members:read"`) and `require_people_write()` (`PEOPLE_WRITE_PERMISSION = "admin:members:manage"`), both **existing** capabilities, never new slugs (a new slug is nobody's grant until an admin creates it, which would switch HR features off for the owner too). `GET /people` serves the basic org chart to any `feature:tasks` holder and projects `people.HR_FIELDS` (skills, skills_source, résumé summary, years of experience, capacity/current-load/available) to null/empty for everyone else; its `?q=` drops the `unnest(skills)` clause for the same caller, because matching on a column that is then stripped turns the search box into an oracle for it. **All FOUR write routes** — `POST /people`, `PATCH /people/{person_id}`, `POST /people/{person_id}/resume` and `capability.py`'s `POST /people/embed` — carry `require_people_write()` as a route dependency; count routes, not call sites. ⚠️ `_row_to_person(row, *, include_hr)` is keyword-only with **no default** on purpose: a route added later must state which audience it serves rather than inherit the permissive answer. ⚠️ The projection is at the **serialization** layer, never in the SQL, because `fetch_people_for_clarify` runs its own query and MUST keep returning full rows — it is the in-process delegation feed for `ai.py`, `capture_email.py` and `planning.py`, takes `db` and no user, and is never reached through the router. Narrowing it degrades agent delegation silently and protects nobody. Pinned by `tests/unit/test_tasks_people_scoping.py`. 13. routes/apps/ -- Custom Apps / App Workshop `/apps` API (RFC: docs/app-workshop/README.md, Phase 0+2a): app CRUD + workspace scaffold (apps_root() = CUSTOM_APPS_ROOT or {agents_clone_dir}/custom_apps, git-inited, app.json manifest per RFC §4.1), edit-gated workspace file passthrough (containment-guarded), publish → immutable app_versions rows + rollback + sandbox-CSP bundle serving, and the App Runtime API the `cc` bridge calls (/me, /data app-scoped JSON storage with shared/per-user partitions, /ai/complete on acb_llm tier aliases with a monthly per-app token budget from app_audit, /usage). Phase 1 draft durability (durability.py): eligible workspace text files mirror into `app_files` (write-through on file PUT, full sync on create/publish/POST /{slug}/sync) and a missing workspace lazily rehydrates from the store via `ensure_workspace` (the read-path choke point in files._edit_workspace + publish's draft reads); per-edit git checkpoints on the workspace repo (POST /{slug}/sync, GET /{slug}/checkpoints, POST /{slug}/restore — additive `checkout -- .` + restore commit, best-effort/never-500). Phase 2a `cc.tools` (tools.py): POST /{slug}/tools/{tool} — a small local `_TOOL_REGISTRY` (currently just `clickup.create_task`) checked against the LIVE manifest's `tool:?constraints` scopes (`parse_tool_scope`/`find_declared_tool_scope`; request args can never override a frozen constraint, `merge_tool_args`); read-only tools execute inline, destructive ones need a per-use confirm or a remembered `app_tool_grants` row ("always allow for this app") and then flow through the Action Broker (`action_broker.propose/submit`, namespaced action names `app.` to avoid colliding with `routes/tasks/broker_handlers.py`'s bare `clickup.create_task` registration — same flat `_HANDLERS` dict, different owners). Publish-time admin review (publish.py): an org-visibility publish declaring a non-read-only tool scope queues an `app.publish_review` proposal (SUGGEST → always NEEDS_APPROVAL) instead of going live, unless the same `scope_set_hash` was already approved on a prior version; approval flips `app_versions.review_status` and repoints `apps.live_version` (tools.py's `_apply_publish_review`, the registered handler). A rejected review has no reconciliation path — republish a new version. Phase 2a manifest `actions` (actions.py): POST /{slug}/actions/{name} + in-process `execute_app_action(slug, action_name, args, user)` (the orchestrator's agent-tool wrapper calls this directly, no HTTP loopback) — the reverse direction from `cc.tools`: named, typed capabilities OTHER callers (API clients, platform agents with `UserContext(role=AGENT)`) invoke INTO an app, checked against the LIVE manifest's `actions` array (four kinds: `storage.list`/`storage.get`/`storage.set` over `app_data`'s shared partition, `tool.call` wrapping a tool the app already declared a `tool:` scope for — reuses tools.py's `find_declared_tool_scope`/`merge_tool_args`/`_TOOL_REGISTRY`/`_broker_action_name`/`propose`/`submit` wholesale). `readonly` is derived, never manifest-trusted (hardcoded for the three storage kinds; `_TOOL_REGISTRY[...].read_only` for `tool.call`). Readonly actions and `storage.set` auto-apply for any viewer (person or agent) — `storage.set` only touches the app's own already-publish-reviewed storage; non-readonly `tool.call` auto-applies via the broker (`AuthorityTier.AUTONOMOUS`) only for a person who can also edit the app, else unconditionally `AuthorityTier.SUGGEST` (`NEEDS_APPROVAL`, no bypass — no confirm-toast is possible for an unattended API/agent caller). Tables: infra/postgres/114_custom_apps.sql + 115_app_files.sql + 116_app_tool_grants.sql. Same core/feature-module layout as routes/tasks (`_common.py` is the leaf). -12c. routes/crm/ -- Native CRM `/crm` API (spec: ai-company-brain/specs/crm_app.md; WS-26a builds §3 + §4, WS-26b the Zoho sync in §7.1, WS-26f the pipeline repair + settings surface in §5.1). Same core-is-the-leaf layout as routes/tasks and routes/notes: `core.py` owns the router, the **entity registry**, the Pydantic models, the row→model mapper and the SQL helpers; `records.py` / `pipeline.py` / `activities.py` / `admin.py` / `deal_contacts.py` / `import_zoho.py` / `stage_metadata.py` / `sync_zoho.py` register routes on it as an import side effect. `broker_handlers.py` registers **no** routes and is deliberately NOT imported from `__init__.py` — `main.py` calls its `register_crm_broker_handlers()` at startup exactly as it calls the tasks app's. Four entities (`crm_leads`/`crm_deals`/`crm_contacts`/`crm_organizations`) with **statuses as data, not enums** (`crm_lead_statuses`/`crm_deal_statuses` carry color/position/**type**/probability — D-CRM-2, so the Zoho importer can represent real stage names and the owner can reshape the pipeline without a deploy), one activity spine (`crm_activities`, four nullable target FKs with a CHECK requiring at least one), and a `crm_status_changes` funnel log. Tables: infra/postgres/144_crm.sql. +12c. routes/crm/ -- Native CRM `/crm` API (spec: project-docs/specs/crm_app.md; WS-26a builds §3 + §4, WS-26b the Zoho sync in §7.1, WS-26f the pipeline repair + settings surface in §5.1). Same core-is-the-leaf layout as routes/tasks and routes/notes: `core.py` owns the router, the **entity registry**, the Pydantic models, the row→model mapper and the SQL helpers; `records.py` / `pipeline.py` / `activities.py` / `admin.py` / `deal_contacts.py` / `import_zoho.py` / `stage_metadata.py` / `sync_zoho.py` register routes on it as an import side effect. `broker_handlers.py` registers **no** routes and is deliberately NOT imported from `__init__.py` — `main.py` calls its `register_crm_broker_handlers()` at startup exactly as it calls the tasks app's. Four entities (`crm_leads`/`crm_deals`/`crm_contacts`/`crm_organizations`) with **statuses as data, not enums** (`crm_lead_statuses`/`crm_deal_statuses` carry color/position/**type**/probability — D-CRM-2, so the Zoho importer can represent real stage names and the owner can reshape the pipeline without a deploy), one activity spine (`crm_activities`, four nullable target FKs with a CHECK requiring at least one), and a `crm_status_changes` funnel log. Tables: infra/postgres/144_crm.sql. - ⚠️ **The engine seam is the point (BO-10 / D-CRM-4).** The gateway had TWELVE module-level `create_async_engine` call sites; this package adds none. `gateway/db.py::get_engine()/get_session_factory()/get_db()` is the `routes/tasks/core.py` block lifted verbatim (same URL coercion, same pool sizing, same connect timeout), and **`routes/tasks/core.py` was converted to consume it in the same change** as the proof that consuming it changes nothing. Tasks keeps its `_get_db`/`_get_session_factory` private aliases because ~50 call sites import them by name and every route test monkeypatches `_get_db` on the SUT submodule — renaming would be a rename of the *test seam*, not of the engine. The other ten call sites are explicitly out of scope. Both directions are grep-asserted in `tests/unit/test_crm_routes.py`. - **A status transition has THREE effects, always** (`pipeline.apply_status_transition`, called by `records.py`'s PATCH — never inlined): a `crm_status_changes` row with `dwell_seconds`, a `status_change` activity, and a re-stamped `status_changed_at`. A PATCH that writes only the new `status_id` looks right in the UI and silently empties the funnel report. Deals carry `status_changed_at`; **leads deliberately do not** (§3.3), so their dwell comes from `max(changed_at)` in the log, falling back to `created_at`. Entering a `lost`-TYPE status without a `lost_reason_id` is a **422 before any of the three effects**; `won`/`lost` stamp `closed_at`; a NULL probability inherits the stage's default (never overwriting a stated one). - **Sort keys are an allowlist dict per entity, never interpolation** (`Entity.sorts`); an unknown key is 422, not a silent fall back to the default. Every identifier reaching an f-string is one of ours (a registry table name, an allowlist value, a model field name); every caller value is a bound parameter. `page_size` is capped at 100 in `list_contract` itself, not only in the route signature, so an agent tool reaching the kernel another way inherits the cap. @@ -55,8 +55,8 @@ webhook receivers, OAuth callbacks, and the Control Plane API. - **A hand-edited `lead_name` survives a PATCH that moves its inputs** (`core.lead_name_is_derived`): the name is re-derived only while the stored value still equals what the fallback chain would produce. Answered by recomputing rather than by a `lead_name_is_custom` column — a flag has to be maintained by every writer (importer, sync engine, agent tools) and the one that forgets it silently reverts a typed name. - ⚠️ **The timeline's THIRD source is email, and it is the ONE place in this package scoped to the CALLER rather than to the org** (WS-26d-email, spec §9). Everything else here follows D-CRM-3 — org-visible to every `feature:crm` holder, no owner predicate. Email cannot: the CRM is org-visible while a mailbox belongs to one person, so an unscoped join publishes one member's inbox to the whole company. `activities._timeline(entity, record_id, limit, user)` therefore **requires the caller** and all four routes pass it; a route that drops `user` again would compile, return a timeline, and have no identity left to scope by. The predicate is `activities._email_account_scope`, a **verbatim copy** of `routes/email/core.py::_account_scope` (D-CRM-4, the same call `broker_handlers.broker_gate` made — importing another route package's private helper is the coupling this package declined once already). ⚠️ **Two copies is a coincidence; a THIRD copy anywhere means promote it to a shared module instead.** The fragment hardcodes the alias `em`, so the query aliases `email_messages` as `em` and it drops in unchanged (`email/automation/analytics.py` had to `.replace()` it). Other invariants: the unit is the **thread** (`DISTINCT ON (account_id, COALESCE(thread_id, id::text))` — a row-per-message timeline double-counts every conversation, and grouping on a raw nullable `thread_id` folds every un-threaded message in an account into one entry); addresses resolve **once per record**, not per source, because a deal's set already contains its originating lead's and a per-source pass would return every inherited thread twice; `crm_deals` has no `email` column so a deal joins through `lead_id → crm_leads.email` **and** `crm_deal_contacts → crm_contacts.email`, unioned, with the lead's threads labelled `origin="lead"`; **no addresses means no query at all** (an empty `IN ()` is a syntax error, and the failure a fallback would produce is the whole mailbox on a record that names nobody); inbound `from_address` only in v1, and organizations deliberately do **not** join by domain (an `@fracktal.in` match would attach the entire company mailbox to our own org record). Index: `(account_id, LOWER(from_address->>'email'))` on `email_messages` — the two FTS GINs bury the address inside a `to_tsvector` and are usable only via `@@`. ⚠️ **`tests/unit/test_crm_email_timeline.py` carries a MUTATION FENCE**: deleting the `_email_account_scope(…)` call must turn `test_a_holder_with_no_mailbox_sees_no_email` and `test_two_holders_each_see_only_their_own_account` RED. `_crm_fakes.py` grew four readers so it can see that (a scope subquery, a lowercased JSONB address comparison, a composite LEFT JOIN, and `DISTINCT ON` grouping) — before them the fake did not merely ignore the scope, `_PLAIN_EQ` MISREAD the subquery's own `user_id = :uid`. Do not simplify the SQL to suit the fake; extend the fake. - ⚠️ **`auto_lead.py` (WS-26d-autolead) — the package's second UNATTENDED writer, and the only one reached from another app's hook.** It registers **no routes** and is therefore NOT imported from `__init__.py` (same reason as `broker_handlers.py`); its one entry point `create_leads_from_new_mail(account_id)` is called from `routes/email/scheduler_hooks.py::process_new_mail`. **It lives here rather than in the email package because what it does is write a CRM record** — it owns `crm_auto_lead_cursors`, it goes through `records.create_record`, and its flag is a CRM owner gate. Unlike the timeline join above it **imports** the automation package's PUBLIC identity primitives (`sender_scope` / `resolve_org_domains` / `normalize_domain`) instead of copying them: D-CRM-4 declined to import another package's *private* helper, and a third copy of "is this person a colleague?" is exactly the drift that rule prevents. Six properties are load-bearing. **(1) The flag is read at the CALL SITE, before the step is entered** — `if auto_lead_enabled(): await create_leads_from_new_mail(...)` — so with `CRM_AUTO_LEAD` off no CRM code runs and no CRM query is issued on the mail path; `auto_lead_enabled` is the flag's ONE definition and a gate moved *inside* the step is pinned red by an AST assertion, not only by a runtime sentinel. **(2) THREE cursor facts, three questions.** `process_new_mail` is also reached by ~1-year deep resyncs and by a newly connected mailbox's first sync, and neither stamps `rules_held_back_at`, so "everything classified" would mint a lead per unknown sender across a year of mail — each born `zoho_dirty` and queued for the LIVE tenant within one 600s cycle (D-CRM-9), with no confirmation card on a scheduler hook and no delete tool. `received_at > activated_at` is the backfill discriminator, `rules_processed_at > processed_watermark` is the incremental cursor, and `last_run_at` is the DORMANCY clock. ⚠️ **The third column is not redundant and `activated_at` is not "set once":** the anchor means *the current ON epoch*, because `activated_at` alone did nothing about a flag turned off for four weeks and back on — the first ON cycle minted the whole OFF window (27 leads, measured). A gap in `last_run_at` beyond `REANCHOR_GAP_SECONDS` (3600) **CLAMPS `activated_at` to `now - 1h` and runs the batch anyway** — clamp, never reset, and never an early return. ⚠️ **This step is not invoked once per scheduler period**: `email_ingestion/scheduler.py:463-472` fires the hook only when a sync PERSISTED mail, so the cycle that trips dormancy always carries the message that woke it, and a reset-to-`now` anchor excluded that message permanently — every night, every weekend (measured). Clamping keeps the OFF-window backlog excluded while admitting the last hour, with that hour recorded as the accepted residual. The watermark is untouched on re-anchor. Dormancy reads `last_run_at` rather than the watermark because a cycle that ran and found no candidates advances one and not the other — and that is the state a deliberately-held cursor (below) sits in, which the watermark version would re-anchor past. **(2b) A failure never advances the cursor past lost work.** The watermark moves over the contiguous PREFIX that wrote its leads and stops at the first that raised; this step opens a second session per lead through `create_record` while holding the batch's own, so pool exhaustion fails many at once and an unconditional advance stepped over all of them (3 leads lost, measured). A held cursor logs `sync.auto_lead_stalled` at WARNING EVERY cycle, because a held cursor and a quiet mailbox both create nothing and only the level tells them apart. A failed first ACTIVITY is the counter-case — counted separately, never holding the cursor, since the lead is already committed and would be skipped on retry. **(3) Dedup is a SELECT guard plus in-batch de-duplication, never `ON CONFLICT`** — `crm_leads` has no unique constraint on email (`idx_crm_leads_email` is a plain index; only `zoho_id` is UNIQUE), so the ticket's original upsert arm could not have fired. The cross-invocation race is ACCEPTED and recorded; **do not "fix" it with a unique index** (1,516 imported rows, the migration-148 shape). **(4) "External" is necessary, not sufficient** — `sender_scope` fails SAFE to `"external"`, which is the wrong direction when the consequence is a lead row for your own CFO in a live Zoho tenant, so the normalised internal-domain list is a second, independent gate that matches SUBDOMAINS too (`cfo@mail.fracktal.in` is the CFO), anchored on a leading dot so `notfracktal.in` is still a prospect. **(5) The lead goes through `records.create_record`, never raw SQL** (`_resolve_status`, the `owner_email` default, `validate_source` and `mark_dirty_on_insert` all live only there, and only the last is visible in the row afterwards), and `lead_name` is left to `compute_lead_name` over a display name STRIPPED before it is split. **(6) The first activity is `type='system'` — outside `sync_zoho.push_activities`' `type IN ('note','task')` predicate — carrying the subject and the sender in `meta` and an EMPTY body.** The step never selects `body_text` or `snippet`: the projection is the privacy boundary (D-CRM-12 applied to what a machine writes). **(7) The Sent probe folds case, unlike `_maybe_block_cold`** — `@>` is case-EXACT, so a reply from `asha@` after we wrote to `Asha@` minted a lead for somebody mid-conversation; this module uses `EXISTS (… jsonb_array_elements … lower(…) = :addr)` and leaves the email package's predicate alone. **(8) Both attacker-controlled strings are clipped** (`MAX_NAME_CHARS`, `MAX_SUBJECT_CHARS`): the display name becomes `lead_name`, which every list, board card and Zoho push then carries. `tests/unit/test_crm_auto_lead.py` (75 cases) carries a FIFTEEN-mutant fence over exactly those properties, and `_crm_fakes.py` gained two readers plus `fail_on(..., after=N)` for it — without the readers the Sent probe was invisible and the fake answered "yes" for every Sent message, and without the offset a prefix-only cursor and an unconditional one are indistinguishable. -14. routes/admin/ -- Org access control `/admin` API + `/auth/me` (spec: ai-company-brain/specs/org_access_control.md, Phase 1): member roster and lifecycle (invite/suspend/remove — soft, because ~every user-scoped table keys people by email — **plus a separate hard delete**, below), role assignment, custom role CRUD, per-user allow/deny overrides, and the feature catalog the admin UI renders from. `GET /auth/me` is deliberately NOT admin-gated — every signed-in member calls it to resolve their own feature/agent access, and it returns resolved OUTCOMES (allowed feature slugs, runnable agent names) rather than raw permission patterns, so the matching rule has exactly one implementation. `GET /admin/members/{email}/access` returns each decision WITH its provenance (which role granted it, which override took it away) — the admin UI shows that verbatim rather than re-deriving it. Invariants enforced in `_common.py`: the org always keeps an owner, nobody assigns a role above their own rank, system roles are immutable, and **nobody locks themselves out**. That fourth one (`assert_not_self_lockout`, `colleague_onboarding.md` §2 Step 5 / N7+N8) is called by `update_member` (PATCH), `remove_member` (DELETE) **and `purge_member` (DELETE …/purge)** — three doors reach the same `is_active = False`, and while the check lived inside DELETE alone the PATCH had none: `PATCH {"status": "suspended"}` on your own row was refused only by `assert_owner_survives` firing coincidentally in a one-owner org, so a second owner opened it. ⚠️ The rule is **"any status that is not `active`"**, never a list of destructive ones — `EffectiveAccess.is_active` is `status == "active"` exactly, so `invited` is a lockout too, and an enumeration would have to remember it. Comparison is case-insensitive and empty-safe on both sides (an IdP that re-cases a UPN must not switch the guard off; a caller with no identity is not everybody). ⚠️ **It and `assert_owner_survives` both answer 409** — a test that asserts the bare status code cannot tell which fired, and for self-suspension the one that fires today on `main` is the wrong one; discriminate on the detail text and on what was written (`tests/unit/test_admin_member_offboarding.py`). **`purge_member` — `DELETE /admin/members/{email}/purge` (N8)** is the hard delete: a SEPARATE route on the same `admin:members:manage`, never a flag on Remove (which would put the irreversible path one typo from the reversible one). Its decision is **purge the person, keep their work** — the `app_user` row, every access grant (`user_role`, `user_permission_override`, `org_group_member`, `chat_session_participant`, `app_grants`, `app_tool_grants`), every credential (`email_accounts`, `wa_accounts`, `task_accounts`), their PRIVATE `chat_session` rows and their `access_request` row go; what they authored and **the audit trail stay** (an audit trail that disappears with the person is not one — `app_audit` already carries a FK-less `app_id` commented "audit survives hard delete"). ⚠️ **Nothing is anonymised, on purpose**: the address is the join key across ~50 tables, so scrubbing `owner_email` would orphan the apps rather than hide the person. ⚠️ **The three credential rows cascade, and the map is `members._CREDENTIAL_CASCADES`** — `email_accounts` takes the whole mirrored mailbox (**17 direct children, 20 with transitives**), `wa_accounts` the whole WhatsApp mirror (**14 / 16**; `wa_media` hangs off `wa_messages`, NOT off the account), `task_accounts` the SYNCED half of `gtd_items` **and `gtd_projects`**; the credential is `NOT NULL` on the row, so it cannot go without it. That map is hand-maintained and says so, and is pinned against `infra/postgres/` by a test that re-derives it — the first version named 15 of the 20 email tables, which on a route whose safety argument is "the admin is told the blast radius before clicking" is the wrong direction of error. ⚠️ **THREE tables are split across both lists, and each predicate is load-bearing:** `chat_session` by `visibility` (private deleted, shared kept — a room cascades `chat_message` and one person's off-boarding must not take a shared transcript), and `gtd_items` + `gtd_projects` by `account_id` (`IS NOT NULL` = the SYNCED mirror, counted and deleted explicitly; `IS NULL` = the LOCAL rows they authored here, kept). ⚠️ **A KEEP clause must exclude everything the delete side CASCADES away, not merely everything it names.** The `tasks` keep clause originally had no `account_id` predicate, so a member with 847 synced tasks was answered `kept: {"tasks": 847}` while all 847 went with `task_accounts` — the response reported a destruction as a survival. `_PURGE_DELETES`/`_PURGE_KEEPS` derive `count_sql` and `delete_sql` from ONE `where` clause so the count and the delete cannot differ, but that is a within-row-spec guarantee and says nothing about a third statement three entries up; one transaction, one commit, and `record_admin_change` fires BEFORE it (`acb_audit` has its own session, so the record of a destruction survives a rollback of it — though `acb_audit/log.py:49` swallows every exception, so a *completed* purge is NOT guaranteed to leave an audit row). Pinned by `tests/unit/test_admin_member_purge.py` — including the structural assertion that no audit table appears on the delete side at all, the exact permission slug on the route (deleting it leaves the `admin:members:read` floor, which `manager` holds), and the cross-table cascade fences built on `tests/unit/_schema_cascade.py`, which derives the FK graph from the numbered migrations. ⚠️ **`_admin_fakes._FakeDB` models no foreign keys and therefore no cascades**, so every cross-table claim here has to be structural; no behavioural case over a seeded fake can make one. Every write calls `invalidate_access` so a change lands immediately instead of after the resolver's 60s TTL. Tables: infra/postgres/130_org_access_control.sql. Same `_common.py`-is-the-leaf layout as routes/apps and routes/tasks — and here the leaf rule is strict: feature modules import from `_common`, **never from each other**. ⚠️ **The `/admin` auth floor is PER-ROUTE, not a package property.** `_common.py` creates the router with **no** `dependencies=`; every route declares `Depends(require_admin_user)` in its own signature. A route added without it inherits no floor at all and is reachable by any authenticated member — the easiest hole to ship in this package. `access_requests.py` — **sign-in requests** (`colleague_onboarding.md` §6 / N6a, migration 143): `/admin` was push-only, so somebody arriving at the front door produced a journald warning nobody read back (53 of them for one address over 18 hours on 2026-08-03/04, and the owner learned out of band). `acb_auth.access.resolve_access` now upserts an `access_request` row when — and ONLY when — `record_request=True`, which exactly one caller passes; `GET /admin/members/requests` + `POST .../{email}/approve|deny` let the owner answer it, both writes on the EXISTING `admin:members:invite` (no new slug — a new slug is nobody's grant until an admin creates it). **Approve provisions AND activates in one action** (`status='active'`, not `'invited'`) because an approval IS the decision to let somebody in and they are already at the door; leaving them `invited` would re-create the two-click trap §2 Step 1b documents. Both provisioning callers go through `_common.provision_member` — ONE path, so invariants 1 and 2 apply to approvals too (it calls `assert_owner_survives` itself, because `set_roles` REPLACES assignments and provisioning the last owner with the default `member` role would otherwise delete the org's only owner grant). ⚠️ **Both writes hold `admin:members:invite`, which is WEAKER than the `admin:members:manage` that suspends or off-boards, so every path by which the weaker one could reverse the stronger is a cross-gate escalation.** Two independent locks, and each is load-bearing for a different sequence: (1) `_load_request(db, email, *, allowed_statuses=…)` — keyword-only, no default — refuses an already-DECIDED row, because decided rows are kept on purpose (dw9) and the tab renders only `pending`, so a decided row is invisible *and* still addressable; approve takes `("pending",)`, deny takes `("pending", "denied")` since re-denying grants nothing, and denying an *approved* request is refused because it could only make the queue contradict the roster. (2) `_common._PROVISION_MEMBER_SQL`'s `ON CONFLICT` arms **name the statuses they rewrite and never negate**: `invited` → the caller's status (the one door to `active`), `removed` → the caller's status **only when it is not `active`** (so invite still returns an off-boarded person as `invited`, byte-for-byte as before, while approve cannot reinstate them — `removed → active` stays `PATCH /admin/members/{email}`), and `active`/`suspended` are never touched. ⚠️ A `<>`/`NOT IN` test against `app_user.status` is the mutation to watch for: it reads as tidier and silently rewrites rows set under a stronger permission. `tests/unit/test_signin_requests.py` pins the SQL **structurally** (`test_provisioning_only_ever_rewrites_a_status_it_names`) — its fake DB re-implements the `ON CONFLICT` arms in Python and a mirror can only agree with itself, so the behavioural cases there cannot see the statement being widened and must not be trusted to. ⚠️ **Lock (2) declines SILENTLY — it just does not rewrite the row — so it is only half an answer, and the other half is `APPROVE_MATRIX`.** Approve used to run its `_decide(…, "approved")` after that quiet decline: HTTP 200, request marked `approved`, `set_roles` re-granting `['member']` to an off-boarded member, and the person gone for good from a tab that renders only `pending` (the resolver's upsert never rewrites `status`). **`access_requests.APPROVE_MATRIX`, read by `_disposition_for` BEFORE anything is written, is the contract:** absent → provision; `invited` → activate + assign the roles; `active` → do nothing, leave their roles alone, resolve the request as `approved` and say so in `ApproveResult.detail`; `suspended`/`removed` → **409, request stays `pending`** so the person stays visible; anything else → refuse (fail closed). The invariant: **approve never rewrites the roles of a member who already exists in a state other than `invited`** — `provision_member` ends in `set_roles`, which REPLACES assignments, and roles are otherwise `admin:members:manage` territory. The matrix is pinned against `members.VALID_STATUSES`, so a fifth member status cannot ship without somebody deciding what approving one means. `_DECIDE_SQL` also binds the read's own status filter into the UPDATE (`AND status = ANY(:allowed) … RETURNING id`) and 409s on zero rows **before `db.commit()`**, so a lost race discards its own provisioning instead of half-applying it; each route must pass `_decide` the same tuple it passed `_load_request` (a test asserts that from the source). -15. routes/workflows/ -- Workflows app `/workflows` API (spec: ai-company-brain/specs/workflows_app.md; RFC: docs/workflow-editor/README.md): workflow CRUD over the React-Flow-native edit-model (`workflows.graph` jsonb, persisted verbatim), publish → compile to an immutable `workflow_versions.serialized` run-model (edit-model ≠ run-model; runs pin versions), run start/history/detail + a per-run SSE event stream (in-process hub in service.py; runs are supervised asyncio tasks — durable queueing is BO‑20), the served node catalog (agents from the live registry, integrations from acb_skills with availability probe, workflow tool registry, ready modules — the palette is never hard-coded, spec D7), Module Studio (workflow_modules CRUD + conversational generate on acb_llm tier routing + AST validate + subprocess test/run), the inbound webhook trigger `POST /workflows/hooks/{hook_token}` (public by token — in PUBLIC_ROUTES + the router's exempt list; optional HMAC `X-CC-Signature`; rate-limited; fires only published workflows with an enabled webhook trigger), and the cron schedule scanner (scheduler.py — apscheduler CronTrigger parsing inside a supervised asyncio loop with CAS claims on `last_fired_at`; started/stopped from main.py lifespan). The engine subpackage (engine/: templating, graph compile/validate, node handlers over injected NodeServices, MAF WorkflowBuilder runner, module AST validator + restricted subprocess runner) is transport-free — no FastAPI/DB imports — so it is unit-testable alone and movable into the orchestrator if isolation later demands. Agent nodes call `orchestrator.executor.run_agent` (source="workflow", MAF batch path — constraint #9); write-class tool nodes dispatch through `action_broker.propose/submit` (fail closed, constraint #4); module code is import-free/pure-transform only (real sandbox is BO‑7). Capability search (search.py): **keyword-only by explicit owner decision** — deterministic token/substring ranking over the live registries (no index table, no embeddings; an embedding-backed variant was built and deliberately removed in favour of BO‑22, the platform-wide semantic-search service, whose ranking backend will swap in behind the same API shape) — `GET /workflows/catalog/search` serves the palette's search box AND the copilot's shortlist from the same ranking. Workflow Copilot (copilot.py): `POST /workflows/{id}/copilot` — chat-to-build; the LLM emits `{reply, graph, new_modules}`; **missing modules are auto-created** (Module Studio AST validation, saved `ready` with `auto_created` provenance, name→id rewired), the graph is validated with one named-issue repair round against the same validators as publish, and the result is returned for CLIENT-side apply — the copilot never writes the workflow row. `_call_copilot` is the stubbing seam for tests. Tables: infra/postgres/132_workflows.sql. Slice 2: **approval node** — an `approval` node pauses the run (engine returns status `paused`; downstream marked `pending`), `service._hold_for_approval` files a `workflow.resume_run` proposal into the EXISTING Action Broker inbox (`pending_actions` → /approvals UI) with everything a resume needs in the `workflow_run_pauses.snapshot`; approving fires `broker_handlers._resume_run_handler` which replays the run with completed nodes' stored outputs (`precomputed` — no repeated side effects) and the gate resolved; a rejected proposal is reconciled lazily on run read (run → `cancelled`). **Event triggers** — `triggers.dispatch_event` starts runs for published workflows whose `kind='event'` binding matches `(source, event_type)` (empty type = all); fed by BOTH `/agent/webhook/{source}` (routes/agent.py calls it after agent routing; response carries `workflow_runs`) and the native ClickUp receiver via `ingestion.event_hooks` (a `post_sync.py`-style sink registry — ingestion never imports upward; main.py registers the dispatcher at startup). Same core-is-the-leaf layout as routes/tasks; ⚠️ `__init__.py` import order is load-bearing (static paths before crud's `/{workflow_id}`; a regression test pins it). Startup: main.py lifespan calls `service.reconcile_orphaned_runs()` BEFORE starting the scheduler — rows still `running` belong to a dead process and are swept to `failed` ("interrupted by a platform restart"); `paused` rows are deliberately untouched (resume rebuilds everything from the pause snapshot), and `runs.py` keeps the per-read lazy patch for reads that race the sweep. Run-history drill-in (spec F9): clicking a history row in the editor's RunConsole fetches the run detail and paints its recorded `node_results` onto the canvas (cleared when a live test run starts). Engine semantics are locked by a CI-blocking golden trajectory eval — `evals/trajectories/test_workflow_engine_trajectory.py`; `skill-eval.yml` triggers on `routes/workflows/**` so engine edits re-run the gate. **Publish authority** (spec Q3, migration 133): `POST /{id}/publish`, `/versions/{v}/rollback`, and `/disable` require the `workflows:publish` capability on top of the router's `feature:workflows` gate — they are the acts that ARM triggers to run unattended. Drafting, validate, Test runs, duplicate, and the copilot stay open to the feature (a draft fires no triggers and its writes are still broker-held). `/auth/me` returns a resolved `capabilities` list so the editor can grey out Publish with a reason instead of a bare 403 — the browser must never re-derive wildcard matching (`permissions` holds raw patterns; an owner has `*`). **Wait node** (F3 logic vocabulary): `{"seconds": N}`, ≤`WAIT_INLINE_MAX_SECONDS` (60) sleeps inline inside the run; longer pauses the run exactly like an approval but with `reason='wait'` + a `resume_at` deadline in the pause snapshot and NO broker proposal (nobody decides anything) — `scheduler.scan_due_waits()` runs in the same loop as cron triggers and hands matured pauses to the SAME `service.resume_run`, which routes by pause reason (`elapsed_waits` vs `resolved_approvals`, so an elapsed wait can never clear an approval downstream). A resumed wait must never sleep again: the handler only sleeps when the duration is inline-short. Lifecycle extras: `POST /{id}/duplicate` (crud.py — copies graph/variables/triggers into a fresh DRAFT; the hook token is ALWAYS regenerated, it is a credential) and `POST /{id}/versions/{v}/rollback` (publish.py — republishes version v's immutable snapshot as a NEW version; deliberately does not re-validate as a gate since rollback is incident response — catalog drift comes back as non-blocking `warnings`, and the draft edit-model is never clobbered). **Automation health** (spec R2, migration 134): every terminal run calls `service.evaluate_automation_health()`, which disables a published workflow after `AUTO_DISABLE_AFTER` (5) consecutive failures **from `UNATTENDED_TRIGGERS` only** (`schedule`/`webhook`/`event` — a maker's Test runs and agent `api` calls must never disable production). The streak is derived from `workflow_runs`, never a counter column, and is scoped to runs after `workflows.health_since`, which publish/rollback/enable each re-stamp — without that window a re-enabled workflow would re-disable on its next failure, since the failures that tripped the policy are still the newest rows. The disable is a CAS on `status='published'` so concurrent failing runs produce exactly one disable; `disabled_reason`/`disabled_at` are written the same way for the human Disable path, so the gallery answers "why is this off?" identically. Notification is in-product (persisted reason → gallery badge + editor banner, `workflows.auto_disabled` log, activity-feed `disabled` event); outward notification would be an outward write and belongs on the broker path. `POST /{id}/enable` (publish.py, same `workflows:publish` gate) is the way back: it re-arms the EXISTING live version rather than minting one, 409s if the workflow was never published, and is idempotent when already live. `_execute_run`'s `trigger_kind` is a REQUIRED keyword — a dropped kwarg would make the whole policy silently inert. **Trigger durability** (spec §3.3a): schedules are DB rows, not OS cron and not an APScheduler process — `CronTrigger` is a parser only. ⚠️ `compute_due_fire` only looks FORWARD, so a trigger with `last_fired_at IS NULL` yields no tick; `_claim_baseline` arms it on first sight instead of firing (a cron says *when*, not *how far back*). Without that step a new schedule never fires **at all** — it produced no tick, so it never got a baseline, so it produced no tick. `config.timezone` is an IANA wall clock (default UTC) validated at save with the cron, so a 9am job stays 9am across DST; the zone is passed to `CronTrigger.from_crontab`, and instants stay UTC-aware throughout. `update_workflow` rewrites trigger rows wholesale but CARRIES `last_fired_at` across for unchanged schedules (`_trigger_identity` = kind + cron@timezone) — otherwise every canvas save re-armed the cron and lost the already-fired-this-tick guarantee. Because the CAS claim commits BEFORE `start_run`, a claimed tick can never be re-offered: every path out of that block calls `service.record_skipped_run()`, which writes a terminal `cancelled` run row (cancelled, not failed — being busy must not feed the R2 auto-disable policy). **Hook URL**: `core.hook_url()` builds it from `settings.public_api_base_url` and `get_workflow` returns `hook_url`/`hook_path`; the browser must NEVER assemble one from `window.location`, because the control-plane `/api` proxy re-serializes JSON (breaking sender HMAC) and drops non-JSON bodies. The Next route `api/workflows/hooks/[token]/route.ts` is a raw-bytes passthrough that attaches no internal bearer. **Typed tool arguments** (`engine/tool_args.py` — n8n's typed-node-parameters pattern, Sim's `subBlocks`): a tool's `args_schema` value is a mini-language `type[?][|description]` over the closed set `{string,number,boolean,object,array}`; an unknown type degrades to `string` rather than raising (one bad declaration must not take the whole catalog down). It is parsed in ONE place and consumed in three — the catalog serves `args[]` (parsed) so the browser never re-implements the grammar, `validate_graph(tool_schemas=…)` blocks publish on a missing/unknown/mistyped argument (`tool_args` issue code), and `execute_tool` re-checks at run time because a draft Test, a copilot graph, or an older published version can all reach a handler that publish never saw. `{{refs}}` satisfy required checks and are exempt from type checks on both sides — they resolve at run time. Type checking is deliberately lenient (only container-vs-scalar category errors) so the messages that fire are worth reading. `tests/unit/test_workflows_tool_contract.py` holds each declaration to its handler by AST-scanning for `args.get("x")`/`args["x"]` — the drift it hunts is a handler growing an input the schema never declares (`_broker_write`'s `target_field` is dynamic, so it has its own explicit test). **Golden workflow fixtures** (`evals/trajectories/workflows/*.json` + `test_workflow_fixtures.py`): whole workflows paired with an expected outcome, one generic runner; `expect.publishable: false` fixtures pin the publish gates. Tool schemas and destructive actions come from the REAL registry so fixtures break when the shipped catalog changes; fixtures assert which seams were crossed (`agent_calls`/`tool_calls`/`tool_args`), because "succeeded" while silently never calling the integration is the failure mode they exist to catch. +14. routes/admin/ -- Org access control `/admin` API + `/auth/me` (spec: project-docs/specs/org_access_control.md, Phase 1): member roster and lifecycle (invite/suspend/remove — soft, because ~every user-scoped table keys people by email — **plus a separate hard delete**, below), role assignment, custom role CRUD, per-user allow/deny overrides, and the feature catalog the admin UI renders from. `GET /auth/me` is deliberately NOT admin-gated — every signed-in member calls it to resolve their own feature/agent access, and it returns resolved OUTCOMES (allowed feature slugs, runnable agent names) rather than raw permission patterns, so the matching rule has exactly one implementation. `GET /admin/members/{email}/access` returns each decision WITH its provenance (which role granted it, which override took it away) — the admin UI shows that verbatim rather than re-deriving it. Invariants enforced in `_common.py`: the org always keeps an owner, nobody assigns a role above their own rank, system roles are immutable, and **nobody locks themselves out**. That fourth one (`assert_not_self_lockout`, `colleague_onboarding.md` §2 Step 5 / N7+N8) is called by `update_member` (PATCH), `remove_member` (DELETE) **and `purge_member` (DELETE …/purge)** — three doors reach the same `is_active = False`, and while the check lived inside DELETE alone the PATCH had none: `PATCH {"status": "suspended"}` on your own row was refused only by `assert_owner_survives` firing coincidentally in a one-owner org, so a second owner opened it. ⚠️ The rule is **"any status that is not `active`"**, never a list of destructive ones — `EffectiveAccess.is_active` is `status == "active"` exactly, so `invited` is a lockout too, and an enumeration would have to remember it. Comparison is case-insensitive and empty-safe on both sides (an IdP that re-cases a UPN must not switch the guard off; a caller with no identity is not everybody). ⚠️ **It and `assert_owner_survives` both answer 409** — a test that asserts the bare status code cannot tell which fired, and for self-suspension the one that fires today on `main` is the wrong one; discriminate on the detail text and on what was written (`tests/unit/test_admin_member_offboarding.py`). **`purge_member` — `DELETE /admin/members/{email}/purge` (N8)** is the hard delete: a SEPARATE route on the same `admin:members:manage`, never a flag on Remove (which would put the irreversible path one typo from the reversible one). Its decision is **purge the person, keep their work** — the `app_user` row, every access grant (`user_role`, `user_permission_override`, `org_group_member`, `chat_session_participant`, `app_grants`, `app_tool_grants`), every credential (`email_accounts`, `wa_accounts`, `task_accounts`), their PRIVATE `chat_session` rows and their `access_request` row go; what they authored and **the audit trail stay** (an audit trail that disappears with the person is not one — `app_audit` already carries a FK-less `app_id` commented "audit survives hard delete"). ⚠️ **Nothing is anonymised, on purpose**: the address is the join key across ~50 tables, so scrubbing `owner_email` would orphan the apps rather than hide the person. ⚠️ **The three credential rows cascade, and the map is `members._CREDENTIAL_CASCADES`** — `email_accounts` takes the whole mirrored mailbox (**17 direct children, 20 with transitives**), `wa_accounts` the whole WhatsApp mirror (**14 / 16**; `wa_media` hangs off `wa_messages`, NOT off the account), `task_accounts` the SYNCED half of `gtd_items` **and `gtd_projects`**; the credential is `NOT NULL` on the row, so it cannot go without it. That map is hand-maintained and says so, and is pinned against `infra/postgres/` by a test that re-derives it — the first version named 15 of the 20 email tables, which on a route whose safety argument is "the admin is told the blast radius before clicking" is the wrong direction of error. ⚠️ **THREE tables are split across both lists, and each predicate is load-bearing:** `chat_session` by `visibility` (private deleted, shared kept — a room cascades `chat_message` and one person's off-boarding must not take a shared transcript), and `gtd_items` + `gtd_projects` by `account_id` (`IS NOT NULL` = the SYNCED mirror, counted and deleted explicitly; `IS NULL` = the LOCAL rows they authored here, kept). ⚠️ **A KEEP clause must exclude everything the delete side CASCADES away, not merely everything it names.** The `tasks` keep clause originally had no `account_id` predicate, so a member with 847 synced tasks was answered `kept: {"tasks": 847}` while all 847 went with `task_accounts` — the response reported a destruction as a survival. `_PURGE_DELETES`/`_PURGE_KEEPS` derive `count_sql` and `delete_sql` from ONE `where` clause so the count and the delete cannot differ, but that is a within-row-spec guarantee and says nothing about a third statement three entries up; one transaction, one commit, and `record_admin_change` fires BEFORE it (`acb_audit` has its own session, so the record of a destruction survives a rollback of it — though `acb_audit/log.py:49` swallows every exception, so a *completed* purge is NOT guaranteed to leave an audit row). Pinned by `tests/unit/test_admin_member_purge.py` — including the structural assertion that no audit table appears on the delete side at all, the exact permission slug on the route (deleting it leaves the `admin:members:read` floor, which `manager` holds), and the cross-table cascade fences built on `tests/unit/_schema_cascade.py`, which derives the FK graph from the numbered migrations. ⚠️ **`_admin_fakes._FakeDB` models no foreign keys and therefore no cascades**, so every cross-table claim here has to be structural; no behavioural case over a seeded fake can make one. Every write calls `invalidate_access` so a change lands immediately instead of after the resolver's 60s TTL. Tables: infra/postgres/130_org_access_control.sql. Same `_common.py`-is-the-leaf layout as routes/apps and routes/tasks — and here the leaf rule is strict: feature modules import from `_common`, **never from each other**. ⚠️ **The `/admin` auth floor is PER-ROUTE, not a package property.** `_common.py` creates the router with **no** `dependencies=`; every route declares `Depends(require_admin_user)` in its own signature. A route added without it inherits no floor at all and is reachable by any authenticated member — the easiest hole to ship in this package. `access_requests.py` — **sign-in requests** (`colleague_onboarding.md` §6 / N6a, migration 143): `/admin` was push-only, so somebody arriving at the front door produced a journald warning nobody read back (53 of them for one address over 18 hours on 2026-08-03/04, and the owner learned out of band). `acb_auth.access.resolve_access` now upserts an `access_request` row when — and ONLY when — `record_request=True`, which exactly one caller passes; `GET /admin/members/requests` + `POST .../{email}/approve|deny` let the owner answer it, both writes on the EXISTING `admin:members:invite` (no new slug — a new slug is nobody's grant until an admin creates it). **Approve provisions AND activates in one action** (`status='active'`, not `'invited'`) because an approval IS the decision to let somebody in and they are already at the door; leaving them `invited` would re-create the two-click trap §2 Step 1b documents. Both provisioning callers go through `_common.provision_member` — ONE path, so invariants 1 and 2 apply to approvals too (it calls `assert_owner_survives` itself, because `set_roles` REPLACES assignments and provisioning the last owner with the default `member` role would otherwise delete the org's only owner grant). ⚠️ **Both writes hold `admin:members:invite`, which is WEAKER than the `admin:members:manage` that suspends or off-boards, so every path by which the weaker one could reverse the stronger is a cross-gate escalation.** Two independent locks, and each is load-bearing for a different sequence: (1) `_load_request(db, email, *, allowed_statuses=…)` — keyword-only, no default — refuses an already-DECIDED row, because decided rows are kept on purpose (dw9) and the tab renders only `pending`, so a decided row is invisible *and* still addressable; approve takes `("pending",)`, deny takes `("pending", "denied")` since re-denying grants nothing, and denying an *approved* request is refused because it could only make the queue contradict the roster. (2) `_common._PROVISION_MEMBER_SQL`'s `ON CONFLICT` arms **name the statuses they rewrite and never negate**: `invited` → the caller's status (the one door to `active`), `removed` → the caller's status **only when it is not `active`** (so invite still returns an off-boarded person as `invited`, byte-for-byte as before, while approve cannot reinstate them — `removed → active` stays `PATCH /admin/members/{email}`), and `active`/`suspended` are never touched. ⚠️ A `<>`/`NOT IN` test against `app_user.status` is the mutation to watch for: it reads as tidier and silently rewrites rows set under a stronger permission. `tests/unit/test_signin_requests.py` pins the SQL **structurally** (`test_provisioning_only_ever_rewrites_a_status_it_names`) — its fake DB re-implements the `ON CONFLICT` arms in Python and a mirror can only agree with itself, so the behavioural cases there cannot see the statement being widened and must not be trusted to. ⚠️ **Lock (2) declines SILENTLY — it just does not rewrite the row — so it is only half an answer, and the other half is `APPROVE_MATRIX`.** Approve used to run its `_decide(…, "approved")` after that quiet decline: HTTP 200, request marked `approved`, `set_roles` re-granting `['member']` to an off-boarded member, and the person gone for good from a tab that renders only `pending` (the resolver's upsert never rewrites `status`). **`access_requests.APPROVE_MATRIX`, read by `_disposition_for` BEFORE anything is written, is the contract:** absent → provision; `invited` → activate + assign the roles; `active` → do nothing, leave their roles alone, resolve the request as `approved` and say so in `ApproveResult.detail`; `suspended`/`removed` → **409, request stays `pending`** so the person stays visible; anything else → refuse (fail closed). The invariant: **approve never rewrites the roles of a member who already exists in a state other than `invited`** — `provision_member` ends in `set_roles`, which REPLACES assignments, and roles are otherwise `admin:members:manage` territory. The matrix is pinned against `members.VALID_STATUSES`, so a fifth member status cannot ship without somebody deciding what approving one means. `_DECIDE_SQL` also binds the read's own status filter into the UPDATE (`AND status = ANY(:allowed) … RETURNING id`) and 409s on zero rows **before `db.commit()`**, so a lost race discards its own provisioning instead of half-applying it; each route must pass `_decide` the same tuple it passed `_load_request` (a test asserts that from the source). +15. routes/workflows/ -- Workflows app `/workflows` API (spec: project-docs/specs/workflows_app.md; RFC: docs/workflow-editor/README.md): workflow CRUD over the React-Flow-native edit-model (`workflows.graph` jsonb, persisted verbatim), publish → compile to an immutable `workflow_versions.serialized` run-model (edit-model ≠ run-model; runs pin versions), run start/history/detail + a per-run SSE event stream (in-process hub in service.py; runs are supervised asyncio tasks — durable queueing is BO‑20), the served node catalog (agents from the live registry, integrations from acb_skills with availability probe, workflow tool registry, ready modules — the palette is never hard-coded, spec D7), Module Studio (workflow_modules CRUD + conversational generate on acb_llm tier routing + AST validate + subprocess test/run), the inbound webhook trigger `POST /workflows/hooks/{hook_token}` (public by token — in PUBLIC_ROUTES + the router's exempt list; optional HMAC `X-CC-Signature`; rate-limited; fires only published workflows with an enabled webhook trigger), and the cron schedule scanner (scheduler.py — apscheduler CronTrigger parsing inside a supervised asyncio loop with CAS claims on `last_fired_at`; started/stopped from main.py lifespan). The engine subpackage (engine/: templating, graph compile/validate, node handlers over injected NodeServices, MAF WorkflowBuilder runner, module AST validator + restricted subprocess runner) is transport-free — no FastAPI/DB imports — so it is unit-testable alone and movable into the orchestrator if isolation later demands. Agent nodes call `orchestrator.executor.run_agent` (source="workflow", MAF batch path — constraint #9); write-class tool nodes dispatch through `action_broker.propose/submit` (fail closed, constraint #4); module code is import-free/pure-transform only (real sandbox is BO‑7). Capability search (search.py): **keyword-only by explicit owner decision** — deterministic token/substring ranking over the live registries (no index table, no embeddings; an embedding-backed variant was built and deliberately removed in favour of BO‑22, the platform-wide semantic-search service, whose ranking backend will swap in behind the same API shape) — `GET /workflows/catalog/search` serves the palette's search box AND the copilot's shortlist from the same ranking. Workflow Copilot (copilot.py): `POST /workflows/{id}/copilot` — chat-to-build; the LLM emits `{reply, graph, new_modules}`; **missing modules are auto-created** (Module Studio AST validation, saved `ready` with `auto_created` provenance, name→id rewired), the graph is validated with one named-issue repair round against the same validators as publish, and the result is returned for CLIENT-side apply — the copilot never writes the workflow row. `_call_copilot` is the stubbing seam for tests. Tables: infra/postgres/132_workflows.sql. Slice 2: **approval node** — an `approval` node pauses the run (engine returns status `paused`; downstream marked `pending`), `service._hold_for_approval` files a `workflow.resume_run` proposal into the EXISTING Action Broker inbox (`pending_actions` → /approvals UI) with everything a resume needs in the `workflow_run_pauses.snapshot`; approving fires `broker_handlers._resume_run_handler` which replays the run with completed nodes' stored outputs (`precomputed` — no repeated side effects) and the gate resolved; a rejected proposal is reconciled lazily on run read (run → `cancelled`). **Event triggers** — `triggers.dispatch_event` starts runs for published workflows whose `kind='event'` binding matches `(source, event_type)` (empty type = all); fed by BOTH `/agent/webhook/{source}` (routes/agent.py calls it after agent routing; response carries `workflow_runs`) and the native ClickUp receiver via `ingestion.event_hooks` (a `post_sync.py`-style sink registry — ingestion never imports upward; main.py registers the dispatcher at startup). Same core-is-the-leaf layout as routes/tasks; ⚠️ `__init__.py` import order is load-bearing (static paths before crud's `/{workflow_id}`; a regression test pins it). Startup: main.py lifespan calls `service.reconcile_orphaned_runs()` BEFORE starting the scheduler — rows still `running` belong to a dead process and are swept to `failed` ("interrupted by a platform restart"); `paused` rows are deliberately untouched (resume rebuilds everything from the pause snapshot), and `runs.py` keeps the per-read lazy patch for reads that race the sweep. Run-history drill-in (spec F9): clicking a history row in the editor's RunConsole fetches the run detail and paints its recorded `node_results` onto the canvas (cleared when a live test run starts). Engine semantics are locked by a CI-blocking golden trajectory eval — `evals/trajectories/test_workflow_engine_trajectory.py`; `skill-eval.yml` triggers on `routes/workflows/**` so engine edits re-run the gate. **Publish authority** (spec Q3, migration 133): `POST /{id}/publish`, `/versions/{v}/rollback`, and `/disable` require the `workflows:publish` capability on top of the router's `feature:workflows` gate — they are the acts that ARM triggers to run unattended. Drafting, validate, Test runs, duplicate, and the copilot stay open to the feature (a draft fires no triggers and its writes are still broker-held). `/auth/me` returns a resolved `capabilities` list so the editor can grey out Publish with a reason instead of a bare 403 — the browser must never re-derive wildcard matching (`permissions` holds raw patterns; an owner has `*`). **Wait node** (F3 logic vocabulary): `{"seconds": N}`, ≤`WAIT_INLINE_MAX_SECONDS` (60) sleeps inline inside the run; longer pauses the run exactly like an approval but with `reason='wait'` + a `resume_at` deadline in the pause snapshot and NO broker proposal (nobody decides anything) — `scheduler.scan_due_waits()` runs in the same loop as cron triggers and hands matured pauses to the SAME `service.resume_run`, which routes by pause reason (`elapsed_waits` vs `resolved_approvals`, so an elapsed wait can never clear an approval downstream). A resumed wait must never sleep again: the handler only sleeps when the duration is inline-short. Lifecycle extras: `POST /{id}/duplicate` (crud.py — copies graph/variables/triggers into a fresh DRAFT; the hook token is ALWAYS regenerated, it is a credential) and `POST /{id}/versions/{v}/rollback` (publish.py — republishes version v's immutable snapshot as a NEW version; deliberately does not re-validate as a gate since rollback is incident response — catalog drift comes back as non-blocking `warnings`, and the draft edit-model is never clobbered). **Automation health** (spec R2, migration 134): every terminal run calls `service.evaluate_automation_health()`, which disables a published workflow after `AUTO_DISABLE_AFTER` (5) consecutive failures **from `UNATTENDED_TRIGGERS` only** (`schedule`/`webhook`/`event` — a maker's Test runs and agent `api` calls must never disable production). The streak is derived from `workflow_runs`, never a counter column, and is scoped to runs after `workflows.health_since`, which publish/rollback/enable each re-stamp — without that window a re-enabled workflow would re-disable on its next failure, since the failures that tripped the policy are still the newest rows. The disable is a CAS on `status='published'` so concurrent failing runs produce exactly one disable; `disabled_reason`/`disabled_at` are written the same way for the human Disable path, so the gallery answers "why is this off?" identically. Notification is in-product (persisted reason → gallery badge + editor banner, `workflows.auto_disabled` log, activity-feed `disabled` event); outward notification would be an outward write and belongs on the broker path. `POST /{id}/enable` (publish.py, same `workflows:publish` gate) is the way back: it re-arms the EXISTING live version rather than minting one, 409s if the workflow was never published, and is idempotent when already live. `_execute_run`'s `trigger_kind` is a REQUIRED keyword — a dropped kwarg would make the whole policy silently inert. **Trigger durability** (spec §3.3a): schedules are DB rows, not OS cron and not an APScheduler process — `CronTrigger` is a parser only. ⚠️ `compute_due_fire` only looks FORWARD, so a trigger with `last_fired_at IS NULL` yields no tick; `_claim_baseline` arms it on first sight instead of firing (a cron says *when*, not *how far back*). Without that step a new schedule never fires **at all** — it produced no tick, so it never got a baseline, so it produced no tick. `config.timezone` is an IANA wall clock (default UTC) validated at save with the cron, so a 9am job stays 9am across DST; the zone is passed to `CronTrigger.from_crontab`, and instants stay UTC-aware throughout. `update_workflow` rewrites trigger rows wholesale but CARRIES `last_fired_at` across for unchanged schedules (`_trigger_identity` = kind + cron@timezone) — otherwise every canvas save re-armed the cron and lost the already-fired-this-tick guarantee. Because the CAS claim commits BEFORE `start_run`, a claimed tick can never be re-offered: every path out of that block calls `service.record_skipped_run()`, which writes a terminal `cancelled` run row (cancelled, not failed — being busy must not feed the R2 auto-disable policy). **Hook URL**: `core.hook_url()` builds it from `settings.public_api_base_url` and `get_workflow` returns `hook_url`/`hook_path`; the browser must NEVER assemble one from `window.location`, because the control-plane `/api` proxy re-serializes JSON (breaking sender HMAC) and drops non-JSON bodies. The Next route `api/workflows/hooks/[token]/route.ts` is a raw-bytes passthrough that attaches no internal bearer. **Typed tool arguments** (`engine/tool_args.py` — n8n's typed-node-parameters pattern, Sim's `subBlocks`): a tool's `args_schema` value is a mini-language `type[?][|description]` over the closed set `{string,number,boolean,object,array}`; an unknown type degrades to `string` rather than raising (one bad declaration must not take the whole catalog down). It is parsed in ONE place and consumed in three — the catalog serves `args[]` (parsed) so the browser never re-implements the grammar, `validate_graph(tool_schemas=…)` blocks publish on a missing/unknown/mistyped argument (`tool_args` issue code), and `execute_tool` re-checks at run time because a draft Test, a copilot graph, or an older published version can all reach a handler that publish never saw. `{{refs}}` satisfy required checks and are exempt from type checks on both sides — they resolve at run time. Type checking is deliberately lenient (only container-vs-scalar category errors) so the messages that fire are worth reading. `tests/unit/test_workflows_tool_contract.py` holds each declaration to its handler by AST-scanning for `args.get("x")`/`args["x"]` — the drift it hunts is a handler growing an input the schema never declares (`_broker_write`'s `target_field` is dynamic, so it has its own explicit test). **Golden workflow fixtures** (`evals/trajectories/workflows/*.json` + `test_workflow_fixtures.py`): whole workflows paired with an expected outcome, one generic runner; `expect.publishable: false` fixtures pin the publish gates. Tool schemas and destructive actions come from the REAL registry so fixtures break when the shipped catalog changes; fixtures assert which seams were crossed (`agent_calls`/`tool_calls`/`tool_args`), because "succeeded" while silently never calling the integration is the failure mode they exist to catch. 16. agents.json -- Dynamic agent registry (persisted alongside pyproject.toml) ## Work Guidance @@ -164,14 +164,14 @@ safe — do not point one of them at the public hostname. ## The user-management contract — binding on every route you add here -Full contract: `ai-company-brain/specs/user_management_contract.md` (**eleven** +Full contract: `project-docs/specs/user_management_contract.md` (**eleven** rules since 2026-08-08). The six that bite in *this* directory, each learned by breaking it — **plus R11, which will bite here hardest when WS-29 lands:** > **R11 — never take the acting TENANT from input.** Not an `X-Organization-Id` > header, not a query parameter, not a body field. It comes from the > authenticated session or a tenant-scoped API key. Under **D15** -> (`ai-company-brain/specs/saas_multitenancy.md` §1) every route in this +> (`project-docs/specs/saas_multitenancy.md` §1) every route in this > directory runs against a tenant-bound session, and the binding happens **once** > in `acb_common.db` — so a route that reaches data any other way is the bug. > Build shapes: `saas_multitenancy_implementation.md` §2. diff --git a/apps/services/gateway/gateway/main.py b/apps/services/gateway/gateway/main.py index 83060fc45..4b863ffea 100644 --- a/apps/services/gateway/gateway/main.py +++ b/apps/services/gateway/gateway/main.py @@ -545,7 +545,7 @@ def docs_enabled(env: str) -> bool: app = FastAPI( title="AI Company Brain — Gateway", version="0.0.1", - description="Pull queries, push notifications, approvals. See ai-company-brain/system_architecture.md §3.", + description="Pull queries, push notifications, approvals. See project-docs/system_architecture.md §3.", lifespan=lifespan, dependencies=[require_authenticated(public=PUBLIC_ROUTES)], docs_url="/docs" if _docs_enabled else None, @@ -996,7 +996,7 @@ async def relayed_generator(): pass try: - # WS-26 — native CRM (spec: ai-company-brain/specs/crm_app.md). Leads, + # WS-26 — native CRM (spec: project-docs/specs/crm_app.md). Leads, # deals, contacts, organizations, the pipeline and one activity timeline. from gateway.routes.crm import router as _crm_router @@ -1006,7 +1006,7 @@ async def relayed_generator(): try: # WS-27 — native project management (spec: - # ai-company-brain/specs/project_management_app.md). Departments, projects, + # project-docs/specs/project_management_app.md). Departments, projects, # subprojects, tasks and subtasks, grant-scoped into every Center. from gateway.routes.projects import router as _projects_router @@ -1016,7 +1016,7 @@ async def relayed_generator(): try: # WS-28 — the People Center's directory (spec: - # ai-company-brain/specs/people_center_app.md). Its own feature gate, but + # project-docs/specs/people_center_app.md). Its own feature gate, but # the HR projection is imported from routes/tasks, never re-implemented. from gateway.routes.people import router as _people_router @@ -1126,7 +1126,7 @@ async def relayed_generator(): pass try: - # Workflows app (ai-company-brain/specs/workflows_app.md) — visual + # Workflows app (project-docs/specs/workflows_app.md) — visual # automation builder: workflow CRUD/publish/runs, Module Studio, the node # catalog, and the inbound webhook trigger (prefix /workflows). from gateway.routes.workflows import router as _workflows_router @@ -1158,7 +1158,7 @@ async def relayed_generator(): pass try: - # Org access control (ai-company-brain/specs/org_access_control.md) — + # Org access control (project-docs/specs/org_access_control.md) — # member roster + lifecycle, roles, per-user overrides (prefix /admin), # plus /auth/me, which every signed-in member calls to resolve their own # feature and agent access. diff --git a/apps/services/gateway/gateway/rooms.py b/apps/services/gateway/gateway/rooms.py index a7bd85870..3141d3973 100644 --- a/apps/services/gateway/gateway/rooms.py +++ b/apps/services/gateway/gateway/rooms.py @@ -1,6 +1,6 @@ """Room membership — the one predicate that replaces ``WHERE user_id = :uid``. -Spec: ``ai-company-brain/specs/groups_sessions_authority.md`` §2 (participants +Spec: ``project-docs/specs/groups_sessions_authority.md`` §2 (participants and visibility), ``docs/multiplayer/README.md`` §4.2 ("the thread is the room") and §4.5 (``resolve_room_access`` replaces ``_thread_owner_ok`` everywhere). diff --git a/apps/services/gateway/gateway/routes/admin/__init__.py b/apps/services/gateway/gateway/routes/admin/__init__.py index 5d8310079..465253e52 100644 --- a/apps/services/gateway/gateway/routes/admin/__init__.py +++ b/apps/services/gateway/gateway/routes/admin/__init__.py @@ -1,7 +1,7 @@ """Org administration routes (prefix ``/admin``) plus ``/auth/me``. Members, groups, roles, and per-user access overrides for the multi-user -organization model. Spec: ``ai-company-brain/specs/org_access_control.md`` +organization model. Spec: ``project-docs/specs/org_access_control.md`` (+ ``department_centers.md`` §3 Phase B for groups). Layout mirrors ``routes/apps/``: ``_common`` is the leaf (DB, org lookup, diff --git a/apps/services/gateway/gateway/routes/admin/_common.py b/apps/services/gateway/gateway/routes/admin/_common.py index d0e79d31a..632006247 100644 --- a/apps/services/gateway/gateway/routes/admin/_common.py +++ b/apps/services/gateway/gateway/routes/admin/_common.py @@ -1,7 +1,7 @@ """Org administration routes — shared kernel. DB access, the org lookup, and the invariants every write path in this package -has to respect. Spec: ``ai-company-brain/specs/org_access_control.md``. +has to respect. Spec: ``project-docs/specs/org_access_control.md``. The four invariants, stated once here because they are the difference between an access model and an outage: diff --git a/apps/services/gateway/gateway/routes/admin/access_requests.py b/apps/services/gateway/gateway/routes/admin/access_requests.py index 6ed31e460..9a0443575 100644 --- a/apps/services/gateway/gateway/routes/admin/access_requests.py +++ b/apps/services/gateway/gateway/routes/admin/access_requests.py @@ -1,6 +1,6 @@ """Sign-in requests — the queue of people knocking at a door nobody opened. -Spec: ``ai-company-brain/specs/colleague_onboarding.md`` §6 (WS-24 / N6a). +Spec: ``project-docs/specs/colleague_onboarding.md`` §6 (WS-24 / N6a). ``/admin/members`` is push-only: the only way an ``app_user`` row was ever created is an admin typing an address into Invite. Somebody arriving at the diff --git a/apps/services/gateway/gateway/routes/admin/groups.py b/apps/services/gateway/gateway/routes/admin/groups.py index a189c34b0..e13bb4db2 100644 --- a/apps/services/gateway/gateway/routes/admin/groups.py +++ b/apps/services/gateway/gateway/routes/admin/groups.py @@ -1,6 +1,6 @@ """Org administration — groups (the teams that scope Centers, rooms, agents). -Spec: ``ai-company-brain/specs/department_centers.md`` §3 Phase B, over the +Spec: ``project-docs/specs/department_centers.md`` §3 Phase B, over the ``org_group`` / ``org_group_member`` tables from ``groups_sessions_authority.md`` §1. diff --git a/apps/services/gateway/gateway/routes/admin/members.py b/apps/services/gateway/gateway/routes/admin/members.py index 93093ab61..e7db0551b 100644 --- a/apps/services/gateway/gateway/routes/admin/members.py +++ b/apps/services/gateway/gateway/routes/admin/members.py @@ -1,6 +1,6 @@ """Org administration — member roster, lifecycle, roles, and per-user access. -Spec: ``ai-company-brain/specs/org_access_control.md`` §6. +Spec: ``project-docs/specs/org_access_control.md`` §6. The interesting endpoint here is ``GET /admin/members/{email}/access``: it returns not just *what* the member can reach but *why* — which role granted it, diff --git a/apps/services/gateway/gateway/routes/admin/roles.py b/apps/services/gateway/gateway/routes/admin/roles.py index 16937c32e..b2a9eeb4e 100644 --- a/apps/services/gateway/gateway/routes/admin/roles.py +++ b/apps/services/gateway/gateway/routes/admin/roles.py @@ -1,6 +1,6 @@ """Org administration — role definitions and the feature catalog. -Spec: ``ai-company-brain/specs/org_access_control.md`` §3.2–§3.3. +Spec: ``project-docs/specs/org_access_control.md`` §3.2–§3.3. Roles are the reusable half of the model: a bundle of permission patterns with a name. Per-user overrides (``members.py``) are the exception half. Reaching diff --git a/apps/services/gateway/gateway/routes/crm/__init__.py b/apps/services/gateway/gateway/routes/crm/__init__.py index b69b09db5..3d1ae2243 100644 --- a/apps/services/gateway/gateway/routes/crm/__init__.py +++ b/apps/services/gateway/gateway/routes/crm/__init__.py @@ -1,6 +1,6 @@ """CRM route package — the gateway `/crm` API (native CRM, Zoho's successor). -Spec: ``ai-company-brain/specs/crm_app.md`` (WS-26a builds §3 + §4). +Spec: ``project-docs/specs/crm_app.md`` (WS-26a builds §3 + §4). Same layout as ``routes/tasks`` and ``routes/notes``: ``core`` is the leaf and the feature modules register their routes on the shared ``router`` as an import diff --git a/apps/services/gateway/gateway/routes/crm/activities.py b/apps/services/gateway/gateway/routes/crm/activities.py index 9886b9ce3..f19bfc6fd 100644 --- a/apps/services/gateway/gateway/routes/crm/activities.py +++ b/apps/services/gateway/gateway/routes/crm/activities.py @@ -1,6 +1,6 @@ """CRM · activities — the timeline: notes, calls, meetings, follow-up tasks. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.8, §4 (``activities.py`` row). +Spec: ``project-docs/specs/crm_app.md`` §3.8, §4 (``activities.py`` row). GET /crm//{id}/timeline activities + status changes, merged POST /crm//{id}/activities log a note / call / meeting / task diff --git a/apps/services/gateway/gateway/routes/crm/admin.py b/apps/services/gateway/gateway/routes/crm/admin.py index 8f2ba6a48..e631b323a 100644 --- a/apps/services/gateway/gateway/routes/crm/admin.py +++ b/apps/services/gateway/gateway/routes/crm/admin.py @@ -1,6 +1,6 @@ """CRM · admin — managing the pipeline itself: statuses and lost reasons. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.6, §3.10, §4 (``admin.py`` row). +Spec: ``project-docs/specs/crm_app.md`` §3.6, §3.10, §4 (``admin.py`` row). GET/POST /crm/statuses/{lead|deal} PATCH/DELETE /crm/statuses/{lead|deal}/{status_id} diff --git a/apps/services/gateway/gateway/routes/crm/auto_lead.py b/apps/services/gateway/gateway/routes/crm/auto_lead.py index bdb41154b..40142da61 100644 --- a/apps/services/gateway/gateway/routes/crm/auto_lead.py +++ b/apps/services/gateway/gateway/routes/crm/auto_lead.py @@ -1,6 +1,6 @@ """CRM · auto_lead — an unknown inbound sender becomes a lead (WS-26d-autolead). -Spec: ``ai-company-brain/specs/crm_app.md`` §9 ``WS-26d-autolead`` · D-CRM-9 · +Spec: ``project-docs/specs/crm_app.md`` §9 ``WS-26d-autolead`` · D-CRM-9 · D-CRM-12. Migration: ``crm_auto_lead_cursors``. **This module registers no routes.** Like ``broker_handlers``, it is deliberately diff --git a/apps/services/gateway/gateway/routes/crm/broker_handlers.py b/apps/services/gateway/gateway/routes/crm/broker_handlers.py index d1ec33ce3..0b8987657 100644 --- a/apps/services/gateway/gateway/routes/crm/broker_handlers.py +++ b/apps/services/gateway/gateway/routes/crm/broker_handlers.py @@ -1,6 +1,6 @@ """CRM · broker_handlers — the Action-Broker gate every Zoho push goes through. -Spec: ``ai-company-brain/specs/crm_app.md`` D-CRM-8 · §7.1 · ticket WS-26b +Spec: ``project-docs/specs/crm_app.md`` D-CRM-8 · §7.1 · ticket WS-26b done-when 1 and 5. Modelled 1:1 on the tasks app's ClickUp path — ``routes/tasks/providers.py::_broker_gate`` (the gate) plus ``routes/tasks/broker_handlers.py`` (the persistent handlers registered from diff --git a/apps/services/gateway/gateway/routes/crm/core.py b/apps/services/gateway/gateway/routes/crm/core.py index 52524beac..0f9c71c08 100644 --- a/apps/services/gateway/gateway/routes/crm/core.py +++ b/apps/services/gateway/gateway/routes/crm/core.py @@ -3,7 +3,7 @@ The leaf module: it imports nothing from its siblings. It owns the shared ``router``, the entity registry, the Pydantic models, the row→model mapper, the list contract, and the small set of SQL helpers every feature module builds on. -Spec: ``ai-company-brain/specs/crm_app.md`` sections 3 and 4 (WS-26a). +Spec: ``project-docs/specs/crm_app.md`` sections 3 and 4 (WS-26a). Two things here are load-bearing and worth stating once: diff --git a/apps/services/gateway/gateway/routes/crm/deal_contacts.py b/apps/services/gateway/gateway/routes/crm/deal_contacts.py index 3329884b8..cb657b083 100644 --- a/apps/services/gateway/gateway/routes/crm/deal_contacts.py +++ b/apps/services/gateway/gateway/routes/crm/deal_contacts.py @@ -1,6 +1,6 @@ """CRM · deal contacts — who on the customer's side this deal runs through. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.5, §9 WS-26c done-when 2. +Spec: ``project-docs/specs/crm_app.md`` §3.5, §9 WS-26c done-when 2. GET /crm/deals/{id}/contacts POST /crm/deals/{id}/contacts {contact_id, role?, is_primary?} diff --git a/apps/services/gateway/gateway/routes/crm/import_zoho.py b/apps/services/gateway/gateway/routes/crm/import_zoho.py index b8cf6698f..989717517 100644 --- a/apps/services/gateway/gateway/routes/crm/import_zoho.py +++ b/apps/services/gateway/gateway/routes/crm/import_zoho.py @@ -1,6 +1,6 @@ """CRM · import_zoho — the Zoho backfill, and the Zoho→native mapping itself. -Spec: ``ai-company-brain/specs/crm_app.md`` §7.1 (the mapping table) · §4 +Spec: ``project-docs/specs/crm_app.md`` §7.1 (the mapping table) · §4 (``import_zoho.py`` row) · ticket WS-26b done-when 2. POST /crm/import/zoho {dry_run: bool} → per-module {fetched, created, diff --git a/apps/services/gateway/gateway/routes/crm/pipeline.py b/apps/services/gateway/gateway/routes/crm/pipeline.py index 9c3f5af51..85544b345 100644 --- a/apps/services/gateway/gateway/routes/crm/pipeline.py +++ b/apps/services/gateway/gateway/routes/crm/pipeline.py @@ -1,6 +1,6 @@ """CRM · pipeline — status transitions, the kanban board, and lead conversion. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.6, §3.7, §3.9, §4. +Spec: ``project-docs/specs/crm_app.md`` §3.6, §3.7, §3.9, §4. GET /crm/pipeline deals grouped by status: per-lane rows, count and ₹ total diff --git a/apps/services/gateway/gateway/routes/crm/records.py b/apps/services/gateway/gateway/routes/crm/records.py index 3388eae2c..2a73d00d4 100644 --- a/apps/services/gateway/gateway/routes/crm/records.py +++ b/apps/services/gateway/gateway/routes/crm/records.py @@ -1,6 +1,6 @@ """CRM · records — CRUD and the shared list contract for the four entities. -Spec: ``ai-company-brain/specs/crm_app.md`` §4 (``records.py`` row). +Spec: ``project-docs/specs/crm_app.md`` §4 (``records.py`` row). GET /crm/{leads,deals,contacts,organizations} → {rows, total} POST /crm/{leads,deals,contacts,organizations} diff --git a/apps/services/gateway/gateway/routes/crm/reports.py b/apps/services/gateway/gateway/routes/crm/reports.py index 4d1c81959..f408f7377 100644 --- a/apps/services/gateway/gateway/routes/crm/reports.py +++ b/apps/services/gateway/gateway/routes/crm/reports.py @@ -1,6 +1,6 @@ """CRM · reports — forecast, funnel, win/loss and the owner leaderboard. -Spec: ``ai-company-brain/specs/crm_app.md`` §5.1 system 2 · ticket WS-26g. +Spec: ``project-docs/specs/crm_app.md`` §5.1 system 2 · ticket WS-26g. GET /crm/reports/pipeline per open/ongoing stage: count, ₹, ₹ weighted GET /crm/reports/funnel per stage: entered, conversion-forward %, diff --git a/apps/services/gateway/gateway/routes/crm/stage_metadata.py b/apps/services/gateway/gateway/routes/crm/stage_metadata.py index 85063b5d2..39cd6adda 100644 --- a/apps/services/gateway/gateway/routes/crm/stage_metadata.py +++ b/apps/services/gateway/gateway/routes/crm/stage_metadata.py @@ -1,6 +1,6 @@ """CRM · stage_metadata — repairing the pipeline from Zoho's own settings. -Spec: ``ai-company-brain/specs/crm_app.md`` §5.1 (system 1) · ticket WS-26f +Spec: ``project-docs/specs/crm_app.md`` §5.1 (system 1) · ticket WS-26f f1 + f4 · decisions D-CRM-10 (probability) and D-CRM-11 (one pipeline). POST /crm/import/zoho/stages → dry run: the full report, no writes diff --git a/apps/services/gateway/gateway/routes/crm/sync_zoho.py b/apps/services/gateway/gateway/routes/crm/sync_zoho.py index 7153a2e72..cc8d2acd0 100644 --- a/apps/services/gateway/gateway/routes/crm/sync_zoho.py +++ b/apps/services/gateway/gateway/routes/crm/sync_zoho.py @@ -1,6 +1,6 @@ """CRM · sync_zoho — the two-way Zoho sync engine, and its scheduled loop. -Spec: ``ai-company-brain/specs/crm_app.md`` §7.1 (all seven sync bullets) · +Spec: ``project-docs/specs/crm_app.md`` §7.1 (all seven sync bullets) · D-CRM-6 · D-CRM-7 · D-CRM-8 · ticket WS-26b done-when 3, 4 and 5. POST /crm/sync/zoho run ONE cycle now (admin:access:manage), with or diff --git a/apps/services/gateway/gateway/routes/notes/__init__.py b/apps/services/gateway/gateway/routes/notes/__init__.py index 97c757840..a025bc260 100644 --- a/apps/services/gateway/gateway/routes/notes/__init__.py +++ b/apps/services/gateway/gateway/routes/notes/__init__.py @@ -2,7 +2,7 @@ Same layout as ``routes/tasks``: ``core`` is the leaf; the feature modules register their routes on the shared ``router`` as an import side effect. -Spec: ai-company-brain/specs/note_taker_app.md §3.7. +Spec: project-docs/specs/note_taker_app.md §3.7. """ from gateway.routes.notes import actions as _actions # noqa: F401 diff --git a/apps/services/gateway/gateway/routes/notes/core.py b/apps/services/gateway/gateway/routes/notes/core.py index c8cadc2b6..470c9ec23 100644 --- a/apps/services/gateway/gateway/routes/notes/core.py +++ b/apps/services/gateway/gateway/routes/notes/core.py @@ -7,7 +7,7 @@ Canonical store: the ``meeting`` / ``meeting_recording`` / ``transcript_segment`` / ``meeting_note`` / ``summary_run`` / ``action_item`` tables (``infra/postgres/01_schema.sql`` + ``95_note_taker.sql``; spec: -ai-company-brain/specs/note_taker_app.md §3.6). +project-docs/specs/note_taker_app.md §3.6). """ from __future__ import annotations diff --git a/apps/services/gateway/gateway/routes/people/__init__.py b/apps/services/gateway/gateway/routes/people/__init__.py index afe4e0417..dfc064f5e 100644 --- a/apps/services/gateway/gateway/routes/people/__init__.py +++ b/apps/services/gateway/gateway/routes/people/__init__.py @@ -1,6 +1,6 @@ """People Center · the directory as its own app. -Spec: ``ai-company-brain/specs/people_center_app.md`` §3, §6 · ticket WS-28b. +Spec: ``project-docs/specs/people_center_app.md`` §3, §6 · ticket WS-28b. GET /people → the directory, searchable and filterable GET /people/{id} → one person, with the login badge diff --git a/apps/services/gateway/gateway/routes/people/core.py b/apps/services/gateway/gateway/routes/people/core.py index 3790e6fa0..1fef6550e 100644 --- a/apps/services/gateway/gateway/routes/people/core.py +++ b/apps/services/gateway/gateway/routes/people/core.py @@ -1,6 +1,6 @@ """People Center · the router, the gate, and the seams it borrows. -Spec: ``ai-company-brain/specs/people_center_app.md`` §3, §6 · ticket WS-28b. +Spec: ``project-docs/specs/people_center_app.md`` §3, §6 · ticket WS-28b. **Two permissions, and they answer different questions.** ``feature:people`` decides whether the directory is reachable at all; diff --git a/apps/services/gateway/gateway/routes/people/directory.py b/apps/services/gateway/gateway/routes/people/directory.py index da9ac61f7..391f1ed9a 100644 --- a/apps/services/gateway/gateway/routes/people/directory.py +++ b/apps/services/gateway/gateway/routes/people/directory.py @@ -1,6 +1,6 @@ """People Center · the directory and the person page (WS-28b). -Spec: ``ai-company-brain/specs/people_center_app.md`` §3.1, §3.2, §5.2. +Spec: ``project-docs/specs/people_center_app.md`` §3.1, §3.2, §5.2. Three reads. Every one of them answers the same two questions in the same order — *may this caller reach the surface at all* (``feature:people``, on the diff --git a/apps/services/gateway/gateway/routes/projects/__init__.py b/apps/services/gateway/gateway/routes/projects/__init__.py index 195022c65..52ea33c0e 100644 --- a/apps/services/gateway/gateway/routes/projects/__init__.py +++ b/apps/services/gateway/gateway/routes/projects/__init__.py @@ -1,6 +1,6 @@ """Projects route package — the gateway `/projects` API (native project management). -Spec: ``ai-company-brain/specs/project_management_app.md`` (WS-27a builds §3 + §4). +Spec: ``project-docs/specs/project_management_app.md`` (WS-27a builds §3 + §4). Same layout as ``routes/crm``, ``routes/tasks`` and ``routes/notes``: ``core`` is the leaf and the feature modules register their routes on the shared ``router`` diff --git a/apps/services/gateway/gateway/routes/projects/activities.py b/apps/services/gateway/gateway/routes/projects/activities.py index 4f602c0af..af68c9396 100644 --- a/apps/services/gateway/gateway/routes/projects/activities.py +++ b/apps/services/gateway/gateway/routes/projects/activities.py @@ -1,6 +1,6 @@ """Projects · activities — the task timeline, comments, and revert. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``activities.py``). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``activities.py``). GET /projects/tasks/{id}/timeline POST /projects/tasks/{id}/comments diff --git a/apps/services/gateway/gateway/routes/projects/admin.py b/apps/services/gateway/gateway/routes/projects/admin.py index ca42c6ce3..7caab7e69 100644 --- a/apps/services/gateway/gateway/routes/projects/admin.py +++ b/apps/services/gateway/gateway/routes/projects/admin.py @@ -1,6 +1,6 @@ """Projects · admin — task statuses and task types, per root project. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``admin.py`` row). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``admin.py`` row). GET /projects/nodes/{project_id}/statuses POST /projects/nodes/{project_id}/statuses diff --git a/apps/services/gateway/gateway/routes/projects/agent_dispatch.py b/apps/services/gateway/gateway/routes/projects/agent_dispatch.py index 52480c0ef..95c93b167 100644 --- a/apps/services/gateway/gateway/routes/projects/agent_dispatch.py +++ b/apps/services/gateway/gateway/routes/projects/agent_dispatch.py @@ -1,6 +1,6 @@ """Projects · assignment IS dispatch — WS-27f / `workflows_app.md` §13 U7. -Spec: `ai-company-brain/specs/project_management_app.md` §6.4. +Spec: `project-docs/specs/project_management_app.md` §6.4. Assigning a task to `agent:` starts an agent run. Not a separate "delegate to AI" button, not a parallel feature with its own field — the same diff --git a/apps/services/gateway/gateway/routes/projects/bulk.py b/apps/services/gateway/gateway/routes/projects/bulk.py index 0c1a18912..877bacc18 100644 --- a/apps/services/gateway/gateway/routes/projects/bulk.py +++ b/apps/services/gateway/gateway/routes/projects/bulk.py @@ -1,6 +1,6 @@ """Projects · bulk edit (WS-27n). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 6, §11.11. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 6, §11.11. POST /projects/tasks/bulk → per-task outcomes diff --git a/apps/services/gateway/gateway/routes/projects/calendar.py b/apps/services/gateway/gateway/routes/projects/calendar.py index 0bec710fb..ff8cf8d58 100644 --- a/apps/services/gateway/gateway/routes/projects/calendar.py +++ b/apps/services/gateway/gateway/routes/projects/calendar.py @@ -1,6 +1,6 @@ """Projects · the calendar window (WS-27q). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 9, §11.16. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 9, §11.16. GET /projects/calendar?from=2026-08-01&to=2026-09-01 → every task in view diff --git a/apps/services/gateway/gateway/routes/projects/core.py b/apps/services/gateway/gateway/routes/projects/core.py index 1cfa69bef..843457fd3 100644 --- a/apps/services/gateway/gateway/routes/projects/core.py +++ b/apps/services/gateway/gateway/routes/projects/core.py @@ -3,7 +3,7 @@ The leaf module: it imports nothing from its siblings. It owns the shared ``router``, the Pydantic models, the row→model mapper, the SQL helpers, the **visibility read model**, the status-transition helper and the event seam. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3 and §4 (WS-27a). +Spec: ``project-docs/specs/project_management_app.md`` §3 and §4 (WS-27a). Four things here are load-bearing and worth stating once: diff --git a/apps/services/gateway/gateway/routes/projects/custom_fields.py b/apps/services/gateway/gateway/routes/projects/custom_fields.py index 6b24b32c0..71c73ffbc 100644 --- a/apps/services/gateway/gateway/routes/projects/custom_fields.py +++ b/apps/services/gateway/gateway/routes/projects/custom_fields.py @@ -1,6 +1,6 @@ """Projects · custom fields — definitions, and the values they police (WS-27l). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 4, §11.9. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 4, §11.9. GET /projects/nodes/{project_id}/fields POST /projects/nodes/{project_id}/fields diff --git a/apps/services/gateway/gateway/routes/projects/filters.py b/apps/services/gateway/gateway/routes/projects/filters.py index bb469811b..1aa370b8e 100644 --- a/apps/services/gateway/gateway/routes/projects/filters.py +++ b/apps/services/gateway/gateway/routes/projects/filters.py @@ -1,6 +1,6 @@ """Projects · the task-list filter vocabulary (WS-27k). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 3. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 3. *"My open bugs in Ops, grouped by assignee"* is a daily question the board had no way to answer. This is the filter half; grouping is presentation and lives in diff --git a/apps/services/gateway/gateway/routes/projects/import_clickup.py b/apps/services/gateway/gateway/routes/projects/import_clickup.py index 51853a553..31ef79a0b 100644 --- a/apps/services/gateway/gateway/routes/projects/import_clickup.py +++ b/apps/services/gateway/gateway/routes/projects/import_clickup.py @@ -1,6 +1,6 @@ """Projects · ClickUp import — plan, then apply. -Spec: ``ai-company-brain/specs/project_management_app.md`` §7.1 · **D-PM-10** · +Spec: ``project-docs/specs/project_management_app.md`` §7.1 · **D-PM-10** · ticket WS-27b. POST /projects/import/clickup/plan → proposes a Center per Space, WRITES NOTHING diff --git a/apps/services/gateway/gateway/routes/projects/import_tasks.py b/apps/services/gateway/gateway/routes/projects/import_tasks.py index 4d5f27ec8..e464a411e 100644 --- a/apps/services/gateway/gateway/routes/projects/import_tasks.py +++ b/apps/services/gateway/gateway/routes/projects/import_tasks.py @@ -1,6 +1,6 @@ """Projects · import the ClickUp mirror the Tasks app already holds. -Spec: ``ai-company-brain/specs/project_management_app.md`` §7.1 · WS-27b. +Spec: ``project-docs/specs/project_management_app.md`` §7.1 · WS-27b. POST /projects/import/from-tasks → one department, everything under it diff --git a/apps/services/gateway/gateway/routes/projects/mapping.py b/apps/services/gateway/gateway/routes/projects/mapping.py index c7370d6c5..9dcf24fc2 100644 --- a/apps/services/gateway/gateway/routes/projects/mapping.py +++ b/apps/services/gateway/gateway/routes/projects/mapping.py @@ -1,6 +1,6 @@ """Projects · mapping — proposing a Center for each ClickUp Space. -Spec: ``ai-company-brain/specs/project_management_app.md`` §7.1 · **D-PM-10** +Spec: ``project-docs/specs/project_management_app.md`` §7.1 · **D-PM-10** (owner-answered 2026-08-06) · ticket WS-27b done-whens 1-3. The owner's rule, and the reason this module is separate from the importer it diff --git a/apps/services/gateway/gateway/routes/projects/me.py b/apps/services/gateway/gateway/routes/projects/me.py index b71235861..fd4fff530 100644 --- a/apps/services/gateway/gateway/routes/projects/me.py +++ b/apps/services/gateway/gateway/routes/projects/me.py @@ -1,6 +1,6 @@ """Projects · me — the caller's own work. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``me.py`` row). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``me.py`` row). GET /projects/assigned-to-me diff --git a/apps/services/gateway/gateway/routes/projects/notifications.py b/apps/services/gateway/gateway/routes/projects/notifications.py index 3e97d688c..a2680d66d 100644 --- a/apps/services/gateway/gateway/routes/projects/notifications.py +++ b/apps/services/gateway/gateway/routes/projects/notifications.py @@ -1,6 +1,6 @@ """Projects · notifications — somebody hears about it (WS-27j). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 2. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 2. GET /projects/notifications → {rows, total, unread} POST /projects/notifications/read → mark some, or all, read diff --git a/apps/services/gateway/gateway/routes/projects/personal.py b/apps/services/gateway/gateway/routes/projects/personal.py index 1e6301e4a..7506bf62c 100644 --- a/apps/services/gateway/gateway/routes/projects/personal.py +++ b/apps/services/gateway/gateway/routes/projects/personal.py @@ -1,6 +1,6 @@ """Projects · the personal lens — one task store, seen as my own work. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3.11-§3.12, §6.1 · +Spec: ``project-docs/specs/project_management_app.md`` §3.11-§3.12, §6.1 · **D-PM-6 (revised 2026-08-06)** · ticket WS-27e. GET /projects/my/inbox → my work, with my overlay diff --git a/apps/services/gateway/gateway/routes/projects/recurrence.py b/apps/services/gateway/gateway/routes/projects/recurrence.py index a76a28cf1..fd975c420 100644 --- a/apps/services/gateway/gateway/routes/projects/recurrence.py +++ b/apps/services/gateway/gateway/routes/projects/recurrence.py @@ -1,6 +1,6 @@ """Projects · recurring tasks (WS-27o). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 7, §11.13. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 7, §11.13. GET /projects/tasks/{task_id}/recurrence PUT /projects/tasks/{task_id}/recurrence → set or replace the rule diff --git a/apps/services/gateway/gateway/routes/projects/relations.py b/apps/services/gateway/gateway/routes/projects/relations.py index ec056fec6..c54d569db 100644 --- a/apps/services/gateway/gateway/routes/projects/relations.py +++ b/apps/services/gateway/gateway/routes/projects/relations.py @@ -1,6 +1,6 @@ """Projects · dependencies and subtasks, made reachable (WS-27p). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 8, §11.14. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 8, §11.14. GET /projects/tasks/{task_id}/relations → subtasks + links, both directions diff --git a/apps/services/gateway/gateway/routes/projects/search.py b/apps/services/gateway/gateway/routes/projects/search.py index 031528492..b8d29dcaf 100644 --- a/apps/services/gateway/gateway/routes/projects/search.py +++ b/apps/services/gateway/gateway/routes/projects/search.py @@ -1,6 +1,6 @@ """Projects · the search surface (WS-27r). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 10, §11.18. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 10, §11.18. GET /projects/search?q=parser → ranked tasks, across every visible project diff --git a/apps/services/gateway/gateway/routes/projects/tags.py b/apps/services/gateway/gateway/routes/projects/tags.py index 8c917d13d..fafc79d96 100644 --- a/apps/services/gateway/gateway/routes/projects/tags.py +++ b/apps/services/gateway/gateway/routes/projects/tags.py @@ -1,6 +1,6 @@ """Projects · tags — the registry the research notes said to build (WS-27m). -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 5, §11.10. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 5, §11.10. GET /projects/nodes/{project_id}/tags → with a usage count each POST /projects/nodes/{project_id}/tags diff --git a/apps/services/gateway/gateway/routes/projects/tasks.py b/apps/services/gateway/gateway/routes/projects/tasks.py index 00497d72d..bd4d81563 100644 --- a/apps/services/gateway/gateway/routes/projects/tasks.py +++ b/apps/services/gateway/gateway/routes/projects/tasks.py @@ -1,6 +1,6 @@ """Projects · tasks — task CRUD, the list contract, assignees and links. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``tasks.py`` row). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``tasks.py`` row). GET /projects/tasks → {rows, total} POST /projects/tasks diff --git a/apps/services/gateway/gateway/routes/projects/tree.py b/apps/services/gateway/gateway/routes/projects/tree.py index b6aa3d362..7af1db0f1 100644 --- a/apps/services/gateway/gateway/routes/projects/tree.py +++ b/apps/services/gateway/gateway/routes/projects/tree.py @@ -1,6 +1,6 @@ """Projects · tree — the project hierarchy and the grants that scope it. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``tree.py`` row). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``tree.py`` row). GET /projects/tree → the granted forest, nested GET /projects/nodes → the same, flat diff --git a/apps/services/gateway/gateway/routes/projects/views.py b/apps/services/gateway/gateway/routes/projects/views.py index 27d323786..14d12f6bb 100644 --- a/apps/services/gateway/gateway/routes/projects/views.py +++ b/apps/services/gateway/gateway/routes/projects/views.py @@ -1,6 +1,6 @@ """Projects · views — saved views and per-view manual order. -Spec: ``ai-company-brain/specs/project_management_app.md`` §4 (``views.py`` row). +Spec: ``project-docs/specs/project_management_app.md`` §4 (``views.py`` row). GET /projects/nodes/{project_id}/views POST /projects/nodes/{project_id}/views diff --git a/apps/services/gateway/gateway/routes/rooms.py b/apps/services/gateway/gateway/routes/rooms.py index 3ad2a1834..e8bea7d2a 100644 --- a/apps/services/gateway/gateway/routes/rooms.py +++ b/apps/services/gateway/gateway/routes/rooms.py @@ -1,7 +1,7 @@ """Rooms — the HTTP surface for who is in a conversation and what it can reach. Spec: ``docs/multiplayer/README.md`` §4.5 (API surface), §7 (UX), -``ai-company-brain/specs/groups_sessions_authority.md`` §2-§4. +``project-docs/specs/groups_sessions_authority.md`` §2-§4. Mounted under ``/chat`` alongside the history CRUD, because a room *is* a chat session — there is no second object. Everything here reads or writes the two diff --git a/apps/services/gateway/gateway/routes/tasks/core.py b/apps/services/gateway/gateway/routes/tasks/core.py index 1e78d2b65..a0ad26b9b 100644 --- a/apps/services/gateway/gateway/routes/tasks/core.py +++ b/apps/services/gateway/gateway/routes/tasks/core.py @@ -5,7 +5,7 @@ package's ``core.py`` (the leaf module: it imports nothing from siblings). Canonical store: the ``task_accounts`` / ``gtd_*`` tables from -``infra/postgres/48_task_manager_gtd.sql`` (spec: ai-company-brain/specs/ +``infra/postgres/48_task_manager_gtd.sql`` (spec: project-docs/specs/ task_manager_app.md §4). Dual-source model (§5.1): LOCAL rows are ours; SYNCED rows mirror a connected PM tool through the provider layer. """ @@ -35,7 +35,7 @@ # ── People directory (HR) access ───────────────────────────────────────────── -# Spec: ai-company-brain/specs/colleague_onboarding.md §4 N4 (owner-answered +# Spec: project-docs/specs/colleague_onboarding.md §4 N4 (owner-answered # 2026-08-04, "directory open, HR fields restricted"). # # `gtd_people` is an ORG roster, not per-user rows, so the owner-predicate diff --git a/apps/services/gateway/gateway/routes/whatsapp/transport/calls.py b/apps/services/gateway/gateway/routes/whatsapp/transport/calls.py index 55c30930c..f5fad89cb 100644 --- a/apps/services/gateway/gateway/routes/whatsapp/transport/calls.py +++ b/apps/services/gateway/gateway/routes/whatsapp/transport/calls.py @@ -7,7 +7,7 @@ Why only personal numbers: Meta's Cloud API Calling product is 1:1-only and has to be enabled per-number by Meta, and our WABA route has no media leg yet. The whatsmeow bridge + meowcaller path works today and is the only one that can do -group calls at all. See ai-company-brain/specs/whatsapp_calls_note_taker.md. +group calls at all. See project-docs/specs/whatsapp_calls_note_taker.md. POST /whatsapp/calls {account_id, to | group_id | targets} POST /whatsapp/calls/hangup {account_id, call_id} diff --git a/apps/services/gateway/gateway/routes/workflows/__init__.py b/apps/services/gateway/gateway/routes/workflows/__init__.py index 427ec60ca..1a0977beb 100644 --- a/apps/services/gateway/gateway/routes/workflows/__init__.py +++ b/apps/services/gateway/gateway/routes/workflows/__init__.py @@ -1,6 +1,6 @@ """Workflows route package — the gateway `/workflows` API (visual automation). -Spec: ai-company-brain/specs/workflows_app.md · RFC: docs/workflow-editor/. +Spec: project-docs/specs/workflows_app.md · RFC: docs/workflow-editor/. ``core`` is the leaf; feature modules register routes on the shared ``router`` as an import side effect (the tasks/notes/apps convention). Import order is diff --git a/apps/services/gateway/gateway/routes/workflows/core.py b/apps/services/gateway/gateway/routes/workflows/core.py index b6f929365..5e54ce2c8 100644 --- a/apps/services/gateway/gateway/routes/workflows/core.py +++ b/apps/services/gateway/gateway/routes/workflows/core.py @@ -1,4 +1,4 @@ -"""Workflows routes — shared kernel (spec: ai-company-brain/specs/workflows_app.md). +"""Workflows routes — shared kernel (spec: project-docs/specs/workflows_app.md). The shared ``router``, DB infrastructure, and the small helpers every feature module uses. Mirrors ``routes/apps/_common.py`` / ``routes/tasks/core.py`` diff --git a/apps/services/ingestion/ingestion/sources/zoho/writer.py b/apps/services/ingestion/ingestion/sources/zoho/writer.py index f8e7bd57e..1b897db11 100644 --- a/apps/services/ingestion/ingestion/sources/zoho/writer.py +++ b/apps/services/ingestion/ingestion/sources/zoho/writer.py @@ -1,6 +1,6 @@ """The **only** Zoho CRM write surface in this repository. -Spec: ``ai-company-brain/specs/crm_app.md`` §7.1 · D-CRM-7 · D-CRM-8 · +Spec: ``project-docs/specs/crm_app.md`` §7.1 · D-CRM-7 · D-CRM-8 · ticket WS-26b done-when 1. Until 2026-08-05 there was no Zoho write path anywhere in the tree — the diff --git a/apps/services/meeting_bot/README.md b/apps/services/meeting_bot/README.md index 6a0cd1dcf..9a24cce80 100644 --- a/apps/services/meeting_bot/README.md +++ b/apps/services/meeting_bot/README.md @@ -271,7 +271,7 @@ curl -s -H "Authorization: Bearer $MEETING_BOT_TOKEN" \ **Zoom's Meeting SDK now explicitly forbids notetaker bots** and its web client is CAPTCHA-walled, so for Zoom the sanctioned routes are RTMS (for meetings you host) or a managed provider. See - `ai-company-brain/specs/meeting_bot_platform_plan.md`. + `project-docs/specs/meeting_bot_platform_plan.md`. - **Joining unattended requires a signed-in bot account.** Anonymous joining is not a degraded mode, it is a blocked one — see the wall section above. - **Browser automation is inherently brittle.** Meet's DOM is not a public API; diff --git a/apps/services/orchestrator/AGENTS.md b/apps/services/orchestrator/AGENTS.md index 10ec99be2..fd46d50f7 100644 --- a/apps/services/orchestrator/AGENTS.md +++ b/apps/services/orchestrator/AGENTS.md @@ -14,15 +14,15 @@ and streams chat responses as AG-UI events. ## Local Contracts -1. executor.py is the single entry point for agent execution (streaming and batch). Injects platform tools, MCP server config from the registry, and integration credentials at runtime. Integration credentials are filtered by the ACTING MEMBER first (`_integration_authorizer` → `build_integrations(is_authorized=)`, org access control): an agent's config.json declares a want, not an entitlement, and the filter runs BEFORE the per-run env injection so an unauthorized credential never enters the run's environment. Runs with no attributable active member (cron, reconciler, webhooks) are deliberately unfiltered — see ai-company-brain/specs/org_access_control.md §8a. ⚠️ Every `/v1` client here (agents.py, _model_resolution.py, code_session.py, mutation.py, the executor's BYOK provider config) MUST read `settings.llm_api_key` — never `gateway_internal_token`, which is the service identity and must not reach model-authored code (§8b). A test in tests/unit/test_service_identity_and_webhook_auth.py fails if one does. +1. executor.py is the single entry point for agent execution (streaming and batch). Injects platform tools, MCP server config from the registry, and integration credentials at runtime. Integration credentials are filtered by the ACTING MEMBER first (`_integration_authorizer` → `build_integrations(is_authorized=)`, org access control): an agent's config.json declares a want, not an entitlement, and the filter runs BEFORE the per-run env injection so an unauthorized credential never enters the run's environment. Runs with no attributable active member (cron, reconciler, webhooks) are deliberately unfiltered — see project-docs/specs/org_access_control.md §8a. ⚠️ Every `/v1` client here (agents.py, _model_resolution.py, code_session.py, mutation.py, the executor's BYOK provider config) MUST read `settings.llm_api_key` — never `gateway_internal_token`, which is the service identity and must not reach model-authored code (§8b). A test in tests/unit/test_service_identity_and_webhook_auth.py fails if one does. 2. copilot_agent.py provides CommandCenterCopilotAgent -- the MAF wrapper for Copilot SDK agents with BYOK + MCP server forwarding 3. agents.py exports build_orchestrator_agent() -- the main orchestrator MAF Agent 4. mutation.py handles Self_Mutation_Node -- spawns Docker sandbox on agent failure 5. stream_relay.py buffers all SSE events to Redis Streams for fire-and-forget chat with live reconnection 6. All agents must go through MAF -- no raw Copilot SDK paths for business execution 7. mutation_runner.py runs inside the Docker sandbox -- uses Copilot SDK directly (by design) -8. workflow_tools.py exposes published Workflows-app workflows to every agent as a three-tool trio (`list_workflows` / `run_workflow` / `get_workflow_run`) — the sibling of app_tools.py, injected through the same `_tool_injection` gated pipeline. Calls go in-process to `gateway.routes.workflows.service` (the same entrypoints the Run button and API use), so concurrency caps, run history, and the approval gates inside a workflow bind agent-triggered runs identically; a run paused at a Human-approval node reports itself as waiting in the approvals inbox — an agent cannot bypass the gate. Spec: ai-company-brain/specs/workflows_app.md F13 -9. Run correlation is bound in **two** places in run_agent_stream, deliberately. `bind_run_context(run_id, thread_id, agent, user, source)` fires at the run boundary — BEFORE `load_agent`, so a failure during load is still correlated — and `_bind_run_instance(instance, run_id)` tops up `instance` (the tenant partition from `_resolve_agent_instance`) immediately after the load, because that value needs `loaded.config` and cannot exist earlier. Binds are additive; the single `clear_run_context()` in the `finally` unbinds every key. Do not "simplify" this into one bind: moving the first one late loses correlation on load failures, and dropping the second silently un-attributes every personal/team agent's spend. The same call patches the live presence key via `acb_common.refresh_run_presence` — the `phase="start"` event predates the load, so `/observability/active` and `/observability/roster` would otherwise never show a partition for any run. `_run_sub_agent_streaming` deliberately does **neither**: a delegated sub-run inherits the caller's `instance` (known asymmetry — spec §7 WS-6a) because `bind_context` cannot unbind and a correct fix needs save/restore around the sub-run. Spec: ai-company-brain/specs/observability_e2.md §7 (WS-6a) +8. workflow_tools.py exposes published Workflows-app workflows to every agent as a three-tool trio (`list_workflows` / `run_workflow` / `get_workflow_run`) — the sibling of app_tools.py, injected through the same `_tool_injection` gated pipeline. Calls go in-process to `gateway.routes.workflows.service` (the same entrypoints the Run button and API use), so concurrency caps, run history, and the approval gates inside a workflow bind agent-triggered runs identically; a run paused at a Human-approval node reports itself as waiting in the approvals inbox — an agent cannot bypass the gate. Spec: project-docs/specs/workflows_app.md F13 +9. Run correlation is bound in **two** places in run_agent_stream, deliberately. `bind_run_context(run_id, thread_id, agent, user, source)` fires at the run boundary — BEFORE `load_agent`, so a failure during load is still correlated — and `_bind_run_instance(instance, run_id)` tops up `instance` (the tenant partition from `_resolve_agent_instance`) immediately after the load, because that value needs `loaded.config` and cannot exist earlier. Binds are additive; the single `clear_run_context()` in the `finally` unbinds every key. Do not "simplify" this into one bind: moving the first one late loses correlation on load failures, and dropping the second silently un-attributes every personal/team agent's spend. The same call patches the live presence key via `acb_common.refresh_run_presence` — the `phase="start"` event predates the load, so `/observability/active` and `/observability/roster` would otherwise never show a partition for any run. `_run_sub_agent_streaming` deliberately does **neither**: a delegated sub-run inherits the caller's `instance` (known asymmetry — spec §7 WS-6a) because `bind_context` cannot unbind and a correct fix needs save/restore around the sub-run. Spec: project-docs/specs/observability_e2.md §7 (WS-6a) 10. _tool_injection.py's static tool collection lives in `_collect_injectable_platform_tools()` (WS-23 S1 — the exact import chain `_inject_agent_tools` always ran, extracted verbatim; injection behavior unchanged). It is the read-only introspection seam the skills catalog (`acb_skills.skill_families` + gateway `GET /integrations/skills`) builds on, and `tests/unit/test_skills_registry.py` drift-fails if an injected tool is missing from the family registry — register any newly injected tool there in exactly one family. WS-23 S2: `_resolve_injected_scope(tool_scope, disabled_families=…)` intersects admin skill toggles (`agent_skill_setting`, loaded once per run by `_load_disabled_skill_families` — same best-effort sync-DB mechanism as app grants/MCP) with the declared scope; the core floor survives anything, the `workflows` toggle is honored at the trio's append site, and NO rows means byte-identical pre-S2 behavior (`tests/unit/test_skill_toggle_enforcement.py` pins all three rules). ## Work Guidance diff --git a/apps/services/orchestrator/orchestrator/declarative.py b/apps/services/orchestrator/orchestrator/declarative.py index 0d434780f..f8d543532 100644 --- a/apps/services/orchestrator/orchestrator/declarative.py +++ b/apps/services/orchestrator/orchestrator/declarative.py @@ -1,6 +1,6 @@ """The one builder every declarative agent runs on. -Spec: ``ai-company-brain/specs/agent_architecture.md`` §1, §6.1. +Spec: ``project-docs/specs/agent_architecture.md`` §1, §6.1. Four of six first-party agents are 24-136 lines of boilerplate a manifest expresses exactly: read ``instructions.md``, import a named set of skill tools, diff --git a/apps/services/orchestrator/orchestrator/mutation.py b/apps/services/orchestrator/orchestrator/mutation.py index 6944d8c01..7cd5296c6 100644 --- a/apps/services/orchestrator/orchestrator/mutation.py +++ b/apps/services/orchestrator/orchestrator/mutation.py @@ -503,7 +503,7 @@ def _build_telemetry( compat_guide: str = "" if incompatibility: for candidate in Path(__file__).parents: - guide = candidate / "ai-company-brain" / "agent_repo_compatibility.md" + guide = candidate / "project-docs" / "agent_repo_compatibility.md" if guide.exists(): compat_guide = guide.read_text(encoding="utf-8", errors="replace") break diff --git a/apps/services/orchestrator/orchestrator/steer.py b/apps/services/orchestrator/orchestrator/steer.py index 2bbc94d8a..a302c1fbb 100644 --- a/apps/services/orchestrator/orchestrator/steer.py +++ b/apps/services/orchestrator/orchestrator/steer.py @@ -3,7 +3,7 @@ Spec: ``docs/multiplayer/README.md`` §3.3 (the destructive race), §4.6 (steer is "a new applier, not new infrastructure" on the existing ``cc:control:`` bus), §5.2 (the correctness fix), and -``ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md`` §QM-1 (the +``project-docs/specs/multiplayer_prior_art_qm_2026-08.md`` §QM-1 (the external reference design this mirrors in shape, not in code). The problem this solves, restated from §3.3: a second ``POST /agent/run/stream`` diff --git a/codegraph.json b/codegraph.json index c3cd818b6..244601cba 100644 --- a/codegraph.json +++ b/codegraph.json @@ -1,5 +1,5 @@ { - "//": "CodeGraph index config — see ai-company-brain/specs/dev_velocity_tooling_2026-07.md (Phase 2). CodeGraph already excludes node_modules/.venv/dist/build/.next and respects .gitignore; these are the CommandCenter-specific additions so the symbol graph stays focused on the Python + TS source that coding agents actually edit.", + "//": "CodeGraph index config — see project-docs/specs/dev_velocity_tooling_2026-07.md (Phase 2). CodeGraph already excludes node_modules/.venv/dist/build/.next and respects .gitignore; these are the CommandCenter-specific additions so the symbol graph stays focused on the Python + TS source that coding agents actually edit.", "exclude": [ "infra/postgres/schema.generated.sql", "**/migrations/**", @@ -8,6 +8,6 @@ "reference/**", "**/__pycache__/**", "**/*.min.js", - "ai-company-brain/**" + "project-docs/**" ] } diff --git a/deploy/hostinger/acb-backup.timer b/deploy/hostinger/acb-backup.timer index e82b1861b..cd73c14bb 100644 --- a/deploy/hostinger/acb-backup.timer +++ b/deploy/hostinger/acb-backup.timer @@ -1,6 +1,6 @@ [Unit] Description=Nightly CommandCenter database backup -Documentation=file:///opt/acb/app/ai-company-brain/specs/backup_and_restore.md +Documentation=file:///opt/acb/app/project-docs/specs/backup_and_restore.md [Timer] # 02:30 UTC — clear of the 05:00 UTC Monday codebase-health job, so a long diff --git a/docs/DESIGN_LIMITATION_native_maf_mutation.md b/docs/DESIGN_LIMITATION_native_maf_mutation.md index 8ba982b39..ac3364184 100644 --- a/docs/DESIGN_LIMITATION_native_maf_mutation.md +++ b/docs/DESIGN_LIMITATION_native_maf_mutation.md @@ -58,7 +58,7 @@ evaluate when we get there (not yet decided): lane. The tenancy model **was settled on 2026-08-08** (D15: organization_id + RLS, -deployment = placement — `ai-company-brain/specs/saas_multitenancy.md` §1), and +deployment = placement — `project-docs/specs/saas_multitenancy.md` §1), and this limitation is ticketed as **MT-0b (WS-29)**: a config gate defaulting to disabled, **BUILT 2026-08-08 pending review** (migration 157 adds `organization.first_party`; mutation refuses non-first-party targets). This note diff --git a/docs/app-workshop/README.md b/docs/app-workshop/README.md index 36703d50d..8ffe40d4d 100644 --- a/docs/app-workshop/README.md +++ b/docs/app-workshop/README.md @@ -1083,7 +1083,7 @@ watch (genUI Phase 3); app analytics; org-research permission model adoption. `packages/acb_skills/acb_skills/{loader.py,integrations.py,permission_policy.py,code_tools.py,write_artifact.py}` · `apps/services/gateway/gateway/routes/{agent.py,workspace.py,integrations.py,actions.py,v1_compat.py}` · `infra/postgres/` (`66_pending_actions`, `71_agent_blob_store`, `03_pending_commits`) -- Specs: `ai-company-brain/specs/agent_coding_skill.md` · +- Specs: `project-docs/specs/agent_coding_skill.md` · `specs/generative_ui_2.md` · `specs/permissions_sandbox_b6.md` · `specs/multi_user_organization_research.md` · `specs/chat_agent_framework_review_2026-07.md` · `/FOUNDATION_BUILDOUT_CHECKLIST.md` (BO-1/2/7/14) diff --git a/docs/multiplayer/README.md b/docs/multiplayer/README.md index 759ac7481..dea3abe0a 100644 --- a/docs/multiplayer/README.md +++ b/docs/multiplayer/README.md @@ -47,9 +47,9 @@ > `'driver'`. Whether the baton still earns its place *on top of steer* is an > **owner re-decision** (§8 Phase 2), not queued work. The authoritative build > state for the access primitives is -> [`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) §6; +> [`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) §6; > the dispatch state for what remains is -> [`work_plan.md`](../../ai-company-brain/work_plan.md) §2, WS-10. +> [`work_plan.md`](../../project-docs/work_plan.md) §2, WS-10. > > **Shipped migrations:** **136** (`agent_blob` instance key), **137** > (quarantine of commingled agent data), **138** (`org_group`, @@ -80,7 +80,7 @@ Interactive mockups live alongside this doc: **Companion docs:** -- [`../../ai-company-brain/specs/agent_platform_hardening_2026-07.md`](../../ai-company-brain/specs/agent_platform_hardening_2026-07.md) +- [`../../project-docs/specs/agent_platform_hardening_2026-07.md`](../../project-docs/specs/agent_platform_hardening_2026-07.md) — **adversarial review of everything below.** 20 findings, the container-isolation decision, and the ordered fix list. Two of them change this document's design rather than its implementation: participant input must be **user-role** (steer is otherwise a @@ -88,7 +88,7 @@ Interactive mockups live alongside this doc: produced them** so replay can be filtered by label, not only by join cursor — otherwise the model launders restricted content into a transcript that later joiners can read. -- [`../../ai-company-brain/specs/groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) +- [`../../project-docs/specs/groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) — **the binding decisions (2026-07-29).** Org access control Phase 1 shipped (resolved principals, `EffectiveAccess`, default-deny, agent-run gating) and handed the rest to this workstream. That spec fixes the three shared primitives: `org_group` (one group model for @@ -99,7 +99,7 @@ Interactive mockups live alongside this doc: one-per-person (coach, email) and which are one-brain-for-a-team (sales assistant). Read this first: instancing decides what a memory compartment even is, and whether an agent's sessions may become rooms at all. -- [`../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md`](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md) +- [`../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md`](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md) — **prior art: `yc-software/qm`, read 2026-08-01.** An outside team reached this document's intersection rule independently and applied it to three resources. Three of its findings change what is written below rather than confirming it: **steer should be built before floor control** @@ -108,11 +108,11 @@ Interactive mockups live alongside this doc: and QM-3 belongs to **WS-2 / WS-1**, not to this workstream), and participant **tenure windows should narrow the model's context**, not only each viewer's replay (§6.5 — QM-5, an undone design comparison). **Reference-only**: it owns no work item and no status - ([`work_plan.md`](../../ai-company-brain/work_plan.md) §4). + ([`work_plan.md`](../../project-docs/work_plan.md) §4). - [`memory-clearance.md`](memory-clearance.md) — how memory is partitioned across sessions *and* across people, and how the agent decides which parts it may use on a given call. Supersedes §6.3 below. -- [`../../ai-company-brain/specs/memory_architecture.md`](../../ai-company-brain/specs/memory_architecture.md) +- [`../../project-docs/specs/memory_architecture.md`](../../project-docs/specs/memory_architecture.md) — the memory system as a whole: seven stores unified into six tiers, how a fact persists and gets corrected, and the always-on **file tier** (`agent-data/`), which is shared across all users of an agent today and matters more than the vector tier because it is injected @@ -180,7 +180,7 @@ This is the plumbing referred to in the framing. It is genuinely most of the pro | **Authoritative run-end persistence** | `gateway/chat_fold.py:410` `persist_final_assistant_message` | The transcript is folded and written server-side at the run boundary, independent of any browser. A room's history does not depend on a participant staying connected. | | **Identity at the edge** | `packages/acb_auth/acb_auth/deps.py` — `UserContext(email, role)`, internal-bearer-verified SSO headers | Every request already carries a verified actor. Membership checks have something to check against. | | **Cost & activity feed** | `packages/acb_common` Redis activity/cost feed; live token tracking (Custom Apps) | Per-participant cost attribution in a room is a re-key, not a new system. | -| **Org/RBAC design already researched** | `ai-company-brain/specs/multi_user_organization_research.md` | Orgs, memberships, permission vocabulary, agent visibility, memory scoping. **This RFC is the session-level layer on top of it**, and deliberately does not re-litigate the org model. | +| **Org/RBAC design already researched** | `project-docs/specs/multi_user_organization_research.md` | Orgs, memberships, permission vocabulary, agent visibility, memory scoping. **This RFC is the session-level layer on top of it**, and deliberately does not re-litigate the org model. | **Relationship to the org research doc.** That doc answers *"who can access which agents and data across the company"* — a static, org-shaped question. This doc answers *"how do several @@ -325,9 +325,9 @@ Why this and not a new abstraction: > **Superseded in part (2026-07-29):** the participant table, visibility values, and > authority model below were drafted before org access control Phase 1 shipped and handed -> off ([`org_access_control.md` §10](../../ai-company-brain/specs/org_access_control.md)). +> off ([`org_access_control.md` §10](../../project-docs/specs/org_access_control.md)). > The binding decisions now live in -> [`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md): +> [`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md): > the table is `chat_session_participant(subject, role ∈ owner|member|viewer)` with the > `app_grants` subject vocabulary (email / `group:` / `org`), visibility is > `private|people|org` mirroring `apps.visibility`, and — most importantly — there is **no @@ -442,7 +442,7 @@ PATCH /chat/sessions/{id}/room visibility, floor_mode, context_ > > **The `/members` half of this section is not this workstream's to build.** > Group membership — the `group:` subjects a room shares to — is owned by -> **WS-13 / Centers B** ([`work_plan.md`](../../ai-company-brain/work_plan.md) +> **WS-13 / Centers B** ([`work_plan.md`](../../project-docs/work_plan.md) > §4): `routes/admin/groups.py` + `/settings/groups`, shipped 2026-08-01. > Rooms consume groups (`gateway/rooms.py` expands `group:` at read > time); they do not administer them. @@ -535,7 +535,7 @@ makes the destructive race impossible rather than merely unlikely. > must not be able to reach `mark_active(reset=True)` and delete a transcript they don't own), and > the carve-out that a human message arriving during an *automation* turn should start its own run > rather than steer into a cron. Detail: -> [`multiplayer_prior_art_qm_2026-08.md` §QM-1](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md). +> [`multiplayer_prior_art_qm_2026-08.md` §QM-1](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md). ### 5.2 The Phase 0 correctness fix that unlocks all of this @@ -553,7 +553,7 @@ This one change converts a silent data-loss bug into an explicit product choice, worth shipping on its own even if multiplayer stops here. > **Update 2026-08-01 (built).** Shipped, and with the sequence inverted per -> [`multiplayer_prior_art_qm_2026-08.md`](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md) +> [`multiplayer_prior_art_qm_2026-08.md`](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md) > §QM-1: **steer first, floor control re-decided afterwards.** What landed: > > - **The supersede rule, stated once: *you may supersede a run you own, and no @@ -673,7 +673,7 @@ Per-participant cost attribution rides the existing activity/cost feed: stamp `participant_email` alongside `thread_id` on each run's token record, and the room header can show "1.2M tokens · Vijay 61% · Sanjay 39%". -> **Not this workstream's to build** ([`work_plan.md`](../../ai-company-brain/work_plan.md) +> **Not this workstream's to build** ([`work_plan.md`](../../project-docs/work_plan.md) > §4 single-owner registry): > > - **Cost attribution** — the `(run_id, member_email, agent, instance)` stamp at the @@ -690,7 +690,7 @@ show "1.2M tokens · Vijay 61% · Sanjay 39%". > **Update 2026-08-01 (doc-truth pass):** the shipped role vocabulary is > **`owner` | `member` | `viewer`** -> ([`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) §2), +> ([`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) §2), > not the `observer` / `contributor` / `owner` triple used below. Read > "observer" as `viewer` and "contributor" as `member`. @@ -845,7 +845,7 @@ Rules: > itself — *"There is NO acting_identity column, deliberately: a shared run acts at the > INTERSECTION of all participants' access (spec §3), never as one member."* §4.3 was > superseded on 2026-07-29 by -> [`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md). +> [`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md). > Any doc (including the prior-art map and the work-plan board) that reads *"rather than one > `acting_identity`"* is describing a road not taken. > @@ -867,7 +867,7 @@ Rules: > (WS-2) instead of sitting beside them. Their documented residual applies to us too: a credential > materialized into a sandbox is plaintext to any process there, and a stated *purpose* is an audit > field, not enforced authorization. Detail: -> [`multiplayer_prior_art_qm_2026-08.md` §QM-3](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md). +> [`multiplayer_prior_art_qm_2026-08.md` §QM-3](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md). > > **Gate: not this workstream's.** Per-credential room grants are net-new work with no > acceptance criteria in any doc, and the prior-art map routes QM-3 to **WS-2** (OWNER-GATE @@ -895,7 +895,7 @@ Total transparency is not the goal; *shared context with private edges* is. > narrows what the agent itself may re-read, not just what that person sees. That is the same > outcome [`memory-clearance.md`](memory-clearance.md) §5.4 reaches by queueing joins to run > boundaries, by a simpler mechanism — worth comparing before building either. - > [`multiplayer_prior_art_qm_2026-08.md` §QM-5](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md). + > [`multiplayer_prior_art_qm_2026-08.md` §QM-5](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md). > > **The gap is real; the design is not done. NOT DISPATCHABLE.** Verified 2026-08-02: > the viewer half is built and correct (mig 138 `:97-98` → `gateway/rooms.py:277-282`, @@ -973,7 +973,7 @@ confidential-deal case safe, and it is detailed in > done-when is testable. > - **OWNER-GATE** — an agent must **refuse** it and say which gate. Two items > in this document are owner gates and are registered by name in -> [`work_plan.md`](../../ai-company-brain/work_plan.md) §6: the **floor-control +> [`work_plan.md`](../../project-docs/work_plan.md) §6: the **floor-control > re-decision** (Phase 2) and the **`prefs`/`user` backfill *apply*** (Phase 3, > [`memory-clearance.md`](memory-clearance.md) §8 Q1). > - **✅ built** — no label needed; the item is history. @@ -1002,7 +1002,7 @@ confidential-deal case safe, and it is detailed in while `MEM0_ENABLED` / `GRAPHITI_ENABLED` are false — check the deployed `.env` for urgency. The **file tier** finding is not gated by either flag: `agent-data/` is shared across all users of an agent whenever an agent uses `save_note` / `recall_notes` - ([`memory_architecture.md`](../../ai-company-brain/specs/memory_architecture.md) §5.3). + ([`memory_architecture.md`](../../project-docs/specs/memory_architecture.md) §5.3). Its instance key lands with the compartment work in Phase 3a, not Phase 0 — but it is the finding to weigh first, because it is the tier that is injected rather than retrieved. - **Acceptance:** two clients on one thread; the second cannot cancel or erase the first's @@ -1055,7 +1055,7 @@ confidential-deal case safe, and it is detailed in > **Update 2026-08-01 (re-scoped and partly built).** The order of this phase is > inverted per -> [`multiplayer_prior_art_qm_2026-08.md`](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md) +> [`multiplayer_prior_art_qm_2026-08.md`](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md) > §QM-1: **steer was built first**, and whether the five floor modes still earn > their place is now a measurement rather than a plan. Steering dissolves most of > the problem the baton was invented for — a second person's message lands in the diff --git a/docs/multiplayer/agent-kinds.md b/docs/multiplayer/agent-kinds.md index 252462ba5..7af00d819 100644 --- a/docs/multiplayer/agent-kinds.md +++ b/docs/multiplayer/agent-kinds.md @@ -9,7 +9,7 @@ warning there) · **Date:** 2026-07-26, updated 2026-08-01 (doc-truth pass) · > **Header correction 2026-08-03 (R4).** This file read `Draft / RFC · 2026-07-26` > while carrying 2026-08-01 update blocks in the body. The date now reflects both. > Also note its **path**: this document lives at `docs/multiplayer/agent-kinds.md`, not -> under `ai-company-brain/specs/` — `work_plan.md` §4 cited it as if it were a spec and +> under `project-docs/specs/` — `work_plan.md` §4 cited it as if it were a spec and > was corrected in the same change. Which agents are one-per-person and which are one-brain-for-the-team — and what that @@ -54,7 +54,7 @@ formatters, linters, and one-shot utilities, and it removes an entire class of q > **Update 2026-08-01 (doc-truth pass): no longer the live behaviour.** > Instance-keying shipped — migration 136 added the instance key to > `agent_blob` and the run path now resolves instance-keyed agent memory -> ([`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) +> ([`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) > §6, step 2). This section stands as the rationale and the pre-change history. Today **every agent has exactly one memory bucket, keyed by agent name, written by every user @@ -360,10 +360,10 @@ layer, not by reading the answers. > That covers "these two instances really are one brain" deliberately and reversibly, without an > aggregate-derivation pipeline. It does **not** answer the cross-instance *learning* case above > (it merges, it does not generalise), so the two are complementary rather than alternatives. - > [`multiplayer_prior_art_qm_2026-08.md` §QM-7](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md). + > [`multiplayer_prior_art_qm_2026-08.md` §QM-7](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md). 2. **Team membership source.** ~~`team_ref` needs a real team object; the org research doc's `module` is the natural home, but it isn't built yet.~~ **Answered** — - [`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) §1: + [`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) §1: `sharing.team` names an `org_group.slug` (migration 138), the single group primitive shared with access control Phase 2 and session sharing. `t:` keys are `t:`. 3. **Per-user agent config.** If everyone has their own email agent, does everyone get their diff --git a/docs/multiplayer/memory-clearance.md b/docs/multiplayer/memory-clearance.md index 9998967fd..700aac4ce 100644 --- a/docs/multiplayer/memory-clearance.md +++ b/docs/multiplayer/memory-clearance.md @@ -8,7 +8,7 @@ adversarially reviewed and repaired twice** — line numbers move; re-verify at **Verification commands:** [§9](#9-verification) · **Owner:** vjvarada **Companion to:** [`README.md`](README.md) (the multiplayer room model) · [`agent-kinds.md`](agent-kinds.md) (personal vs shared agents) · -[`../../ai-company-brain/specs/multi_user_organization_research.md`](../../ai-company-brain/specs/multi_user_organization_research.md) (org/RBAC) +[`../../project-docs/specs/multi_user_organization_research.md`](../../project-docs/specs/multi_user_organization_research.md) (org/RBAC) > **What the three passes corrected, so the anchors below can be trusted for the reason they > should be — that they were wrong before and were caught.** @@ -263,7 +263,7 @@ Falcon" in a system prompt is a request. Not passing the scope key is a boundary > **Now grounded in shipped machinery (2026-07-29):** this same rule was independently > chosen for *permissions* — a shared run's authority is > `EffectiveAccess.intersect()` folded over all participants -> ([`groups_sessions_authority.md`](../../ai-company-brain/specs/groups_sessions_authority.md) §3, +> ([`groups_sessions_authority.md`](../../project-docs/specs/groups_sessions_authority.md) §3, > primitive in `packages/acb_auth/acb_auth/permissions.py`). Memory clearance and run > authority are the same shape applied to two resources: what may be *read into* the run, > and what the run may *do*. One rule, two enforcement points — credentials via @@ -278,7 +278,7 @@ Falcon" in a system prompt is a request. Not passing the scope key is a boundary > over-engineering of ours — it is what the problem forces. Second, egress is a third enforcement > point we do not use: we intersect what a run may *read* and what it may *do*, not where it may > *connect*. Noted against WS-1/WS-3 in -> [`multiplayer_prior_art_qm_2026-08.md` §QM-0](../../ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md). +> [`multiplayer_prior_art_qm_2026-08.md` §QM-0](../../project-docs/specs/multiplayer_prior_art_qm_2026-08.md). ### 3.4 The write rule @@ -319,7 +319,7 @@ do is file a fact somewhere too narrow — recoverable, and visible in the inspe | Memory *tools* write scope | ✅ built — **this row is done** | `routes/agent.py:1785` already calls `_set_memory_user_id(_clearance.write)`, so `remember` / `save_memory` file into the room's compartment in a room and the caller's when solo. The rename to `_set_memory_write_scope` was cosmetic and was not done; the behaviour it described is in place. | | Authorize the memory API | ✅ built | The gate is `_authorize_scope` at `routes/memory.py:128-167`, with one helper per shape (`_authorize_org` `:73`, `_authorize_room` `:82`, `_authorize_prefs` `:95`, `_authorize_agent` `:103`, `_authorize_person` `:112`). It is applied at **five** routes, not four: `:187` list, `:200` search, `:214` delete, `:244` add, `:266` status. **Not `:59,71,84,97`.** An unrecognised shape raises 404 (`:167`) — which is why `subject:` is a 404 today. | | Graphiti grouping | not built | `add_episode(group_id=)` — `packages/acb_memory/acb_memory/graphiti_client.py:187`. Latent while `GRAPHITI_ENABLED` is false. | -| **Instance-key the file tier** | ✅ built (migration **136**) | `agent_blob` PK `(agent_name, path)` → `(agent_name, instance, path)`, with migration 137 quarantining commingled rows — [`memory_architecture.md`](../../ai-company-brain/specs/memory_architecture.md) §6.1. | +| **Instance-key the file tier** | ✅ built (migration **136**) | `agent_blob` PK `(agent_name, path)` → `(agent_name, instance, path)`, with migration 137 quarantining commingled rows — [`memory_architecture.md`](../../project-docs/specs/memory_architecture.md) §6.1. | The read path is the only one with a cost change: N compartment searches instead of one. Bounded by clearance size (typically 4–6), parallelisable, and absorbed across turns by the @@ -343,7 +343,7 @@ Do not copy this pattern into new code — R5.]* It needs the same instance key and the same write rule as the compartments, and the two must land in the same phase — partitioning the vector tier while leaving the file tier shared fixes the smaller half of the problem. Full design: -[`memory_architecture.md`](../../ai-company-brain/specs/memory_architecture.md). +[`memory_architecture.md`](../../project-docs/specs/memory_architecture.md). --- @@ -526,7 +526,7 @@ wants before adding someone to a room. ## 7. Phasing Slots into the room plan in [`README.md`](README.md) §8. Dispatch state lives in -[`work_plan.md`](../../ai-company-brain/work_plan.md) §2, **WS-10**. +[`work_plan.md`](../../project-docs/work_plan.md) §2, **WS-10**. **Gate labels** (`work_plan.md` §1 contract item 7): **AGENT-SAFE** = an independent agent may build it; **OWNER-GATE** = an agent must refuse it and name the gate. diff --git a/docs/workflow-editor/README.md b/docs/workflow-editor/README.md index cd96e16f2..585cb2762 100644 --- a/docs/workflow-editor/README.md +++ b/docs/workflow-editor/README.md @@ -1,7 +1,7 @@ # Visual Workflow Editor — Analysis & Implementation Plan **Status:** RFC accepted — v1 slice built 2026-07-30 (product spec: -`ai-company-brain/specs/workflows_app.md` · policy: ADR-028 · code: gateway +`project-docs/specs/workflows_app.md` · policy: ADR-028 · code: gateway `routes/workflows/` + workbench `/workflows` + migration `132_workflows.sql`) · **Date:** 2026-07-16 · **Owner:** vjvarada diff --git a/infra/AGENTS.md b/infra/AGENTS.md index ef27e5bd2..79d6654c8 100644 --- a/infra/AGENTS.md +++ b/infra/AGENTS.md @@ -5,7 +5,7 @@ Docker Compose, Postgres schema, LiteLLM tier config. LLM routing is via the gat ## Key Files - docker-compose.yml -- core services (Postgres 16 + pgvector, Redis 7) -- postgres/ -- schema files (00-10) + 09_app_user.sql (NextAuth users) + 11_integration_credentials.sql (unified credential store) + 130_org_access_control.sql (organization, membership lifecycle on app_user, org_role/org_role_permission/user_role, user_permission_override, feature_catalog — spec: ai-company-brain/specs/org_access_control.md) + 131_integration_memory_permissions.sql (additive: grants `integrations:use:*` + org-memory permissions to the seeded roles; `member` reads org memory but does not write it) + 143_access_request.sql (the sign-in queue — one row per address that authenticated with no `app_user` row, unique on `lower(email)`; `status` carries a `CHECK (pending|approved|denied)` because the vocabulary is load-bearing for ACCESS — approve acts only on `pending`, so a typo'd status would fall out of both the queue and the decided record; deliberately a SEPARATE table and not a fifth `app_user.status`, because an `app_user` row IS the org's member record and a stranger who merely knocked must not acquire one that a future join can surface — spec: ai-company-brain/specs/colleague_onboarding.md §6) + 144_crm.sql (the native CRM spine — organizations/contacts/leads/deals, **statuses as data** rather than enums, one `crm_activities` timeline whose four target FKs are all nullable under a CHECK requiring at least one, and a `crm_status_changes` dwell log; also seeds the `crm` feature_catalog row. Contains the schema's **one FK cycle** — `crm_leads.converted_deal_id` ⟷ `crm_deals.lead_id` — closed by a guarded `DO $$` on `pg_constraint`, because `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`. Idempotency is pinned STATICALLY by `tests/unit/test_crm_migration.py`, which reads the file as text: the unit suite runs no database, so an idempotency claim that holds only by inspection is unenforceable. Spec: ai-company-brain/specs/crm_app.md §3) + 145_crm_zoho_sync.sql (what the two-way Zoho sync needs: `zoho_dirty`/`zoho_synced_at` on the four CRM record tables — and **only** those four, since pipeline vocabulary flows down-only and an activity's push signal is its NULL `zoho_id` — plus `crm_zoho_tombstones` (FK-less on purpose: the row it describes is gone by the time anyone reads it) and `crm_sync_cursors` (`module` PK, so the pull cannot silently rewind). Idempotent via `ADD COLUMN IF NOT EXISTS`, which — unlike `ADD CONSTRAINT` — Postgres supports directly, so no guarded `DO $$` is needed. Pinned by the same static `tests/unit/test_crm_migration.py`, which finds BOTH CRM migrations by CONTENT rather than by number. Spec: crm_app.md §7.1) + 163_crm_auto_lead_cursor.sql (WS-26d-autolead, renumbered from 158 at merge: `crm_auto_lead_cursors`, one row per `email_accounts` id, carrying THREE timestamps because they answer three different questions: `activated_at` (the start of the current ON epoch — `received_at > activated_at` is what tells a deep resync's year-old backlog apart from new mail on the shared `process_new_mail` hook), `processed_watermark` (the incremental cursor, advanced over a batch's successful PREFIX only), and `last_run_at` (the dormancy clock, stamped every cycle — a gap beyond an hour CLAMPS the epoch to `now - 1h`, so a flag turned off for weeks and back on mints nothing from the gap except its final hour, which is the recorded residual). ⚠️ The clamp is not a reset: the step runs only when a sync persisted mail, so the cycle that detects the gap always carries the message that woke it. All three NOT NULL: a NULL cursor is a predicate that matches nothing, which reads exactly like a working feature. ⚠️ **It deliberately adds NO unique index on `crm_leads.email`**: the cross-invocation double-mint race is accepted (one visible, hand-deletable duplicate) because a UNIQUE constraint on a column where 1,516 imported rows may already carry duplicates is a deploy-blocking migration of exactly the shape 148 had to defuse. Inert until `CRM_AUTO_LEAD` is flipped, which is OWNER-GATE. Spec: crm_app.md §9 WS-26d-autolead). ⚠️ `CREATE TABLE IF NOT EXISTS` means a column or constraint added to an ALREADY-APPLIED migration file is silently skipped on that deployment; 143, 144 and 145 have never been applied anywhere (merging 143 is the OWNER-GATE), which is the only reason they are still editable in place. +- postgres/ -- schema files (00-10) + 09_app_user.sql (NextAuth users) + 11_integration_credentials.sql (unified credential store) + 130_org_access_control.sql (organization, membership lifecycle on app_user, org_role/org_role_permission/user_role, user_permission_override, feature_catalog — spec: project-docs/specs/org_access_control.md) + 131_integration_memory_permissions.sql (additive: grants `integrations:use:*` + org-memory permissions to the seeded roles; `member` reads org memory but does not write it) + 143_access_request.sql (the sign-in queue — one row per address that authenticated with no `app_user` row, unique on `lower(email)`; `status` carries a `CHECK (pending|approved|denied)` because the vocabulary is load-bearing for ACCESS — approve acts only on `pending`, so a typo'd status would fall out of both the queue and the decided record; deliberately a SEPARATE table and not a fifth `app_user.status`, because an `app_user` row IS the org's member record and a stranger who merely knocked must not acquire one that a future join can surface — spec: project-docs/specs/colleague_onboarding.md §6) + 144_crm.sql (the native CRM spine — organizations/contacts/leads/deals, **statuses as data** rather than enums, one `crm_activities` timeline whose four target FKs are all nullable under a CHECK requiring at least one, and a `crm_status_changes` dwell log; also seeds the `crm` feature_catalog row. Contains the schema's **one FK cycle** — `crm_leads.converted_deal_id` ⟷ `crm_deals.lead_id` — closed by a guarded `DO $$` on `pg_constraint`, because `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`. Idempotency is pinned STATICALLY by `tests/unit/test_crm_migration.py`, which reads the file as text: the unit suite runs no database, so an idempotency claim that holds only by inspection is unenforceable. Spec: project-docs/specs/crm_app.md §3) + 145_crm_zoho_sync.sql (what the two-way Zoho sync needs: `zoho_dirty`/`zoho_synced_at` on the four CRM record tables — and **only** those four, since pipeline vocabulary flows down-only and an activity's push signal is its NULL `zoho_id` — plus `crm_zoho_tombstones` (FK-less on purpose: the row it describes is gone by the time anyone reads it) and `crm_sync_cursors` (`module` PK, so the pull cannot silently rewind). Idempotent via `ADD COLUMN IF NOT EXISTS`, which — unlike `ADD CONSTRAINT` — Postgres supports directly, so no guarded `DO $$` is needed. Pinned by the same static `tests/unit/test_crm_migration.py`, which finds BOTH CRM migrations by CONTENT rather than by number. Spec: crm_app.md §7.1) + 163_crm_auto_lead_cursor.sql (WS-26d-autolead, renumbered from 158 at merge: `crm_auto_lead_cursors`, one row per `email_accounts` id, carrying THREE timestamps because they answer three different questions: `activated_at` (the start of the current ON epoch — `received_at > activated_at` is what tells a deep resync's year-old backlog apart from new mail on the shared `process_new_mail` hook), `processed_watermark` (the incremental cursor, advanced over a batch's successful PREFIX only), and `last_run_at` (the dormancy clock, stamped every cycle — a gap beyond an hour CLAMPS the epoch to `now - 1h`, so a flag turned off for weeks and back on mints nothing from the gap except its final hour, which is the recorded residual). ⚠️ The clamp is not a reset: the step runs only when a sync persisted mail, so the cycle that detects the gap always carries the message that woke it. All three NOT NULL: a NULL cursor is a predicate that matches nothing, which reads exactly like a working feature. ⚠️ **It deliberately adds NO unique index on `crm_leads.email`**: the cross-invocation double-mint race is accepted (one visible, hand-deletable duplicate) because a UNIQUE constraint on a column where 1,516 imported rows may already carry duplicates is a deploy-blocking migration of exactly the shape 148 had to defuse. Inert until `CRM_AUTO_LEAD` is flipped, which is OWNER-GATE. Spec: crm_app.md §9 WS-26d-autolead). ⚠️ `CREATE TABLE IF NOT EXISTS` means a column or constraint added to an ALREADY-APPLIED migration file is silently skipped on that deployment; 143, 144 and 145 have never been applied anywhere (merging 143 is the OWNER-GATE), which is the only reason they are still editable in place. ## Conventions - Postgres migrations are numbered SQL files @@ -22,7 +22,7 @@ Docker Compose, Postgres schema, LiteLLM tier config. LLM routing is via the gat - **The Postgres data volume `acb-postgres-data` is not the only copy any more** — `scripts/backup_db.sh` dumps every non-template database (`acb`, `litellm_proxy`, …) plus cluster globals to `/opt/acb/backups`, **outside `/opt/acb/app`** because the deploy runs `git reset --hard` - `scripts/apply_migrations.sh` now takes that dump **before** replaying the ladder and **fails closed** if it cannot. Escape hatch: `SKIP_PRE_MIGRATION_BACKUP=1`, which you should have a reason for - Restore is `scripts/restore_db.sh` — **defaults to a scratch database**; live needs `--target acb --force` -- ⚠️ Nothing is scheduled yet and no restore has been exercised. Runbook + the owner steps: `ai-company-brain/specs/backup_and_restore.md` +- ⚠️ Nothing is scheduled yet and no restore has been exercised. Runbook + the owner steps: `project-docs/specs/backup_and_restore.md` - Redis still has **no** `appendonly` — its persistence gap is real and separate (see BO-20b's retry counter, which does not survive a Redis restart) ## Verification diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 32990e4a0..de685a9d8 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -7,7 +7,7 @@ # Full (with observability): docker compose --profile core --profile obs up -d # # AGE (Apache property-graph) is deferred to Phase 2 — Phase 0 only needs -# pgvector + relational tables. See ai-company-brain/system_architecture.md §4. +# pgvector + relational tables. See project-docs/system_architecture.md §4. name: acb diff --git a/infra/postgres/01_schema.sql b/infra/postgres/01_schema.sql index 2fd45fe43..78af7f978 100644 --- a/infra/postgres/01_schema.sql +++ b/infra/postgres/01_schema.sql @@ -1,5 +1,5 @@ -- AI Company Brain — Phase-0 graph schema v0 (WBS 0.2). --- Aligned with ai-company-brain/system_architecture.md §4. +-- Aligned with project-docs/system_architecture.md §4. -- 1. Required extensions ---------------------------------------------------- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; diff --git a/infra/postgres/102_whatsapp.sql b/infra/postgres/102_whatsapp.sql index 67be8f768..55c890a9f 100644 --- a/infra/postgres/102_whatsapp.sql +++ b/infra/postgres/102_whatsapp.sql @@ -9,7 +9,7 @@ -- *channel* shape all along. -- -- Official Meta WhatsApp Business Cloud API only (coexistence). No unofficial --- linked-device transport (see ai-company-brain/specs/whatsapp_message_manager.md +-- linked-device transport (see project-docs/specs/whatsapp_message_manager.md -- §3). Credentials for the WABA / phone-number-id / system-user token are stored -- as an AES-256-GCM encrypted JSONB blob, encrypted at the application layer, the -- same as email_accounts.credentials_encrypted. diff --git a/infra/postgres/120_live_session.sql b/infra/postgres/120_live_session.sql index 0ed071672..a1ae3c2a5 100644 --- a/infra/postgres/120_live_session.sql +++ b/infra/postgres/120_live_session.sql @@ -13,7 +13,7 @@ -- Phase B (the orchestrator). Off by default — the copilot never runs unless -- explicitly enabled for a session. -- Depends on: 95_note_taker.sql (meeting). ADDITIVE + idempotent. --- Spec: ai-company-brain/specs/live_meeting_copilot.md §7, §8, §10. +-- Spec: project-docs/specs/live_meeting_copilot.md §7, §8, §10. CREATE TABLE IF NOT EXISTS live_session ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), diff --git a/infra/postgres/121_copilot_event.sql b/infra/postgres/121_copilot_event.sql index a815cc8ed..b870c5939 100644 --- a/infra/postgres/121_copilot_event.sql +++ b/infra/postgres/121_copilot_event.sql @@ -13,7 +13,7 @@ -- Note: `token_cost` lets a session's spend be reconstructed per event, which is -- what makes the per-meeting budget guardrail auditable rather than a black box. -- Depends on: 120_live_session.sql (live_session). ADDITIVE + idempotent. --- Spec: ai-company-brain/specs/live_meeting_copilot.md §10, §13 (Phase B). +-- Spec: project-docs/specs/live_meeting_copilot.md §10, §13 (Phase B). CREATE TABLE IF NOT EXISTS copilot_event ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), diff --git a/infra/postgres/124_meeting_brief.sql b/infra/postgres/124_meeting_brief.sql index 567ce4f3c..452c22416 100644 --- a/infra/postgres/124_meeting_brief.sql +++ b/infra/postgres/124_meeting_brief.sql @@ -14,7 +14,7 @@ -- costs real tokens BEFORE the meeting produces a word. A quick internal -- standup shouldn't trigger a CRM sweep, so it's off unless asked for. -- Depends on: 95_note_taker.sql (meeting), 120_live_session.sql. ADDITIVE. --- Spec: ai-company-brain/specs/live_meeting_copilot.md §5 (business context). +-- Spec: project-docs/specs/live_meeting_copilot.md §5 (business context). ALTER TABLE meeting ADD COLUMN IF NOT EXISTS copilot_brief TEXT; diff --git a/infra/postgres/125_meeting_agenda.sql b/infra/postgres/125_meeting_agenda.sql index 409518a85..3023d9c39 100644 --- a/infra/postgres/125_meeting_agenda.sql +++ b/infra/postgres/125_meeting_agenda.sql @@ -20,7 +20,7 @@ -- stops being used. -- -- Depends on: 95_note_taker.sql (meeting). ADDITIVE + idempotent. --- Spec: ai-company-brain/specs/live_meeting_copilot.md §6 (bidirectional). +-- Spec: project-docs/specs/live_meeting_copilot.md §6 (bidirectional). ALTER TABLE meeting ADD COLUMN IF NOT EXISTS agenda JSONB NOT NULL DEFAULT '[]'::JSONB; diff --git a/infra/postgres/130_org_access_control.sql b/infra/postgres/130_org_access_control.sql index 03fc3f93a..ae86f331b 100644 --- a/infra/postgres/130_org_access_control.sql +++ b/infra/postgres/130_org_access_control.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 130_org_access_control.sql — Organization membership, roles, permissions -- ============================================================================ --- Phase 1 of ai-company-brain/specs/org_access_control.md. Turns the +-- Phase 1 of project-docs/specs/org_access_control.md. Turns the -- single-tenant deployment into a real multi-user organization: -- -- organization the tenant boundary (one row today; the column is diff --git a/infra/postgres/131_integration_memory_permissions.sql b/infra/postgres/131_integration_memory_permissions.sql index 55e5a5207..82ff89347 100644 --- a/infra/postgres/131_integration_memory_permissions.sql +++ b/infra/postgres/131_integration_memory_permissions.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 131_integration_memory_permissions.sql — per-user integration + org memory -- ============================================================================ --- Phase 3 (partial) of ai-company-brain/specs/org_access_control.md: extends +-- Phase 3 (partial) of project-docs/specs/org_access_control.md: extends -- the seeded roles with two permission families that migration 130 did not -- have vocabulary for. -- diff --git a/infra/postgres/132_workflows.sql b/infra/postgres/132_workflows.sql index 1d4c07416..c884dc320 100644 --- a/infra/postgres/132_workflows.sql +++ b/infra/postgres/132_workflows.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 132_workflows.sql — Workflows app data model (visual automation builder) -- ============================================================================ --- Spec: ai-company-brain/specs/workflows_app.md §4 +-- Spec: project-docs/specs/workflows_app.md §4 -- RFC: docs/workflow-editor/README.md §4 (edit-model vs run-model split) -- -- workflows the editable definition (edit-model): React-Flow JSON diff --git a/infra/postgres/133_workflows_publish_permission.sql b/infra/postgres/133_workflows_publish_permission.sql index ef8150850..bdfb41c71 100644 --- a/infra/postgres/133_workflows_publish_permission.sql +++ b/infra/postgres/133_workflows_publish_permission.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 133_workflows_publish_permission.sql — who may make a workflow LIVE -- ============================================================================ --- Resolves spec question Q3 of ai-company-brain/specs/workflows_app.md: "any +-- Resolves spec question Q3 of project-docs/specs/workflows_app.md: "any -- `workflows`-granted member may draft; publish requires `workflows:publish` -- (default: executives)". -- diff --git a/infra/postgres/134_workflows_automation_health.sql b/infra/postgres/134_workflows_automation_health.sql index cd7a9e69b..2535045d8 100644 --- a/infra/postgres/134_workflows_automation_health.sql +++ b/infra/postgres/134_workflows_automation_health.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 134_workflows_automation_health.sql — why a workflow stopped, and since when -- ============================================================================ --- Mitigates risk R2 of ai-company-brain/specs/workflows_app.md ("silent +-- Mitigates risk R2 of project-docs/specs/workflows_app.md ("silent -- automation drift"): a published workflow keeps firing on its schedule / -- webhook / event binding long after the business around it changed. The -- named mitigation is "disabled-on-repeated-failure policy with notification"; diff --git a/infra/postgres/136_agent_blob_instance.sql b/infra/postgres/136_agent_blob_instance.sql index 9eafe42d9..83c9715ff 100644 --- a/infra/postgres/136_agent_blob_instance.sql +++ b/infra/postgres/136_agent_blob_instance.sql @@ -1,8 +1,8 @@ -- ============================================================================ -- 138_agent_blob_instance.sql — partition the agent file store by instance -- ============================================================================ --- Spec: ai-company-brain/specs/memory_architecture.md §5.3, §6.1 --- ai-company-brain/specs/agent_architecture.md §2 +-- Spec: project-docs/specs/memory_architecture.md §5.3, §6.1 +-- project-docs/specs/agent_architecture.md §2 -- docs/multiplayer/agent-kinds.md §4 -- -- WHY diff --git a/infra/postgres/137_quarantine_commingled_agent_data.sql b/infra/postgres/137_quarantine_commingled_agent_data.sql index 8f75c1e09..c2af67b97 100644 --- a/infra/postgres/137_quarantine_commingled_agent_data.sql +++ b/infra/postgres/137_quarantine_commingled_agent_data.sql @@ -1,8 +1,8 @@ -- ============================================================================ -- 139_quarantine_commingled_agent_data.sql — retire pre-instance file content -- ============================================================================ --- Spec: ai-company-brain/specs/memory_architecture.md §5.3 --- ai-company-brain/specs/agent_platform_hardening_2026-07.md H4 +-- Spec: project-docs/specs/memory_architecture.md §5.3 +-- project-docs/specs/agent_platform_hardening_2026-07.md H4 -- docs/multiplayer/agent-kinds.md §6 -- -- WHY diff --git a/infra/postgres/138_groups_and_session_participants.sql b/infra/postgres/138_groups_and_session_participants.sql index baa6e1a3f..a605c405c 100644 --- a/infra/postgres/138_groups_and_session_participants.sql +++ b/infra/postgres/138_groups_and_session_participants.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 138_groups_and_session_participants.sql — the two shared primitives -- ============================================================================ --- Spec: ai-company-brain/specs/groups_sessions_authority.md §1–§2 +-- Spec: project-docs/specs/groups_sessions_authority.md §1–§2 -- (answers org_access_control.md §10.2 collisions 1 and 2) -- -- Two workstreams — access control Phase 2 ("modules/teams") and multiplayer diff --git a/infra/postgres/139_room_authorship_and_agents.sql b/infra/postgres/139_room_authorship_and_agents.sql index 03b34a55d..63a86a8bb 100644 --- a/infra/postgres/139_room_authorship_and_agents.sql +++ b/infra/postgres/139_room_authorship_and_agents.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 139_room_authorship_and_agents.sql — a transcript that knows who said what -- ============================================================================ --- Spec: ai-company-brain/specs/groups_sessions_authority.md §4 (the transcript +-- Spec: project-docs/specs/groups_sessions_authority.md §4 (the transcript -- boundary) + docs/multiplayer/README.md §3.4, §4.3. -- -- Migration 134 said WHO IS IN a session. This one makes the session's contents diff --git a/infra/postgres/141_seed_center_groups.sql b/infra/postgres/141_seed_center_groups.sql index a1fdc2726..f9e91ce18 100644 --- a/infra/postgres/141_seed_center_groups.sql +++ b/infra/postgres/141_seed_center_groups.sql @@ -1,6 +1,6 @@ -- 141_seed_center_groups.sql — the six departmental groups (Centers Phase B) -- --- Spec: ai-company-brain/specs/department_centers.md §1 + §3 Phase B; +-- Spec: project-docs/specs/department_centers.md §1 + §3 Phase B; -- groups_sessions_authority.md §1 (org_group is the primitive). -- -- Group slug = center slug, 1:1 — the naming rule in department_centers.md §1: diff --git a/infra/postgres/142_agent_skill_settings.sql b/infra/postgres/142_agent_skill_settings.sql index c48cfa4d1..035283d17 100644 --- a/infra/postgres/142_agent_skill_settings.sql +++ b/infra/postgres/142_agent_skill_settings.sql @@ -1,6 +1,6 @@ -- 142_agent_skill_settings.sql — per-agent skill-family toggles (WS-23 S2) -- --- Spec: ai-company-brain/specs/skills_registry.md §3. One row = one explicit +-- Spec: project-docs/specs/skills_registry.md §3. One row = one explicit -- admin decision about one skill family for one agent. The three rules the -- enforcement seam (orchestrator/_tool_injection.py::_resolve_injected_scope) -- lives by: diff --git a/infra/postgres/143_access_request.sql b/infra/postgres/143_access_request.sql index b3dae3877..16dc3c0ac 100644 --- a/infra/postgres/143_access_request.sql +++ b/infra/postgres/143_access_request.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 143_access_request.sql — the sign-in queue (WS-24 / N6a) -- ============================================================================ --- Spec: ai-company-brain/specs/colleague_onboarding.md §6. +-- Spec: project-docs/specs/colleague_onboarding.md §6. -- -- Before this table, an authenticated stranger who reached the app produced -- exactly one artefact: a `access_unprovisioned_signin` warning in journald. diff --git a/infra/postgres/144_crm.sql b/infra/postgres/144_crm.sql index 2e2ba2d4c..e60c7afe1 100644 --- a/infra/postgres/144_crm.sql +++ b/infra/postgres/144_crm.sql @@ -2,7 +2,7 @@ -- 144_crm.sql — the native CRM spine: organizations, contacts, leads, deals, -- statuses-as-data, one activity timeline, a status-change log. -- --- What: spec ai-company-brain/specs/crm_app.md §3.1–§3.10 (WS-26a). Four +-- What: spec project-docs/specs/crm_app.md §3.1–§3.10 (WS-26a). Four -- entities on Frappe's lead → convert → deal+contact+organization shape, -- trycompai's single activity spine + `source` provenance + the denormalized -- `last_activity_at` column every write bumps. diff --git a/infra/postgres/145_crm_zoho_sync.sql b/infra/postgres/145_crm_zoho_sync.sql index 93635f557..12ca41945 100644 --- a/infra/postgres/145_crm_zoho_sync.sql +++ b/infra/postgres/145_crm_zoho_sync.sql @@ -2,7 +2,7 @@ -- 145_crm_zoho_sync.sql — the schema the two-way Zoho sync needs: per-row -- dirty tracking, delete tombstones, pull cursors. -- --- What: spec ai-company-brain/specs/crm_app.md §7.1 (WS-26b done-when 3). +-- What: spec project-docs/specs/crm_app.md §7.1 (WS-26b done-when 3). -- Three additions, no changes to anything 144 created: -- 1. `zoho_dirty` + `zoho_synced_at` on the FOUR record tables. A native -- write sets `zoho_dirty`; the push phase clears it and stamps diff --git a/infra/postgres/146_projects.sql b/infra/postgres/146_projects.sql index 227755a76..1b5675798 100644 --- a/infra/postgres/146_projects.sql +++ b/infra/postgres/146_projects.sql @@ -3,7 +3,7 @@ -- projects, subprojects, tasks, subtasks, grants, -- statuses-as-data, one activity timeline, per-view order. -- --- What: spec ai-company-brain/specs/project_management_app.md §3.1–§3.10 +-- What: spec project-docs/specs/project_management_app.md §3.1–§3.10 -- (WS-27a). Paca's shape — the whole hierarchy is TWO self-referencing -- foreign keys (pm_projects.parent_project_id, pm_tasks.parent_task_id) and -- task types are rows, not levels. There is deliberately no department table, diff --git a/infra/postgres/147_projects_personal.sql b/infra/postgres/147_projects_personal.sql index 91fb08f71..9df6bbbca 100644 --- a/infra/postgres/147_projects_personal.sql +++ b/infra/postgres/147_projects_personal.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 147_projects_personal.sql — the personal lens, on the SAME task store. -- --- What: spec ai-company-brain/specs/project_management_app.md §3.11–§3.12 and +-- What: spec project-docs/specs/project_management_app.md §3.11–§3.12 and -- §6.1 (WS-27e). Two additions to migration 146's spine: -- * pm_projects.personal_owner — a project that belongs to one member -- * pm_task_personal — that member's GTD overlay on a task diff --git a/infra/postgres/148_people_key_shape.sql b/infra/postgres/148_people_key_shape.sql index 86fa78821..7c2c8f5dd 100644 --- a/infra/postgres/148_people_key_shape.sql +++ b/infra/postgres/148_people_key_shape.sql @@ -11,7 +11,7 @@ -- same address — so an email join could silently attribute one person's -- capacity to another. The People Center is about to make this table the -- assignment source, so the shape is fixed before anything relies on it. --- Spec: ai-company-brain/specs/people_center_app.md §2, §5 (P-1/P-2), §7. +-- Spec: project-docs/specs/people_center_app.md §2, §5 (P-1/P-2), §7. -- Depends on: 49_gtd_people.sql, 74_gtd_people_editable_and_resumes.sql. -- -- ⚠️ NOTHING HERE MAY BLOCK A DEPLOY. `apply_migrations.sh` runs under diff --git a/infra/postgres/149_people.sql b/infra/postgres/149_people.sql index 64c27a276..395a375a9 100644 --- a/infra/postgres/149_people.sql +++ b/infra/postgres/149_people.sql @@ -7,7 +7,7 @@ -- `/tasks/people` API, because the People Center is a different audience: -- a manager who needs the org chart and the assignee picker should not -- have to be given the personal GTD task manager to get them. --- Spec: ai-company-brain/specs/people_center_app.md §6. +-- Spec: project-docs/specs/people_center_app.md §6. -- Registration is FIVE places and this is one of them — the others are -- `acb_auth.permissions.FEATURES`, `nav.ts`, `access.ts` and -- `centers.ts`; a both-ways test fails if this row and FEATURES disagree. diff --git a/infra/postgres/150_projects_attachments.sql b/infra/postgres/150_projects_attachments.sql index fe04cf0f3..4d96ba657 100644 --- a/infra/postgres/150_projects_attachments.sql +++ b/infra/postgres/150_projects_attachments.sql @@ -6,7 +6,7 @@ -- Why: spec §11.2 item 1, the first ClickUp-parity gap: "a task with a photo -- of the failed print is the normal case in a hardware company, and today -- there is nowhere to put it." --- Spec: ai-company-brain/specs/project_management_app.md §11. +-- Spec: project-docs/specs/project_management_app.md §11. -- Depends on: 52_gtd_attachments.sql (the file registry), 146_projects.sql. -- -- **One file store, not two.** Paca's shape is a central `files` registry plus diff --git a/infra/postgres/152_projects_notifications.sql b/infra/postgres/152_projects_notifications.sql index f82480713..959e23797 100644 --- a/infra/postgres/152_projects_notifications.sql +++ b/infra/postgres/152_projects_notifications.sql @@ -5,7 +5,7 @@ -- Why: spec §11.2 item 2, the second ClickUp-parity gap: "assignment is -- silent. A tool nobody hears from is a tool nobody opens, and the whole -- assignment→agent chain assumes somebody noticed." --- Spec: ai-company-brain/specs/project_management_app.md §11. +-- Spec: project-docs/specs/project_management_app.md §11. -- Depends on: 146_projects.sql (pm_tasks, pm_activities), 150 (which last -- edited the activity CHECK). -- diff --git a/infra/postgres/155_projects_custom_fields.sql b/infra/postgres/155_projects_custom_fields.sql index d1f0e3191..764deddeb 100644 --- a/infra/postgres/155_projects_custom_fields.sql +++ b/infra/postgres/155_projects_custom_fields.sql @@ -1,6 +1,6 @@ -- 155_projects_custom_fields.sql — WS-27l -- --- Spec: ai-company-brain/specs/project_management_app.md §11.2 item 4, §11.9. +-- Spec: project-docs/specs/project_management_app.md §11.2 item 4, §11.9. -- -- ClickUp's signature feature, and the fourth row of the parity backlog. The -- shape is the one recorded as the additive path back in §5's non-goals: diff --git a/infra/postgres/156_projects_tags.sql b/infra/postgres/156_projects_tags.sql index c989aeea2..1adc47501 100644 --- a/infra/postgres/156_projects_tags.sql +++ b/infra/postgres/156_projects_tags.sql @@ -1,6 +1,6 @@ -- 156_projects_tags.sql — WS-27m -- --- Spec: ai-company-brain/specs/project_management_app.md §11.2 item 5, §11.10. +-- Spec: project-docs/specs/project_management_app.md §11.2 item 5, §11.10. -- -- The fifth row of the parity backlog, and the one the research notes left -- open on purpose. `paca_pm_research_2026-08.md` row 13 REFUSED Paca's model — diff --git a/infra/postgres/157_org_first_party.sql b/infra/postgres/157_org_first_party.sql index 931fa6709..83b63f680 100644 --- a/infra/postgres/157_org_first_party.sql +++ b/infra/postgres/157_org_first_party.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 157_org_first_party.sql — MT-0b: self-mutation is first-party-only -- ============================================================================ --- Spec: ai-company-brain/specs/saas_multitenancy.md §6.2 / MT-0b · board WS-29. +-- Spec: project-docs/specs/saas_multitenancy.md §6.2 / MT-0b · board WS-29. -- -- Root AGENTS.md non-negotiable 3 has said this since it was written: -- diff --git a/infra/postgres/158_per_org_credentials.sql b/infra/postgres/158_per_org_credentials.sql index 096aad804..9dd77620a 100644 --- a/infra/postgres/158_per_org_credentials.sql +++ b/infra/postgres/158_per_org_credentials.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 158_per_org_credentials.sql — MT-0d: credentials stop being deployment-wide -- ============================================================================ --- Spec: ai-company-brain/specs/saas_multitenancy.md §6.3 / MT-0d · board WS-29. +-- Spec: project-docs/specs/saas_multitenancy.md §6.3 / MT-0d · board WS-29. -- -- `provider_keys` is `provider TEXT PRIMARY KEY` (08_provider_keys.sql:6-7) — -- one key per provider for the whole box. `mcp_servers`, `plugins` and diff --git a/infra/postgres/159_control_plane.sql b/infra/postgres/159_control_plane.sql index 691c993fd..768d177e6 100644 --- a/infra/postgres/159_control_plane.sql +++ b/infra/postgres/159_control_plane.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 159_control_plane.sql — MT-1a: the tenant catalog -- ============================================================================ --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.5 / §0.9.5 / MT-1a · +-- Spec: project-docs/specs/saas_multitenancy.md §1.5 / §0.9.5 / MT-1a · -- shapes in saas_multitenancy_implementation.md §3 · board WS-29 · D15. -- -- Three things a pooled deployment needs that a single-tenant one never did: diff --git a/infra/postgres/160_projects_recurrence.sql b/infra/postgres/160_projects_recurrence.sql index 948abb609..f05d92a74 100644 --- a/infra/postgres/160_projects_recurrence.sql +++ b/infra/postgres/160_projects_recurrence.sql @@ -1,6 +1,6 @@ -- 160_projects_recurrence.sql — WS-27o -- --- Spec: ai-company-brain/specs/project_management_app.md §11.2 item 7, §11.13. +-- Spec: project-docs/specs/project_management_app.md §11.2 item 7, §11.13. -- -- "Every operations cadence is recurring. Without it those live in someone's -- head or in ClickUp." diff --git a/infra/postgres/161_projects_tenancy.sql b/infra/postgres/161_projects_tenancy.sql index 48c34a87e..1967ac3a9 100644 --- a/infra/postgres/161_projects_tenancy.sql +++ b/infra/postgres/161_projects_tenancy.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- 161_projects_tenancy.sql — the tenant key on all 17 `pm_*` tables (WS-29a). -- --- Spec: ai-company-brain/specs/multi_tenancy.md §3 (D-MT-1, D-MT-3) and §5. +-- Spec: project-docs/specs/multi_tenancy.md §3 (D-MT-1, D-MT-3) and §5. -- -- WHY NOW, AND ONLY NOW. §2 is blunt about it: `POST /projects/import/clickup` -- is the next thing WS-27 wants, and it writes hundreds of rows into these 17 diff --git a/infra/postgres/163_crm_auto_lead_cursor.sql b/infra/postgres/163_crm_auto_lead_cursor.sql index 0f4b811f3..c7f817fa3 100644 --- a/infra/postgres/163_crm_auto_lead_cursor.sql +++ b/infra/postgres/163_crm_auto_lead_cursor.sql @@ -95,7 +95,7 @@ -- migration 148 had to defuse. The accepted race is recorded in -- `crm_app.md` §9 WS-26d-autolead. Do not "fix" it with that index. -- --- Spec: ai-company-brain/specs/crm_app.md §9 WS-26d-autolead (the cursor +-- Spec: project-docs/specs/crm_app.md §9 WS-26d-autolead (the cursor -- paragraph) · D-CRM-9. -- Depends on: 17_email_accounts.sql (email_accounts, the FK target) and -- 144_crm.sql (the CRM spine this cursor guards writes into). diff --git a/infra/postgres/48_task_manager_gtd.sql b/infra/postgres/48_task_manager_gtd.sql index 4b44da23d..65b657319 100644 --- a/infra/postgres/48_task_manager_gtd.sql +++ b/infra/postgres/48_task_manager_gtd.sql @@ -1,6 +1,6 @@ -- 48_task_manager_gtd.sql — canonical GTD store for the Task Manager app. -- --- What: the seven task-manager tables from ai-company-brain/specs/task_manager_app.md §4 — +-- What: the seven task-manager tables from project-docs/specs/task_manager_app.md §4 — -- task_accounts (connected PM-tool workspaces, multi-account/multi-provider, like -- email_accounts), gtd_contexts, gtd_horizons, gtd_projects, gtd_items, gtd_waiting, -- gtd_reviews. diff --git a/infra/postgres/95_note_taker.sql b/infra/postgres/95_note_taker.sql index fb1406ee0..d1480dbc7 100644 --- a/infra/postgres/95_note_taker.sql +++ b/infra/postgres/95_note_taker.sql @@ -1,5 +1,5 @@ -- 94_note_taker.sql — AI Note Taker app: recordings, transcript segments, --- notes, and pipeline runs (spec: ai-company-brain/specs/note_taker_app.md §3.6). +-- notes, and pipeline runs (spec: project-docs/specs/note_taker_app.md §3.6). -- -- What: activates the dormant meeting/action_item tables from 01_schema.sql as -- the store for the /notes app. `meeting` gains title/status/summary fields diff --git a/infra/postgres/generated/01_add_columns.sql b/infra/postgres/generated/01_add_columns.sql index 3d234cfe3..603039b7d 100644 --- a/infra/postgres/generated/01_add_columns.sql +++ b/infra/postgres/generated/01_add_columns.sql @@ -2,7 +2,7 @@ -- MT-1b · phase 1/4 add_columns — GENERATED, DO NOT EDIT BY HAND -- ============================================================================ -- Regenerate with: uv run python scripts/gen_tenant_migration.py --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 +-- Spec: project-docs/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 -- -- Nullable ADD COLUMN. No table scan, no lock of consequence. Safe to apply on a live system. -- diff --git a/infra/postgres/generated/02_backfill.sql b/infra/postgres/generated/02_backfill.sql index 43f5adca8..bf6e72267 100644 --- a/infra/postgres/generated/02_backfill.sql +++ b/infra/postgres/generated/02_backfill.sql @@ -2,7 +2,7 @@ -- MT-1b · phase 2/4 backfill — GENERATED, DO NOT EDIT BY HAND -- ============================================================================ -- Regenerate with: uv run python scripts/gen_tenant_migration.py --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 +-- Spec: project-docs/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 -- -- Batched UPDATE. Re-runnable and interruptible — each statement is idempotent, so a run that aborts can simply be run again. This is the slow phase; expect it to be the long pole on any table with real volume. -- diff --git a/infra/postgres/generated/03_constraints.sql b/infra/postgres/generated/03_constraints.sql index ad7301f12..f3d67b8a4 100644 --- a/infra/postgres/generated/03_constraints.sql +++ b/infra/postgres/generated/03_constraints.sql @@ -2,7 +2,7 @@ -- MT-1b · phase 3/4 constraints — GENERATED, DO NOT EDIT BY HAND -- ============================================================================ -- Regenerate with: uv run python scripts/gen_tenant_migration.py --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 +-- Spec: project-docs/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 -- -- SET NOT NULL + FK + index. ⚠️ THIS IS THE ACCESS EXCLUSIVE PHASE — it scans each table. Apply in a window, table by table if necessary, and never behind a long-running transaction (see the generator docstring: that is the exact shape of the 14h44m outage). -- diff --git a/infra/postgres/generated/04_policies.sql b/infra/postgres/generated/04_policies.sql index 4861ecda4..5bb9ae375 100644 --- a/infra/postgres/generated/04_policies.sql +++ b/infra/postgres/generated/04_policies.sql @@ -2,7 +2,7 @@ -- MT-1b · phase 4/4 policies — GENERATED, DO NOT EDIT BY HAND -- ============================================================================ -- Regenerate with: uv run python scripts/gen_tenant_migration.py --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 +-- Spec: project-docs/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 -- -- ENABLE + FORCE ROW LEVEL SECURITY + the policy. Instant — no scan. ⚠️ AND IT IS A CLIFF: the moment this applies, any connection that has not bound app.tenant_id reads ZERO ROWS. That is the fail-closed property working (§0.1). MT-1c must be deployed AND VERIFIED first, or the product goes dark. -- diff --git a/learning-resources/04-cicd.md b/learning-resources/04-cicd.md index bf3327a8d..c3ea00b31 100644 --- a/learning-resources/04-cicd.md +++ b/learning-resources/04-cicd.md @@ -26,7 +26,7 @@ Four jobs, chained with `needs:` so each waits for the previous. The trigger: on: push: branches: [main] - paths-ignore: ["**.md", "ai-company-brain/**", "skills/**", "workbench/e2e/**"] + paths-ignore: ["**.md", "project-docs/**", "skills/**", "workbench/e2e/**"] workflow_dispatch: # manual run, with an emergency skip_tests toggle ``` diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 788376d5a..014676f1b 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -8,9 +8,9 @@ Reusable Python packages shared across all CommandCenter services. - acb_llm/ -- LiteLLM integration, unified credential store (LLM + integration keys), model routing - acb_memory/ -- Mem0 and Graphiti memory providers. `compartments.py` owns the scope vocabulary (`scope_key`/`scope_kind`: a bare email, `prefs:`, `room:`, `agent:`, `org:global`) and `resolve_clearance` — WHICH compartments a run may read and the ONE it may write (spec: docs/multiplayer/memory-clearance.md §3). Solo resolves to exactly the three scopes and the write target it always had; a shared run swaps the actor's private compartment for the room's and keeps their prefs. The point is that an excluded compartment's scope key is never passed to `search()` — a boundary, not a request in a system prompt. Dependency-free on purpose: the CALLER decides `shared` (the gateway has `resolve_room_access`), which keeps acb_memory below acb_auth and the gateway in the import graph. `session_cache` keys on the clearance fingerprint as well as the thread — without it a thread cached while solo keeps serving the owner's private block after it is shared. - acb_graph/ -- Postgres entity graph (SQLAlchemy sessions) -- acb_common/ -- Shared settings, logging, activity/cost feed, utilities. **`db.py` is the ONE async engine and pool for a process (BO-10)** — `get_db()` / `get_session_factory()` / `get_engine()`, cached at module level, SQLAlchemy's async stack imported INSIDE the functions so importing `acb_common` costs a process that never opens a connection nothing (`acb_common/__init__.py` deliberately does not import it). It lives here rather than in the gateway because `acb_auth.access` resolves permissions from Postgres on the request path *inside* the gateway process and cannot import `gateway` — a gateway-owned seam could never get that process below two pools. Every gateway route package re-exports from it under whatever `get_db`/`_get_db` name it has always used; `gateway/db.py` is a re-export too. The engine is **never disposed**: the pool's lifetime is the process's, and a dispose seam is a way to close connections other handlers are still using. Sized by `db_pool_size`/`db_max_overflow` (10+20), a deployment-wide number — stock Postgres allows 100 and Langfuse/LiteLLM/ingestion share the server. Adding a second `create_async_engine` anywhere under `apps/` or `packages/` fails `tests/unit/test_db_engine_seam.py`; the allowlist there is `acb_graph`'s sync engine and `email_ingestion`'s per-run engines, each with its reason. `_log.py` owns the run-correlation stamp: `_RUN_CONTEXT_KEYS` = `(run_id, thread_id, agent, user, source, instance)` — decision D1's attribution four-tuple plus the thread. Binds are **additive and non-empty-only**, so a caller may top up one field later (the executor resolves `instance` only after the agent config loads) and the shared partition `''` binds nothing rather than an empty value. `activity.py::_INHERIT` copies the same keys onto any event whose emitter omits them — which is why a model call inside a run is attributed with no change at its call site. Extend those two tuples together or attribution silently half-lands (`tests/unit/test_observability.py::test_inherit_and_run_context_keys_match` drift-fails if you don't). `instance` names the partition of the run that RESOLVED it — inheritance means a delegated sub-run carries its caller's key while writing blobs under the shared partition, so the stamp is not a join key onto `agent_blob` without knowing which run emitted it. The presence key (`cc:activity:live:{run_id}`) is written once from the `phase="start"` body, so a field resolved after start reaches `/observability/active` only via `activity.refresh_run_presence`. Spec: ai-company-brain/specs/observability_e2.md §7 +- acb_common/ -- Shared settings, logging, activity/cost feed, utilities. **`db.py` is the ONE async engine and pool for a process (BO-10)** — `get_db()` / `get_session_factory()` / `get_engine()`, cached at module level, SQLAlchemy's async stack imported INSIDE the functions so importing `acb_common` costs a process that never opens a connection nothing (`acb_common/__init__.py` deliberately does not import it). It lives here rather than in the gateway because `acb_auth.access` resolves permissions from Postgres on the request path *inside* the gateway process and cannot import `gateway` — a gateway-owned seam could never get that process below two pools. Every gateway route package re-exports from it under whatever `get_db`/`_get_db` name it has always used; `gateway/db.py` is a re-export too. The engine is **never disposed**: the pool's lifetime is the process's, and a dispose seam is a way to close connections other handlers are still using. Sized by `db_pool_size`/`db_max_overflow` (10+20), a deployment-wide number — stock Postgres allows 100 and Langfuse/LiteLLM/ingestion share the server. Adding a second `create_async_engine` anywhere under `apps/` or `packages/` fails `tests/unit/test_db_engine_seam.py`; the allowlist there is `acb_graph`'s sync engine and `email_ingestion`'s per-run engines, each with its reason. `_log.py` owns the run-correlation stamp: `_RUN_CONTEXT_KEYS` = `(run_id, thread_id, agent, user, source, instance)` — decision D1's attribution four-tuple plus the thread. Binds are **additive and non-empty-only**, so a caller may top up one field later (the executor resolves `instance` only after the agent config loads) and the shared partition `''` binds nothing rather than an empty value. `activity.py::_INHERIT` copies the same keys onto any event whose emitter omits them — which is why a model call inside a run is attributed with no change at its call site. Extend those two tuples together or attribution silently half-lands (`tests/unit/test_observability.py::test_inherit_and_run_context_keys_match` drift-fails if you don't). `instance` names the partition of the run that RESOLVED it — inheritance means a delegated sub-run carries its caller's key while writing blobs under the shared partition, so the stamp is not a join key onto `agent_blob` without knowing which run emitted it. The presence key (`cc:activity:live:{run_id}`) is written once from the `phase="start"` body, so a field resolved after start reaches `/observability/active` only via `activity.refresh_run_presence`. Spec: project-docs/specs/observability_e2.md §7 - acb_audit/ -- Audit event recording. `record()` logs always and DB-writes best-effort (`_persist`, which swallows its own errors — `routes/admin/members.py::purge_member` reasons explicitly about what that does and does not guarantee). The write goes through `acb_graph.get_session()`, a **sync** engine, so on an event loop `record()` hands it to `asyncio.to_thread` and returns immediately (BO-10); called from sync code, with no running loop, it still writes inline, which is what the orchestrator's agents expect. The cost is that an async caller's row is not written by the time the call returns — `await drain()` if you need it now, and the gateway lifespan awaits `drain()` last on shutdown so exiting cannot cancel an in-flight row. Pinned by `tests/unit/test_audit_non_blocking.py`. -- acb_auth/ -- Authentication, roles, and org access control. Two guard styles coexist: the original coarse `require_role(UserRole.EXECUTIVE)` (unchanged) and `require_permission("feature:whatsapp")`, backed by DB roles + per-user allow/deny overrides (`permissions.py` is pure and testable; `access.py` does the I/O with a 60s cache). `require_authenticated(public=...)` is the app-wide default-deny guard (BO-2 #1) — authentication, not authorization. **`permissions.FEATURES` is the feature vocabulary, not `feature_catalog`**: `allowed_features()` iterates that tuple and `/auth/me` returns exactly it, so a slug seeded in SQL but absent from the tuple is unreachable for every principal *including an owner holding `*`* (the wildcard is only matched against those literals). Seeding a feature_catalog row therefore always means editing `FEATURES` too — the Centers case is pinned by `tests/unit/test_org_access_control.py::test_every_center_has_a_feature_slug` (anchored on a literal, not derived from another tuple, so shortening a source cannot make it vacuous) and `::test_centers_registry_matches_the_feature_vocabulary` (parses `workbench/control_plane/src/lib/centers.ts`, the registry the UI actually renders, and pins it to `FEATURES` in both directions). Full registration checklist: `ai-company-brain/specs/department_centers.md` §2. Two DISTINCT secrets: `GATEWAY_INTERNAL_TOKEN` is service identity (grants everything, never handed to agents) and `LITELLM_MASTER_KEY` is the /v1 API key agents hold, checked only by `require_llm_api_auth`. **The separation only exists once `GATEWAY_INTERNAL_TOKEN` is provisioned** — unset, `_get_internal_token()` falls back to the LLM key (that precedence ladder lives in exactly one place, `_resolve_internal_token() -> (token, is_fallback)`; `_get_internal_token` and `_internal_token_is_llm_fallback` are both readers of it, because a second copy that drifted would start refusing a legitimately provisioned token), and while that holds a Bearer proves nothing more than "holds the key every agent holds", so it can assert any colleague's `X-User-Email` (branch 1a) or act as the platform (1b). `GATEWAY_REFUSE_LLM_KEY_IDENTITY=1` refuses the fallback **for identity only** (`get_current_user`; the two Bearer guards keep accepting it, so a mis-provisioned box degrades to "nobody is anybody" rather than to "Bearer auth disabled"). It defaults OFF and flipping it is OWNER-GATE: the Next.js BFF mirrors the same fallback, so flipping before the token is set in BOTH `.env` and the workbench `.env.local` 401s every member. `X-User-Email`/`X-User-Role` are internal-only headers that nothing in this package can distinguish from forged ones — the boundary is the reverse proxy stripping them at the public vhost. `resolve_access(..., record_request=False)` files an unprovisioned email into `access_request` (migration 143, spec colleague_onboarding.md §6) so the owner can see who is locked out instead of the refusal dying in journald. ⚠️ **The default must stay False and `deps._with_resolved_access` must stay its only caller passing True**: `resolve_access` is NOT sign-in-only — `routes/rooms.py`, `resolve_session_access`, `routes/chat.py` and `orchestrator/executor.py` all call it over emails that are not somebody knocking, and filing those would queue people who never tried to sign in. **Do not read that list as the safety argument** — it is illustrative and has already been wrong twice. The property is "the default is `False`", enforced by `tests/unit/test_signin_requests.py`, which reads the tree and fails if a second `record_request=True` appears anywhere; a fifth caller needs no edit here. The write is best-effort (a failure changes neither the refusal nor the log line) and its volume is bounded by the same 60s cache, not the request rate. `allowed_email_domain()` / `is_company_email()` are the one **live** reader of `ALLOWED_EMAIL_DOMAIN` (`acb_common/settings.py` declares an `allowed_email_domain` field too, with the same default and zero consumers — collapse them if anything ever reads it) — a **label, not a boundary**: branch 1a only LOGS an off-domain identity, and the Entra tenant pin does not exclude these addresses either (a B2B guest IS a directory member), so the sign-in queue marks such a row `is_external` for the admin rather than pretending it cannot happen. Spec: ai-company-brain/specs/org_access_control.md +- acb_auth/ -- Authentication, roles, and org access control. Two guard styles coexist: the original coarse `require_role(UserRole.EXECUTIVE)` (unchanged) and `require_permission("feature:whatsapp")`, backed by DB roles + per-user allow/deny overrides (`permissions.py` is pure and testable; `access.py` does the I/O with a 60s cache). `require_authenticated(public=...)` is the app-wide default-deny guard (BO-2 #1) — authentication, not authorization. **`permissions.FEATURES` is the feature vocabulary, not `feature_catalog`**: `allowed_features()` iterates that tuple and `/auth/me` returns exactly it, so a slug seeded in SQL but absent from the tuple is unreachable for every principal *including an owner holding `*`* (the wildcard is only matched against those literals). Seeding a feature_catalog row therefore always means editing `FEATURES` too — the Centers case is pinned by `tests/unit/test_org_access_control.py::test_every_center_has_a_feature_slug` (anchored on a literal, not derived from another tuple, so shortening a source cannot make it vacuous) and `::test_centers_registry_matches_the_feature_vocabulary` (parses `workbench/control_plane/src/lib/centers.ts`, the registry the UI actually renders, and pins it to `FEATURES` in both directions). Full registration checklist: `project-docs/specs/department_centers.md` §2. Two DISTINCT secrets: `GATEWAY_INTERNAL_TOKEN` is service identity (grants everything, never handed to agents) and `LITELLM_MASTER_KEY` is the /v1 API key agents hold, checked only by `require_llm_api_auth`. **The separation only exists once `GATEWAY_INTERNAL_TOKEN` is provisioned** — unset, `_get_internal_token()` falls back to the LLM key (that precedence ladder lives in exactly one place, `_resolve_internal_token() -> (token, is_fallback)`; `_get_internal_token` and `_internal_token_is_llm_fallback` are both readers of it, because a second copy that drifted would start refusing a legitimately provisioned token), and while that holds a Bearer proves nothing more than "holds the key every agent holds", so it can assert any colleague's `X-User-Email` (branch 1a) or act as the platform (1b). `GATEWAY_REFUSE_LLM_KEY_IDENTITY=1` refuses the fallback **for identity only** (`get_current_user`; the two Bearer guards keep accepting it, so a mis-provisioned box degrades to "nobody is anybody" rather than to "Bearer auth disabled"). It defaults OFF and flipping it is OWNER-GATE: the Next.js BFF mirrors the same fallback, so flipping before the token is set in BOTH `.env` and the workbench `.env.local` 401s every member. `X-User-Email`/`X-User-Role` are internal-only headers that nothing in this package can distinguish from forged ones — the boundary is the reverse proxy stripping them at the public vhost. `resolve_access(..., record_request=False)` files an unprovisioned email into `access_request` (migration 143, spec colleague_onboarding.md §6) so the owner can see who is locked out instead of the refusal dying in journald. ⚠️ **The default must stay False and `deps._with_resolved_access` must stay its only caller passing True**: `resolve_access` is NOT sign-in-only — `routes/rooms.py`, `resolve_session_access`, `routes/chat.py` and `orchestrator/executor.py` all call it over emails that are not somebody knocking, and filing those would queue people who never tried to sign in. **Do not read that list as the safety argument** — it is illustrative and has already been wrong twice. The property is "the default is `False`", enforced by `tests/unit/test_signin_requests.py`, which reads the tree and fails if a second `record_request=True` appears anywhere; a fifth caller needs no edit here. The write is best-effort (a failure changes neither the refusal nor the log line) and its volume is bounded by the same 60s cache, not the request rate. `allowed_email_domain()` / `is_company_email()` are the one **live** reader of `ALLOWED_EMAIL_DOMAIN` (`acb_common/settings.py` declares an `allowed_email_domain` field too, with the same default and zero consumers — collapse them if anything ever reads it) — a **label, not a boundary**: branch 1a only LOGS an off-domain identity, and the Entra tenant pin does not exclude these addresses either (a B2B guest IS a directory member), so the sign-in queue marks such a row `is_external` for the admin rather than pretending it cannot happen. Spec: project-docs/specs/org_access_control.md ## Conventions - Each package has its own pyproject.toml diff --git a/packages/acb_auth/acb_auth/__init__.py b/packages/acb_auth/acb_auth/__init__.py index 985fa38e7..2fdb4ced7 100644 --- a/packages/acb_auth/acb_auth/__init__.py +++ b/packages/acb_auth/acb_auth/__init__.py @@ -5,7 +5,7 @@ * ``require_role(UserRole.EXECUTIVE)`` — the original coarse gate. Unchanged. * ``require_permission("feature:whatsapp")`` — org access control: DB-backed roles plus per-user allow/deny overrides. See - ``ai-company-brain/specs/org_access_control.md``. + ``project-docs/specs/org_access_control.md``. """ from acb_auth.access import ensure_owner_bootstrap from acb_auth.access import invalidate as invalidate_access diff --git a/packages/acb_auth/acb_auth/access.py b/packages/acb_auth/acb_auth/access.py index 4da0b503c..8351178ee 100644 --- a/packages/acb_auth/acb_auth/access.py +++ b/packages/acb_auth/acb_auth/access.py @@ -5,7 +5,7 @@ Pure matching logic lives in :mod:`acb_auth.permissions`; this module is the I/O half. -Spec: ``ai-company-brain/specs/org_access_control.md`` §5. +Spec: ``project-docs/specs/org_access_control.md`` §5. Why resolve per request instead of stuffing permissions in the session JWT: a JWT outlives an access change. "I revoked WhatsApp an hour ago and they still @@ -213,7 +213,7 @@ def _degraded(legacy_role: str | None) -> EffectiveAccess: async def _record_signin_request(email: str, display_name: str = "") -> None: """File an unprovisioned sign-in in ``access_request``. Best-effort. - Spec: ``ai-company-brain/specs/colleague_onboarding.md`` §6 (N6a). + Spec: ``project-docs/specs/colleague_onboarding.md`` §6 (N6a). **Never raises, never changes the caller's answer.** The queue is a convenience for the owner; the refusal above it is the security answer, and diff --git a/packages/acb_auth/acb_auth/permissions.py b/packages/acb_auth/acb_auth/permissions.py index aa9d57cf6..943ed0f8c 100644 --- a/packages/acb_auth/acb_auth/permissions.py +++ b/packages/acb_auth/acb_auth/permissions.py @@ -6,7 +6,7 @@ DB-backed side lives in :mod:`acb_auth.access`; the request-time side in :mod:`acb_auth.deps`. -Spec: ``ai-company-brain/specs/org_access_control.md`` §3.3–§3.4. +Spec: ``project-docs/specs/org_access_control.md`` §3.3–§3.4. Grammar ------- diff --git a/packages/acb_auth/acb_auth/roles.py b/packages/acb_auth/acb_auth/roles.py index 8c66c9425..09e56a544 100644 --- a/packages/acb_auth/acb_auth/roles.py +++ b/packages/acb_auth/acb_auth/roles.py @@ -10,7 +10,7 @@ **Effective access** (``UserContext.access``) — the DB-backed permission set resolved from the member's roles and per-user overrides. New code guards on this via ``require_permission()``. See -``ai-company-brain/specs/org_access_control.md``. +``project-docs/specs/org_access_control.md``. The two coexist by design. `app_user.role` is dual-written and the migration maps executive→admin / employee→member, so a route can move from one to the diff --git a/packages/acb_common/acb_common/placement.py b/packages/acb_common/acb_common/placement.py index 35aecb524..dab1c452b 100644 --- a/packages/acb_common/acb_common/placement.py +++ b/packages/acb_common/acb_common/placement.py @@ -1,6 +1,6 @@ """Tenant placement — which data plane serves an organization (MT-1a). -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §1.5 · D15 · board WS-29. +Spec: ``project-docs/specs/saas_multitenancy.md`` §1.5 · D15 · board WS-29. Under D15 the tenant boundary is a **row** (`organization_id` + Postgres RLS) and the *deployment* is demoted to a **placement**: a region and a tier, not an diff --git a/packages/acb_common/acb_common/settings.py b/packages/acb_common/acb_common/settings.py index 5a2d1d830..30ae5b34d 100644 --- a/packages/acb_common/acb_common/settings.py +++ b/packages/acb_common/acb_common/settings.py @@ -408,7 +408,7 @@ def llm_api_key(self) -> str: Callers that hit gateway *business* APIs (``/tasks``, ``/email``, ``/whatsapp``, workspace upload) still need ``gateway_internal_token`` — until they act on behalf of a member - instead. See ai-company-brain/specs/org_access_control.md §8b. + instead. See project-docs/specs/org_access_control.md §8b. """ return (self.litellm_master_key or "").strip() or "sk-local" diff --git a/packages/acb_skills/AGENTS.md b/packages/acb_skills/AGENTS.md index 5d036a9a6..e8be1ba6b 100644 --- a/packages/acb_skills/AGENTS.md +++ b/packages/acb_skills/AGENTS.md @@ -19,7 +19,7 @@ clone cache. 3. agent_tools.py -- call_agent, call_agents_parallel, call_agent_background for cross-agent delegation. 4. web_tools.py -- web_search (DuckDuckGo) and fetch_page (Jina Reader). Zero credential. 5. write_artifact.py -- write_artifact tool for surfacing created files in the UI. -5a. skill_families.py -- WS-23 skill-family registry (spec: ai-company-brain/specs/skills_registry.md). +5a. skill_families.py -- WS-23 skill-family registry (spec: project-docs/specs/skills_registry.md). `SKILL_FAMILIES` maps family slug -> {label, description, tool names} and must cover EVERY tool `orchestrator._tool_injection` injects, each in exactly ONE family; the `core` family must equal `_CORE_STANDARD_TOOL_NAMES` verbatim diff --git a/packages/acb_skills/acb_skills/agent_paths.py b/packages/acb_skills/acb_skills/agent_paths.py index 53f0c9421..c5331a2d4 100644 --- a/packages/acb_skills/acb_skills/agent_paths.py +++ b/packages/acb_skills/acb_skills/agent_paths.py @@ -1,7 +1,7 @@ """Where an agent's code lives, and where each tenant's state lives. -Spec: ai-company-brain/specs/agent_architecture.md §2 - ai-company-brain/specs/memory_architecture.md §5.3 +Spec: project-docs/specs/agent_architecture.md §2 + project-docs/specs/memory_architecture.md §5.3 THE PROBLEM THIS SOLVES ----------------------- diff --git a/packages/acb_skills/acb_skills/integrations.py b/packages/acb_skills/acb_skills/integrations.py index a1e558e04..aa5408427 100644 --- a/packages/acb_skills/acb_skills/integrations.py +++ b/packages/acb_skills/acb_skills/integrations.py @@ -7,7 +7,7 @@ Agents and skills must only read credentials from ``state["integrations"]``. They must never call ``os.getenv()`` for secrets or import ``Settings`` -directly — see §5 of ``ai-company-brain/agent_repo_compatibility.md``. +directly — see §5 of ``project-docs/agent_repo_compatibility.md``. ---- diff --git a/packages/acb_skills/acb_skills/manifest.py b/packages/acb_skills/acb_skills/manifest.py index 3b238eefe..59d669809 100644 --- a/packages/acb_skills/acb_skills/manifest.py +++ b/packages/acb_skills/acb_skills/manifest.py @@ -1,6 +1,6 @@ """Agent manifest — the single declaration an agent is derived from. -Spec: ``ai-company-brain/specs/agent_architecture.md``. +Spec: ``project-docs/specs/agent_architecture.md``. Today an agent's behaviour is spread across ``config.json`` (tool scope, skills, integrations), ``agents.py`` (runtime, model, permission handler) and hardcoded diff --git a/packages/acb_stt/acb_stt/__init__.py b/packages/acb_stt/acb_stt/__init__.py index 06075f99b..c337a7dbc 100644 --- a/packages/acb_stt/acb_stt/__init__.py +++ b/packages/acb_stt/acb_stt/__init__.py @@ -6,7 +6,7 @@ client, no separate key resolution. Swap the ``tier-stt`` model (Groq/OpenAI whisper, Deepgram, or the future self-host faster-whisper endpoint) without touching app code. -Spec: ai-company-brain/specs/note_taker_app.md §3.4. +Spec: project-docs/specs/note_taker_app.md §3.4. """ from acb_stt.assemblyai_provider import AssemblyAISTT from acb_stt.base import SttProvider diff --git a/packages/acb_stt/acb_stt/assemblyai_provider.py b/packages/acb_stt/acb_stt/assemblyai_provider.py index d3d19e7ac..f3fffdd35 100644 --- a/packages/acb_stt/acb_stt/assemblyai_provider.py +++ b/packages/acb_stt/acb_stt/assemblyai_provider.py @@ -29,7 +29,7 @@ (see ``routes/notes/live.py::_live_model`` / ``ASSEMBLYAI_LIVE_MODEL``) — picking universal-2 in Settings -> Models does not make streaming use universal-2. -Spec: ai-company-brain/specs/note_taker_app.md §3.4. +Spec: project-docs/specs/note_taker_app.md §3.4. """ from __future__ import annotations diff --git a/ai-company-brain/AGENTS.md b/project-docs/AGENTS.md similarity index 100% rename from ai-company-brain/AGENTS.md rename to project-docs/AGENTS.md diff --git a/ai-company-brain/HANDOVER.md b/project-docs/HANDOVER.md similarity index 98% rename from ai-company-brain/HANDOVER.md rename to project-docs/HANDOVER.md index f27ac5e9a..993294fd5 100644 --- a/ai-company-brain/HANDOVER.md +++ b/project-docs/HANDOVER.md @@ -1,5 +1,10 @@ # Handover — branch `claude/paca-research-task-management-a1f6zd` +> ⚠️ **EXECUTED — HISTORICAL (2026-08-10 consolidation, D26).** No work dispatches from +> this document — this was PR #399's branch handover; the branch merged 2026-08-09 (its 157/158/159 landed as 160/161/162). The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Written 2026-08-08 for a coding agent with database access.** Everything here was built in > a sandbox with a *scratch* Postgres and **no access to production, no deploy, and no ability > to apply a migration to the real box**. That is the gap you are picking up. diff --git a/project-docs/INDEX.md b/project-docs/INDEX.md new file mode 100644 index 000000000..cc5f5c9bc --- /dev/null +++ b/project-docs/INDEX.md @@ -0,0 +1,98 @@ +# INDEX — what is active, what is not (the classification of record) + +**Created 2026-08-10 (D26, owner-directed consolidation).** This folder was +renamed from `ai-company-brain/` to **`project-docs/`** the same day — it holds +the project-management documentation: the work plan, the specifications for +every sub-app, and the overall architecture. *(The owner's suggested name +"Project Management documentation" was realized path-safe and short; "PM docs" +was avoided because the Projects app's own spec lives inside and the collision +would confuse exactly the agents this cleanup serves.)* + +**How an agent navigates:** start at **`work_plan.md` §2** (the dispatch +board — ordering, states, gates). The board row names the **owning spec**; build +only from owning specs listed **ACTIVE** here. Anything in DEFERRED or +HISTORICAL is banner-marked and dispatches nothing. `work_plan.md` §6 is the +owner-gate registry an agent must refuse by name. Decisions (D1–D26) live in +`work_plan.md` §3 and are never re-litigated in specs. + +**The two documentation roots:** `project-docs/` (this folder) = plan + +product/app specs + architecture. **`docs/`** (repo root) = engineering +reference tied to code (`docs/multiplayer/` — ⚠️ contains one ACTIVE owning +spec, listed below — `docs/workflow-editor/`, `docs/app-workshop/`, design +limitations). Do not add product specs to `docs/`. + +--- + +## ACTIVE — owning specs of live board rows (dispatch happens from these) + +| Spec | Board row(s) | +|---|---| +| `work_plan.md` | THE BOARD — §1 contract · §2 rows · §3 decisions · §4 owners · §6 gates | +| `../FOUNDATION_BUILDOUT_CHECKLIST.md` *(repo ROOT)* | WS-1 · WS-4 · WS-5 | +| `specs/saas_multitenancy.md` (+ `_implementation`, `_handover`) | WS-29 — hand the **handover** to the executing agent | +| `specs/subscription_console.md` | WS-30 | +| `specs/future_modules_roadmap.md` | D21 roadmap (no rows until specced) | +| `specs/crm_app.md` | WS-26 | +| `specs/project_management_app.md` | WS-27 | +| `specs/people_center_app.md` | WS-28 | +| `specs/department_centers.md` | WS-13 · WS-14 · WS-15 · WS-16 (+ §5 Center roster of record, D22) | +| `specs/colleague_onboarding.md` | WS-24 | +| `specs/deploy_delivery_path.md` | WS-25 | +| `specs/email_app_master_plan.md` | WS-17 | +| `specs/task_manager_app.md` | WS-18 | +| `specs/note_taker_app.md` + `specs/meeting_bot_platform_plan.md` | WS-19 | +| `specs/whatsapp_message_manager.md` | WS-20 | +| `specs/calendar_focus_os.md` + `specs/calendar_timeboxing.md` | WS-21 | +| `specs/skills_registry.md` + `specs/skills_scope_out.md` | WS-23 | +| `specs/workflows_app.md` | WS-11 | +| `specs/multi_agent_orchestration.md` | WS-12 — **Phase 4 ONLY** (D6); rest superseded | +| `specs/agent_architecture.md` | WS-8 | +| `specs/memory_architecture.md` | WS-9 | +| `specs/observability_e2.md` | WS-6 | +| `specs/permissions_sandbox_b6.md` | WS-3 | +| `../docs/multiplayer/memory-clearance.md` *(in `docs/`!)* | WS-10 (S1 only — floor control CUT, D25.4) | + +## CONTRACTS & DOCTRINE — binding rules; read before building, no rows of their own + +| Spec | Role | +|---|---| +| `specs/user_management_contract.md` | The rules every app must follow (identity, lifecycle, permissions) | +| `specs/org_access_control.md` | The access model of record | +| `specs/tenancy_and_visibility.md` | §2–§5 visibility doctrine (D12) — ⚠️ §1/§6 SUPERSEDED by D15 | +| `specs/groups_sessions_authority.md` | Session authority + intersection rule | +| `specs/generative_ui_2.md` | Chat HITL model (shipped doctrine) | +| `AGENTS.md` | This folder's guide + spec index | +| `agent_repo_compatibility.md` | Agent-repo contract | +| `specs/backup_and_restore.md` | BO-23 operations + restore runbook | + +## REFERENCE — background; may be stale, verify before relying + +`system_architecture.md` · `reference.md` *(both carry stale-warning +banners)* · `project_plan.md` *(superseded for sequencing by `work_plan.md`)* · +`specs/core_module_map.md` *(internal engineering subsystems — NOT the product +module map; that is `saas_multitenancy.md` §2.4b)* · +`agents-workspaces-artifacts.md` · `specs/agent_file_and_memory_framework.md` · +`specs/agent_persistence_implementation.md` · `specs/agent_coding_skill.md` · +`specs/llm_caching_memory.md` · `specs/mcp_plugin_integration.md` *(Phase A +shipped; B/C research)* · `specs/task_manager_hr_planning_and_memory.md` *(HR +data layer WS-28 reads)* · `specs/multi_tenancy_leak_audit.md` *(the 14-finding +audit feeding WS-29 MT-1i)* · `specs/multi_tenancy.md` *(#399's measured +record — SUPERSEDED for architecture, banner at top)* + +## DEFERRED / PARKED / HISTORICAL — banner-marked; nothing dispatches from these + +| Spec | Why | +|---|---| +| `specs/drawio_integration.md` + `specs/drawio_diagram_svc_contract.md` | ⏸ PARKED by owner (D25.7) | +| `specs/chat_ux.md` | Superseded by generative_ui_2 §2 (protocol reference; §12 VII–XI items open but unscheduled) | +| `specs/live_meeting_copilot.md` · `specs/whatsapp_calls_note_taker.md` | Future scope, no board row | +| `specs/multi_user_organization_research.md` · `specs/note_taker_research_2026-07.md` · `specs/paca_pm_research_2026-08.md` · `specs/plane_pm_research_2026-08.md` · `specs/multiplayer_prior_art_qm_2026-08.md` | Research inputs — reference-only by their own declaration | +| `specs/agent_platform_hardening_2026-07.md` · `specs/competitive_hardening_2026-07.md` · `specs/harness_hardening_2026-07.md` · `specs/task_manager_harness_2026-07.md` · `specs/single_agent_chat_bug_audit_2026-07.md` · `specs/chat_agent_framework_review_2026-07.md` | 2026-07 audit/build records — history, superseded where D15/D25 touched them | +| `specs/calendar_ai_review.md` · `specs/calendar_ux_review.md` | Calendar sub-docs — consult via `calendar_focus_os.md` §9 (ux_review is the sole home of the block-reminders item) | +| `HANDOVER.md` | #399's branch handover — EXECUTED (merged 2026-08-09); historical | +| `archive/` | Pre-consolidation archives | + +**Rule (binds all future docs):** a new spec enters this INDEX in the same PR +that creates it, in exactly one section. A spec leaving ACTIVE gets a banner in +the same PR. An agent finding a spec absent from this INDEX should treat that +as a defect and say so. diff --git a/ai-company-brain/agent_repo_compatibility.md b/project-docs/agent_repo_compatibility.md similarity index 100% rename from ai-company-brain/agent_repo_compatibility.md rename to project-docs/agent_repo_compatibility.md diff --git a/ai-company-brain/agents-workspaces-artifacts.md b/project-docs/agents-workspaces-artifacts.md similarity index 100% rename from ai-company-brain/agents-workspaces-artifacts.md rename to project-docs/agents-workspaces-artifacts.md diff --git a/ai-company-brain/project_plan.md b/project-docs/project_plan.md similarity index 100% rename from ai-company-brain/project_plan.md rename to project-docs/project_plan.md diff --git a/ai-company-brain/reference.md b/project-docs/reference.md similarity index 100% rename from ai-company-brain/reference.md rename to project-docs/reference.md diff --git a/ai-company-brain/specs/agent_architecture.md b/project-docs/specs/agent_architecture.md similarity index 99% rename from ai-company-brain/specs/agent_architecture.md rename to project-docs/specs/agent_architecture.md index 4794d2ca7..9d0b61045 100644 --- a/ai-company-brain/specs/agent_architecture.md +++ b/project-docs/specs/agent_architecture.md @@ -909,7 +909,7 @@ and `npx tsc --noEmit` + the vitest suite are clean. **WS-8n — specify the Agent Workshop before building it. AGENT-SAFE (a spec, not a build).** Phase C's entire content in this document is one phase-table row and the §10 lifecycle diagram. Nothing is dispatchable from that. Done when this spec (or a child spec under -`ai-company-brain/specs/`) carries: the `/build/agents` route inventory, the describe-to-create +`project-docs/specs/`) carries: the `/build/agents` route inventory, the describe-to-create conversation contract, the draft→validate→eval→publish→rollback state machine mapped onto `agent_defs.status` / `live_version`, who may publish (§13 Q2), and per-slice acceptance — each stated the way the App Workshop's `docs/app-workshop/README.md` states its own, since diff --git a/ai-company-brain/specs/agent_coding_skill.md b/project-docs/specs/agent_coding_skill.md similarity index 100% rename from ai-company-brain/specs/agent_coding_skill.md rename to project-docs/specs/agent_coding_skill.md diff --git a/ai-company-brain/specs/agent_file_and_memory_framework.md b/project-docs/specs/agent_file_and_memory_framework.md similarity index 100% rename from ai-company-brain/specs/agent_file_and_memory_framework.md rename to project-docs/specs/agent_file_and_memory_framework.md diff --git a/ai-company-brain/specs/agent_persistence_implementation.md b/project-docs/specs/agent_persistence_implementation.md similarity index 100% rename from ai-company-brain/specs/agent_persistence_implementation.md rename to project-docs/specs/agent_persistence_implementation.md diff --git a/ai-company-brain/specs/agent_platform_hardening_2026-07.md b/project-docs/specs/agent_platform_hardening_2026-07.md similarity index 99% rename from ai-company-brain/specs/agent_platform_hardening_2026-07.md rename to project-docs/specs/agent_platform_hardening_2026-07.md index 3fe0276d8..68a0534c0 100644 --- a/ai-company-brain/specs/agent_platform_hardening_2026-07.md +++ b/project-docs/specs/agent_platform_hardening_2026-07.md @@ -1,5 +1,10 @@ # Agent Platform Hardening Review — 2026-07 +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document — 2026-07 hardening log; D15/D16 re-scoped its premises (annotations inline). The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + **Status:** Review · **Verified against code on 2026-08-03** · **Owner:** vjvarada **Scope:** The multiplayer room model, the memory/clearance model, and the agent architecture — reviewed together, because most of what follows only appears where two of them meet. diff --git a/ai-company-brain/specs/archive/README.md b/project-docs/specs/archive/README.md similarity index 100% rename from ai-company-brain/specs/archive/README.md rename to project-docs/specs/archive/README.md diff --git a/ai-company-brain/specs/archive/artifact_viewer.md b/project-docs/specs/archive/artifact_viewer.md similarity index 100% rename from ai-company-brain/specs/archive/artifact_viewer.md rename to project-docs/specs/archive/artifact_viewer.md diff --git a/ai-company-brain/specs/archive/chat_implementation_review_2026-07.md b/project-docs/specs/archive/chat_implementation_review_2026-07.md similarity index 100% rename from ai-company-brain/specs/archive/chat_implementation_review_2026-07.md rename to project-docs/specs/archive/chat_implementation_review_2026-07.md diff --git a/ai-company-brain/specs/archive/chat_ui_agui_hitl_review_2026-07.md b/project-docs/specs/archive/chat_ui_agui_hitl_review_2026-07.md similarity index 100% rename from ai-company-brain/specs/archive/chat_ui_agui_hitl_review_2026-07.md rename to project-docs/specs/archive/chat_ui_agui_hitl_review_2026-07.md diff --git a/ai-company-brain/specs/archive/context_assembly_c2.md b/project-docs/specs/archive/context_assembly_c2.md similarity index 100% rename from ai-company-brain/specs/archive/context_assembly_c2.md rename to project-docs/specs/archive/context_assembly_c2.md diff --git a/ai-company-brain/specs/archive/core_loop_unification.md b/project-docs/specs/archive/core_loop_unification.md similarity index 100% rename from ai-company-brain/specs/archive/core_loop_unification.md rename to project-docs/specs/archive/core_loop_unification.md diff --git a/ai-company-brain/specs/archive/dev_velocity_tooling_2026-07.md b/project-docs/specs/archive/dev_velocity_tooling_2026-07.md similarity index 99% rename from ai-company-brain/specs/archive/dev_velocity_tooling_2026-07.md rename to project-docs/specs/archive/dev_velocity_tooling_2026-07.md index 4026032b5..e2739ae88 100644 --- a/ai-company-brain/specs/archive/dev_velocity_tooling_2026-07.md +++ b/project-docs/specs/archive/dev_velocity_tooling_2026-07.md @@ -116,7 +116,7 @@ and doesn't fit a shared-VPS runtime agent. - `.mcp.json` at repo root — Claude Code's project-scoped MCP config, so any coding agent in this repo gets the `codegraph_explore` tool automatically. - `codegraph.json` — CommandCenter-specific index excludes (generated SQL, - migrations, `.next/`, learning-resources, `ai-company-brain/`) on top of + migrations, `.next/`, learning-resources, `project-docs/`) on top of CodeGraph's defaults. - `.gitignore` — `.codegraph/` + db files (the local index is per-checkout, never committed). diff --git a/ai-company-brain/specs/archive/email_ai_assistant.md b/project-docs/specs/archive/email_ai_assistant.md similarity index 100% rename from ai-company-brain/specs/archive/email_ai_assistant.md rename to project-docs/specs/archive/email_ai_assistant.md diff --git a/ai-company-brain/specs/archive/email_app_review.md b/project-docs/specs/archive/email_app_review.md similarity index 100% rename from ai-company-brain/specs/archive/email_app_review.md rename to project-docs/specs/archive/email_app_review.md diff --git a/ai-company-brain/specs/archive/email_feature_review_2026-07.md b/project-docs/specs/archive/email_feature_review_2026-07.md similarity index 100% rename from ai-company-brain/specs/archive/email_feature_review_2026-07.md rename to project-docs/specs/archive/email_feature_review_2026-07.md diff --git a/ai-company-brain/specs/archive/email_inbox_zero_parity_plan.md b/project-docs/specs/archive/email_inbox_zero_parity_plan.md similarity index 100% rename from ai-company-brain/specs/archive/email_inbox_zero_parity_plan.md rename to project-docs/specs/archive/email_inbox_zero_parity_plan.md diff --git a/ai-company-brain/specs/archive/email_tool_consolidation.md b/project-docs/specs/archive/email_tool_consolidation.md similarity index 100% rename from ai-company-brain/specs/archive/email_tool_consolidation.md rename to project-docs/specs/archive/email_tool_consolidation.md diff --git a/ai-company-brain/specs/archive/pixel_art_office_pipeline.md b/project-docs/specs/archive/pixel_art_office_pipeline.md similarity index 100% rename from ai-company-brain/specs/archive/pixel_art_office_pipeline.md rename to project-docs/specs/archive/pixel_art_office_pipeline.md diff --git a/ai-company-brain/specs/archive/runtime_agent_effectiveness_2026-07.md b/project-docs/specs/archive/runtime_agent_effectiveness_2026-07.md similarity index 100% rename from ai-company-brain/specs/archive/runtime_agent_effectiveness_2026-07.md rename to project-docs/specs/archive/runtime_agent_effectiveness_2026-07.md diff --git a/ai-company-brain/specs/archive/stream_reconnection.md b/project-docs/specs/archive/stream_reconnection.md similarity index 100% rename from ai-company-brain/specs/archive/stream_reconnection.md rename to project-docs/specs/archive/stream_reconnection.md diff --git a/ai-company-brain/specs/archive/vscode_tool_integration.md b/project-docs/specs/archive/vscode_tool_integration.md similarity index 100% rename from ai-company-brain/specs/archive/vscode_tool_integration.md rename to project-docs/specs/archive/vscode_tool_integration.md diff --git a/ai-company-brain/specs/backup_and_restore.md b/project-docs/specs/backup_and_restore.md similarity index 99% rename from ai-company-brain/specs/backup_and_restore.md rename to project-docs/specs/backup_and_restore.md index b5a70b13b..2b104552e 100644 --- a/ai-company-brain/specs/backup_and_restore.md +++ b/project-docs/specs/backup_and_restore.md @@ -249,7 +249,7 @@ TimeoutStartSec=1800 ```ini [Unit] Description=Nightly CommandCenter database backup -Documentation=file:///opt/acb/app/ai-company-brain/specs/backup_and_restore.md +Documentation=file:///opt/acb/app/project-docs/specs/backup_and_restore.md [Timer] # 02:30 UTC — clear of the 05:00 UTC Monday codebase-health job, so a long diff --git a/ai-company-brain/specs/calendar_ai_review.md b/project-docs/specs/calendar_ai_review.md similarity index 97% rename from ai-company-brain/specs/calendar_ai_review.md rename to project-docs/specs/calendar_ai_review.md index 11362adfd..1ff508ad5 100644 --- a/ai-company-brain/specs/calendar_ai_review.md +++ b/project-docs/specs/calendar_ai_review.md @@ -1,5 +1,10 @@ # Calendar × Tasks × AI — comprehensive review +> ⚠️ **SUB-DOC — CONSULT VIA `calendar_focus_os.md` §9 (2026-08-10 consolidation, D26).** No work dispatches from +> this document; cited by shipped migrations 92/97/100. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** review record (2026-07-22) — findings since triaged into `calendar_focus_os.md` §9 / `calendar_timeboxing.md` §13, which own all acceptance. Cited by migration headers 92/97/100. Not re-verified since. *(Header added 2026-08-09.)* Date: 2026-07-22 · branch `claude/calendar-productivity-redesign-rdh50k`. diff --git a/ai-company-brain/specs/calendar_focus_os.md b/project-docs/specs/calendar_focus_os.md similarity index 99% rename from ai-company-brain/specs/calendar_focus_os.md rename to project-docs/specs/calendar_focus_os.md index 9053724d9..d381608c5 100644 --- a/ai-company-brain/specs/calendar_focus_os.md +++ b/project-docs/specs/calendar_focus_os.md @@ -455,7 +455,7 @@ No feature above is an island; each plugs into a surface that already exists: ## 8. Mockups -`ai-company-brain/specs/mockups/calendar_focus_os.html` — self-contained HTML +`project-docs/specs/mockups/calendar_focus_os.html` — self-contained HTML (open in any browser) showing: Plan mode grid with leverage lens / batch / break / ritual blocks + meters; Focus Mode; Gap Filler; Startup ritual; Shutdown review; mobile Today timeline. Visual language matches the control @@ -720,8 +720,8 @@ filter); `test_email_calendar_context.py` = the email-side calendar context; `calendar_timeboxing.md`, `calendar_ai_review.md` and `calendar_ux_review.md`. The WS-21 row names two; `calendar_ai_review.md` is cited by three migration headers (`92`, `97`, `98`) yet is referenced by no other spec, no `work_plan.md` - row and no `ai-company-brain/AGENTS.md` index entry — and **the specs index in - `ai-company-brain/AGENTS.md` has no calendar row at all.** Cross-deferral + row and no `project-docs/AGENTS.md` index entry — and **the specs index in + `project-docs/AGENTS.md` has no calendar row at all.** Cross-deferral between focus_os and timeboxing is clean (§5 here is canonical for `gtd_time_blocks`; `calendar_timeboxing.md` §13 is canonical for P4); the other two docs are unregistered. diff --git a/ai-company-brain/specs/calendar_timeboxing.md b/project-docs/specs/calendar_timeboxing.md similarity index 100% rename from ai-company-brain/specs/calendar_timeboxing.md rename to project-docs/specs/calendar_timeboxing.md diff --git a/ai-company-brain/specs/calendar_ux_review.md b/project-docs/specs/calendar_ux_review.md similarity index 97% rename from ai-company-brain/specs/calendar_ux_review.md rename to project-docs/specs/calendar_ux_review.md index c206ffb30..c5ca82cae 100644 --- a/ai-company-brain/specs/calendar_ux_review.md +++ b/project-docs/specs/calendar_ux_review.md @@ -1,5 +1,10 @@ # Calendar-for-task-management — UX & backend review +> ⚠️ **SUB-DOC — CONSULT VIA `calendar_focus_os.md` §9 (2026-08-10 consolidation, D26).** No work dispatches from +> this document; ⚠️ sole home of the block-reminders/notifications item. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** designer's audit record (2026-07-18) of PR #71. Sole home of the block-reminders/notifications item (`calendar_focus_os.md` §9.13 points here); everything else since triaged into the two owning calendar specs. Many fixes it proposes HAVE landed — check `calendar_focus_os.md` §9 before citing a gap as open. *(Header added 2026-08-09.)* A designer's-eye audit of the calendar built in PR #71 (2026-07-18). What's diff --git a/ai-company-brain/specs/chat_agent_framework_review_2026-07.md b/project-docs/specs/chat_agent_framework_review_2026-07.md similarity index 98% rename from ai-company-brain/specs/chat_agent_framework_review_2026-07.md rename to project-docs/specs/chat_agent_framework_review_2026-07.md index 33aab762d..bb8aa63c6 100644 --- a/ai-company-brain/specs/chat_agent_framework_review_2026-07.md +++ b/project-docs/specs/chat_agent_framework_review_2026-07.md @@ -1,5 +1,10 @@ # Chat & Agent Framework Review — 2026-07 +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + **Status:** review complete · **Date:** 2026-07-22 · **Requested by:** Vijay **Scope:** multi-agent architecture, orchestration & handoff, context management, memory, artifacts/files, HITL/AG-UI, document co-authoring — and the standing question of whether to @@ -130,7 +135,7 @@ agent moved off this path stops paying all of them at once. `_AGENT_REGISTRY` + Postgres `dynamic_agents` + per-agent `config.json`. Delete or regenerate the root file from the live registry; make `config.json` the single declaration of runtime/engine, tools, and triggers. -5. **Reconcile the constraint docs.** `ai-company-brain/AGENTS.md` (2026-07-07) still says +5. **Reconcile the constraint docs.** `project-docs/AGENTS.md` (2026-07-07) still says "MAF is the sole runtime; Copilot SDK mutation-only"; root `AGENTS.md` (2026-07-13) sanctions Tier 1.5 interactive chat; `apps/services/orchestrator/AGENTS.md:22` and the webhook comment (`agent.py:2745`) still deny it. One statement, one place. diff --git a/ai-company-brain/specs/chat_ux.md b/project-docs/specs/chat_ux.md similarity index 99% rename from ai-company-brain/specs/chat_ux.md rename to project-docs/specs/chat_ux.md index 8cd75a3d5..acece4376 100644 --- a/ai-company-brain/specs/chat_ux.md +++ b/project-docs/specs/chat_ux.md @@ -1,5 +1,10 @@ # Chat UX Specification — CommandCenter Control Plane +> ⚠️ **SUPERSEDED / DEFERRED (2026-08-10 consolidation, D26).** No work dispatches from +> this document — `generative_ui_2.md` §2 owns the HITL model; kept as protocol reference (§12 VII–XI open, unscheduled). The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Type:** Implementation Spec > **Date:** 2026-06-05 > **Status:** Active — Phase 1 complete, Phase 2 (CopilotKit patterns) in progress · **Header re-dated 2026-08-09:** §12.3 is superseded by `generative_ui_2.md` §2; the live remainder is §12 items **V, VII–XI** plus §11 **H** (dev console) and **J** (push-to-talk) — the 2026-08-01 doc-truth note below is authoritative; the body is retained as protocol reference per `work_plan.md` §5 item 3 diff --git a/ai-company-brain/specs/colleague_onboarding.md b/project-docs/specs/colleague_onboarding.md similarity index 100% rename from ai-company-brain/specs/colleague_onboarding.md rename to project-docs/specs/colleague_onboarding.md diff --git a/ai-company-brain/specs/competitive_hardening_2026-07.md b/project-docs/specs/competitive_hardening_2026-07.md similarity index 98% rename from ai-company-brain/specs/competitive_hardening_2026-07.md rename to project-docs/specs/competitive_hardening_2026-07.md index 2b0084e0f..213fe596a 100644 --- a/ai-company-brain/specs/competitive_hardening_2026-07.md +++ b/project-docs/specs/competitive_hardening_2026-07.md @@ -1,5 +1,10 @@ # Competitive Hardening — Learnings from Hermes Agent & OpenClaw +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document — build log for the 2026-07-27 passes. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** Planned (annealed into the backlog **(2026-07-13; 'no code yet' is stale — BO-20a/20f built 2026-08-02, BO-20b slice 1 2026-08-03; per-item state lives in FOUNDATION_BUILDOUT_CHECKLIST.md)**) · **Created:** 2026-07-13 > **Source:** [`/COMPETITIVE_COMPARISON.md`](../../COMPETITIVE_COMPARISON.md) — an evidence-based three-way > comparison of CommandCenter against the two most-visible self-hosted agent platforms of 2026: diff --git a/ai-company-brain/specs/core_module_map.md b/project-docs/specs/core_module_map.md similarity index 100% rename from ai-company-brain/specs/core_module_map.md rename to project-docs/specs/core_module_map.md diff --git a/ai-company-brain/specs/crm_app.md b/project-docs/specs/crm_app.md similarity index 100% rename from ai-company-brain/specs/crm_app.md rename to project-docs/specs/crm_app.md diff --git a/ai-company-brain/specs/department_centers.md b/project-docs/specs/department_centers.md similarity index 100% rename from ai-company-brain/specs/department_centers.md rename to project-docs/specs/department_centers.md diff --git a/ai-company-brain/specs/deploy_delivery_path.md b/project-docs/specs/deploy_delivery_path.md similarity index 99% rename from ai-company-brain/specs/deploy_delivery_path.md rename to project-docs/specs/deploy_delivery_path.md index be31b4c5d..61579f281 100644 --- a/ai-company-brain/specs/deploy_delivery_path.md +++ b/project-docs/specs/deploy_delivery_path.md @@ -328,7 +328,7 @@ Also: `flock` so a slow apply cannot have the next tick start a second one; and a non-zero exit on failure so `systemctl --failed` shows it. **`paths-ignore` was removed from `deploy.yml`** in the same change. It skipped -the whole workflow for `**.md` and `ai-company-brain/**` — and a workflow-level +the whole workflow for `**.md` and `project-docs/**` — and a workflow-level path filter skips the jobs too, so a docs-only merge would never move `release` and the box would never converge. It also made this outage harder to see: #357 was documentation-only, so **no run was ever queued for it**, and "no failed run" diff --git a/ai-company-brain/specs/drawio_diagram_svc_contract.md b/project-docs/specs/drawio_diagram_svc_contract.md similarity index 98% rename from ai-company-brain/specs/drawio_diagram_svc_contract.md rename to project-docs/specs/drawio_diagram_svc_contract.md index 47d774e3f..579363748 100644 --- a/ai-company-brain/specs/drawio_diagram_svc_contract.md +++ b/project-docs/specs/drawio_diagram_svc_contract.md @@ -1,5 +1,10 @@ # draw.io Integration — Technical Contracts & Interfaces +> ⚠️ **⏸ PARKED WITH ITS PARENT (D25.7) (2026-08-10 consolidation, D26).** No work dispatches from +> this document — companion contract of `drawio_integration.md`. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Type:** Technical design / interface contract > **Date:** 2026-06-29 > **Status:** Proposed — freeze before ST-DRW-03 starts diff --git a/ai-company-brain/specs/drawio_integration.md b/project-docs/specs/drawio_integration.md similarity index 98% rename from ai-company-brain/specs/drawio_integration.md rename to project-docs/specs/drawio_integration.md index dbe857c25..0bc4e5856 100644 --- a/ai-company-brain/specs/drawio_integration.md +++ b/project-docs/specs/drawio_integration.md @@ -1,5 +1,10 @@ # draw.io Integration — Architecture, Plan & Roadmap +> ⚠️ **⏸ PARKED BY OWNER (D25.7) (2026-08-10 consolidation, D26).** No work dispatches from +> this document — it re-enters the board when a real need pulls it; re-verify anchors at un-park. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Type:** Master design + ticketed implementation plan (merged from the former > `drawio_integration_review.md` + `drawio_integration_plan.md`). > **Date:** 2026-06-29 · **Status:** Proposed — ready to schedule · **Owner:** TBD (backend + frontend) diff --git a/ai-company-brain/specs/email_app_master_plan.md b/project-docs/specs/email_app_master_plan.md similarity index 100% rename from ai-company-brain/specs/email_app_master_plan.md rename to project-docs/specs/email_app_master_plan.md diff --git a/ai-company-brain/specs/future_modules_roadmap.md b/project-docs/specs/future_modules_roadmap.md similarity index 100% rename from ai-company-brain/specs/future_modules_roadmap.md rename to project-docs/specs/future_modules_roadmap.md diff --git a/ai-company-brain/specs/generative_ui_2.md b/project-docs/specs/generative_ui_2.md similarity index 100% rename from ai-company-brain/specs/generative_ui_2.md rename to project-docs/specs/generative_ui_2.md diff --git a/ai-company-brain/specs/groups_sessions_authority.md b/project-docs/specs/groups_sessions_authority.md similarity index 100% rename from ai-company-brain/specs/groups_sessions_authority.md rename to project-docs/specs/groups_sessions_authority.md diff --git a/ai-company-brain/specs/harness_hardening_2026-07.md b/project-docs/specs/harness_hardening_2026-07.md similarity index 97% rename from ai-company-brain/specs/harness_hardening_2026-07.md rename to project-docs/specs/harness_hardening_2026-07.md index 98d4b4ea9..7c4afbd53 100644 --- a/ai-company-brain/specs/harness_hardening_2026-07.md +++ b/project-docs/specs/harness_hardening_2026-07.md @@ -1,5 +1,10 @@ # Harness Hardening — Best-Practices Gap Analysis & Work Queue +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document — HH-1..8 log; HH-6/7 deferred. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** In progress · **Created:** 2026-07-02 > Source: comprehensive comparison of the CommandCenter orchestrator against the practices catalogued in [awesome-harness-engineering](https://github.com/ai-boost/awesome-harness-engineering) (agent loops, planning artifacts, context engineering, tool design, permissions, memory, evals, observability, HITL, sandboxing). > Companions: [`archive/chat_implementation_review_2026-07.md`](archive/chat_implementation_review_2026-07.md) (streaming/HITL audit), [`llm_caching_memory.md`](llm_caching_memory.md) (caching plan), [`archive/email_tool_consolidation.md`](archive/email_tool_consolidation.md) (tool-surface plan). diff --git a/ai-company-brain/specs/live_meeting_copilot.md b/project-docs/specs/live_meeting_copilot.md similarity index 98% rename from ai-company-brain/specs/live_meeting_copilot.md rename to project-docs/specs/live_meeting_copilot.md index 0596e7d7e..86a9b10a6 100644 --- a/ai-company-brain/specs/live_meeting_copilot.md +++ b/project-docs/specs/live_meeting_copilot.md @@ -1,5 +1,10 @@ # Live Meeting Copilot — architecture plan +> ⚠️ **DEFERRED — FUTURE SCOPE (2026-08-10 consolidation, D26).** No work dispatches from +> this document; no board row exists. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + **Status:** Phases A-D BUILT (presence, console, passive copilot, business context, agenda + standing instructions). Phase E (speaking into the call) still planned. *(status undated when found; last git-touch 2026-07-28 — treat as of that date; not re-verified since. Dated 2026-08-09.)* **Builds on:** `note_taker_app.md` §3.13 (meeting bot + live-transcript bus), the browser recorder + live captions, `acb_llm` tiers, the agent/skills/connector diff --git a/ai-company-brain/specs/llm_caching_memory.md b/project-docs/specs/llm_caching_memory.md similarity index 99% rename from ai-company-brain/specs/llm_caching_memory.md rename to project-docs/specs/llm_caching_memory.md index 9156ffe95..dbd2999ed 100644 --- a/ai-company-brain/specs/llm_caching_memory.md +++ b/project-docs/specs/llm_caching_memory.md @@ -545,8 +545,8 @@ For each phase: ## DOX Update Required After Implementation When each phase is implemented: -- Update `ai-company-brain/system_architecture.md` ADR-008 status from "approved / unimplemented" to "implemented" -- Update `ai-company-brain/project_plan.md` WBS 2.6 status (§6 Phase 2) +- Update `project-docs/system_architecture.md` ADR-008 status from "approved / unimplemented" to "implemented" +- Update `project-docs/project_plan.md` WBS 2.6 status (§6 Phase 2) - Update `apps/services/orchestrator/AGENTS.md` to note sentinel marker convention - Update `packages/acb_llm/AGENTS.md` (or create if absent) to document cache parameters - Update `packages/acb_memory/` AGENTS.md to document session-scoped memory TTL contract diff --git a/ai-company-brain/specs/mcp_plugin_integration.md b/project-docs/specs/mcp_plugin_integration.md similarity index 99% rename from ai-company-brain/specs/mcp_plugin_integration.md rename to project-docs/specs/mcp_plugin_integration.md index 9733ff29e..7a9cffafb 100644 --- a/ai-company-brain/specs/mcp_plugin_integration.md +++ b/project-docs/specs/mcp_plugin_integration.md @@ -568,5 +568,5 @@ a product. - [OpenAPI 3.0 specification](https://spec.openapis.org/oas/v3.0.3) - CommandCenter Integration Registry: `packages/acb_skills/acb_skills/integrations.py` - Agent executor: `apps/services/orchestrator/orchestrator/executor.py` (MCP injection now in `apps/services/orchestrator/orchestrator/_tool_injection.py`) -- System architecture: `ai-company-brain/system_architecture.md` -- Agent builder guide: `ai-company-brain/agent_repo_compatibility.md` +- System architecture: `project-docs/system_architecture.md` +- Agent builder guide: `project-docs/agent_repo_compatibility.md` diff --git a/ai-company-brain/specs/meeting_bot_platform_plan.md b/project-docs/specs/meeting_bot_platform_plan.md similarity index 100% rename from ai-company-brain/specs/meeting_bot_platform_plan.md rename to project-docs/specs/meeting_bot_platform_plan.md diff --git a/ai-company-brain/specs/memory_architecture.md b/project-docs/specs/memory_architecture.md similarity index 100% rename from ai-company-brain/specs/memory_architecture.md rename to project-docs/specs/memory_architecture.md diff --git a/ai-company-brain/specs/mockups/calendar_focus_os.html b/project-docs/specs/mockups/calendar_focus_os.html similarity index 100% rename from ai-company-brain/specs/mockups/calendar_focus_os.html rename to project-docs/specs/mockups/calendar_focus_os.html diff --git a/ai-company-brain/specs/mockups/whatsapp_message_manager.html b/project-docs/specs/mockups/whatsapp_message_manager.html similarity index 100% rename from ai-company-brain/specs/mockups/whatsapp_message_manager.html rename to project-docs/specs/mockups/whatsapp_message_manager.html diff --git a/ai-company-brain/specs/multi_agent_orchestration.md b/project-docs/specs/multi_agent_orchestration.md similarity index 100% rename from ai-company-brain/specs/multi_agent_orchestration.md rename to project-docs/specs/multi_agent_orchestration.md diff --git a/ai-company-brain/specs/multi_tenancy.md b/project-docs/specs/multi_tenancy.md similarity index 99% rename from ai-company-brain/specs/multi_tenancy.md rename to project-docs/specs/multi_tenancy.md index 62e8797e0..fc2403111 100644 --- a/ai-company-brain/specs/multi_tenancy.md +++ b/project-docs/specs/multi_tenancy.md @@ -3,7 +3,7 @@ > ## ⚠️ SUPERSEDED FOR ARCHITECTURE (2026-08-09) — read `saas_multitenancy.md` first > > A parallel workstream landed the full design in **PR #404** while this branch was open, and -> it is canonical: **`ai-company-brain/specs/saas_multitenancy.md`** (architecture, §11 +> it is canonical: **`project-docs/specs/saas_multitenancy.md`** (architecture, §11 > tickets), plus `saas_multitenancy_handover.md` (the H1→H8 runbook) and > `saas_multitenancy_implementation.md` (shapes). Where the two disagree, that one wins. > diff --git a/ai-company-brain/specs/multi_tenancy_leak_audit.md b/project-docs/specs/multi_tenancy_leak_audit.md similarity index 100% rename from ai-company-brain/specs/multi_tenancy_leak_audit.md rename to project-docs/specs/multi_tenancy_leak_audit.md diff --git a/ai-company-brain/specs/multi_user_organization_research.md b/project-docs/specs/multi_user_organization_research.md similarity index 99% rename from ai-company-brain/specs/multi_user_organization_research.md rename to project-docs/specs/multi_user_organization_research.md index 838719f63..02c69503d 100644 --- a/ai-company-brain/specs/multi_user_organization_research.md +++ b/project-docs/specs/multi_user_organization_research.md @@ -1,5 +1,10 @@ # Multi-User / Organization Architecture Research +> ⚠️ **RESEARCH — REFERENCE-ONLY (2026-08-10 consolidation, D26).** No work dispatches from +> this document; planning superseded by `tenancy_and_visibility.md` + `saas_multitenancy.md`. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** Research & design proposal. **§4 (identity, membership, roles, permissions) is now IMPLEMENTED** — see [`org_access_control.md`](org_access_control.md), which also carries §8 credential scoping and part of §7 memory scoping. The rest (§5 modules, §9 entity-graph RLS, §16 data-heavy scoping, §17 SaaS) remains research, and the modules/session-sharing portion is owned by the **multiplayer agent collaboration** workstream — see that spec's §10 handoff contract before building from this document. **2026-08-08/09:** §9 and §17 were un-superseded and are now **input to `saas_multitenancy.md` (D15, WS-29)** — the decision of record, which adopts §17.2's pooled-first recommendation and **rejects §17.3's header-based tenant resolution by name**. Read §17 as background; build only from saas_multitenancy.md. > **Created:** 2026-07-07 > **Scope:** How CommandCenter evolves from a single-tenant "internal company brain" into a multi-user organization account where personal data stays private, shared resources are selectively visible, and an administrator controls settings, modules, and agents. diff --git a/ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md b/project-docs/specs/multiplayer_prior_art_qm_2026-08.md similarity index 98% rename from ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md rename to project-docs/specs/multiplayer_prior_art_qm_2026-08.md index 5619b05b6..73f07fbfe 100644 --- a/ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md +++ b/project-docs/specs/multiplayer_prior_art_qm_2026-08.md @@ -1,5 +1,10 @@ # Multiplayer Prior Art — Learnings from `qm` (yc-software) +> ⚠️ **RESEARCH — REFERENCE-ONLY (2026-08-10 consolidation, D26).** No work dispatches from +> this document; owns no work and no status. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** Analysis complete; findings annealed into existing workstreams. **This doc owns no > work item and no acceptance criteria** — it is reference-only, and the workstream rows it feeds > carry the status. **Two of its findings have since been taken and built:** QM-1 (steer) shipped diff --git a/ai-company-brain/specs/note_taker_app.md b/project-docs/specs/note_taker_app.md similarity index 100% rename from ai-company-brain/specs/note_taker_app.md rename to project-docs/specs/note_taker_app.md diff --git a/ai-company-brain/specs/note_taker_research_2026-07.md b/project-docs/specs/note_taker_research_2026-07.md similarity index 99% rename from ai-company-brain/specs/note_taker_research_2026-07.md rename to project-docs/specs/note_taker_research_2026-07.md index d61d30d25..57211a06e 100644 --- a/ai-company-brain/specs/note_taker_research_2026-07.md +++ b/project-docs/specs/note_taker_research_2026-07.md @@ -1,5 +1,10 @@ # AI Note Taker — Research Appendix (landscape, engines, browser facts) +> ⚠️ **RESEARCH — REFERENCE-ONLY (2026-08-10 consolidation, D26).** No work dispatches from +> this document. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Product:** CommandCenter · **Feature:** AI Note Taker app (`/notes`) · **Updated:** 2026-07-23 · **Version:** 0.1 (research complete) > **Status:** 🟢 research complete — feeds the master spec [`note_taker_app.md`](note_taker_app.md). This doc is the evidence base: what Meetily and its peers actually are, what the 2026 open-source ASR/diarization SOTA looks like, and what the browser can and cannot capture. The master spec makes the decisions; this doc justifies them. > **Method:** primary-source review (GitHub repos, HF model cards, release pages, vendor benchmarks) performed 2026-07-23. Star counts / versions / dates are as-of that day. diff --git a/ai-company-brain/specs/observability_e2.md b/project-docs/specs/observability_e2.md similarity index 99% rename from ai-company-brain/specs/observability_e2.md rename to project-docs/specs/observability_e2.md index 4c6f20033..11931a117 100644 --- a/ai-company-brain/specs/observability_e2.md +++ b/project-docs/specs/observability_e2.md @@ -1118,7 +1118,7 @@ office/cost views change. ### R4 — what to update when this ships Any WS-6 PR updates this spec's `## Status` header + changelog in the same PR, -and flips the row in `ai-company-brain/AGENTS.md`'s spec index (which currently +and flips the row in `project-docs/AGENTS.md`'s spec index (which currently reads "distributed/OTel tracing **dead** → **BO-5**") plus `FOUNDATION_BUILDOUT_CHECKLIST.md` §BO-5. Sequencing/ownership lives in `work_plan.md` — that board wins over this spec. diff --git a/ai-company-brain/specs/org_access_control.md b/project-docs/specs/org_access_control.md similarity index 100% rename from ai-company-brain/specs/org_access_control.md rename to project-docs/specs/org_access_control.md diff --git a/ai-company-brain/specs/paca_pm_research_2026-08.md b/project-docs/specs/paca_pm_research_2026-08.md similarity index 98% rename from ai-company-brain/specs/paca_pm_research_2026-08.md rename to project-docs/specs/paca_pm_research_2026-08.md index 5612125db..051805342 100644 --- a/ai-company-brain/specs/paca_pm_research_2026-08.md +++ b/project-docs/specs/paca_pm_research_2026-08.md @@ -1,5 +1,10 @@ # Paca PM-platform research — what to adopt, adapt, and refuse (2026-08) +> ⚠️ **RESEARCH — REFERENCE-ONLY (2026-08-10 consolidation, D26).** No work dispatches from +> this document; owns no work by its own declaration. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Product:** CommandCenter · **Concern:** research appendix for the native project-management > app (WS-27) · **Created:** 2026-08-05 · **Status:** 🟢 research complete — **reference-only, > owns no work and no status**; every adaptation verdict below is annealed into diff --git a/ai-company-brain/specs/people_center_app.md b/project-docs/specs/people_center_app.md similarity index 100% rename from ai-company-brain/specs/people_center_app.md rename to project-docs/specs/people_center_app.md diff --git a/ai-company-brain/specs/permissions_sandbox_b6.md b/project-docs/specs/permissions_sandbox_b6.md similarity index 100% rename from ai-company-brain/specs/permissions_sandbox_b6.md rename to project-docs/specs/permissions_sandbox_b6.md diff --git a/ai-company-brain/specs/plane_pm_research_2026-08.md b/project-docs/specs/plane_pm_research_2026-08.md similarity index 98% rename from ai-company-brain/specs/plane_pm_research_2026-08.md rename to project-docs/specs/plane_pm_research_2026-08.md index 69c08025c..812e02dc8 100644 --- a/ai-company-brain/specs/plane_pm_research_2026-08.md +++ b/project-docs/specs/plane_pm_research_2026-08.md @@ -1,5 +1,10 @@ # Plane PM-platform research — what to adopt, adapt, and refuse (2026-08) +> ⚠️ **RESEARCH — REFERENCE-ONLY (2026-08-10 consolidation, D26).** No work dispatches from +> this document; its P-queue was minted as WS-27u–z in `project_management_app.md` §9.1. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Product:** CommandCenter · **Concern:** second research appendix for the native > project-management app (WS-27), beside `paca_pm_research_2026-08.md` · **Created:** > 2026-08-09 · **Status:** 🟢 research complete — **reference-only, owns no work and no diff --git a/ai-company-brain/specs/project_management_app.md b/project-docs/specs/project_management_app.md similarity index 100% rename from ai-company-brain/specs/project_management_app.md rename to project-docs/specs/project_management_app.md diff --git a/ai-company-brain/specs/saas_multitenancy.md b/project-docs/specs/saas_multitenancy.md similarity index 100% rename from ai-company-brain/specs/saas_multitenancy.md rename to project-docs/specs/saas_multitenancy.md diff --git a/ai-company-brain/specs/saas_multitenancy_handover.md b/project-docs/specs/saas_multitenancy_handover.md similarity index 98% rename from ai-company-brain/specs/saas_multitenancy_handover.md rename to project-docs/specs/saas_multitenancy_handover.md index d0b124140..1c8bfd471 100644 --- a/ai-company-brain/specs/saas_multitenancy_handover.md +++ b/project-docs/specs/saas_multitenancy_handover.md @@ -21,9 +21,9 @@ block; prod apply rides **PR #404**, the owner's merge) · **Created:** 2026-08- You are executing WS-29 (multi-tenancy) on CommandCenter. READ IN THIS ORDER, FULLY, BEFORE TOUCHING ANYTHING: - 1. ai-company-brain/specs/saas_multitenancy_handover.md (this runbook — order + gates) - 2. ai-company-brain/specs/saas_multitenancy.md (§0.1, §0.9, §1, §5.1, §11) - 3. ai-company-brain/specs/saas_multitenancy_implementation.md (SQL + seam shapes) + 1. project-docs/specs/saas_multitenancy_handover.md (this runbook — order + gates) + 2. project-docs/specs/saas_multitenancy.md (§0.1, §0.9, §1, §5.1, §11) + 3. project-docs/specs/saas_multitenancy_implementation.md (SQL + seam shapes) 4. AGENTS.md at the repo root, then every AGENTS.md on the path to each file you edit NON-NEGOTIABLE: diff --git a/ai-company-brain/specs/saas_multitenancy_implementation.md b/project-docs/specs/saas_multitenancy_implementation.md similarity index 100% rename from ai-company-brain/specs/saas_multitenancy_implementation.md rename to project-docs/specs/saas_multitenancy_implementation.md diff --git a/ai-company-brain/specs/single_agent_chat_bug_audit_2026-07.md b/project-docs/specs/single_agent_chat_bug_audit_2026-07.md similarity index 98% rename from ai-company-brain/specs/single_agent_chat_bug_audit_2026-07.md rename to project-docs/specs/single_agent_chat_bug_audit_2026-07.md index c064d5e14..7e5bb2b9c 100644 --- a/ai-company-brain/specs/single_agent_chat_bug_audit_2026-07.md +++ b/project-docs/specs/single_agent_chat_bug_audit_2026-07.md @@ -1,5 +1,10 @@ # Single-Agent Chat Bug Audit — 2026-07 +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document — the audited defects are closed. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + **Status:** audit complete · P0 fixes landed 2026-07-22 (T1, C2, C1/CX1 — regression tests in `tests/unit/test_run_agent_stream_e2e.py` and `tests/unit/test_v1_context_fit.py`) · P1 fixes landed 2026-07-22 (R2 diff --git a/ai-company-brain/specs/skills_registry.md b/project-docs/specs/skills_registry.md similarity index 100% rename from ai-company-brain/specs/skills_registry.md rename to project-docs/specs/skills_registry.md diff --git a/ai-company-brain/specs/skills_scope_out.md b/project-docs/specs/skills_scope_out.md similarity index 100% rename from ai-company-brain/specs/skills_scope_out.md rename to project-docs/specs/skills_scope_out.md diff --git a/ai-company-brain/specs/subscription_console.md b/project-docs/specs/subscription_console.md similarity index 100% rename from ai-company-brain/specs/subscription_console.md rename to project-docs/specs/subscription_console.md diff --git a/ai-company-brain/specs/task_manager_app.md b/project-docs/specs/task_manager_app.md similarity index 99% rename from ai-company-brain/specs/task_manager_app.md rename to project-docs/specs/task_manager_app.md index 9cfc38adc..2d04db024 100644 --- a/ai-company-brain/specs/task_manager_app.md +++ b/project-docs/specs/task_manager_app.md @@ -837,7 +837,7 @@ cd workbench/control_plane && npx eslint src/app/tasks && npm run build **Where everything lives** | Layer | Path | |---|---| -| Spec (this doc) | `ai-company-brain/specs/task_manager_app.md` | +| Spec (this doc) | `project-docs/specs/task_manager_app.md` | | DB migrations | `infra/postgres/48_task_manager_gtd.sql` · `49_gtd_people.sql` | | HR seed + import | `infra/seed/hr/` · `scripts/import_hr_people.py` | | Gateway API (21 endpoints, §8) | `apps/services/gateway/gateway/routes/tasks/` (`core` · `accounts` · `items` · `ai` · `people` · `providers` — path corrected 2026-08-01, doc-truth pass; the package has since grown: `sync` · `calendar` · `capture_email` · `planning` · `priority` · …) | diff --git a/ai-company-brain/specs/task_manager_harness_2026-07.md b/project-docs/specs/task_manager_harness_2026-07.md similarity index 96% rename from ai-company-brain/specs/task_manager_harness_2026-07.md rename to project-docs/specs/task_manager_harness_2026-07.md index b10764c30..4b35c9ae1 100644 --- a/ai-company-brain/specs/task_manager_harness_2026-07.md +++ b/project-docs/specs/task_manager_harness_2026-07.md @@ -1,5 +1,10 @@ # Task Manager × Harness Engineering (2026-07-03) +> ⚠️ **HISTORICAL RECORD (2026-08-10 consolidation, D26).** No work dispatches from +> this document. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Status:** Tier 1 shipped 2026-07-03 · Tier 2 planned, no board row (WS-18 owns task-manager work) · not verified against code since 2026-07-03. *(Header added 2026-08-09.)* > **What this is.** The task-manager app reviewed against the practice areas in diff --git a/ai-company-brain/specs/task_manager_hr_planning_and_memory.md b/project-docs/specs/task_manager_hr_planning_and_memory.md similarity index 100% rename from ai-company-brain/specs/task_manager_hr_planning_and_memory.md rename to project-docs/specs/task_manager_hr_planning_and_memory.md diff --git a/ai-company-brain/specs/tenancy_and_visibility.md b/project-docs/specs/tenancy_and_visibility.md similarity index 100% rename from ai-company-brain/specs/tenancy_and_visibility.md rename to project-docs/specs/tenancy_and_visibility.md diff --git a/ai-company-brain/specs/user_management_contract.md b/project-docs/specs/user_management_contract.md similarity index 100% rename from ai-company-brain/specs/user_management_contract.md rename to project-docs/specs/user_management_contract.md diff --git a/ai-company-brain/specs/whatsapp_calls_note_taker.md b/project-docs/specs/whatsapp_calls_note_taker.md similarity index 99% rename from ai-company-brain/specs/whatsapp_calls_note_taker.md rename to project-docs/specs/whatsapp_calls_note_taker.md index 83ef3114d..07647a074 100644 --- a/ai-company-brain/specs/whatsapp_calls_note_taker.md +++ b/project-docs/specs/whatsapp_calls_note_taker.md @@ -1,5 +1,10 @@ # WhatsApp Calls → Note Taker — feasibility study & UX design +> ⚠️ **DEFERRED — FUTURE SCOPE (2026-08-10 consolidation, D26).** No work dispatches from +> this document; no board row exists. The active plan is `project-docs/work_plan.md` §2; +> the classification of record is `project-docs/INDEX.md`. + + > **Product:** CommandCenter · **Feature:** extend the AI Note Taker (`/notes`) to WhatsApp voice calls, including group calls > **Created:** 2026-08-01 · **Updated:** 2026-08-02 · **Status:** 🔬 feasibility study (§0–§11, the design record) · ✅ **Surface C SHIPPED + DEPLOYED** — see §12. Place and answer 1:1 and group WhatsApp calls from `/whatsapp/calls` or a chat's Call button, **speak and listen** through the browser, and every call is recorded server-side. **Transcription is not wired yet** (§12.5) — the recording is produced but nothing consumes it. > **Siblings:** [`note_taker_app.md`](note_taker_app.md) (the note taker we're extending) · [`meeting_bot_platform_plan.md`](meeting_bot_platform_plan.md) (the bot-joins-a-call pattern) · [`whatsapp_message_manager.md`](whatsapp_message_manager.md) (the WhatsApp vertical we'd hang this off) diff --git a/ai-company-brain/specs/whatsapp_message_manager.md b/project-docs/specs/whatsapp_message_manager.md similarity index 100% rename from ai-company-brain/specs/whatsapp_message_manager.md rename to project-docs/specs/whatsapp_message_manager.md diff --git a/ai-company-brain/specs/workflows_app.md b/project-docs/specs/workflows_app.md similarity index 99% rename from ai-company-brain/specs/workflows_app.md rename to project-docs/specs/workflows_app.md index 7e65650bf..3546c34ca 100644 --- a/ai-company-brain/specs/workflows_app.md +++ b/project-docs/specs/workflows_app.md @@ -318,7 +318,7 @@ Anything in Slice 4 that looks reachable today is reachable only because its dep ## 10. Policy reconciliation - **ADR-028** (new, `system_architecture.md`): visual workflow composition as DB-persisted configuration compiled to MAF Workflows — amends **ADR-014** (whose VS-Code-only rule now applies to *code* artifacts: agents, skills, app code) and supersedes the "no visual workflow canvas / no n8n-style second runtime" non-goal as written in `project_plan.md` §2 and **C-09**, which are updated to say what they always meant: **no second runtime engine, no in-app *code* authoring** — both preserved by this design. -- Root `AGENTS.md` constraint #1 and `ai-company-brain/AGENTS.md` non-negotiable #1 gain the same carve-out sentence; #6 ("MAF is the sole runtime — no n8n") is *strengthened* by this feature, not weakened: the graph runs *on MAF*. +- Root `AGENTS.md` constraint #1 and `project-docs/AGENTS.md` non-negotiable #1 gain the same carve-out sentence; #6 ("MAF is the sole runtime — no n8n") is *strengthened* by this feature, not weakened: the graph runs *on MAF*. ## 11. Risks & open questions diff --git a/ai-company-brain/system_architecture.md b/project-docs/system_architecture.md similarity index 100% rename from ai-company-brain/system_architecture.md rename to project-docs/system_architecture.md diff --git a/ai-company-brain/work_plan.md b/project-docs/work_plan.md similarity index 95% rename from ai-company-brain/work_plan.md rename to project-docs/work_plan.md index b24cc15b4..994cce439 100644 --- a/ai-company-brain/work_plan.md +++ b/project-docs/work_plan.md @@ -32,7 +32,8 @@ and the five member/group write endpoints join §4 and §6; **D14** records that "org-wide visibility" is a name and the department-privacy question is really about `admin:members:read`) · **Owner:** vjvarada **Purpose:** the single sequencing document from which independent agents are -dispatched. Content lives in the owning specs; *this* doc owns ordering, +dispatched. **Navigation for agents: `project-docs/INDEX.md` is the +classification of record (D26) — build only from specs it lists ACTIVE.** Content lives in the owning specs; *this* doc owns ordering, ownership, and the rules that make a spec executable without questions. Built from a three-way audit (2026-07-31) of the foundation docs, the app @@ -118,7 +119,7 @@ looks. Full statements live in `FOUNDATION_BUILDOUT_CHECKLIST.md`. | # | Exception | Where it lives | Why it can't wait for "after the apps" | |---|---|---|---| -| 1 | ~~**`main` has no branch protection**~~ — **CLOSED 2026-08-03** | WS-5 · checklist §BO-17 | Was `404 Branch not protected` with rulesets `[]` under both mechanisms, so every CI gate in the YAMLs was decorative. **Enabled 2026-08-03** (owner-authorised in-session): PRs required, `required_approving_review_count: 0`, **`enforce_admins: true`**, force-push and deletion blocked. Verified by reading the protection back. ⚠️ **`required_status_checks` is deliberately `null`**: `pr-check.yml` has `paths-ignore: ["**.md", "ai-company-brain/**"]`, so a docs-only PR produces **no** check-runs — requiring those contexts would make every docs PR permanently unmergeable (this row's own PR included). Tightening path: add an always-runs sentinel job to `pr-check`, then require **that** one context. | +| 1 | ~~**`main` has no branch protection**~~ — **CLOSED 2026-08-03** | WS-5 · checklist §BO-17 | Was `404 Branch not protected` with rulesets `[]` under both mechanisms, so every CI gate in the YAMLs was decorative. **Enabled 2026-08-03** (owner-authorised in-session): PRs required, `required_approving_review_count: 0`, **`enforce_admins: true`**, force-push and deletion blocked. Verified by reading the protection back. ⚠️ **`required_status_checks` is deliberately `null`**: `pr-check.yml` has `paths-ignore: ["**.md", "project-docs/**"]`, so a docs-only PR produces **no** check-runs — requiring those contexts would make every docs PR permanently unmergeable (this row's own PR included). Tightening path: add an always-runs sentinel job to `pr-check`, then require **that** one context. | | 2 | ~~**No backup / restore path**~~ — **CLOSED in substance 2026-08-07** | checklist §BO-23 | Nightly `acb-backup.timer` **verified scheduled 2026-08-07** (after three same-day defects: #382 wrong script, #383 fork bomb, #384 mig-148 cast); a restore was rehearsed for real 2026-08-05 (`live=228 restored=228`); the migration **ledger is merged** (`5f025d80`, renumbered to 153), so a deploy stops replaying the whole ladder. Residue: `BACKUP_REMOTE` unset — off-box copy **deferred by owner decision 2026-08-05** (`backup_and_restore.md` §4.2); losing the disk, box or provider account still falls back to the weekly two-deep Hostinger image. Verify backups by deploy-log lines, never job conclusion. | | 3 | ~~**DB engine sprawl**~~ **CLOSED 2026-08-06** | checklist §BO-10 | Measured 2026-08-03: **12 `create_async_engine(...)` call sites across 10 modules** (`acb_auth/access.py:69`; gateway `routes/{admin,apps,email,notes,tasks,whatsapp,workflows}/*core*.py`; `email_ingestion/{inbound,scheduler}.py` ×4), plus a 13th **sync** `create_engine` in `acb_graph/db.py:32`. Eight are module-level cached `_ENGINE` singletons and **none of them is disposed on shutdown** — the only `engine.dispose()` calls in the tree are the four `email_ingestion` per-call engines cleaning up after themselves. **This is the one that compounds: one engine per app, added by each app.** The next app should extend a shared seam, not add engine 13. **CLOSED 2026-08-06:** every async caller now resolves to ONE engine and pool in `packages/acb_common/acb_common/db.py` — not `acb_graph` (the gateway does not depend on it, and its engine is sync) and not `gateway/db.py` (which `acb_auth/access.py` cannot import, so a gateway-owned seam could never get below two pools in the gateway process). The six remaining route packages plus `acb_auth.access` were converted, each keeping its historical `get_db`/`_get_db`/`_get_session_factory` name as a re-export so ~50 call sites and every test monkeypatch are untouched; `gateway/db.py` is a re-export. `acb_auth`'s engine had never carried the 2026-08-06 connect/`idle in transaction` bounds — it does now. Pool ceiling 30 (tunable via `db_pool_size`/`db_max_overflow`), deliberately not the old ~165 sum, which exceeded a stock `max_connections` of 100 shared with Langfuse/LiteLLM/ingestion. `acb_audit.record()` is non-blocking on the loop (`to_thread` only when a loop is running; sync callers still inline) and `acb_audit.drain()` is awaited last in the gateway lifespan. Guarded by `tests/unit/test_db_engine_seam.py` + `tests/unit/test_audit_non_blocking.py`. Still open by design: `acb_graph/db.py`'s **sync** `create_engine` and `email_ingestion`'s per-run engines. | @@ -185,12 +186,12 @@ owning specs are the archive; this file owns ordering, gates and states only. | WS-20 | **WhatsApp activation + remainder** | 🟡 owner | `whatsapp_message_manager.md` §11 | Search UI 🟢 AGENT-SAFE; ~~OCR needs a vision-tier decision~~ **OCR tier ANSWERED 2026-08-10 (D25.6): fast/cheap vision via LiteLLM — the OCR ticket is dispatchable**; Odoo/Zoho-bound items bind to `crm` `entity_ref` per WS-26d instead — the linker (nothing writes `wa_contacts.entity_ref`) is owed by whoever takes them. 🔴 Meta env/app review · `WHATSAPP_ENRICHMENT` flip (§6). (2026-08-01) | | WS-21 | **Calendar F2/F3** | 🟡 partial | `calendar_focus_os.md` §9 (+§5) + `calendar_timeboxing.md` §13 · board record 2026-08-09 | P3 roll-over + ideal-week + packer-breaks all shipped (struck from scope 2026-08-03). `gtd_time_blocks` is **four slices S1–S4** — the "one non-breaking PR" claim was false (17 TS files + 3 gateway modules + skill + agent). Focus Shield is AGENT-SAFE (needs a design, not a credential). Owns Horizons (§4) — DO-NOT-DISPATCH, no acceptance. 🔴 external-sync OAuth credentials (§6) · shared nudge-send gate (§6). Never `pytest tests/unit -k calendar` (collection hangs). (2026-08-03) | | WS-22 | **draw.io** | ⏸ PARKED | `drawio_integration.md` | **PARKED BY OWNER 2026-08-10 (D25.7)** — no agent time until a real need (proposal diagrams, KB visuals) pulls it back. The spec's acceptance structure keeps; anchors need re-verification at un-park. | -| WS-26 | **CRM app — native CRM + Zoho retirement** *(minted 2026-08-05)* | ✅ a–g · D5 PR open | `specs/crm_app.md` · board record 2026-08-09 | a + b + c + d (read · email · write) **merged + deployed** (d-write log-verified via deploy `31217978773`, 2026-08-08); f + g **merged to main** (#391, #397 — the old "on branch, NOT run against prod" wording is struck; f's stage repair still needs its 🔴 `?apply=true` run, §6 WS-26 (d)). **D5 d-autolead BUILT, PR #403 OPEN** — owner: merge, then 🔴 `CRM_AUTO_LEAD` flip (§6 WS-26 (b); clamp-anchor design, never reset-to-now). Zoho sync loop **ENABLED by the owner 2026-08-06** (§6 WS-26 (a)) — every "ships OFF / never run" sentence about it is struck. Next: **h** stage entry-requirements + rot badges (after f2) · **i** merge/bulk/CSV/saved-views — spec-thin, audit-narrow first · **e** cutover + retirement 🔴 (§6 WS-26 (c)). ⚠️ D15 coda: built single-Zoho-tenant by design; per-org credentials (migration 158) + per-org sync flags arrive with MT-1/MT-2, and D-CRM-3's org-wide read becomes org-scoped **by RLS**, not by hand-written predicates. (2026-08-08) | -| WS-27 | **Projects app — native PM + ClickUp retirement** *(minted 2026-08-05)* | ✅ a–n merged · **o–t on PR #399** · c/g/h gated | `specs/project_management_app.md` · board record 2026-08-09 | a b d e f i j k l m n **merged to main** (#390, #393, #394, #398 + fixes — the board's "BUILT on branch" wording is struck). ~~Open defect: **§11.12** — WS-27j's `notifications.deliverable` probes `project_clause`~~ ✅ **FIXED on #399** (assignees without a project grant were judged undeliverable, so assignment notified nobody). 🟡 **c** two-way sync waits on WS-1's BO-1a + BO-1b; 🔴 push enable (§6 WS-27 (b)) · 🔴 **g** cutover + retirement incl. the root-`AGENTS.md` constraint-8 amendment — ships in the g PR, never before (§6 WS-27 (c)) · **h** `gtd_items` retirement after e; the data move is 🔴. ~~Remaining letters: recurring, dependency UI, calendar view, search.~~ ✅ **the §11.2 ClickUp-parity backlog is CLOSED** — o recurrence · p dependencies+subtasks · q calendar · r ⌘K search · s shared task card · t timeline, all on **PR #399** with D-PM-11/D-PM-12 recorded. **Second reference studied 2026-08-09: `makeplane/plane` v1.4.1 (⚠️ AGPL-3.0 — patterns only, never code)** → `specs/plane_pm_research_2026-08.md` + spec §11.19: 12 shipped decisions validated, beyond-parity queue P-1…P-31 minted → **minted as dispatchable tickets WS-27u–z (spec §9.1)**: u intake/triage · v watchers+mention-diff · w read-path/history hardening · x spreadsheet+shown-fields · y board upgrades · z lifecycle policy (🟡 per-project, default off) + a deferred small basket, 2 owner questions ANSWERED same day → **D-PM-13** (project docs live in the knowledge base — creator-owned, grant-shared; PM links, never owns) · **D-PM-14** (public boards deferred). ⚠️ granting `feature:projects`/`data:org:read` is §6 WS-27 (d) — D14's zero-consumer measurement is retired by this row. (2026-08-07) | +| WS-26 | **CRM app — native CRM + Zoho retirement** *(minted 2026-08-05)* | ✅ a–g · D5 PR open | `specs/crm_app.md` · board record 2026-08-09 | a + b + c + d (read · email · write) **merged + deployed** (d-write log-verified via deploy `31217978773`, 2026-08-08); f + g **merged to main** (#391, #397 — the old "on branch, NOT run against prod" wording is struck; f's stage repair still needs its 🔴 `?apply=true` run, §6 WS-26 (d)). **D5 d-autolead MERGED 2026-08-09 (#403; migration renumbered 158→163 at merge)** — remaining: 🔴 `CRM_AUTO_LEAD` flip (§6 WS-26 (b); clamp-anchor design, never reset-to-now). Zoho sync loop **ENABLED by the owner 2026-08-06** (§6 WS-26 (a)) — every "ships OFF / never run" sentence about it is struck. Next: **h** stage entry-requirements + rot badges (after f2) · **i** merge/bulk/CSV/saved-views — spec-thin, audit-narrow first · **e** cutover + retirement 🔴 (§6 WS-26 (c)). ⚠️ D15 coda: built single-Zoho-tenant by design; per-org credentials (migration 158) + per-org sync flags arrive with MT-1/MT-2, and D-CRM-3's org-wide read becomes org-scoped **by RLS**, not by hand-written predicates. (2026-08-08) | +| WS-27 | **Projects app — native PM + ClickUp retirement** *(minted 2026-08-05)* | ✅ a–t merged (o–t via #399, 2026-08-09) · c/g/h gated | `specs/project_management_app.md` · board record 2026-08-09 | a b d e f i j k l m n **merged to main** (#390, #393, #394, #398 + fixes — the board's "BUILT on branch" wording is struck). ~~Open defect: **§11.12** — WS-27j's `notifications.deliverable` probes `project_clause`~~ ✅ **FIXED on #399** (assignees without a project grant were judged undeliverable, so assignment notified nobody). 🟡 **c** two-way sync waits on WS-1's BO-1a + BO-1b; 🔴 push enable (§6 WS-27 (b)) · 🔴 **g** cutover + retirement incl. the root-`AGENTS.md` constraint-8 amendment — ships in the g PR, never before (§6 WS-27 (c)) · **h** `gtd_items` retirement after e; the data move is 🔴. ~~Remaining letters: recurring, dependency UI, calendar view, search.~~ ✅ **the §11.2 ClickUp-parity backlog is CLOSED** — o recurrence · p dependencies+subtasks · q calendar · r ⌘K search · s shared task card · t timeline, all on **PR #399** with D-PM-11/D-PM-12 recorded. **Second reference studied 2026-08-09: `makeplane/plane` v1.4.1 (⚠️ AGPL-3.0 — patterns only, never code)** → `specs/plane_pm_research_2026-08.md` + spec §11.19: 12 shipped decisions validated, beyond-parity queue P-1…P-31 minted → **minted as dispatchable tickets WS-27u–z (spec §9.1)**: u intake/triage · v watchers+mention-diff · w read-path/history hardening · x spreadsheet+shown-fields · y board upgrades · z lifecycle policy (🟡 per-project, default off) + a deferred small basket, 2 owner questions ANSWERED same day → **D-PM-13** (project docs live in the knowledge base — creator-owned, grant-shared; PM links, never owns) · **D-PM-14** (public boards deferred). ⚠️ granting `feature:projects`/`data:org:read` is §6 WS-27 (d) — D14's zero-consumer measurement is retired by this row. (2026-08-07) | | WS-28 | **People Center — directory, org chart, assignment seam** *(minted 2026-08-06)* | ✅ a+b+b-write | `specs/people_center_app.md` · board record 2026-08-09 | a (key shape, mig 148 + quarantine table) · b (directory + person page, mig 149, five-place registration) · b-write (create/edit UI restored; found three ways mig 148 had broken the write routes) — built 2026-08-06/07; **closes WS-13's directory item**. 🟢 c org chart · d capability search (**ranking EVAL-LOCKED**) · e Projects seams; 🔴 f seats/roles writes (§6 WS-24 (d) analogue). ⚠️ `schema.generated.sql` regeneration is **due**: stale since ~migration 113, and 148 reached prod ~2026-08-07 (after the #384 cast fix). (2026-08-07) | --- -## 3. Decisions recorded (D1–D14: 2026-07-31→08-04 · D15/D16: 2026-08-08 · D17–D21: 2026-08-09 · D22–D25: 2026-08-10) +## 3. Decisions recorded (D1–D14: 2026-07-31→08-04 · D15/D16: 2026-08-08 · D17–D21: 2026-08-09 · D22–D26: 2026-08-10) Resolutions for the cross-doc conflicts the audit surfaced. D1–D8, **D13**, **D14**, **D16** and **D17** are **proposed defaults, adopted unless the owner objects** @@ -199,6 +200,19 @@ calls, taken and dated. ⚠️ Two entries below are superseded and kept as reco **D11** (re-taken by D15) and **D10 part 1's planning premise** (re-scoped by D15/D16) — read their banners before citing either. +- **D26 — The docs consolidation: `ai-company-brain/` → `project-docs/`, and an + ACTIVE/DEFERRED classification of record.** *(owner-directed 2026-08-10 — + "retire what is not in the immediate work plan, make active vs deferred + clear, simplify so agents focus on required code".)* The folder renamed + (261 tracked files swept, CI path filters included); **`project-docs/INDEX.md` + is the classification of record** — ACTIVE (dispatch) · CONTRACTS & DOCTRINE · + REFERENCE · DEFERRED/HISTORICAL (19 files banner-marked, dispatching + nothing). Standing rule: a new spec enters INDEX in its creating PR, in + exactly one section; leaving ACTIVE takes a banner in the same PR. The + `docs/` root stays engineering reference (one ACTIVE owning spec lives + there: `docs/multiplayer/memory-clearance.md`). #399's docs classified: + `multi_tenancy.md` superseded-measured-record, leak audit = MT-1i reference, + branch HANDOVER historical, plane research reference-only. - **D25 — Seven standing gates answered in one round (owner, 2026-08-10).** 1. **Identity/IdP (MT-1a):** homegrown auth through the silo phase; an external IdP (WorkOS/Clerk/Keycloak) becomes a precondition of the §5.1 @@ -691,7 +705,7 @@ D15/D16) — read their banners before citing either. | **Colleague onboarding readiness** (the pre-invite gate, the invite runbook, the role × app capability matrix) | **WS-24 — `specs/colleague_onboarding.md`** | `org_access_control.md` §7 (bootstrap) and its role table, lines 79-83 — **line 81's `manager` row was corrected 2026-08-04 per D14** (intent no longer claims org-wide data; the grants cell is now the literal seeded array, because *"all `feature:*` except `build.*`"* was wrong in five slugs) · `department_centers.md` §2 (the five-place Center registration checklist the runbook's step 3 depends on) · `tenancy_and_visibility.md` §5 (the app-by-app gap table — that doc owns the *doctrine*, this one owns the *measured current state per role*) | | **The single member of record** | **`vjvarada@fracktal.in` is the only signed-in member** (⚠️ **owner-reported 2026-08-04, NOT measured** — it is a live-DB fact and §6 forbids an agent the tool that could measure it; re-check on the box before relying on it) | There is exactly one. `EXECUTIVE_EMAILS` is the bootstrap candidate list (`acb_auth/access.py:467-519`) and is **not** a role — a member's real access is resolved from `app_user` + `user_role` + `user_permission_override`. **No agent may add, promote or suspend a member**: `POST /admin/members`, `PUT /admin/members/{email}/roles` and `PATCH/DELETE /admin/members/{email}` are live-DB writes to the access model and are registered in §6. Onboarding runbook: `specs/colleague_onboarding.md` §2 | | Groups admin UI + seeding | **WS-13 / Centers B** | groups_sessions_authority §6.5 · org_access §8 Ph2 · multiplayer §4.5 | -| Team-instanced agents | **WS-14 / Centers C3** (mechanism per D3) | `docs/multiplayer/agent-kinds.md` §6/§8 — **note the path: it is under `docs/multiplayer/`, not `ai-company-brain/specs/`**, and its §6 roster is a **design proposal, not a work list** (seven of its twelve agents do not exist; it contradicts three shipped `config.json` files — both annotated there 2026-08-03) · agent_architecture §6/§12A · memory_architecture §6.1 · groups §6.2 | +| Team-instanced agents | **WS-14 / Centers C3** (mechanism per D3) | `docs/multiplayer/agent-kinds.md` §6/§8 — **note the path: it is under `docs/multiplayer/`, not `project-docs/specs/`**, and its §6 roster is a **design proposal, not a work list** (seven of its twelve agents do not exist; it contradicts three shipped `config.json` files — both annotated there 2026-08-03) · agent_architecture §6/§12A · memory_architecture §6.1 · groups §6.2 | | Shared mailboxes | ⚠️ **OWNERLESS IN FACT — do not dispatch** (measured 2026-08-03) | D5 assigns implementation to `email_app_master_plan.md`, sequenced by WS-14. That spec contains **zero** occurrences of "shared mailbox", and `email_account_member` — cited as Phase-2 content by `department_centers.md` and `org_access_control.md:311` — **exists nowhere in the repo** (0 hits in `*.sql` and `*.py`). D5's *sequencing* stands; its *ownership* is nominal. **Next action is a doc action, not a build:** either `email_app_master_plan.md` gains a section for it, or this row names a different owner. Recorded in `department_centers.md` C2. | org_access §8 Ph2 · groups §1 · research §16.7 | | Per-Center approvals routing | **WS-14 C4 — 🔴 OWNER-DECISION, not dispatchable** | `org_access_control.md:405` Q2 is open verbatim (*"who is asked? … per-module approvers is a Phase 2 question"*), **and there is no column to route on**: `infra/postgres/66_pending_actions.sql:13-38` carries no requesting-member, group or Center column. ⚠️ **Corrected 2026-08-03** — this cell used to add "(`actor` is the proposing *agent*)", which is **false**: two of `actor`'s six writers put the requesting human in the string (`routes/apps/tools.py:393`, `routes/apps/actions.py:345`, both `actor=f"app:{slug}:{email}"`), so a group **is** derivable there. The verdict holds on the real evidence — `actor` is free text with five shapes (`app::` · `app:` · `workflow:` · `tasks:` · `tasks:clickup:ws:`) and a human in only **two of six** proposers, so a Center inbox filtered on it would be silently empty for every workflow-, publish- and provider-originated proposal. The new column must be written by every proposer, not parsed from an ad-hoc string. Evidence: `department_centers.md` C4. The ticket is "answer Q2, then add a column", never "add a filter" | | Cost attribution | **WS-6** (D1) | multiplayer §5.3/Ph4 · agent-kinds §9 Q4 · Centers D | @@ -717,7 +731,7 @@ D15/D16) — read their banners before citing either. > **Update 2026-08-01 (doc-truth pass): EXECUTED.** All Tier 1–3 items below > were applied by a six-agent pass, each edit verified against code first. > Kept as the record of what changed. **Residual items** (new or deferred): -> 1. ~~`ai-company-brain/AGENTS.md` build-table rows are themselves stale~~ +> 1. ~~`project-docs/AGENTS.md` build-table rows are themselves stale~~ > **CLOSED 2026-08-09** — the "What Has Already Been Built (as of > 2026-06-20)" table was retired outright rather than refreshed: it was a > second competing status description (40%+ wrong: broker/meeting-bot/ @@ -738,16 +752,16 @@ D15/D16) — read their banners before citing either. > 5. ~~D9 (Pomad Centre) remains an owner call~~ — resolved 2026-08-01, all > 12 sites rewritten as "a second tenant deployment" (see D9). > 6. **Spec-index and docstring staleness (new, 2026-08-03).** -> `ai-company-brain/AGENTS.md`'s per-feature spec index is missing rows: it +> `project-docs/AGENTS.md`'s per-feature spec index is missing rows: it > has **no calendar row at all** (four calendar specs, none listed) and no > `agent_architecture.md` entry. Separately, -> `ai-company-brain/AGENTS.md:190` and `apps/AGENTS.md:23` still carry the +> `project-docs/AGENTS.md:190` and `apps/AGENTS.md:23` still carry the > struck falsehood that the Action Broker *"ships with zero handlers and is > not yet wired into the write path"* — untrue since 2026-07-13; see WS-1's > five registration sites. ~~Both are AGENT-SAFE doc fixes, neither is in this > change.~~ **CLOSED 2026-08-09** — spec index completed (16 missing rows > added, incl. the whole calendar cluster and `crm_app.md`) and the broker -> falsehood corrected at `ai-company-brain/AGENTS.md` (glossary + build-row + +> falsehood corrected at `project-docs/AGENTS.md` (glossary + build-row + > priorities) and `apps/AGENTS.md:24`. > 7. **2026-08-09 — WS-29 consolidation pass EXECUTED** (this change). One > sweep, driven by four parallel audits (board digest · MT plan-of-record · diff --git a/pyproject.toml b/pyproject.toml index 8b6c0ba3f..e8eeb59dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ # stitched together as a uv workspace so they share a single .venv + lockfile. [project] -name = "ai-company-brain" +name = "project-docs" version = "0.0.1" description = "Fracktal Works internal multi-agent brain — mirror + reasoning + action layer over ClickUp/Zoho/Odoo." readme = "README.md" diff --git a/scripts/README.md b/scripts/README.md index c02297ad2..1b1743546 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,7 +6,7 @@ One-off operational scripts (migrations, data backfills, secret rotations). Each | Script | Answers | Safe for an agent to run? | |---|---|---| -| `onboarding_preflight.py` | "Is it safe to invite a colleague yet?" — seven readiness checks, PASS/FAIL/SKIP with exact remediation, non-zero exit on any FAIL. Owning spec: `ai-company-brain/specs/colleague_onboarding.md` §1 (board row WS-24). | **Only with `--mode local`.** Its database and systemd checks read the live box, so an agent must never point it at production; `--mode local` refuses those checks and says so rather than guessing. | +| `onboarding_preflight.py` | "Is it safe to invite a colleague yet?" — seven readiness checks, PASS/FAIL/SKIP with exact remediation, non-zero exit on any FAIL. Owning spec: `project-docs/specs/colleague_onboarding.md` §1 (board row WS-24). | **Only with `--mode local`.** Its database and systemd checks read the live box, so an agent must never point it at production; `--mode local` refuses those checks and says so rather than guessing. | | `feature_check.py` | "Is chat + each AI app actually working right now?" — live smoke against a running gateway. | No — it drives real endpoints. | | `check_infra.py` | Container / port / dependency sanity for the local stack. | Yes, locally. | diff --git a/scripts/check_whatsapp_calling.py b/scripts/check_whatsapp_calling.py index 938475ddb..63142643b 100644 --- a/scripts/check_whatsapp_calling.py +++ b/scripts/check_whatsapp_calling.py @@ -1,6 +1,6 @@ """Is the WhatsApp Business Calling API enabled on our number? -Hard gate for Phase A of ai-company-brain/specs/whatsapp_calls_note_taker.md: +Hard gate for Phase A of project-docs/specs/whatsapp_calls_note_taker.md: we can't build a WhatsApp note taker on the official path until Meta has switched calling on for the WABA. Rollout is gated per-number, so this is a question only the live API can answer. diff --git a/scripts/codebase_health.py b/scripts/codebase_health.py index dd69b5f46..0d42d3bb2 100644 --- a/scripts/codebase_health.py +++ b/scripts/codebase_health.py @@ -2,7 +2,7 @@ """Codebase-health measurement — one command, complexity + size + trend report. The shared measurement engine behind the dev-velocity tooling -(ai-company-brain/specs/dev_velocity_tooling_2026-07.md). It reports the +(project-docs/specs/dev_velocity_tooling_2026-07.md). It reports the structural-health signals that predict *agent drag* — the cost a coding agent pays to edit a file: cyclomatic complexity (per function), file size (LOC), and the maintainability index. It is deliberately dependency-light (radon only) and diff --git a/scripts/gen_tenant_migration.py b/scripts/gen_tenant_migration.py index 84a5caec3..8db2eb26d 100644 --- a/scripts/gen_tenant_migration.py +++ b/scripts/gen_tenant_migration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Generate the MT-1b tenancy migration — org_id + FORCE RLS on every table. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §1.3 / MT-1b · +Spec: ``project-docs/specs/saas_multitenancy.md`` §1.3 / MT-1b · shapes in ``saas_multitenancy_implementation.md`` §1 · board WS-29 · D15. WHY A GENERATOR AND NOT A HAND-WRITTEN MIGRATION @@ -198,7 +198,7 @@ def _header(phase: str, why: str, tables: int) -> str: -- MT-1b · phase {phase} — GENERATED, DO NOT EDIT BY HAND -- ============================================================================ -- Regenerate with: uv run python scripts/gen_tenant_migration.py --- Spec: ai-company-brain/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 +-- Spec: project-docs/specs/saas_multitenancy.md §1.3 · MT-1b · WS-29 · D15 -- -- {why} -- diff --git a/scripts/onboarding_preflight.py b/scripts/onboarding_preflight.py index c36001d61..7dc89c351 100644 --- a/scripts/onboarding_preflight.py +++ b/scripts/onboarding_preflight.py @@ -9,7 +9,7 @@ version of that derivation: run it on the box, read the table, fix what is red, and only then invite anybody. -The owning spec is ``ai-company-brain/specs/colleague_onboarding.md`` §1 (the +The owning spec is ``project-docs/specs/colleague_onboarding.md`` §1 (the readiness gate). This file is the executable half of that section — if a criterion moves there, move it here in the same change. @@ -742,7 +742,7 @@ def check_caddy_strip(env: Env, box: bool) -> Check: "deploy/hostinger/RESTORE.md", "deploy/hostinger/BACKUP-RESTORE.md", "deploy/hostinger/BACKUP_RESTORE.md", - "ai-company-brain/specs/backup_and_restore.md", + "project-docs/specs/backup_and_restore.md", "docs/runbooks/restore.md", ) @@ -1210,7 +1210,7 @@ def check_centers_reachable(db: Psql) -> Check: "Add each missing slug to the FEATURES tuple in " "packages/acb_auth/acb_auth/permissions.py, in the migration's " "sort_order. Registering a Center takes five edits, not one — see " - "ai-company-brain/specs/department_centers.md §2 for the " + "project-docs/specs/department_centers.md §2 for the " "checklist. tests/unit/test_org_access_control.py pins both " "directions." ) @@ -1370,7 +1370,7 @@ def render(checks: list[Check], box: bool) -> str: lines = [ "", "COLLEAGUE ONBOARDING PREFLIGHT (WS-24)", - "spec: ai-company-brain/specs/colleague_onboarding.md §1", + "spec: project-docs/specs/colleague_onboarding.md §1", f"mode: {'BOX — full check' if box else 'LOCAL — box-only checks are SKIPped, see below'}", "", ] diff --git a/skills/AGENTS.md b/skills/AGENTS.md index 9ac6f29bf..e2640aab2 100644 --- a/skills/AGENTS.md +++ b/skills/AGENTS.md @@ -8,7 +8,7 @@ Skill definitions and SKILL.md patterns. Each skill is a domain-specific capabil - skills/{category}/{skill_name}/scripts/ -- Python scripts - Categories: delivery/, maintenance/, reconciler/, sales/, triage/, upstream/ (maintenance/ = dev-velocity skills that operate ON this codebase, e.g. - codebase_health_audit — see ai-company-brain/specs/archive/dev_velocity_tooling_2026-07.md) + codebase_health_audit — see project-docs/specs/archive/dev_velocity_tooling_2026-07.md) ## Conventions - SKILL.md has YAML frontmatter with name and description diff --git a/skills/README.md b/skills/README.md index 45294a868..d8bda8746 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,6 +1,6 @@ # Skills (Anthropic `SKILL.md` format · ADR-013) -Phase-0 home for skills. From Phase 0.5 (WBS 0.5.1) onward these move to a dedicated repo `ai-company-brain-skills`, with weekly upstream sync from `anthropics/skills` and `VoltAgent/awesome-agent-skills`. +Phase-0 home for skills. From Phase 0.5 (WBS 0.5.1) onward these move to a dedicated repo `project-docs-skills`, with weekly upstream sync from `anthropics/skills` and `VoltAgent/awesome-agent-skills`. Layout per skill: ``` diff --git a/tests/README.md b/tests/README.md index 2c67d58b4..d060b2c2f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,7 +40,7 @@ CI runs `uv run python -m pytest tests/unit/ -x -v` on every PR ## Email automation tests The email app is the most actively tested area (inbox-zero parity work — see -`ai-company-brain/specs/archive/email_inbox_zero_parity_plan.md` §7). Files: +`project-docs/specs/archive/email_inbox_zero_parity_plan.md` §7). Files: | File | Covers | |---|---| diff --git a/tests/unit/test_admin_groups.py b/tests/unit/test_admin_groups.py index 623831206..ffb1d5d22 100644 --- a/tests/unit/test_admin_groups.py +++ b/tests/unit/test_admin_groups.py @@ -1,6 +1,6 @@ """Groups admin API (Centers Phase B — work_plan.md WS-13). -Spec: ai-company-brain/specs/department_centers.md §3 Phase B; +Spec: project-docs/specs/department_centers.md §3 Phase B; groups_sessions_authority.md §1/§6 (org_group is the primitive, the admin surface was the flagged gap). diff --git a/tests/unit/test_admin_member_offboarding.py b/tests/unit/test_admin_member_offboarding.py index 5544a78a5..c3b9ed592 100644 --- a/tests/unit/test_admin_member_offboarding.py +++ b/tests/unit/test_admin_member_offboarding.py @@ -1,6 +1,6 @@ """Off-boarding yourself — the guard on both doors to ``is_active = False``. -Spec: ``ai-company-brain/specs/colleague_onboarding.md`` §2 Step 5 (WS-24 / N7). +Spec: ``project-docs/specs/colleague_onboarding.md`` §2 Step 5 (WS-24 / N7). ``DELETE /admin/members/{email}`` refused the caller. ``PATCH /admin/members/{email} {"status": "suspended"}`` reaches the same outcome by the diff --git a/tests/unit/test_admin_member_purge.py b/tests/unit/test_admin_member_purge.py index 7a8ec74a4..4d4e1526e 100644 --- a/tests/unit/test_admin_member_purge.py +++ b/tests/unit/test_admin_member_purge.py @@ -1,6 +1,6 @@ """Deleting a member permanently — ``DELETE /admin/members/{email}/purge``. -Spec: ``ai-company-brain/specs/colleague_onboarding.md`` §2 Step 5 (WS-24 / N8). +Spec: ``project-docs/specs/colleague_onboarding.md`` §2 Step 5 (WS-24 / N8). A **second, harder action beside** Remove, not a flag on it. Remove is a soft off-boarding and its reasoning stands; this one destroys the identity and every diff --git a/tests/unit/test_admin_tenancy.py b/tests/unit/test_admin_tenancy.py index d5bde210b..eb6b350d6 100644 --- a/tests/unit/test_admin_tenancy.py +++ b/tests/unit/test_admin_tenancy.py @@ -1,7 +1,7 @@ """The admin plane · the TENANT boundary — what one organization's admin cannot reach in another (WS-29e). -Spec: ``ai-company-brain/specs/multi_tenancy_leak_audit.md`` S1-1 and +Spec: ``project-docs/specs/multi_tenancy_leak_audit.md`` S1-1 and ``multi_tenancy.md`` §3 (D-MT-1 (a)). ``test_projects_tenancy.py`` fences the READ path: one company's portfolio diff --git a/tests/unit/test_agent_manifest.py b/tests/unit/test_agent_manifest.py index 6efc2ffcd..d70a19b7b 100644 --- a/tests/unit/test_agent_manifest.py +++ b/tests/unit/test_agent_manifest.py @@ -1,6 +1,6 @@ """Agent manifest — derivation, validation, and equivalence with today. -Spec: ``ai-company-brain/specs/agent_architecture.md``. +Spec: ``project-docs/specs/agent_architecture.md``. The manifest is introduced as a PARALLEL representation: it computes values the platform currently derives from ``config.json`` plus hardcoded defaults, but is diff --git a/tests/unit/test_agent_paths.py b/tests/unit/test_agent_paths.py index eb13bc6b4..05521bf1c 100644 --- a/tests/unit/test_agent_paths.py +++ b/tests/unit/test_agent_paths.py @@ -1,6 +1,6 @@ """Agent code and per-tenant state resolve to separate directories. -Spec: ai-company-brain/specs/agent_architecture.md §2 +Spec: project-docs/specs/agent_architecture.md §2 Two properties carry the whole design: diff --git a/tests/unit/test_agent_run_identity.py b/tests/unit/test_agent_run_identity.py index 409e6800e..43e5ad3c6 100644 --- a/tests/unit/test_agent_run_identity.py +++ b/tests/unit/test_agent_run_identity.py @@ -1,6 +1,6 @@ """S1-4 — an agent run's acting user must not outlive the run, or cross into another. -Spec: ``ai-company-brain/specs/multi_tenancy_leak_audit.md`` §S1-4. +Spec: ``project-docs/specs/multi_tenancy_leak_audit.md`` §S1-4. Both executors used to open every run with:: diff --git a/tests/unit/test_blob_store_instance.py b/tests/unit/test_blob_store_instance.py index 8a402e5df..3999a46c5 100644 --- a/tests/unit/test_blob_store_instance.py +++ b/tests/unit/test_blob_store_instance.py @@ -1,6 +1,6 @@ """The agent file store partitions by instance (migration 136). -Spec: ai-company-brain/specs/memory_architecture.md §5.3, §6.1 +Spec: project-docs/specs/memory_architecture.md §5.3, §6.1 Before 117, ``agent_blob`` was keyed by ``(agent_name, path)`` — one ``agent-data/NOTES.md`` per agent, shared by every user of it, returned whole by diff --git a/tests/unit/test_chat_hardening.py b/tests/unit/test_chat_hardening.py index 9f48bc019..b6c3532da 100644 --- a/tests/unit/test_chat_hardening.py +++ b/tests/unit/test_chat_hardening.py @@ -1,7 +1,7 @@ """Regression tests — chat-stack hardening batches 1-2. Batch 1: review P0-1/2/4/5, P1-8. Batch 2: P0-7/8, P1-4. -See ai-company-brain/specs/chat_implementation_review_2026-07.md. +See project-docs/specs/chat_implementation_review_2026-07.md. """ from __future__ import annotations diff --git a/tests/unit/test_crm_agent.py b/tests/unit/test_crm_agent.py index 6aca39b0b..1204e0aae 100644 --- a/tests/unit/test_crm_agent.py +++ b/tests/unit/test_crm_agent.py @@ -1,6 +1,6 @@ """The CRM assistant agent (WS-26d) — registration, routes, verb + path fences. -Spec: ``ai-company-brain/specs/crm_app.md`` §6 (Agent, Phase D) + §9 WS-26d. +Spec: ``project-docs/specs/crm_app.md`` §6 (Agent, Phase D) + §9 WS-26d. The write tools' own behaviour — confirm-before-write, fail-closed, payload shapes — lives in ``test_crm_agent_write.py``; what is asserted HERE is the whole-surface properties, which is why every one of them enumerates ``_TOOLS`` diff --git a/tests/unit/test_crm_agent_write.py b/tests/unit/test_crm_agent_write.py index ab34793af..6ccf2f576 100644 --- a/tests/unit/test_crm_agent_write.py +++ b/tests/unit/test_crm_agent_write.py @@ -1,6 +1,6 @@ """The CRM assistant's four write tools (WS-26d-write). -Spec: ``ai-company-brain/specs/crm_app.md`` §9 ``### WS-26d-write``. The +Spec: ``project-docs/specs/crm_app.md`` §9 ``### WS-26d-write``. The whole-surface properties — registration, the verb allowlist, the path fence, risk annotations across all eight tools — live in ``test_crm_agent.py``; what is asserted HERE is what each write tool *does*. diff --git a/tests/unit/test_crm_auto_lead.py b/tests/unit/test_crm_auto_lead.py index a3debf542..3e0b29041 100644 --- a/tests/unit/test_crm_auto_lead.py +++ b/tests/unit/test_crm_auto_lead.py @@ -1,6 +1,6 @@ """CRM · auto-lead from inbound email (WS-26d-autolead). -Spec: ``ai-company-brain/specs/crm_app.md`` §9 ``WS-26d-autolead`` — +Spec: ``project-docs/specs/crm_app.md`` §9 ``WS-26d-autolead`` — done-when 1-7, each named in a test below · D-CRM-9 · D-CRM-12. **What this file is defending.** With ``CRM_AUTO_LEAD`` on, a scheduler hook diff --git a/tests/unit/test_crm_convert.py b/tests/unit/test_crm_convert.py index 56e42557f..413bbd67d 100644 --- a/tests/unit/test_crm_convert.py +++ b/tests/unit/test_crm_convert.py @@ -1,6 +1,6 @@ """CRM · lead → deal conversion (§3.7) — dedup, provenance, and the 409. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.7 · WS-26a done-when 5. +Spec: ``project-docs/specs/crm_app.md`` §3.7 · WS-26a done-when 5. Conversion is the one place this app decides that two records are the same person or the same company. Each of those decisions has exactly one rule and diff --git a/tests/unit/test_crm_email_timeline.py b/tests/unit/test_crm_email_timeline.py index 3cd7fbdd8..a498907d0 100644 --- a/tests/unit/test_crm_email_timeline.py +++ b/tests/unit/test_crm_email_timeline.py @@ -1,6 +1,6 @@ """CRM · the email→CRM timeline join (WS-26d-email). -Spec: ``ai-company-brain/specs/crm_app.md`` §9 "WS-26d-email" · done-when 1-6. +Spec: ``project-docs/specs/crm_app.md`` §9 "WS-26d-email" · done-when 1-6. **The rule this file exists to defend.** The CRM is org-visible to every ``feature:crm`` holder (D-CRM-3) while a mailbox is owner-scoped, so an diff --git a/tests/unit/test_crm_migration.py b/tests/unit/test_crm_migration.py index a96905531..705990e1e 100644 --- a/tests/unit/test_crm_migration.py +++ b/tests/unit/test_crm_migration.py @@ -1,6 +1,6 @@ """The CRM migration, read as text — idempotency and the shape §3 specifies. -Spec: ``ai-company-brain/specs/crm_app.md`` §3 · WS-26a done-when 1. +Spec: ``project-docs/specs/crm_app.md`` §3 · WS-26a done-when 1. **Why static.** ``infra/postgres/README.md`` requires every ``02+`` migration to be idempotent, because ``apply_migrations.sh`` replays the whole ladder on every diff --git a/tests/unit/test_crm_pipeline.py b/tests/unit/test_crm_pipeline.py index c5bcf098a..e437b2f5d 100644 --- a/tests/unit/test_crm_pipeline.py +++ b/tests/unit/test_crm_pipeline.py @@ -1,6 +1,6 @@ """CRM · pipeline — status transitions, the lost gate, and the kanban board. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.6, §3.9, §4 · WS-26a done-when 4. +Spec: ``project-docs/specs/crm_app.md`` §3.6, §3.9, §4 · WS-26a done-when 4. The headline claim under test: **a status transition has three effects, always.** A ``PATCH`` that writes only the new ``status_id`` looks correct in the UI and diff --git a/tests/unit/test_crm_reports.py b/tests/unit/test_crm_reports.py index 6332a35b0..cfed1e11a 100644 --- a/tests/unit/test_crm_reports.py +++ b/tests/unit/test_crm_reports.py @@ -1,6 +1,6 @@ """CRM · forecast & funnel reports — WS-26g. -Spec: ``ai-company-brain/specs/crm_app.md`` §5.1 system 2 · ticket WS-26g. +Spec: ``project-docs/specs/crm_app.md`` §5.1 system 2 · ticket WS-26g. Hermetic: no Postgres, no network. The route functions are called directly with ``core._get_db`` monkeypatched onto each SUT submodule (the ``_crm_fakes`` diff --git a/tests/unit/test_crm_routes.py b/tests/unit/test_crm_routes.py index 03095c046..2574d1fb8 100644 --- a/tests/unit/test_crm_routes.py +++ b/tests/unit/test_crm_routes.py @@ -1,6 +1,6 @@ """CRM · records, activities and admin — the list contract and the write paths. -Spec: ``ai-company-brain/specs/crm_app.md`` §3.8, §4 · ticket WS-26a done-when +Spec: ``project-docs/specs/crm_app.md`` §3.8, §4 · ticket WS-26a done-when 4 and 6. Hermetic: no Postgres, no network, no TestClient. Route functions are called diff --git a/tests/unit/test_crm_stage_metadata.py b/tests/unit/test_crm_stage_metadata.py index 0eba64f3e..ea7de0941 100644 --- a/tests/unit/test_crm_stage_metadata.py +++ b/tests/unit/test_crm_stage_metadata.py @@ -1,6 +1,6 @@ """CRM · the stage-metadata repair — WS-26f f1 + f4. -Spec: ``ai-company-brain/specs/crm_app.md`` §5.1 (system 1) · ticket WS-26f +Spec: ``project-docs/specs/crm_app.md`` §5.1 (system 1) · ticket WS-26f done-when 1-4 · decisions D-CRM-10 and D-CRM-11. Hermetic: no Postgres, no network, no Zoho. The route function is called diff --git a/tests/unit/test_crm_zoho_import.py b/tests/unit/test_crm_zoho_import.py index 9357a315e..e719f53f7 100644 --- a/tests/unit/test_crm_zoho_import.py +++ b/tests/unit/test_crm_zoho_import.py @@ -1,6 +1,6 @@ """CRM · the Zoho backfill — the mapping, the floor, and what dry-run does. -Spec: ``ai-company-brain/specs/crm_app.md`` §7.1 (the mapping table) · ticket +Spec: ``project-docs/specs/crm_app.md`` §7.1 (the mapping table) · ticket WS-26b done-when 2 and 5. Hermetic: no Postgres, no network, no Zoho. The route function is called diff --git a/tests/unit/test_crm_zoho_sync.py b/tests/unit/test_crm_zoho_sync.py index 1ca935a94..5ca37048a 100644 --- a/tests/unit/test_crm_zoho_sync.py +++ b/tests/unit/test_crm_zoho_sync.py @@ -1,6 +1,6 @@ """CRM · the two-way Zoho sync engine — the seven rules §7.1 makes binding. -Spec: ``ai-company-brain/specs/crm_app.md`` §7.1 · D-CRM-6/7/8 · ticket WS-26b +Spec: ``project-docs/specs/crm_app.md`` §7.1 · D-CRM-6/7/8 · ticket WS-26b done-when 3, 4 and 5. Hermetic: no Postgres, no network, no Zoho, no credential. The engine's two diff --git a/tests/unit/test_declarative_builder.py b/tests/unit/test_declarative_builder.py index d4cdede01..4ba00c841 100644 --- a/tests/unit/test_declarative_builder.py +++ b/tests/unit/test_declarative_builder.py @@ -1,6 +1,6 @@ """The generic declarative builder — equivalence with the hand-written factories. -Spec: ``ai-company-brain/specs/agent_architecture.md`` §1, §6.1. +Spec: ``project-docs/specs/agent_architecture.md`` §1, §6.1. The claim being tested is the whole argument for declarative agents: that a hand-written ``agents.py`` for a tools-plus-instructions agent produces nothing diff --git a/tests/unit/test_email_assistant_settings.py b/tests/unit/test_email_assistant_settings.py index f1b886088..e610f7d42 100644 --- a/tests/unit/test_email_assistant_settings.py +++ b/tests/unit/test_email_assistant_settings.py @@ -1,7 +1,7 @@ """Unit tests for the email Assistant settings model. Guards the serialization contract the Settings UI and the `/assistant/settings` -endpoints depend on. See ai-company-brain/specs/email_inbox_zero_parity_plan.md. +endpoints depend on. See project-docs/specs/email_inbox_zero_parity_plan.md. """ from __future__ import annotations diff --git a/tests/unit/test_groups_session_participants.py b/tests/unit/test_groups_session_participants.py index 45c897989..db13eba01 100644 --- a/tests/unit/test_groups_session_participants.py +++ b/tests/unit/test_groups_session_participants.py @@ -1,6 +1,6 @@ """Groups and session participants (migration 138). -Spec: ai-company-brain/specs/groups_sessions_authority.md §1–§2 +Spec: project-docs/specs/groups_sessions_authority.md §1–§2 What matters and is asserted against a live database: diff --git a/tests/unit/test_instance_wiring.py b/tests/unit/test_instance_wiring.py index 783b4f8da..3a06ee917 100644 --- a/tests/unit/test_instance_wiring.py +++ b/tests/unit/test_instance_wiring.py @@ -1,7 +1,7 @@ """The instance key flows run path → disk → store, and back through the gateway. -Spec: ai-company-brain/specs/memory_architecture.md §5.3 - ai-company-brain/specs/agent_architecture.md §2 +Spec: project-docs/specs/memory_architecture.md §5.3 + project-docs/specs/agent_architecture.md §2 This is the flip that makes instancing real: migrations 132/133 partitioned the store, ``agent_paths`` split the disk, and this wiring makes every seam diff --git a/tests/unit/test_integration_env_scoping.py b/tests/unit/test_integration_env_scoping.py index 34393cc6e..8c05d0031 100644 --- a/tests/unit/test_integration_env_scoping.py +++ b/tests/unit/test_integration_env_scoping.py @@ -27,7 +27,7 @@ - an operator-provided value still wins (unchanged precedence); - only the run's own integrations are visible (scope). -See ``ai-company-brain/specs/saas_multitenancy.md`` §6.1 and MT-0a, and +See ``project-docs/specs/saas_multitenancy.md`` §6.1 and MT-0a, and ``saas_multitenancy_implementation.md`` §6. """ from __future__ import annotations diff --git a/tests/unit/test_mt0b_self_mutation_containment.py b/tests/unit/test_mt0b_self_mutation_containment.py index 15957b34b..c2de60b96 100644 --- a/tests/unit/test_mt0b_self_mutation_containment.py +++ b/tests/unit/test_mt0b_self_mutation_containment.py @@ -15,7 +15,7 @@ org is first-party must refuse. Availability of self-mutation is worth far less than the guarantee that a customer's agent never pushes to our repository. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §6.2 / MT-0b · WS-29. +Spec: ``project-docs/specs/saas_multitenancy.md`` §6.2 / MT-0b · WS-29. """ from __future__ import annotations diff --git a/tests/unit/test_mt0d_per_org_credentials.py b/tests/unit/test_mt0d_per_org_credentials.py index 3ae4bec5f..1e5395686 100644 --- a/tests/unit/test_mt0d_per_org_credentials.py +++ b/tests/unit/test_mt0d_per_org_credentials.py @@ -22,7 +22,7 @@ existing call sites keeps working today and **fails closed** rather than leaking when MT-1 lands. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §6.3 / MT-0d · WS-29. +Spec: ``project-docs/specs/saas_multitenancy.md`` §6.3 / MT-0d · WS-29. """ from __future__ import annotations diff --git a/tests/unit/test_notes_owner_scoping.py b/tests/unit/test_notes_owner_scoping.py index 98a086d70..65bb5ca3c 100644 --- a/tests/unit/test_notes_owner_scoping.py +++ b/tests/unit/test_notes_owner_scoping.py @@ -1,6 +1,6 @@ """Notes is private to its owner — reads, deletes, and acting-as. -Spec: ``ai-company-brain/specs/note_taker_app.md``. Four live defects, all +Spec: ``project-docs/specs/note_taker_app.md``. Four live defects, all reachable by any member holding ``feature:notes``: 1. ``GET /notes/meetings`` had no owner filter, and its ``query`` parameter diff --git a/tests/unit/test_orchestrator_registration.py b/tests/unit/test_orchestrator_registration.py index d65c5de98..d69efb63c 100644 --- a/tests/unit/test_orchestrator_registration.py +++ b/tests/unit/test_orchestrator_registration.py @@ -1,6 +1,6 @@ """The orchestrator is reachable through the named-agent path. -Spec: ``ai-company-brain/specs/agent_architecture.md`` §11.1.1. +Spec: ``project-docs/specs/agent_architecture.md`` §11.1.1. ``apps/agents/agent-orchestrator/`` was written to "eliminate the separate ``/copilot/chat`` endpoint path in main.py and the ``isOrchestrator`` branching diff --git a/tests/unit/test_org_access_control.py b/tests/unit/test_org_access_control.py index 68d43dca4..0991a98df 100644 --- a/tests/unit/test_org_access_control.py +++ b/tests/unit/test_org_access_control.py @@ -1,6 +1,6 @@ """Org access control — permission model and guard behaviour. -Spec: ai-company-brain/specs/org_access_control.md +Spec: project-docs/specs/org_access_control.md These cover the pure resolution layer (acb_auth.permissions) and the FastAPI guards, which is where a mistake is silent: a permission that matches nothing diff --git a/tests/unit/test_org_access_credentials.py b/tests/unit/test_org_access_credentials.py index 69e5f6bcd..1fe8d3863 100644 --- a/tests/unit/test_org_access_credentials.py +++ b/tests/unit/test_org_access_credentials.py @@ -1,6 +1,6 @@ """Org access control — per-user integration credentials and org memory. -Spec: ``ai-company-brain/specs/org_access_control.md`` §5 seam 3. +Spec: ``project-docs/specs/org_access_control.md`` §5 seam 3. Two things are being protected here, and they fail in opposite directions: diff --git a/tests/unit/test_org_access_enforcement.py b/tests/unit/test_org_access_enforcement.py index d20528cc2..c1271a990 100644 --- a/tests/unit/test_org_access_enforcement.py +++ b/tests/unit/test_org_access_enforcement.py @@ -1,6 +1,6 @@ """Org access control — enforcement at the gateway route layer. -Spec: ``ai-company-brain/specs/org_access_control.md`` §5, seam 1. +Spec: ``project-docs/specs/org_access_control.md`` §5, seam 1. ``test_org_access_control.py`` covers the permission *model*. This file covers the thing the model is for: that a member denied a feature is actually refused diff --git a/tests/unit/test_people_directory.py b/tests/unit/test_people_directory.py index b31a92ec8..835c57998 100644 --- a/tests/unit/test_people_directory.py +++ b/tests/unit/test_people_directory.py @@ -1,6 +1,6 @@ """WS-28b — the People Center's directory and person page. -Spec: `ai-company-brain/specs/people_center_app.md` §3.1, §3.2, §5.2, §6. +Spec: `project-docs/specs/people_center_app.md` §3.1, §3.2, §5.2, §6. The claims worth testing are about the *shape of the answer*, because this app's whole permission story is a projection rather than a refusal: diff --git a/tests/unit/test_people_key_shape.py b/tests/unit/test_people_key_shape.py index 57b9937a0..e82167759 100644 --- a/tests/unit/test_people_key_shape.py +++ b/tests/unit/test_people_key_shape.py @@ -1,6 +1,6 @@ """WS-28a — gtd_people's key shape (People Center P-1/P-2). -Spec: `ai-company-brain/specs/people_center_app.md` §2, §5, §7. +Spec: `project-docs/specs/people_center_app.md` §2, §5, §7. Migration 49 made `name` UNIQUE and left `email` unconstrained. That is backwards for a directory that has to join on email: two real people cannot diff --git a/tests/unit/test_people_write.py b/tests/unit/test_people_write.py index fbe78abc3..390d29d12 100644 --- a/tests/unit/test_people_write.py +++ b/tests/unit/test_people_write.py @@ -1,6 +1,6 @@ """WS-28b-write — the person write half, restored and made survivable. -Spec: `ai-company-brain/specs/people_center_app.md` §3.2, §7 (WS-28b-write). +Spec: `project-docs/specs/people_center_app.md` §3.2, §7 (WS-28b-write). The write routes themselves are old; what is new is that migration 148 changed the table under them and nobody told them. Three of the claims below are about diff --git a/tests/unit/test_projects_automation.py b/tests/unit/test_projects_automation.py index 8820226b1..e5bb67386 100644 --- a/tests/unit/test_projects_automation.py +++ b/tests/unit/test_projects_automation.py @@ -1,7 +1,7 @@ """WS-27f — the automation write seam and agent dispatch. -Spec: `ai-company-brain/specs/project_management_app.md` §6.3/§6.4 · -`ai-company-brain/specs/workflows_app.md` §13 U1 and U7. +Spec: `project-docs/specs/project_management_app.md` §6.3/§6.4 · +`project-docs/specs/workflows_app.md` §13 U1 and U7. Two halves, and they are deliberately independent: diff --git a/tests/unit/test_projects_bulk.py b/tests/unit/test_projects_bulk.py index ebbf2fb6f..72cac962a 100644 --- a/tests/unit/test_projects_bulk.py +++ b/tests/unit/test_projects_bulk.py @@ -1,6 +1,6 @@ """WS-27n — bulk edit. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 6, §11.11. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 6, §11.11. **This ticket gates WS-27g.** §11.3: the cutover imports a real workspace, and an import that cannot be re-triaged in bulk is one somebody abandons halfway, diff --git a/tests/unit/test_projects_calendar.py b/tests/unit/test_projects_calendar.py index c97ac08b3..89c42075e 100644 --- a/tests/unit/test_projects_calendar.py +++ b/tests/unit/test_projects_calendar.py @@ -1,6 +1,6 @@ """WS-27q — the calendar window. -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 9, §11.16. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 9, §11.16. The third view, and the first one that cannot be a page. The claims worth pinning are the ones where the wrong implementation looks right: diff --git a/tests/unit/test_projects_cards.py b/tests/unit/test_projects_cards.py index 5f6e73031..5d108bc98 100644 --- a/tests/unit/test_projects_cards.py +++ b/tests/unit/test_projects_cards.py @@ -1,6 +1,6 @@ """WS-27s — the badges a card needs, on the LIST endpoint. -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.15. +Spec: ``project-docs/specs/project_management_app.md`` §11.15. A Projects card should read like a Tasks card. Most of what makes those cards legible is data Projects already stores and never sends to the board: how many diff --git a/tests/unit/test_projects_custom_fields.py b/tests/unit/test_projects_custom_fields.py index 7fea73b4a..16527a4a0 100644 --- a/tests/unit/test_projects_custom_fields.py +++ b/tests/unit/test_projects_custom_fields.py @@ -1,6 +1,6 @@ """WS-27l — custom fields. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 4, §11.9. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 4, §11.9. Definitions are rows; values are JSONB on the task keyed by `field_key`. The denormalisation is Paca's and is deliberate (migration 155 says why), and it diff --git a/tests/unit/test_projects_filters.py b/tests/unit/test_projects_filters.py index 71718b180..efb84b3e7 100644 --- a/tests/unit/test_projects_filters.py +++ b/tests/unit/test_projects_filters.py @@ -1,6 +1,6 @@ """WS-27k — filters and saved views. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 3. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 3. *"My open bugs in Ops, grouped by assignee"* is a daily question the board could not answer. This is the filter half. diff --git a/tests/unit/test_projects_grants.py b/tests/unit/test_projects_grants.py index c0a48048c..c50059ca8 100644 --- a/tests/unit/test_projects_grants.py +++ b/tests/unit/test_projects_grants.py @@ -1,6 +1,6 @@ """Projects · the grant read model — who can see what, and what they get told. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3.2, §4, §8 D-PM-3 · +Spec: ``project-docs/specs/project_management_app.md`` §3.2, §4, §8 D-PM-3 · ticket WS-27a done-when 4. This is the security fence for the whole app. Unlike the CRM — where D-CRM-3 diff --git a/tests/unit/test_projects_import_mapping.py b/tests/unit/test_projects_import_mapping.py index d7e9e2f4a..b7add1c74 100644 --- a/tests/unit/test_projects_import_mapping.py +++ b/tests/unit/test_projects_import_mapping.py @@ -1,6 +1,6 @@ """Projects · the ClickUp importer and its Space→Center mapping plan. -Spec: ``ai-company-brain/specs/project_management_app.md`` §7.1 · **D-PM-10** · +Spec: ``project-docs/specs/project_management_app.md`` §7.1 · **D-PM-10** · ticket WS-27b done-whens 1-8. Hermetic: no Postgres, no ClickUp, no LLM. The provider is a fake returning the diff --git a/tests/unit/test_projects_migration.py b/tests/unit/test_projects_migration.py index 43ee91efb..e4b05be39 100644 --- a/tests/unit/test_projects_migration.py +++ b/tests/unit/test_projects_migration.py @@ -1,6 +1,6 @@ """The projects migration, read as text — idempotency and the shape §3 specifies. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3 · WS-27a done-when 1. +Spec: ``project-docs/specs/project_management_app.md`` §3 · WS-27a done-when 1. **Why static.** ``infra/postgres/README.md`` requires every ``02+`` migration to be idempotent, because ``apply_migrations.sh`` replays the whole ladder on every @@ -335,7 +335,7 @@ def test_every_activity_type_the_routes_write_is_in_the_vocabulary() -> None: # explains itself at length and an assertion its own prose can satisfy is not an # assertion. # -# Spec: ai-company-brain/specs/multi_tenancy.md §3 (D-MT-1 (a), D-MT-3). +# Spec: project-docs/specs/multi_tenancy.md §3 (D-MT-1 (a), D-MT-3). #: Every table §3 specifies, plus the six added by 147/150/152/155/156/160. #: Listed rather than derived, so a table quietly dropped from the tenant diff --git a/tests/unit/test_projects_notifications.py b/tests/unit/test_projects_notifications.py index c376171ca..586b8f94e 100644 --- a/tests/unit/test_projects_notifications.py +++ b/tests/unit/test_projects_notifications.py @@ -1,6 +1,6 @@ """WS-27j — notifications and @mentions. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 2. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 2. The complaint this closes is one sentence: *"assignment is silent."* So the claims worth testing are the ones that decide whether a person hears about diff --git a/tests/unit/test_projects_personal.py b/tests/unit/test_projects_personal.py index 0ae6fb5cd..f866c5a28 100644 --- a/tests/unit/test_projects_personal.py +++ b/tests/unit/test_projects_personal.py @@ -1,6 +1,6 @@ """Projects · the personal lens — one store, seen as my own work. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3.11-§3.12, §6.1 · +Spec: ``project-docs/specs/project_management_app.md`` §3.11-§3.12, §6.1 · **D-PM-6 (revised 2026-08-06)** · ticket WS-27e. The claims worth testing here are the *cohesion* ones, because they are what the diff --git a/tests/unit/test_projects_recurrence.py b/tests/unit/test_projects_recurrence.py index a95d54a84..072de06f6 100644 --- a/tests/unit/test_projects_recurrence.py +++ b/tests/unit/test_projects_recurrence.py @@ -1,6 +1,6 @@ """WS-27o — recurring tasks. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 7, §11.13. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 7, §11.13. Recurrence looks trivial and is not. Each of these is a different way to be quietly wrong for a year: diff --git a/tests/unit/test_projects_relations.py b/tests/unit/test_projects_relations.py index e32d2d123..3784afb22 100644 --- a/tests/unit/test_projects_relations.py +++ b/tests/unit/test_projects_relations.py @@ -1,6 +1,6 @@ """WS-27p — dependencies and subtasks, made reachable. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 8, §11.14. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 8, §11.14. *"`pm_task_links` and `parent_task_id` both exist, unreachable from the board. Data with no surface is a promise the product does not keep."* diff --git a/tests/unit/test_projects_routes.py b/tests/unit/test_projects_routes.py index 7fa932280..53d94f24d 100644 --- a/tests/unit/test_projects_routes.py +++ b/tests/unit/test_projects_routes.py @@ -1,6 +1,6 @@ """Projects · routes — the write paths, the hierarchy rules and the wiring. -Spec: ``ai-company-brain/specs/project_management_app.md`` §3, §4 · ticket +Spec: ``project-docs/specs/project_management_app.md`` §3, §4 · ticket WS-27a done-whens 3, 5 and 6. Hermetic: no Postgres, no network, no TestClient. Route functions are called diff --git a/tests/unit/test_projects_search.py b/tests/unit/test_projects_search.py index 87a9e431e..abdacf0eb 100644 --- a/tests/unit/test_projects_search.py +++ b/tests/unit/test_projects_search.py @@ -1,6 +1,6 @@ """WS-27r — the search surface, and the LIKE defect it uncovered. -Spec: ``ai-company-brain/specs/project_management_app.md`` §11.2 item 10, §11.18. +Spec: ``project-docs/specs/project_management_app.md`` §11.2 item 10, §11.18. The last row of the parity backlog. `?q=` has existed on the list endpoint since WS-27a; what was missing was a way to reach it, and — it turned out — an diff --git a/tests/unit/test_projects_tags.py b/tests/unit/test_projects_tags.py index ff1a53014..b39676439 100644 --- a/tests/unit/test_projects_tags.py +++ b/tests/unit/test_projects_tags.py @@ -1,6 +1,6 @@ """WS-27m — the tag registry. -Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 5, §11.10. +Spec: `project-docs/specs/project_management_app.md` §11.2 item 5, §11.10. `paca_pm_research_2026-08.md` row 13 REFUSED Paca's bare array — "no registry, no colors, no rename/merge" — and §5 shipped `pm_tasks.tags TEXT[]` in its diff --git a/tests/unit/test_projects_tenancy.py b/tests/unit/test_projects_tenancy.py index 511a46061..f38b30420 100644 --- a/tests/unit/test_projects_tenancy.py +++ b/tests/unit/test_projects_tenancy.py @@ -1,6 +1,6 @@ """Projects · the TENANT boundary — what one organization cannot see (WS-29b). -Spec: ``ai-company-brain/specs/multi_tenancy.md`` §3 (D-MT-1 (a), D-MT-3) and +Spec: ``project-docs/specs/multi_tenancy.md`` §3 (D-MT-1 (a), D-MT-3) and §6. Schema: migration 161. ``test_projects_grants.py`` is the fence between two *departments of one diff --git a/tests/unit/test_rooms.py b/tests/unit/test_rooms.py index 79f9361bd..c86a81dda 100644 --- a/tests/unit/test_rooms.py +++ b/tests/unit/test_rooms.py @@ -1,6 +1,6 @@ """Room membership: what each person may do in a shared conversation. -Spec: ``ai-company-brain/specs/groups_sessions_authority.md`` §2, +Spec: ``project-docs/specs/groups_sessions_authority.md`` §2, ``docs/multiplayer/README.md`` §4.2, §5.4. The properties, asserted against a live database: diff --git a/tests/unit/test_session_authority.py b/tests/unit/test_session_authority.py index 2e554e6df..c1b6b7c5e 100644 --- a/tests/unit/test_session_authority.py +++ b/tests/unit/test_session_authority.py @@ -1,6 +1,6 @@ """A shared run acts at the intersection of its participants' access. -Spec: ai-company-brain/specs/groups_sessions_authority.md §3 +Spec: project-docs/specs/groups_sessions_authority.md §3 (answers org_access_control.md §10.2 collision 3) The properties that carry the rule, asserted against a live database where diff --git a/tests/unit/test_signin_requests.py b/tests/unit/test_signin_requests.py index ea2a9cf8f..8958b74c6 100644 --- a/tests/unit/test_signin_requests.py +++ b/tests/unit/test_signin_requests.py @@ -1,6 +1,6 @@ """The sign-in request queue — capture the knock, and let the owner answer it. -Spec: ``ai-company-brain/specs/colleague_onboarding.md`` §6 (WS-24 / N6a). +Spec: ``project-docs/specs/colleague_onboarding.md`` §6 (WS-24 / N6a). Before N6a an authenticated stranger produced one artefact — a journald warning nobody read back — so the owner's only way to learn that a colleague diff --git a/tests/unit/test_steer_routing.py b/tests/unit/test_steer_routing.py index b11b9d38c..d55ff5bc7 100644 --- a/tests/unit/test_steer_routing.py +++ b/tests/unit/test_steer_routing.py @@ -1,7 +1,7 @@ """Steer — routing, the no-double-post marker, durable replay, carve-outs. Covers ``docs/multiplayer/README.md`` §4.6 and -``ai-company-brain/specs/multiplayer_prior_art_qm_2026-08.md`` §QM-1. +``project-docs/specs/multiplayer_prior_art_qm_2026-08.md`` §QM-1. The four outcomes are tested against the PURE function rather than through the route, because the whole reason ``route_turn`` has no I/O is so the rule can be diff --git a/tests/unit/test_tenant_coverage.py b/tests/unit/test_tenant_coverage.py index 78a1b8fe0..c67e4e359 100644 --- a/tests/unit/test_tenant_coverage.py +++ b/tests/unit/test_tenant_coverage.py @@ -1,6 +1,6 @@ """MT-1b — every application table is tenant-scoped, or exempt with a reason. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §1.3 / MT-1b · WS-29 · D15. +Spec: ``project-docs/specs/saas_multitenancy.md`` §1.3 / MT-1b · WS-29 · D15. The point of a generated migration is that nobody hand-writes 143 policies and omits ``FORCE`` on one of them. The point of *this* file is the other half: that diff --git a/tests/unit/test_tenant_placement.py b/tests/unit/test_tenant_placement.py index b86bddd59..66677f2bc 100644 --- a/tests/unit/test_tenant_placement.py +++ b/tests/unit/test_tenant_placement.py @@ -1,6 +1,6 @@ """MT-1a — tenant placement resolves, or refuses. It never guesses. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §1.5 / §1.6 / MT-1a · +Spec: ``project-docs/specs/saas_multitenancy.md`` §1.5 / §1.6 / MT-1a · board WS-29 · D15. Placement is the indirection that makes the tenancy decision **reversible**: diff --git a/tests/unit/test_tenant_session.py b/tests/unit/test_tenant_session.py index b8b124bb5..beb6d786a 100644 --- a/tests/unit/test_tenant_session.py +++ b/tests/unit/test_tenant_session.py @@ -1,6 +1,6 @@ """MT-1c — the tenant binding seam. ``SET LOCAL``, in a transaction, or refuse. -Spec: ``ai-company-brain/specs/saas_multitenancy.md`` §0.1 / §1.3 · WS-29 · D15. +Spec: ``project-docs/specs/saas_multitenancy.md`` §0.1 / §1.3 · WS-29 · D15. RLS enforces isolation server-side, but only against a session that has told the server which tenant it acts for. ``tenant_session`` is the one place that diff --git a/tests/unit/test_think_mode.py b/tests/unit/test_think_mode.py index e6cd0e556..b04c423aa 100644 --- a/tests/unit/test_think_mode.py +++ b/tests/unit/test_think_mode.py @@ -1,6 +1,6 @@ """Reasoning-depth (``think_mode``) on the named-agent path. -Spec: ``ai-company-brain/specs/agent_architecture.md`` §11.1.1. +Spec: ``project-docs/specs/agent_architecture.md`` §11.1.1. ``think_mode`` was honoured only on ``/copilot/chat``, so the chat UI's thinking toggle silently did nothing for every named agent — even though ``route.ts`` has diff --git a/uv.lock b/uv.lock index a80ae8d87..aca910d4a 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,7 @@ members = [ "agent-orchestrator", "agent-task-manager", "agent-whatsapp-assistant", - "ai-company-brain", + "project-docs", "email-ingestion", "gateway", "ingestion", @@ -432,7 +432,7 @@ requires-dist = [ ] [[package]] -name = "ai-company-brain" +name = "project-docs" version = "0.0.1" source = { virtual = "." } dependencies = [ diff --git a/workbench/AGENTS.md b/workbench/AGENTS.md index 6f1ca7016..89280385d 100644 --- a/workbench/AGENTS.md +++ b/workbench/AGENTS.md @@ -11,8 +11,8 @@ Control Plane (Next.js browser UI) and local development tools. - control_plane/src/components/ViewModeProvider.tsx -- Mobile/desktop view decision + "Request desktop" toggle (persisted) - control_plane/src/lib/nav.ts -- Shared primary navigation config (used by Sidebar + mobile drawer). Each pane carries a `feature` slug; `visibleSections(allowedFeatures)` drops panes the member cannot reach and any section left empty. Passing `null` (access not yet resolved) returns everything, so the nav does not visibly shrink on first paint - control_plane/src/app/page.tsx -- Home grid. **Known issue (2026-08-03, deliberately unfixed):** it renders `NAV_SECTIONS` directly (`:11-12`) with **no** access filter, so every pane — Centers included — is advertised to every viewer, including signed-out ones, while the sidebar correctly hides them via `visibleSections()`. Whoever fixes it must decide what the grid shows while access is still resolving (`useAccess()` returns null on first paint, and nav's convention is "show everything" so the UI does not visibly shrink — the wrong default for a landing page) -- control_plane/src/lib/centers.ts -- THE Departmental Center registry: `nav.ts` builds the Centers nav section from it and `access.ts` builds the `/centers/` → `center.` route map from it. Editing this file alone does NOT add a Center — a slug here whose feature is absent from `acb_auth.permissions.FEATURES` is dropped from the nav and 403s at AccessGate for every principal, owner included (that was the live defect of 2026-08-03). `tests/unit/test_org_access_control.py::test_centers_registry_matches_the_feature_vocabulary` parses this file and pins it both ways to `FEATURES`; the full five-place registration checklist is `ai-company-brain/specs/department_centers.md` §2. ⚠️ **That invariant reads each Center's `feature:` field and NOTHING else, so it fences nothing inside `apps[]`** — which is what the Center page actually renders. `CenterApp` is therefore a union discriminated on `status`: a `live` entry without an `href` is a **compile** error (the page links to `app.href ?? "#"`, so it would ship as a clickable card that goes nowhere), and a `planned` entry WITH one is too (it is live and mis-labelled, and the page files it under "coming soon"). Runtime twin: `src/lib/centers.test.ts` -- control_plane/src/app/crm/ -- The native CRM (spec: `ai-company-brain/specs/crm_app.md` §5; WS-26c, extended by WS-26f). Deals kanban landing tab + a list per entity on one shared list contract, a record sheet that opens OVER the list from `?deal=`/`?lead=`/`?contact=`/`?organization=` (no `/[id]` routes — Back closes the sheet, and v1 has no saved-views table so view state IS the URL), timeline with a note/task/call composer, inline-edit fields panel, convert modal, quick-create. ⚠️ **Everything server-shaped is pure and lives in `lib/`, each file unit-tested**: `urlState.ts` (the URL grammar), `board.ts` (lane order/tone, the move plan, the optimistic re-tally, `needsLostReason`), `filters.ts` (the list contract — including *never* sending `?status_id` to contacts/organizations, which the gateway 422s by design), `convert.ts` (§3.7's match rules mirrored so the modal pre-selects what the server would do), `format.ts` (₹ in `en-IN` lakh/crore grouping), `api.ts` (the BFF client; refusals keep their status so a 409 is explained, not reported as a failure). `components/` is composition only. Proxy: `src/app/api/crm/[...path]/route.ts` (the tasks-proxy shape). Three rules here were learned by breaking them: **(1) every list filter — `sort`/`dir` included — lives in `CrmView` and therefore in the URL**, because the load effect keys on view fields and a filter held in component state is a control that changes its own appearance and issues no request (`sort` is also cleared by `selectTab`: the keys are a per-entity server allowlist and a carried-over one is a 422, not an odd order). **(2) A write re-reads what is on screen, whatever the response was** — `refreshCollection()` off the store's `lastView`. The refusal half is obvious (a stale row after a 409 reads as success); the success half is the same lie told backwards, and it shipped first as a created record invisible until manual refresh and a rename that left the row behind the sheet unchanged. **(3) A post-write re-read carries the filters the view was loaded with** — `moveDeal`'s re-read used to fetch the pipeline unscoped, silently widening an owner-filtered board the moment somebody dragged a card on it. **WS-26f adds a fourth tab, `?tab=settings`** (three grids over the EXISTING admin API — deal stages, lead statuses, lost reasons; `lib/settings.ts` owns what a drag-reorder renumbers, which rows that actually PATCHes, and the client half of D-CRM-10's won=100/lost=0 rule). ⚠️ **`settings` is a tab that is NOT a collection, and every unchecked `as EntitySlug` cast on this page was a place that assumed otherwise** — `isEntity()` from `urlState.ts` is the guard, and the load effect, `store.refreshCollection()`, `setCreating` and `PARAM_FOR` all go through it; `parseView`'s old `isEntity(tab) ? tab : "board"` *swallowed* `?tab=settings` and landed the deep link on the kanban. Weighted ₹ (`lane.weighted`) comes from the gateway per lane and is **never** recomputed from `lane.rows`, which is one page of it; `board.ts` keeps the same formula (`weightedDeal`/`weightedRows`, `WEIGHTED_TYPES` = open+ongoing, a NULL probability inheriting the stage default) for the header rollup and the record sheet. **WS-26g adds a fifth tab, `?tab=reports`** (`components/Reports.tsx` + pure `lib/reports.ts`, over `GET /crm/reports/*`): forecast by stage, funnel, win/loss, owner leaderboard. It is another entry in the SAME `NON_ENTITY_TABS` grammar rather than a route, so the record sheet stays open across it — `?tab=reports&deal=` is a link somebody sends. ⚠️ **The reports surface computes nothing**: every figure is server-side and rendered verbatim, and `lib/reports.ts` holds bar widths and wording only. The one formula that legitimately lives on both sides is `board.ts`'s weighted ₹, and it is held to the gateway's SQL by a fixture BOTH runners read — `tests/fixtures/crm_weighted_parity.json`, from pytest through the emitted statement and from `board.test.ts` through `weightedDeal`/`weightedRows`. Adding a non-collection tab is never one edit: `NON_ENTITY_TABS`, the `page.tsx` branch chain AND the load effect all have to learn it, or the tab parses, the pill highlights, and the screen keeps showing whatever was there. ⚠️ **A new tab also has to be reachable from `refreshCollection()`, and every write has to GO through it** — `moveDeal` used to hard-code a board fetch, so the reports branch was unreachable from the one write its own comment named and a deal moved from `?tab=reports&deal=` left all four blocks stale while the fetched pipeline landed in state that tab does not render. `store.test.ts` pins WHICH request each write issues, not the state it settles into: the store's job is deciding what to re-read, and a final-state assertion cannot see a decision that was never made +- control_plane/src/lib/centers.ts -- THE Departmental Center registry: `nav.ts` builds the Centers nav section from it and `access.ts` builds the `/centers/` → `center.` route map from it. Editing this file alone does NOT add a Center — a slug here whose feature is absent from `acb_auth.permissions.FEATURES` is dropped from the nav and 403s at AccessGate for every principal, owner included (that was the live defect of 2026-08-03). `tests/unit/test_org_access_control.py::test_centers_registry_matches_the_feature_vocabulary` parses this file and pins it both ways to `FEATURES`; the full five-place registration checklist is `project-docs/specs/department_centers.md` §2. ⚠️ **That invariant reads each Center's `feature:` field and NOTHING else, so it fences nothing inside `apps[]`** — which is what the Center page actually renders. `CenterApp` is therefore a union discriminated on `status`: a `live` entry without an `href` is a **compile** error (the page links to `app.href ?? "#"`, so it would ship as a clickable card that goes nowhere), and a `planned` entry WITH one is too (it is live and mis-labelled, and the page files it under "coming soon"). Runtime twin: `src/lib/centers.test.ts` +- control_plane/src/app/crm/ -- The native CRM (spec: `project-docs/specs/crm_app.md` §5; WS-26c, extended by WS-26f). Deals kanban landing tab + a list per entity on one shared list contract, a record sheet that opens OVER the list from `?deal=`/`?lead=`/`?contact=`/`?organization=` (no `/[id]` routes — Back closes the sheet, and v1 has no saved-views table so view state IS the URL), timeline with a note/task/call composer, inline-edit fields panel, convert modal, quick-create. ⚠️ **Everything server-shaped is pure and lives in `lib/`, each file unit-tested**: `urlState.ts` (the URL grammar), `board.ts` (lane order/tone, the move plan, the optimistic re-tally, `needsLostReason`), `filters.ts` (the list contract — including *never* sending `?status_id` to contacts/organizations, which the gateway 422s by design), `convert.ts` (§3.7's match rules mirrored so the modal pre-selects what the server would do), `format.ts` (₹ in `en-IN` lakh/crore grouping), `api.ts` (the BFF client; refusals keep their status so a 409 is explained, not reported as a failure). `components/` is composition only. Proxy: `src/app/api/crm/[...path]/route.ts` (the tasks-proxy shape). Three rules here were learned by breaking them: **(1) every list filter — `sort`/`dir` included — lives in `CrmView` and therefore in the URL**, because the load effect keys on view fields and a filter held in component state is a control that changes its own appearance and issues no request (`sort` is also cleared by `selectTab`: the keys are a per-entity server allowlist and a carried-over one is a 422, not an odd order). **(2) A write re-reads what is on screen, whatever the response was** — `refreshCollection()` off the store's `lastView`. The refusal half is obvious (a stale row after a 409 reads as success); the success half is the same lie told backwards, and it shipped first as a created record invisible until manual refresh and a rename that left the row behind the sheet unchanged. **(3) A post-write re-read carries the filters the view was loaded with** — `moveDeal`'s re-read used to fetch the pipeline unscoped, silently widening an owner-filtered board the moment somebody dragged a card on it. **WS-26f adds a fourth tab, `?tab=settings`** (three grids over the EXISTING admin API — deal stages, lead statuses, lost reasons; `lib/settings.ts` owns what a drag-reorder renumbers, which rows that actually PATCHes, and the client half of D-CRM-10's won=100/lost=0 rule). ⚠️ **`settings` is a tab that is NOT a collection, and every unchecked `as EntitySlug` cast on this page was a place that assumed otherwise** — `isEntity()` from `urlState.ts` is the guard, and the load effect, `store.refreshCollection()`, `setCreating` and `PARAM_FOR` all go through it; `parseView`'s old `isEntity(tab) ? tab : "board"` *swallowed* `?tab=settings` and landed the deep link on the kanban. Weighted ₹ (`lane.weighted`) comes from the gateway per lane and is **never** recomputed from `lane.rows`, which is one page of it; `board.ts` keeps the same formula (`weightedDeal`/`weightedRows`, `WEIGHTED_TYPES` = open+ongoing, a NULL probability inheriting the stage default) for the header rollup and the record sheet. **WS-26g adds a fifth tab, `?tab=reports`** (`components/Reports.tsx` + pure `lib/reports.ts`, over `GET /crm/reports/*`): forecast by stage, funnel, win/loss, owner leaderboard. It is another entry in the SAME `NON_ENTITY_TABS` grammar rather than a route, so the record sheet stays open across it — `?tab=reports&deal=` is a link somebody sends. ⚠️ **The reports surface computes nothing**: every figure is server-side and rendered verbatim, and `lib/reports.ts` holds bar widths and wording only. The one formula that legitimately lives on both sides is `board.ts`'s weighted ₹, and it is held to the gateway's SQL by a fixture BOTH runners read — `tests/fixtures/crm_weighted_parity.json`, from pytest through the emitted statement and from `board.test.ts` through `weightedDeal`/`weightedRows`. Adding a non-collection tab is never one edit: `NON_ENTITY_TABS`, the `page.tsx` branch chain AND the load effect all have to learn it, or the tab parses, the pill highlights, and the screen keeps showing whatever was there. ⚠️ **A new tab also has to be reachable from `refreshCollection()`, and every write has to GO through it** — `moveDeal` used to hard-code a board fetch, so the reports branch was unreachable from the one write its own comment named and a deal moved from `?tab=reports&deal=` left all four blocks stale while the fetched pipeline landed in state that tab does not render. `store.test.ts` pins WHICH request each write issues, not the state it settles into: the store's job is deciding what to re-read, and a final-state assertion cannot see a decision that was never made - control_plane/src/lib/access.ts -- Org access control client helpers: the `Access` shape, href→feature mapping, and `canSeePath`. It deliberately does NOT re-implement wildcard matching — the gateway returns resolved outcomes and this is a lookup. Nothing here is a security boundary - control_plane/src/lib/gateway.ts -- THE shared gateway proxy helper (`gatewayHeaders`, `proxyToGateway`). Header building was copy-pasted into ~50 route files, each with its own EXECUTIVE_EMAILS parsing; new routes use this one, and the existing copies are being migrated onto it - control_plane/src/app/api/email/oauth/[provider]/authorize/route.ts -- The mailbox-connect navigation target. **Nothing in the browser may navigate to a gated gateway route directly**: a top-level navigation carries no Bearer and no `X-User-Email` (the session cookie is on this origin, not `api.*`), so it 401s — which is exactly why nobody could connect an email account between 2026-07-29 and 2026-08-04. This route has the session, calls the gated `/email/oauth/{provider}/authorize` with `gatewayHeaders()` and `redirect: "manual"`, and re-issues the upstream `Location` so the provider's consent screen is reached by the browser with the provider's own cookies. It forwards `redirect_after` and deliberately drops `user_email` — identity is the session's. It is a more-specific sibling of the `api/email/[...path]` catch-all and must stay one: the catch-all fetches with the default `redirect: "follow"` and would return the provider's login HTML through the proxy. Pinned by `tests/unit/test_email_oauth_authorize_wiring.py`; failures redirect to `/email/oauth/callback?error=…`, the same page the gateway's own callback errors land on @@ -22,7 +22,7 @@ Control Plane (Next.js browser UI) and local development tools. - control_plane/src/app/settings/roles/ -- Role definitions. System roles are read-only; the copy steers toward per-user overrides, because the failure mode of any role system is one role per employee. The permission chips are grouped by `feature_catalog.category` (Apps · Build · Centers · Configure, in the order the gateway returns them); an unknown category falls back to its raw slug rather than vanishing from the dialog - control_plane/src/proxy.ts -- Route protection via NextAuth (Next 16: the former middleware.ts, exporting `proxy()`) - control_plane/src/auth.ts -- NextAuth v5 config (Microsoft Entra ID SSO, JWT callbacks; auth is disabled when AUTH_MICROSOFT_ENTRA_ID_ID is unset) -- control_plane/src/app/workflows/ -- Workflows app (spec: ai-company-brain/specs/workflows_app.md): gallery + Module Studio tab (`page.tsx`), visual editor at `[id]/page.tsx` — React Flow (`@xyflow/react`) canvas with the three-pane layout (left pane tabs Palette | Copilot · canvas · inspector) + run console, node categories color-coded per the RFC (amber/violet/teal/sky/slate/emerald), Test ▸ streams per-node status onto the canvas over SSE. The palette leads with search (`/api/workflows/catalog/search`, debounced; keyword-ranked until BO‑22's platform semantic-search service lands) and rolls tools up under their integration; `CopilotPanel.tsx` is chat-to-build — applies returned graphs to the canvas with one-click undo, surfaces auto-created-module chips, refreshes the catalog when the copilot creates modules. `lib/api.ts` is the typed client (notes/lib/api.ts shape); proxies live at `src/app/api/workflows/[[...path]]/route.ts` (uses lib/gateway helpers) + a dedicated SSE relay at `api/workflows/runs/[runId]/stream/route.ts` (the catch-all buffers bodies and cannot stream) +- control_plane/src/app/workflows/ -- Workflows app (spec: project-docs/specs/workflows_app.md): gallery + Module Studio tab (`page.tsx`), visual editor at `[id]/page.tsx` — React Flow (`@xyflow/react`) canvas with the three-pane layout (left pane tabs Palette | Copilot · canvas · inspector) + run console, node categories color-coded per the RFC (amber/violet/teal/sky/slate/emerald), Test ▸ streams per-node status onto the canvas over SSE. The palette leads with search (`/api/workflows/catalog/search`, debounced; keyword-ranked until BO‑22's platform semantic-search service lands) and rolls tools up under their integration; `CopilotPanel.tsx` is chat-to-build — applies returned graphs to the canvas with one-click undo, surfaces auto-created-module chips, refreshes the catalog when the copilot creates modules. `lib/api.ts` is the typed client (notes/lib/api.ts shape); proxies live at `src/app/api/workflows/[[...path]]/route.ts` (uses lib/gateway helpers) + a dedicated SSE relay at `api/workflows/runs/[runId]/stream/route.ts` (the catch-all buffers bodies and cannot stream) - control_plane/src/app/api/agent/chat/route.ts -- UNIFIED AG-UI to frontend SSE translation. ALL agents (orchestrator, task-manager, cc-dev, any named/dynamic) go through the same /agent/run/stream gateway endpoint. No more isOrchestrator branching — one code path for all. - control_plane/src/components/AgentChat.tsx -- Main chat component - control_plane/src/components/MarkdownMessage.tsx -- GFM rendering with inline images @@ -37,7 +37,7 @@ Control Plane (Next.js browser UI) and local development tools. - Microsoft Entra ID SSO (NextAuth v5) restricted to org domain - Route protection via proxy.ts (auth-gated when Entra ID credentials are set) - Identity chain: NextAuth session → X-User-Email / X-User-Role headers → gateway UserContext -- Role resolution: DB-backed org roles + per-user overrides, resolved server-side per request by the gateway (spec: ai-company-brain/specs/org_access_control.md). EXECUTIVE_EMAILS remains only as the bootstrap path for a deployment whose access tables have not been migrated yet. Permissions are deliberately NOT put in the NextAuth JWT — a JWT outlives an access change, and access revoked an hour ago must not still work +- Role resolution: DB-backed org roles + per-user overrides, resolved server-side per request by the gateway (spec: project-docs/specs/org_access_control.md). EXECUTIVE_EMAILS remains only as the bootstrap path for a deployment whose access tables have not been migrated yet. Permissions are deliberately NOT put in the NextAuth JWT — a JWT outlives an access change, and access revoked an hour ago must not still work - All API routes that proxy to gateway forward user identity headers alongside Bearer token - Agent-generated files (artefacts) are proxied via /api/agent/workspace/{sessionId}/file?path= - Image URLs in markdown are rewritten through the workspace file proxy automatically @@ -97,7 +97,7 @@ Control Plane (Next.js browser UI) and local development tools. ## Identity — the BFF is not optional, it is where identity comes from -Contract: `ai-company-brain/specs/user_management_contract.md` (**eleven** rules +Contract: `project-docs/specs/user_management_contract.md` (**eleven** rules since 2026-08-08). Two rules bind every page in this app — and a third arrives with WS-29: **R11, never take the acting tenant from input.** The tenant rides the authenticated session; a subdomain is a *lookup to be verified against the diff --git a/workbench/README.md b/workbench/README.md index b32aca5b3..de1584fc2 100644 --- a/workbench/README.md +++ b/workbench/README.md @@ -2,7 +2,7 @@ This folder will host the **Next.js + CopilotKit + AG-UI** three-pane control plane (WBS 0.5.x). Empty until Phase 0.5 starts. -Planned panes (per `ai-company-brain/system_architecture.md` §11): +Planned panes (per `project-docs/system_architecture.md` §11): 1. **Chat / Agent Inbox** — CopilotKit + AG-UI (MAF agents). 2. **Skill Studio** — Monaco + Promptfoo runner. 3. **Observability** — audit log + spend (OTLP backend TBD). diff --git a/workbench/control_plane/README.md b/workbench/control_plane/README.md index 497576c36..3edcc5292 100644 --- a/workbench/control_plane/README.md +++ b/workbench/control_plane/README.md @@ -5,7 +5,7 @@ Next.js 16 + React 19 + Tailwind v4 shell for the Skill Workbench. ## Panes - `/workflows` - Workflows app: visual automation editor (React Flow → compiled to MAF Workflows server-side) + Module Studio. Spec: - `ai-company-brain/specs/workflows_app.md` + `project-docs/specs/workflows_app.md` - `/observability` - Audit / escalations / traces / spend - (full pane list: `src/lib/nav.ts` — this README lists only the ones this file used to misdescribe) diff --git a/workbench/control_plane/src/app/centers/[slug]/page.tsx b/workbench/control_plane/src/app/centers/[slug]/page.tsx index 0b82c7d14..4a64f40e5 100644 --- a/workbench/control_plane/src/app/centers/[slug]/page.tsx +++ b/workbench/control_plane/src/app/centers/[slug]/page.tsx @@ -9,7 +9,7 @@ // // This page is presentation scaffolding. Real scoping (a Center showing only // its team's tasks, mailbox, agents) arrives with access-control Phase 2 -// (modules over org_group) — see ai-company-brain/specs/org_access_control.md §5. +// (modules over org_group) — see project-docs/specs/org_access_control.md §5. import Link from "next/link"; import { useParams } from "next/navigation"; diff --git a/workbench/control_plane/src/app/crm/lib/board.ts b/workbench/control_plane/src/app/crm/lib/board.ts index c3029eefb..eca89d28a 100644 --- a/workbench/control_plane/src/app/crm/lib/board.ts +++ b/workbench/control_plane/src/app/crm/lib/board.ts @@ -4,7 +4,7 @@ // be tested against fixtures without a DOM: lane assembly, lane headers, and // the request a drag issues. // -// Spec: ai-company-brain/specs/crm_app.md §5 surface 1. +// Spec: project-docs/specs/crm_app.md §5 surface 1. import type { Deal, Pipeline, PipelineLane, Status, StatusType } from "./types"; diff --git a/workbench/control_plane/src/app/crm/lib/reports.ts b/workbench/control_plane/src/app/crm/lib/reports.ts index 6c77d88e7..6cdb9c920 100644 --- a/workbench/control_plane/src/app/crm/lib/reports.ts +++ b/workbench/control_plane/src/app/crm/lib/reports.ts @@ -13,7 +13,7 @@ // calculation added to this file is a second answer to a question that already // has one. // -// Spec: ai-company-brain/specs/crm_app.md §5.1 system 2 (WS-26g). +// Spec: project-docs/specs/crm_app.md §5.1 system 2 (WS-26g). import type { FunnelReport, diff --git a/workbench/control_plane/src/app/crm/lib/types.ts b/workbench/control_plane/src/app/crm/lib/types.ts index 5398d7c98..350f581b2 100644 --- a/workbench/control_plane/src/app/crm/lib/types.ts +++ b/workbench/control_plane/src/app/crm/lib/types.ts @@ -7,7 +7,7 @@ // be a second vocabulary to keep in step with migration 144 for no gain, and // the one that drifts is the copy. // -// Spec: ai-company-brain/specs/crm_app.md §3, §4, §5. +// Spec: project-docs/specs/crm_app.md §3, §4, §5. /** The four record types, and the URL segment each one lives at. */ export const ENTITIES = ["deals", "leads", "contacts", "organizations"] as const; diff --git a/workbench/control_plane/src/app/crm/page.tsx b/workbench/control_plane/src/app/crm/page.tsx index a3e7047a2..80b01fd49 100644 --- a/workbench/control_plane/src/app/crm/page.tsx +++ b/workbench/control_plane/src/app/crm/page.tsx @@ -1,7 +1,7 @@ "use client"; /** - * /crm — the native CRM (spec: ai-company-brain/specs/crm_app.md §5). + * /crm — the native CRM (spec: project-docs/specs/crm_app.md §5). * * Five tabs over one dataset: the deals kanban (the landing tab) and a list * per entity on the shared list contract. The record sheet opens OVER diff --git a/workbench/control_plane/src/app/people/lib/directory.ts b/workbench/control_plane/src/app/people/lib/directory.ts index 6eabf7bf8..51e2d3ee3 100644 --- a/workbench/control_plane/src/app/people/lib/directory.ts +++ b/workbench/control_plane/src/app/people/lib/directory.ts @@ -1,7 +1,7 @@ /** * People Center · presentation logic for the directory and person page. * - * Spec: `ai-company-brain/specs/people_center_app.md` §3.1, §3.2, §5.2. + * Spec: `project-docs/specs/people_center_app.md` §3.1, §3.2, §5.2. * * Pure functions only. The server already decided which rows come back and * whether the HR half is filled in; this decides how they read. Keeping the diff --git a/workbench/control_plane/src/app/people/lib/registration.test.ts b/workbench/control_plane/src/app/people/lib/registration.test.ts index ccaf77ec4..cab409d10 100644 --- a/workbench/control_plane/src/app/people/lib/registration.test.ts +++ b/workbench/control_plane/src/app/people/lib/registration.test.ts @@ -1,7 +1,7 @@ /** * People · the app is registered in every place that must know about it. * - * Spec: `ai-company-brain/specs/people_center_app.md` §6 · ticket WS-28b. + * Spec: `project-docs/specs/people_center_app.md` §6 · ticket WS-28b. * * The failure this exists to catch is the one migration 140 shipped for real: * a surface seeded in one place and missing from another is **unreachable, diff --git a/workbench/control_plane/src/app/people/page.tsx b/workbench/control_plane/src/app/people/page.tsx index c6521996b..fc25b3cbf 100644 --- a/workbench/control_plane/src/app/people/page.tsx +++ b/workbench/control_plane/src/app/people/page.tsx @@ -3,7 +3,7 @@ /** * People Center · the directory (§3.1) and the person page (§3.2). * - * Spec: `ai-company-brain/specs/people_center_app.md` · ticket WS-28b. + * Spec: `project-docs/specs/people_center_app.md` · ticket WS-28b. * * ONE app, like Projects. The People Center links here; so does the Projects * assignee picker (WS-28e) once it lands. A person findable in one is findable diff --git a/workbench/control_plane/src/app/projects/components/FilterBar.tsx b/workbench/control_plane/src/app/projects/components/FilterBar.tsx index e0497ea43..158e0bced 100644 --- a/workbench/control_plane/src/app/projects/components/FilterBar.tsx +++ b/workbench/control_plane/src/app/projects/components/FilterBar.tsx @@ -3,7 +3,7 @@ /** * Projects · the filter bar and saved-view chips (WS-27k). * - * Spec: `ai-company-brain/specs/project_management_app.md` §11.2 item 3. + * Spec: `project-docs/specs/project_management_app.md` §11.2 item 3. * * *"My open bugs in Ops, grouped by assignee"* — this is where that sentence * gets typed. Filters go to the server (`routes/projects/filters.py` turns them diff --git a/workbench/control_plane/src/app/projects/components/MyWork.tsx b/workbench/control_plane/src/app/projects/components/MyWork.tsx index 904e2b047..33c733e47 100644 --- a/workbench/control_plane/src/app/projects/components/MyWork.tsx +++ b/workbench/control_plane/src/app/projects/components/MyWork.tsx @@ -3,7 +3,7 @@ /** * Projects · My work — the personal lens over the one task store. * - * Spec: `ai-company-brain/specs/project_management_app.md` §3.11-§3.12, §6.1 · + * Spec: `project-docs/specs/project_management_app.md` §3.11-§3.12, §6.1 · * **D-PM-6 (revised)** · ticket WS-27e. * * This is **not a second app**. The rows here are `pm_tasks` rows — the same diff --git a/workbench/control_plane/src/app/projects/lib/assignees.ts b/workbench/control_plane/src/app/projects/lib/assignees.ts index 47c6c1a67..6def8b2da 100644 --- a/workbench/control_plane/src/app/projects/lib/assignees.ts +++ b/workbench/control_plane/src/app/projects/lib/assignees.ts @@ -1,7 +1,7 @@ /** * Projects · the assignee vocabulary, client side. * - * Spec: `ai-company-brain/specs/project_management_app.md` §3.7 · **D-PM-4**. + * Spec: `project-docs/specs/project_management_app.md` §3.7 · **D-PM-4**. * * An assignee is an **email or `agent:`** — one vocabulary for both * species. That is what makes handing work to an agent the same action as diff --git a/workbench/control_plane/src/app/projects/lib/board.test.ts b/workbench/control_plane/src/app/projects/lib/board.test.ts index c4595b913..1f79634ff 100644 --- a/workbench/control_plane/src/app/projects/lib/board.test.ts +++ b/workbench/control_plane/src/app/projects/lib/board.test.ts @@ -1,7 +1,7 @@ /** * Projects · board maths — the arithmetic a drag depends on. * - * Spec: `ai-company-brain/specs/project_management_app.md` §3.10 · D-PM-5 · + * Spec: `project-docs/specs/project_management_app.md` §3.10 · D-PM-5 · * ticket WS-27d done-when 2. */ import { describe, expect, it } from "vitest"; diff --git a/workbench/control_plane/src/app/projects/lib/board.ts b/workbench/control_plane/src/app/projects/lib/board.ts index e3dfcdaf8..355f94da4 100644 --- a/workbench/control_plane/src/app/projects/lib/board.ts +++ b/workbench/control_plane/src/app/projects/lib/board.ts @@ -1,7 +1,7 @@ /** * Projects · board and ordering maths. * - * Spec: `ai-company-brain/specs/project_management_app.md` §3.10, §5 · D-PM-5. + * Spec: `project-docs/specs/project_management_app.md` §3.10, §5 · D-PM-5. * * Pure functions only, so the arithmetic that decides where a dragged card * lands is testable without a DOM. Everything here is the client half of the diff --git a/workbench/control_plane/src/app/projects/lib/mywork.ts b/workbench/control_plane/src/app/projects/lib/mywork.ts index b1a180246..59b6a6fb3 100644 --- a/workbench/control_plane/src/app/projects/lib/mywork.ts +++ b/workbench/control_plane/src/app/projects/lib/mywork.ts @@ -1,7 +1,7 @@ /** * Projects · the personal lens — grouping and ordering for "My work". * - * Spec: `ai-company-brain/specs/project_management_app.md` §3.11-§3.12, §6.1 · + * Spec: `project-docs/specs/project_management_app.md` §3.11-§3.12, §6.1 · * **D-PM-6 (revised)** · ticket WS-27e. * * Pure functions only. The server already decided *which* rows are mine and diff --git a/workbench/control_plane/src/app/projects/lib/registration.test.ts b/workbench/control_plane/src/app/projects/lib/registration.test.ts index 9af729621..e3646b9fb 100644 --- a/workbench/control_plane/src/app/projects/lib/registration.test.ts +++ b/workbench/control_plane/src/app/projects/lib/registration.test.ts @@ -1,7 +1,7 @@ /** * Projects · the app is registered in every place that must know about it. * - * Spec: `ai-company-brain/specs/project_management_app.md` §5 · ticket WS-27d + * Spec: `project-docs/specs/project_management_app.md` §5 · ticket WS-27d * done-whens 3 and 4. * * The failure this exists to catch is the one `department_centers.md` §2 diff --git a/workbench/control_plane/src/app/projects/lib/tree.test.ts b/workbench/control_plane/src/app/projects/lib/tree.test.ts index 6ef4a2cca..c7e8436c4 100644 --- a/workbench/control_plane/src/app/projects/lib/tree.test.ts +++ b/workbench/control_plane/src/app/projects/lib/tree.test.ts @@ -1,7 +1,7 @@ /** * Projects · the tree and the Center slice. * - * Spec: `ai-company-brain/specs/project_management_app.md` §5 · ticket WS-27d + * Spec: `project-docs/specs/project_management_app.md` §5 · ticket WS-27d * done-when 4 — including the one claim that must not be misread as a security * boundary. */ diff --git a/workbench/control_plane/src/app/projects/lib/tree.ts b/workbench/control_plane/src/app/projects/lib/tree.ts index 493ec7df5..ee142e9ad 100644 --- a/workbench/control_plane/src/app/projects/lib/tree.ts +++ b/workbench/control_plane/src/app/projects/lib/tree.ts @@ -1,7 +1,7 @@ /** * Projects · the project tree, and the Center slice. * - * Spec: `ai-company-brain/specs/project_management_app.md` §5 · D-PM-2. + * Spec: `project-docs/specs/project_management_app.md` §5 · D-PM-2. * * Pure functions. The `?center=` filter in particular is asserted here rather * than in a component, because its correctness claim is a *security* one and diff --git a/workbench/control_plane/src/app/projects/page.tsx b/workbench/control_plane/src/app/projects/page.tsx index 04ac7413f..025650dfa 100644 --- a/workbench/control_plane/src/app/projects/page.tsx +++ b/workbench/control_plane/src/app/projects/page.tsx @@ -3,7 +3,7 @@ /** * Projects — departments, projects, subprojects, tasks and subtasks. * - * Spec: `ai-company-brain/specs/project_management_app.md` §5 · ticket WS-27d. + * Spec: `project-docs/specs/project_management_app.md` §5 · ticket WS-27d. * * ONE app, projected into every Center. `?center=` pre-filters the tree * to that Center's granted departments — **presentation only**: the server's diff --git a/workbench/control_plane/src/app/settings/groups/page.tsx b/workbench/control_plane/src/app/settings/groups/page.tsx index 2db8d4bf3..8aad3a54d 100644 --- a/workbench/control_plane/src/app/settings/groups/page.tsx +++ b/workbench/control_plane/src/app/settings/groups/page.tsx @@ -3,7 +3,7 @@ /** * Settings → Teams — the org's groups (Centers Phase B). * - * Spec: ai-company-brain/specs/department_centers.md §3 Phase B; + * Spec: project-docs/specs/department_centers.md §3 Phase B; * groups_sessions_authority.md §1/§6. * * "Team" is the human word; the primitive is `org_group`, and for the six diff --git a/workbench/control_plane/src/app/settings/members/[email]/page.tsx b/workbench/control_plane/src/app/settings/members/[email]/page.tsx index f64743cb5..754218668 100644 --- a/workbench/control_plane/src/app/settings/members/[email]/page.tsx +++ b/workbench/control_plane/src/app/settings/members/[email]/page.tsx @@ -3,7 +3,7 @@ /** * Settings → Members → one person's access. * - * Spec: ai-company-brain/specs/org_access_control.md §6. + * Spec: project-docs/specs/org_access_control.md §6. * * The screen the whole feature exists for: give someone a role, then add or * remove individual pieces on top of it. Every row shows three things — the diff --git a/workbench/control_plane/src/app/settings/members/confirmPurge.ts b/workbench/control_plane/src/app/settings/members/confirmPurge.ts index 1f876f146..8091a2f8c 100644 --- a/workbench/control_plane/src/app/settings/members/confirmPurge.ts +++ b/workbench/control_plane/src/app/settings/members/confirmPurge.ts @@ -1,6 +1,6 @@ // ── The type-to-confirm rule for a permanent deletion ───────────────────── // -// Spec: ai-company-brain/specs/colleague_onboarding.md §2 Step 5 (N8), +// Spec: project-docs/specs/colleague_onboarding.md §2 Step 5 (N8), // done-when 6 — "the confirmation names both halves and requires the address // to be typed". // diff --git a/workbench/control_plane/src/app/settings/members/page.tsx b/workbench/control_plane/src/app/settings/members/page.tsx index 706ba04e8..2fcc309f3 100644 --- a/workbench/control_plane/src/app/settings/members/page.tsx +++ b/workbench/control_plane/src/app/settings/members/page.tsx @@ -4,8 +4,8 @@ * Settings → Members — the organization roster, and the queue of people * trying to get into it. * - * Spec: ai-company-brain/specs/org_access_control.md §6; - * ai-company-brain/specs/colleague_onboarding.md §6 (the Requests tab). + * Spec: project-docs/specs/org_access_control.md §6; + * project-docs/specs/colleague_onboarding.md §6 (the Requests tab). * * Invite, suspend, change roles, and drill into one person's access. The * per-person editor is where the interesting work happens (./[email]); this diff --git a/workbench/control_plane/src/app/settings/members/selfGuard.ts b/workbench/control_plane/src/app/settings/members/selfGuard.ts index 14dab9433..4414cec6f 100644 --- a/workbench/control_plane/src/app/settings/members/selfGuard.ts +++ b/workbench/control_plane/src/app/settings/members/selfGuard.ts @@ -1,6 +1,6 @@ // ── Who is the viewer looking at, and what may they do to them ──────────── // -// Spec: ai-company-brain/specs/colleague_onboarding.md §2 Step 5 (N7). +// Spec: project-docs/specs/colleague_onboarding.md §2 Step 5 (N7). // // Nothing here is a boundary. It is a courtesy — the same one `lib/access.ts` // describes: hiding a control stops an owner clicking a button that would diff --git a/workbench/control_plane/src/app/settings/members/types.ts b/workbench/control_plane/src/app/settings/members/types.ts index b1a8dabce..27c60a85d 100644 --- a/workbench/control_plane/src/app/settings/members/types.ts +++ b/workbench/control_plane/src/app/settings/members/types.ts @@ -1,5 +1,5 @@ // Wire types for the org administration API (gateway routes/admin/*). -// Spec: ai-company-brain/specs/org_access_control.md §6. +// Spec: project-docs/specs/org_access_control.md §6. export type Member = { email: string; diff --git a/workbench/control_plane/src/app/settings/roles/page.tsx b/workbench/control_plane/src/app/settings/roles/page.tsx index 76623768c..6d6d83a71 100644 --- a/workbench/control_plane/src/app/settings/roles/page.tsx +++ b/workbench/control_plane/src/app/settings/roles/page.tsx @@ -3,7 +3,7 @@ /** * Settings → Roles — the permission bundles members are assigned. * - * Spec: ai-company-brain/specs/org_access_control.md §3.2. + * Spec: project-docs/specs/org_access_control.md §3.2. * * The five system roles are read-only: they are the floor the bootstrap path * depends on, and an org whose `admin` role has been edited into uselessness diff --git a/workbench/control_plane/src/app/tasks/lib/types.ts b/workbench/control_plane/src/app/tasks/lib/types.ts index f31ad77f8..3ffd9ae04 100644 --- a/workbench/control_plane/src/app/tasks/lib/types.ts +++ b/workbench/control_plane/src/app/tasks/lib/types.ts @@ -1,7 +1,7 @@ // GTD Task Manager — canonical client types. // // These mirror the canonical Postgres model in -// `ai-company-brain/specs/task_manager_app.md` §4 (gtd_items / gtd_projects / +// `project-docs/specs/task_manager_app.md` §4 (gtd_items / gtd_projects / // gtd_contexts), trimmed to what the UI needs. The app is built UI-first // against mock data (see mockData.ts); when the gateway `/tasks` API lands, // these types stay and only the data source swaps. diff --git a/workbench/control_plane/src/app/tasks/lib/waiting.ts b/workbench/control_plane/src/app/tasks/lib/waiting.ts index 3a955126d..d60b66cf8 100644 --- a/workbench/control_plane/src/app/tasks/lib/waiting.ts +++ b/workbench/control_plane/src/app/tasks/lib/waiting.ts @@ -1,5 +1,5 @@ // Waiting-For math — the pure predicates behind the "who / what / since-when" -// list (spec: ai-company-brain/specs/task_manager_app.md §1 line 46, §6). +// list (spec: project-docs/specs/task_manager_app.md §1 line 46, §6). // No React, no store: just Date arithmetic over an already-loaded GtdItem, so // the view and its unit tests read the same rules. Same shape as // lib/ordering.ts and lib/scheduling.ts. diff --git a/workbench/control_plane/src/app/tasks/page.tsx b/workbench/control_plane/src/app/tasks/page.tsx index ce13b8ada..2b60330c4 100644 --- a/workbench/control_plane/src/app/tasks/page.tsx +++ b/workbench/control_plane/src/app/tasks/page.tsx @@ -26,7 +26,7 @@ import { DelegatePopup } from "./components/DelegatePopup"; // Task Manager (GTD) — 4-panel shell, mirroring the email app's layout // philosophy: Lists/Contexts · Item list (+ capture) · Item detail · Assistant. // UI-first: runs entirely on mock data (lib/mockData.ts); the gateway `/tasks` -// API is wired later. See ai-company-brain/specs/task_manager_app.md. +// API is wired later. See project-docs/specs/task_manager_app.md. export default function TasksPage() { const { isMobile } = useViewMode(); const { open: openDrawer, close: closeDrawer } = useMobileDrawer(); diff --git a/workbench/control_plane/src/app/whatsapp/calls/page.tsx b/workbench/control_plane/src/app/whatsapp/calls/page.tsx index 07d3bf95e..556cb8ed8 100644 --- a/workbench/control_plane/src/app/whatsapp/calls/page.tsx +++ b/workbench/control_plane/src/app/whatsapp/calls/page.tsx @@ -10,7 +10,7 @@ * * This is the media seam for the note taker — every call's audio is recorded to * a WAV on the bridge, which is what the transcription pipeline will consume. - * See ai-company-brain/specs/whatsapp_calls_note_taker.md. + * See project-docs/specs/whatsapp_calls_note_taker.md. */ import Button from "@/components/ui/Button"; diff --git a/workbench/control_plane/src/app/whatsapp/page.tsx b/workbench/control_plane/src/app/whatsapp/page.tsx index 566d52ae3..c6537f629 100644 --- a/workbench/control_plane/src/app/whatsapp/page.tsx +++ b/workbench/control_plane/src/app/whatsapp/page.tsx @@ -2,7 +2,7 @@ // WhatsApp Message Manager — W0 read-only surface. // -// The calm v3 IA (ai-company-brain/specs/mockups/whatsapp_message_manager.html): +// The calm v3 IA (project-docs/specs/mockups/whatsapp_message_manager.html): // one organizing spine (triage streams in the nav), quiet near-textual rows, a // two-pane list→thread view. No stat wall, no chip cloud, no resident row // buttons — capability arrives in later phases as streams / settings / drawers, diff --git a/workbench/control_plane/src/app/workflows/lib/types.ts b/workbench/control_plane/src/app/workflows/lib/types.ts index 9cdfe82e6..1b8fc7e14 100644 --- a/workbench/control_plane/src/app/workflows/lib/types.ts +++ b/workbench/control_plane/src/app/workflows/lib/types.ts @@ -1,4 +1,4 @@ -// Workflows app — shared types (spec: ai-company-brain/specs/workflows_app.md). +// Workflows app — shared types (spec: project-docs/specs/workflows_app.md). // The graph shapes mirror the gateway's edit-model (React-Flow-native JSON // persisted verbatim — docs/workflow-editor/README.md §4). diff --git a/workbench/control_plane/src/components/AccessProvider.tsx b/workbench/control_plane/src/components/AccessProvider.tsx index 72300c244..9254f6aae 100644 --- a/workbench/control_plane/src/components/AccessProvider.tsx +++ b/workbench/control_plane/src/components/AccessProvider.tsx @@ -4,7 +4,7 @@ * AccessProvider — the signed-in member's effective access, fetched once and * shared by the whole shell. * - * Spec: ai-company-brain/specs/org_access_control.md §5 (seams 4 and 5). + * Spec: project-docs/specs/org_access_control.md §5 (seams 4 and 5). * * Two consumers: the Sidebar filters nav panes with it, and AccessGate blocks * direct navigation to a route the member cannot reach. Neither is a security diff --git a/workbench/control_plane/src/components/ThinkingContainer.tsx b/workbench/control_plane/src/components/ThinkingContainer.tsx index 810da916c..72c1b7c5c 100644 --- a/workbench/control_plane/src/components/ThinkingContainer.tsx +++ b/workbench/control_plane/src/components/ThinkingContainer.tsx @@ -16,7 +16,7 @@ * • Auto-expand during active streaming, auto-collapse on completion * * Patterns sourced from VS Code Copilot Chat UI study — - * see ai-company-brain/spec_chat_ux.md. + * see project-docs/spec_chat_ux.md. */ import AppIcon, { themedIcon } from "@/components/Icon"; diff --git a/workbench/control_plane/src/lib/access.ts b/workbench/control_plane/src/lib/access.ts index e758c4d35..2c744fde5 100644 --- a/workbench/control_plane/src/lib/access.ts +++ b/workbench/control_plane/src/lib/access.ts @@ -1,6 +1,6 @@ // ── Org access control — client-side types and helpers ─────────────────── // -// Spec: ai-company-brain/specs/org_access_control.md +// Spec: project-docs/specs/org_access_control.md // // The gateway resolves permissions and returns *outcomes* — a list of allowed // feature slugs and runnable agent names. This module deliberately does NOT diff --git a/workbench/control_plane/src/lib/centers.ts b/workbench/control_plane/src/lib/centers.ts index 974fe71ca..afd1bb310 100644 --- a/workbench/control_plane/src/lib/centers.ts +++ b/workbench/control_plane/src/lib/centers.ts @@ -12,7 +12,7 @@ // to a real surface today (sometimes unscoped — noted per item); `planned` // items are the buildout backlog, visible so the destination is legible. // -// Spec trail: ai-company-brain/specs/org_access_control.md §5 (modules are +// Spec trail: project-docs/specs/org_access_control.md §5 (modules are // Phase 2), groups_sessions_authority.md §1 (org_group is the primitive), // docs/multiplayer/agent-kinds.md §6 (team-instanced agents per department). diff --git a/workbench/control_plane/src/lib/nav.ts b/workbench/control_plane/src/lib/nav.ts index 769e5700f..0ea44498c 100644 --- a/workbench/control_plane/src/lib/nav.ts +++ b/workbench/control_plane/src/lib/nav.ts @@ -1,7 +1,7 @@ // ── Navigation structure for CommandCenter Control Plane ───────────────── // // The sidebar is organised around the Centers model (one platform, many -// projections — see ai-company-brain/specs/org_access_control.md §5): +// projections — see project-docs/specs/org_access_control.md §5): // 1. Personal Center — apps mapped one-to-one with the signed-in user // 2. Centers — departmental projections (Sales, Marketing, …), // each gated by its `center.` feature and diff --git a/workbench/control_plane/src/lib/providers.ts b/workbench/control_plane/src/lib/providers.ts index b02209b1e..f60ecc231 100644 --- a/workbench/control_plane/src/lib/providers.ts +++ b/workbench/control_plane/src/lib/providers.ts @@ -8,7 +8,7 @@ * Mirror list: apps/services/gateway/gateway/routes/settings.py (_PROVIDER_ENV_MAP) * Backend allowlist: apps/services/gateway/gateway/routes/integrations.py (_ALLOWED_ENV_KEYS) * Both are kept in sync via a comment; see the "Adding a provider" instructions - * in ai-company-brain/system_architecture.md. + * in project-docs/system_architecture.md. */ export interface ProviderGuide {