coding-os 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adapters/claude/README.md +6 -0
- adapters/claude/_install_helpers/extract_stacks.py +43 -0
- adapters/claude/_install_helpers/update_mcp_json.py +75 -0
- adapters/claude/adapter.yaml +164 -0
- adapters/claude/hooks/README.md +40 -0
- adapters/claude/hooks/agent_memory_sync.py +131 -0
- adapters/claude/hooks/ensure-agent-memory-link.sh +36 -0
- adapters/claude/hooks/sync-agent-memory.sh +21 -0
- adapters/claude/install.sh +86 -0
- adapters/claude/sdk_dispatcher.py +871 -0
- adapters/claude/settings.local.template.json +31 -0
- adapters/claude/settings.template.json +808 -0
- adapters/claude/update_mcp_json.py +85 -0
- adapters/codex/adapter.yaml +254 -0
- adapters/codex/chat_provider.py +230 -0
- adapters/codex/commands/formula-f1.md +129 -0
- adapters/codex/commands/formula-f10.md +100 -0
- adapters/codex/commands/formula-f11.md +123 -0
- adapters/codex/commands/formula-f2.md +139 -0
- adapters/codex/commands/formula-f3.md +127 -0
- adapters/codex/commands/formula-f4.md +101 -0
- adapters/codex/commands/formula-f5.md +135 -0
- adapters/codex/commands/formula-f6.md +147 -0
- adapters/codex/commands/formula-f7.md +111 -0
- adapters/codex/commands/formula-f8.md +133 -0
- adapters/codex/commands/formula-f9.md +112 -0
- adapters/codex/enable_codex_hooks.py +94 -0
- adapters/codex/ensure_codex_mcp.py +124 -0
- adapters/codex/hooks/codex-merge-hook-output.py +72 -0
- adapters/codex/hooks/codex-normalize-edit.py +96 -0
- adapters/codex/hooks/codex-postedit-dispatch.sh +75 -0
- adapters/codex/hooks/codex-posttool-dispatch.sh +70 -0
- adapters/codex/hooks/codex-preedit-dispatch.sh +83 -0
- adapters/codex/hooks/codex-pretool-dispatch.sh +82 -0
- adapters/codex/hooks/codex-sessionend-dispatch.sh +20 -0
- adapters/codex/hooks/codex-sessionstart-dispatch.sh +68 -0
- adapters/codex/hooks/codex-stop-dispatch.sh +73 -0
- adapters/codex/hooks/codex-userpromptsubmit-dispatch.sh +74 -0
- adapters/codex/hooks.template.json +208 -0
- adapters/codex/install.sh +83 -0
- adapters/codex/sdk_dispatcher.py +449 -0
- board_os/__init__.py +39 -0
- board_os/_agent_runtime.py +256 -0
- board_os/config.py +421 -0
- board_os/git_coherence.py +107 -0
- board_os/hub_adapter_manifest.py +140 -0
- board_os/mcp_tools.py +3228 -0
- board_os/migration.py +166 -0
- board_os/parser.py +317 -0
- board_os/presence.py +156 -0
- board_os/sync.py +320 -0
- board_os/transition_gates.py +224 -0
- board_os/transition_gates_cli.py +272 -0
- board_os/transition_gates_validator.py +551 -0
- board_os/verify_suites.py +126 -0
- board_os/verify_suites_cli.py +328 -0
- board_os/workflow.py +967 -0
- cli/__init__.py +0 -0
- cli/_data_types.py +248 -0
- cli/_init_helpers.py +587 -0
- cli/_resources.py +100 -0
- cli/adapter_registry.py +239 -0
- cli/add_stack.py +315 -0
- cli/aggregator.py +438 -0
- cli/board_commands.py +1218 -0
- cli/brain_commands.py +255 -0
- cli/cognition.py +345 -0
- cli/config_composer.py +349 -0
- cli/core_version.py +41 -0
- cli/cron_commands.py +278 -0
- cli/db_reset.py +298 -0
- cli/doc_commands.py +111 -0
- cli/doctor.py +2953 -0
- cli/doctor_board.py +365 -0
- cli/doctor_extras.py +1121 -0
- cli/doctor_graph.py +608 -0
- cli/doctor_tokens.py +254 -0
- cli/graph_commands.py +1265 -0
- cli/hook_renderer.py +393 -0
- cli/hub_commands.py +580 -0
- cli/list_adapters.py +79 -0
- cli/list_stacks.py +105 -0
- cli/logs_commands.py +89 -0
- cli/main.py +3070 -0
- cli/materialize_file.py +65 -0
- cli/mcp_start.py +153 -0
- cli/module_commands.py +513 -0
- cli/pr_commands.py +2024 -0
- cli/preset_commands.py +126 -0
- cli/preset_registry.py +171 -0
- cli/project_overrides.py +119 -0
- cli/registry.py +365 -0
- cli/remove_stack.py +492 -0
- cli/renderer.py +620 -0
- cli/setup.py +464 -0
- cli/skill_commands.py +689 -0
- cli/skill_registry.py +235 -0
- cli/skills_list.py +332 -0
- cli/stack_lint.py +351 -0
- cli/stack_registry.py +688 -0
- cli/subsystems.py +335 -0
- cli/sync_all.py +310 -0
- cli/tail_command.py +410 -0
- cli/update.py +608 -0
- cli/verify_since_edit.py +439 -0
- coding_os-0.3.2.dist-info/METADATA +508 -0
- coding_os-0.3.2.dist-info/RECORD +1304 -0
- coding_os-0.3.2.dist-info/WHEEL +5 -0
- coding_os-0.3.2.dist-info/entry_points.txt +5 -0
- coding_os-0.3.2.dist-info/licenses/LICENSE +201 -0
- coding_os-0.3.2.dist-info/top_level.txt +10 -0
- core/__init__.py +0 -0
- core/board_os/__init__.py +39 -0
- core/board_os/_agent_runtime.py +256 -0
- core/board_os/config.py +421 -0
- core/board_os/git_coherence.py +107 -0
- core/board_os/hub_adapter_manifest.py +140 -0
- core/board_os/mcp_tools.py +3228 -0
- core/board_os/migration.py +166 -0
- core/board_os/parser.py +317 -0
- core/board_os/presence.py +156 -0
- core/board_os/sync.py +320 -0
- core/board_os/transition-gates.yaml +176 -0
- core/board_os/transition_gates.py +224 -0
- core/board_os/transition_gates_cli.py +272 -0
- core/board_os/transition_gates_validator.py +551 -0
- core/board_os/verify-suites.yaml +113 -0
- core/board_os/verify_suites.py +126 -0
- core/board_os/verify_suites_cli.py +328 -0
- core/board_os/workflow.py +967 -0
- core/commands/board.md +27 -0
- core/commands/classify.md +23 -0
- core/commands/compose.md +23 -0
- core/commands/daily.md +31 -0
- core/commands/diagnose.md +7 -0
- core/commands/memory-search.md +23 -0
- core/commands/new-project.md +33 -0
- core/commands/retro.md +38 -0
- core/commands/review.md +14 -0
- core/commands/task.md +17 -0
- core/commands/verify.md +34 -0
- core/docs/thinking_os-final-edition.md +1449 -0
- core/doctor-config.yaml +74 -0
- core/graph_os/__init__.py +29 -0
- core/graph_os/backend.py +233 -0
- core/graph_os/backends/__init__.py +13 -0
- core/graph_os/backends/sqlite_backend.py +1053 -0
- core/graph_os/bench/__init__.py +17 -0
- core/graph_os/bench/fixtures.py +56 -0
- core/graph_os/bench/harness.py +95 -0
- core/graph_os/bench/persian_precision.py +142 -0
- core/graph_os/bench/scale_500k.py +120 -0
- core/graph_os/bench/token_cost.py +170 -0
- core/graph_os/bench/viewer_fps.py +110 -0
- core/graph_os/communities.py +410 -0
- core/graph_os/enterprise.py +218 -0
- core/graph_os/entry_points.py +226 -0
- core/graph_os/extractors/__init__.py +25 -0
- core/graph_os/extractors/code_generic.py +914 -0
- core/graph_os/extractors/code_go.py +1422 -0
- core/graph_os/extractors/code_json.py +340 -0
- core/graph_os/extractors/code_php.py +979 -0
- core/graph_os/extractors/code_python.py +1454 -0
- core/graph_os/extractors/code_shell.py +538 -0
- core/graph_os/extractors/code_toml.py +302 -0
- core/graph_os/extractors/code_ts.py +1665 -0
- core/graph_os/extractors/code_yaml.py +394 -0
- core/graph_os/extractors/contracts.py +1592 -0
- core/graph_os/extractors/md_links.py +890 -0
- core/graph_os/extractors/task_deps.py +345 -0
- core/graph_os/groups/__init__.py +22 -0
- core/graph_os/groups/cross_repo.py +156 -0
- core/graph_os/groups/manifest.py +141 -0
- core/graph_os/ingest/__init__.py +19 -0
- core/graph_os/ingest/base.py +306 -0
- core/graph_os/ingest/github.py +112 -0
- core/graph_os/ingest/zip.py +95 -0
- core/graph_os/toolchain.py +393 -0
- core/graph_os/tools/__init__.py +9 -0
- core/graph_os/tools/graph.py +5573 -0
- core/graph_os/tools/reindex_dispatch.py +730 -0
- core/graph_os/tree_sitter_overlay.py +235 -0
- core/graph_os/types.py +252 -0
- core/graph_os/vec_index.py +277 -0
- core/graph_os/viewer/__init__.py +12 -0
- core/graph_os/viewer/exporter.py +93 -0
- core/graph_os/viewer/template.py +189 -0
- core/hooks/_helpers/_paths.py +40 -0
- core/hooks/_helpers/advance_role.py +72 -0
- core/hooks/_helpers/auto_compose.py +228 -0
- core/hooks/_helpers/auto_validate_lessons.py +55 -0
- core/hooks/_helpers/branch_guard_check.py +796 -0
- core/hooks/_helpers/check_commit_message.py +108 -0
- core/hooks/_helpers/check_dangerous_rm.py +80 -0
- core/hooks/_helpers/check_git_bypass.py +154 -0
- core/hooks/_helpers/check_git_destructive.py +77 -0
- core/hooks/_helpers/check_settings_write.py +97 -0
- core/hooks/_helpers/consume_override.py +51 -0
- core/hooks/_helpers/context_budget.py +77 -0
- core/hooks/_helpers/cos_say_json.py +103 -0
- core/hooks/_helpers/destructive_edit_check.py +163 -0
- core/hooks/_helpers/detect_status_transition.py +82 -0
- core/hooks/_helpers/digest_regen.py +56 -0
- core/hooks/_helpers/doc_sync_check.py +498 -0
- core/hooks/_helpers/drain_embedding_outbox.py +52 -0
- core/hooks/_helpers/extract_additional_context.py +51 -0
- core/hooks/_helpers/extract_commit_msg_arg.py +74 -0
- core/hooks/_helpers/git_command_parse.py +424 -0
- core/hooks/_helpers/git_settings_fields.py +47 -0
- core/hooks/_helpers/graph_context_match.py +37 -0
- core/hooks/_helpers/graph_marker_check.py +70 -0
- core/hooks/_helpers/jit_recall.py +56 -0
- core/hooks/_helpers/json_field.py +41 -0
- core/hooks/_helpers/narrative_signal.py +59 -0
- core/hooks/_helpers/observation_count.py +31 -0
- core/hooks/_helpers/pre_commit_batch.py +177 -0
- core/hooks/_helpers/pre_commit_fake_input.py +42 -0
- core/hooks/_helpers/presence_gc.py +102 -0
- core/hooks/_helpers/presence_write.py +167 -0
- core/hooks/_helpers/recover_indirect.py +35 -0
- core/hooks/_helpers/routing_evolution.py +104 -0
- core/hooks/_helpers/session_recap.py +72 -0
- core/hooks/_helpers/skill_primer.py +229 -0
- core/hooks/_helpers/task_sync.py +59 -0
- core/hooks/_helpers/tool_failure_capture.py +147 -0
- core/hooks/_helpers/trajectory_autosnap.py +278 -0
- core/hooks/_helpers/trajectory_startup.py +62 -0
- core/hooks/_helpers/turn_summary.py +82 -0
- core/hooks/_helpers/validate_task_frontmatter.py +98 -0
- core/hooks/_helpers/wip_limit_check.py +103 -0
- core/hooks/_helpers/wip_lines.py +53 -0
- core/hooks/_helpers/work_log_append.py +89 -0
- core/hooks/_helpers/wrap_dispatch_output.py +82 -0
- core/hooks/advance-role.sh +48 -0
- core/hooks/agent-presence.sh +179 -0
- core/hooks/auto-brain-decay.sh +184 -0
- core/hooks/auto-compose-roles.sh +83 -0
- core/hooks/auto-graph-reconcile-shell.sh +119 -0
- core/hooks/auto-regen-doc-index.sh +120 -0
- core/hooks/auto-reindex-docs.sh +130 -0
- core/hooks/auto-task-sync.sh +56 -0
- core/hooks/auto-trace-rotate.sh +88 -0
- core/hooks/block-bad-patterns.sh +212 -0
- core/hooks/block-dangerous-commands.sh +182 -0
- core/hooks/block-hardcoded-literals.sh +90 -0
- core/hooks/block-migration-conflict.sh +114 -0
- core/hooks/block-protected-files.sh +129 -0
- core/hooks/block-secrets.sh +185 -0
- core/hooks/block-shared-tree-edit.sh +75 -0
- core/hooks/block-uv-heredoc.sh +78 -0
- core/hooks/branch-guard.sh +122 -0
- core/hooks/capture-observation.sh +76 -0
- core/hooks/capture-tool-failure.sh +24 -0
- core/hooks/capture-work-log.sh +89 -0
- core/hooks/check-agents-md-refs.sh +75 -0
- core/hooks/check-agents-md-size.sh +49 -0
- core/hooks/check-capture-worked.sh +148 -0
- core/hooks/check-doc-size.sh +61 -0
- core/hooks/check-mcp-extras.sh +92 -0
- core/hooks/check-state.sh +87 -0
- core/hooks/classify-task-mode.sh +103 -0
- core/hooks/cos-env.sh +1301 -0
- core/hooks/drain-embedding-outbox.sh +24 -0
- core/hooks/enforce-anti-ambiguity.sh +74 -0
- core/hooks/enforce-commit-message.sh +73 -0
- core/hooks/enforce-doc-anchor.sh +224 -0
- core/hooks/enforce-doc-sync.sh +206 -0
- core/hooks/enforce-graph-context.sh +89 -0
- core/hooks/enforce-graph-first-read.sh +94 -0
- core/hooks/enforce-memory-check.sh +128 -0
- core/hooks/enforce-rename-plan.sh +78 -0
- core/hooks/enforce-scaffold-boundary.sh +68 -0
- core/hooks/enforce-skill.sh +125 -0
- core/hooks/enforce-task-body.sh +52 -0
- core/hooks/enforce-task-start.sh +81 -0
- core/hooks/enforce-task-transition.sh +75 -0
- core/hooks/enforce-template.sh +143 -0
- core/hooks/enforce-verify.sh +112 -0
- core/hooks/enforce-wip-limit.sh +43 -0
- core/hooks/enforce-zoom.sh +69 -0
- core/hooks/ensure-hub-up.sh +67 -0
- core/hooks/inject-mcp-caller-session.sh +70 -0
- core/hooks/jit-recall.sh +65 -0
- core/hooks/link-commit-to-task.sh +143 -0
- core/hooks/lint-task.sh +40 -0
- core/hooks/nudge-docs-first.sh +71 -0
- core/hooks/nudge-git-mode.sh +29 -0
- core/hooks/nudge-graph-os.sh +118 -0
- core/hooks/nudge-learn-narrative.sh +36 -0
- core/hooks/nudge-model-routing.sh +32 -0
- core/hooks/nudge-reentry.sh +101 -0
- core/hooks/nudge-reuse-first.sh +68 -0
- core/hooks/nudge-task-discovery.sh +81 -0
- core/hooks/nudge-thinking-os.sh +109 -0
- core/hooks/pr-reap.sh +23 -0
- core/hooks/reclaim-sweep.sh +58 -0
- core/hooks/record-verify-auto.sh +77 -0
- core/hooks/record-verify.sh +74 -0
- core/hooks/regen-reminder.sh +104 -0
- core/hooks/registry.yaml +1262 -0
- core/hooks/remind-daily.sh +27 -0
- core/hooks/remind-dogfood.sh +70 -0
- core/hooks/remind-learn-validate.sh +94 -0
- core/hooks/rules-primer.sh +50 -0
- core/hooks/search-enforce-inventory.sh +108 -0
- core/hooks/search-verify-remaining.sh +132 -0
- core/hooks/session-context.sh +729 -0
- core/hooks/session-end.sh +145 -0
- core/hooks/session-skill-primer.sh +43 -0
- core/hooks/snapshot-transcript.sh +56 -0
- core/hooks/sync-task-current.sh +85 -0
- core/hooks/test-first-reminder.sh +120 -0
- core/hooks/test-governor.sh +173 -0
- core/hooks/thinking_os-gate.sh +52 -0
- core/hooks/track-backtrack.sh +35 -0
- core/hooks/track-discovery.sh +121 -0
- core/hooks/track-skill.sh +53 -0
- core/hooks/validate-task-frontmatter.sh +49 -0
- core/hooks/verify-rename-callers.sh +119 -0
- core/hooks/warn-abandoned-task.sh +99 -0
- core/hooks/warn-destructive-edit.sh +64 -0
- core/hooks/warn-diff-size.sh +43 -0
- core/hooks/warn-graph-empty.sh +81 -0
- core/hooks/warn-mcp-down.sh +190 -0
- core/hooks/write-state.sh +55 -0
- core/logging_os/__init__.py +33 -0
- core/logging_os/api.py +127 -0
- core/logging_os/bridge.py +80 -0
- core/logging_os/config.py +172 -0
- core/logging_os/fingerprint.py +25 -0
- core/logging_os/redact.py +53 -0
- core/logging_os/render.py +83 -0
- core/logging_os/sinks.py +164 -0
- core/rules/anti-overengineering.md +44 -0
- core/rules/api-contract-discipline.md +41 -0
- core/rules/dimension-registry.md +155 -0
- core/rules/git-workflow.md +57 -0
- core/rules/memory.md +46 -0
- core/rules/model-routing.md +22 -0
- core/rules/skill-enforcement.md +75 -0
- core/rules/test-discipline.md +38 -0
- core/rules/thinking_os.md +48 -0
- core/rules/transparency-banner.md +37 -0
- core/runtime_paths.yaml +36 -0
- core/scaffold_manifest.json +14430 -0
- core/scheduled/__init__.py +0 -0
- core/scheduled/_activity.py +126 -0
- core/scheduled/_state.py +113 -0
- core/scheduled/config.py +86 -0
- core/scheduled/dep_reconcile.py +135 -0
- core/scheduled/error_sweep.py +137 -0
- core/scheduled/nightly.py +930 -0
- core/scheduled/responsive_extract.py +65 -0
- core/schemas/adapter.schema.json +269 -0
- core/schemas/preset.schema.json +50 -0
- core/schemas/skill.schema.json +81 -0
- core/schemas/stack.schema.json +404 -0
- core/scripts/_lib.sh +9 -0
- core/scripts/docs-lint.sh +228 -0
- core/scripts/docs-nav-fix.sh +133 -0
- core/scripts/docs-staleness-check.sh +154 -0
- core/scripts/install-adapter.sh +266 -0
- core/scripts/link-stack-skills.sh +52 -0
- core/scripts/log-latest.sh +106 -0
- core/scripts/log-search.sh +89 -0
- core/scripts/log-write.sh +134 -0
- core/scripts/ref-resolve.sh +71 -0
- core/skills/a11y/SKILL.md +305 -0
- core/skills/a11y/assets/a11y-checklist.md +137 -0
- core/skills/a11y/references/aria-and-focus.md +247 -0
- core/skills/a11y/references/rn-accessibility.md +343 -0
- core/skills/a11y/references/screen-reader-testing.md +190 -0
- core/skills/agent-memory/SKILL.md +191 -0
- core/skills/agent-memory/assets/memory-checklist.md +21 -0
- core/skills/agent-memory/references/memory-recipes.md +57 -0
- core/skills/api-design/SKILL.md +232 -0
- core/skills/api-design/assets/api-design-checklist.md +110 -0
- core/skills/api-design/references/error-envelope.md +381 -0
- core/skills/api-design/references/idempotency-pagination.md +312 -0
- core/skills/api-design/references/rest-contracts.md +426 -0
- core/skills/auth-patterns/SKILL.md +352 -0
- core/skills/auth-patterns/assets/auth-checklist.md +118 -0
- core/skills/auth-patterns/references/jwt-and-service-tokens.md +343 -0
- core/skills/auth-patterns/references/passkeys-2fa.md +289 -0
- core/skills/auth-patterns/references/sessions-vs-jwt.md +230 -0
- core/skills/auth-patterns/scripts/cookie-flag-check.py +146 -0
- core/skills/backend-fundamentals/SKILL.md +238 -0
- core/skills/backend-fundamentals/assets/backend-checklist.md +27 -0
- core/skills/backend-fundamentals/references/backend-patterns.md +56 -0
- core/skills/backend-fundamentals/scripts/check_layering.py +83 -0
- core/skills/clean-code/SKILL.md +642 -0
- core/skills/clean-code/scripts/audit-fail-closed.py +167 -0
- core/skills/codebase-explorer/SKILL.md +89 -0
- core/skills/codebase-explorer/assets/reading-checklist.md +24 -0
- core/skills/codebase-explorer/references/reading-strategies.md +52 -0
- core/skills/codebase-explorer/scripts/outline.py +99 -0
- core/skills/db-design/SKILL.md +327 -0
- core/skills/db-design/assets/migration-template.sql +49 -0
- core/skills/db-design/references/migration-discipline.md +290 -0
- core/skills/db-design/references/postgres-patterns.md +340 -0
- core/skills/db-design/scripts/migration-safety.sh +150 -0
- core/skills/deployment-cicd/SKILL.md +260 -0
- core/skills/deployment-cicd/assets/deploy-checklist.md +26 -0
- core/skills/deployment-cicd/references/pipeline-and-release.md +54 -0
- core/skills/deployment-cicd/scripts/lint_workflow.py +79 -0
- core/skills/docker/SKILL.md +114 -0
- core/skills/docker/assets/dockerfile-checklist.md +31 -0
- core/skills/docker/references/compose-patterns.md +66 -0
- core/skills/docker/references/dockerfile-optimization.md +64 -0
- core/skills/docker/scripts/lint_dockerfile.sh +48 -0
- core/skills/docker/versions.json +16 -0
- core/skills/end-to-end-testing/SKILL.md +101 -0
- core/skills/end-to-end-testing/assets/e2e-checklist.md +23 -0
- core/skills/end-to-end-testing/references/maestro.md +63 -0
- core/skills/end-to-end-testing/references/playwright.md +68 -0
- core/skills/end-to-end-testing/scripts/lint_e2e.py +88 -0
- core/skills/end-to-end-testing/versions.json +16 -0
- core/skills/frontend-design/SKILL.md +76 -0
- core/skills/frontend-design/assets/design-checklist.md +29 -0
- core/skills/frontend-design/references/design-principles.md +65 -0
- core/skills/frontend-design/scripts/check_contrast.py +89 -0
- core/skills/frontend-fundamentals/SKILL.md +213 -0
- core/skills/frontend-fundamentals/assets/frontend-checklist.md +25 -0
- core/skills/frontend-fundamentals/references/rendering-and-state.md +66 -0
- core/skills/frontend-fundamentals/scripts/check_frontend.py +86 -0
- core/skills/graph-explorer/SKILL.md +215 -0
- core/skills/graph-explorer/scripts/explain-impact.sh +64 -0
- core/skills/graphql/SKILL.md +187 -0
- core/skills/grpc-microservices/SKILL.md +174 -0
- core/skills/hexagonal-architecture/SKILL.md +199 -0
- core/skills/hexagonal-architecture/assets/folder-scaffold.md +233 -0
- core/skills/hexagonal-architecture/references/anti-patterns.md +129 -0
- core/skills/hexagonal-architecture/references/go-fiber-layout.md +429 -0
- core/skills/hexagonal-architecture/references/python-fastapi-layout.md +453 -0
- core/skills/hexagonal-architecture/references/react-native-layout.md +428 -0
- core/skills/i18n/SKILL.md +126 -0
- core/skills/incident-response/SKILL.md +225 -0
- core/skills/incident-response/assets/incident-checklist.md +29 -0
- core/skills/incident-response/references/severity-and-runbook.md +53 -0
- core/skills/incident-response/scripts/classify_severity.py +86 -0
- core/skills/linux-sysadmin/SKILL.md +115 -0
- core/skills/linux-sysadmin/assets/hardening-checklist.md +29 -0
- core/skills/linux-sysadmin/references/ssh-hardening.md +62 -0
- core/skills/linux-sysadmin/references/systemd-and-services.md +77 -0
- core/skills/linux-sysadmin/scripts/triage.sh +50 -0
- core/skills/linux-sysadmin/versions.json +17 -0
- core/skills/llm-patterns/SKILL.md +410 -0
- core/skills/llm-patterns/assets/llm-feature-checklist.md +26 -0
- core/skills/llm-patterns/references/rag-and-evals.md +59 -0
- core/skills/llm-patterns/scripts/estimate_tokens.py +75 -0
- core/skills/messaging-queues/SKILL.md +142 -0
- core/skills/mobile-fundamentals/SKILL.md +406 -0
- core/skills/mobile-fundamentals/assets/mobile-launch-checklist.md +130 -0
- core/skills/mobile-fundamentals/references/navigation-and-deep-links.md +337 -0
- core/skills/mobile-fundamentals/references/offline-sync.md +339 -0
- core/skills/node-backend/SKILL.md +114 -0
- core/skills/node-backend/assets/node-checklist.md +25 -0
- core/skills/node-backend/references/async-and-errors.md +67 -0
- core/skills/node-backend/references/event-loop.md +65 -0
- core/skills/node-backend/scripts/check_package.py +78 -0
- core/skills/node-backend/versions.json +17 -0
- core/skills/observability/SKILL.md +289 -0
- core/skills/observability/assets/observability-checklist.md +27 -0
- core/skills/observability/references/instrumentation.md +57 -0
- core/skills/observability/scripts/lint_logging.py +77 -0
- core/skills/payments/SKILL.md +102 -0
- core/skills/performance/SKILL.md +305 -0
- core/skills/performance/assets/perf-checklist.md +143 -0
- core/skills/performance/references/mobile-performance.md +249 -0
- core/skills/performance/references/web-vitals.md +209 -0
- core/skills/php/SKILL.md +116 -0
- core/skills/php/assets/php-checklist.md +26 -0
- core/skills/php/references/modern-php.md +64 -0
- core/skills/php/references/security.md +72 -0
- core/skills/php/scripts/scan_php_smells.py +99 -0
- core/skills/php/versions.json +9 -0
- core/skills/pr-mode-driver/SKILL.md +65 -0
- core/skills/realtime-websockets/SKILL.md +152 -0
- core/skills/redis/SKILL.md +105 -0
- core/skills/redis/assets/redis-checklist.md +27 -0
- core/skills/redis/references/operations.md +66 -0
- core/skills/redis/references/patterns.md +62 -0
- core/skills/redis/scripts/analyze_info.py +101 -0
- core/skills/redis/versions.json +9 -0
- core/skills/search/SKILL.md +91 -0
- core/skills/search/references/grep.md +76 -0
- core/skills/search/scripts/verify-count.sh +73 -0
- core/skills/search-infra/SKILL.md +109 -0
- core/skills/security-mobile/SKILL.md +394 -0
- core/skills/security-mobile/assets/mobile-security-checklist.md +117 -0
- core/skills/security-mobile/references/masvs-l1-checklist.md +127 -0
- core/skills/security-web/SKILL.md +217 -0
- core/skills/security-web/assets/security-web-checklist.md +167 -0
- core/skills/security-web/references/owasp-top-10.md +551 -0
- core/skills/security-web/references/supply-chain.md +179 -0
- core/skills/security-web/scripts/csp-check.sh +153 -0
- core/skills/shell-scripting/SKILL.md +128 -0
- core/skills/shell-scripting/assets/script-checklist.md +33 -0
- core/skills/shell-scripting/references/argument-parsing.md +84 -0
- core/skills/shell-scripting/references/bash-robustness.md +78 -0
- core/skills/shell-scripting/scripts/lint_script.sh +53 -0
- core/skills/shell-scripting/scripts/new_script.py +131 -0
- core/skills/shell-scripting/versions.json +23 -0
- core/skills/sql-authoring/SKILL.md +104 -0
- core/skills/sql-authoring/assets/query-review-checklist.md +26 -0
- core/skills/sql-authoring/references/query-patterns.md +89 -0
- core/skills/sql-authoring/references/reading-explain.md +52 -0
- core/skills/sql-authoring/scripts/analyze_plan.py +100 -0
- core/skills/sql-authoring/versions.json +17 -0
- core/skills/state-management/SKILL.md +428 -0
- core/skills/state-management/references/tanstack-query-recipes.md +301 -0
- core/skills/state-management/references/zustand-recipes.md +364 -0
- core/skills/supabase/SKILL.md +108 -0
- core/skills/supabase/assets/supabase-checklist.md +26 -0
- core/skills/supabase/references/realtime-and-storage.md +55 -0
- core/skills/supabase/references/rls-and-auth.md +65 -0
- core/skills/supabase/scripts/check_rls.py +88 -0
- core/skills/supabase/versions.json +9 -0
- core/skills/task-driver/SKILL.md +272 -0
- core/skills/task-driver/scripts/task-lint.sh +163 -0
- core/skills/technical-writing/SKILL.md +85 -0
- core/skills/technical-writing/assets/doc-checklist.md +29 -0
- core/skills/technical-writing/references/doc-anatomy.md +53 -0
- core/skills/technical-writing/references/writing-craft.md +59 -0
- core/skills/technical-writing/scripts/new_doc.py +81 -0
- core/skills/terraform-k8s/SKILL.md +133 -0
- core/skills/testing-strategy/SKILL.md +266 -0
- core/skills/testing-strategy/assets/test-review-checklist.md +25 -0
- core/skills/testing-strategy/references/test-types.md +52 -0
- core/skills/testing-strategy/scripts/coverage_gate.py +79 -0
- core/skills/thinking_os/SKILL.md +288 -0
- core/skills/thinking_os/scripts/classify.sh +122 -0
- core/skills/typescript/SKILL.md +110 -0
- core/skills/typescript/assets/typescript-checklist.md +24 -0
- core/skills/typescript/references/strictness.md +53 -0
- core/skills/typescript/references/type-system.md +86 -0
- core/skills/typescript/scripts/check_tsconfig.py +92 -0
- core/skills/typescript/versions.json +9 -0
- core/subsystems.yaml +202 -0
- core/thinking_os/__init__.py +1 -0
- core/thinking_os/_agent_markers.py +32 -0
- core/thinking_os/agents/README.md +71 -0
- core/thinking_os/agents/analyst.md +139 -0
- core/thinking_os/agents/architect.md +127 -0
- core/thinking_os/agents/debugger.md +111 -0
- core/thinking_os/agents/deployer.md +112 -0
- core/thinking_os/agents/distiller.md +28 -0
- core/thinking_os/agents/documenter.md +101 -0
- core/thinking_os/agents/implementer.md +135 -0
- core/thinking_os/agents/internal/session_observer.md +39 -0
- core/thinking_os/agents/observer.md +100 -0
- core/thinking_os/agents/onboarder.md +81 -0
- core/thinking_os/agents/refactorer.md +123 -0
- core/thinking_os/agents/repairer.md +47 -0
- core/thinking_os/agents/researcher.md +129 -0
- core/thinking_os/agents/reviewer.md +147 -0
- core/thinking_os/agents/security_auditor.md +133 -0
- core/thinking_os/background.py +405 -0
- core/thinking_os/bootstrap_outcomes.py +200 -0
- core/thinking_os/budget.py +302 -0
- core/thinking_os/capture.py +495 -0
- core/thinking_os/cognition.py +516 -0
- core/thinking_os/cognition_schemas.py +517 -0
- core/thinking_os/compress.py +192 -0
- core/thinking_os/concepts.py +233 -0
- core/thinking_os/dashboard.py +159 -0
- core/thinking_os/database.py +2883 -0
- core/thinking_os/decay.py +393 -0
- core/thinking_os/digest.py +295 -0
- core/thinking_os/dispatcher.py +192 -0
- core/thinking_os/dispatcher_helpers.py +48 -0
- core/thinking_os/dispatchers/__init__.py +3 -0
- core/thinking_os/dispatchers/default.py +47 -0
- core/thinking_os/distill.py +192 -0
- core/thinking_os/doc_indexer.py +905 -0
- core/thinking_os/embeddings.py +943 -0
- core/thinking_os/formula_composer.py +556 -0
- core/thinking_os/gate_marker.py +75 -0
- core/thinking_os/graph.py +296 -0
- core/thinking_os/graph_indexer.py +360 -0
- core/thinking_os/health_check.py +517 -0
- core/thinking_os/impact.py +119 -0
- core/thinking_os/memory_gc.py +356 -0
- core/thinking_os/migrator_embeddings.py +318 -0
- core/thinking_os/precision.py +194 -0
- core/thinking_os/presets/registry.yaml +127 -0
- core/thinking_os/record_outcome.py +399 -0
- core/thinking_os/repair.py +105 -0
- core/thinking_os/retrieval_quality.py +239 -0
- core/thinking_os/roles/analyst.yaml +83 -0
- core/thinking_os/roles/architect.yaml +88 -0
- core/thinking_os/roles/debugger.yaml +71 -0
- core/thinking_os/roles/deployer.yaml +71 -0
- core/thinking_os/roles/documenter.yaml +72 -0
- core/thinking_os/roles/implementer.yaml +83 -0
- core/thinking_os/roles/observer.yaml +70 -0
- core/thinking_os/roles/refactorer.yaml +71 -0
- core/thinking_os/roles/researcher.yaml +72 -0
- core/thinking_os/roles/reviewer.yaml +79 -0
- core/thinking_os/roles/security_auditor.yaml +83 -0
- core/thinking_os/roles_state.py +168 -0
- core/thinking_os/sanitizer.py +320 -0
- core/thinking_os/server.py +3160 -0
- core/thinking_os/session_enrich.py +272 -0
- core/thinking_os/session_observe_worker.py +111 -0
- core/thinking_os/session_startup.py +74 -0
- core/thinking_os/session_summary.py +245 -0
- core/thinking_os/situations/registry.yaml +99 -0
- core/thinking_os/task_analyzer.py +462 -0
- core/thinking_os/task_parser.py +342 -0
- core/thinking_os/task_sync.py +73 -0
- core/thinking_os/tools/__init__.py +6 -0
- core/thinking_os/tools/_shared.py +947 -0
- core/thinking_os/tools/cognition.py +1867 -0
- core/thinking_os/tools/docs.py +770 -0
- core/thinking_os/tools/learning.py +2078 -0
- core/thinking_os/tools/logs.py +79 -0
- core/thinking_os/tools/memory.py +840 -0
- core/thinking_os/tools/metrics.py +200 -0
- core/thinking_os/tools/retrieve.py +415 -0
- core/thinking_os/tools/routing.py +658 -0
- core/thinking_os/tools/tasks.py +449 -0
- core/thinking_os/tools/trajectory.py +181 -0
- core/thinking_os/tracing.py +235 -0
- core/web/__init__.py +5 -0
- core/web/_cache.py +118 -0
- core/web/_deps.py +56 -0
- core/web/_envelope.py +85 -0
- core/web/_project_context.py +140 -0
- core/web/chat_providers.py +108 -0
- core/web/init_jobs.py +216 -0
- core/web/routes/__init__.py +25 -0
- core/web/routes/_bounded_read.py +76 -0
- core/web/routes/board.py +1089 -0
- core/web/routes/cognition.py +1838 -0
- core/web/routes/config.py +635 -0
- core/web/routes/graph.py +513 -0
- core/web/routes/health.py +180 -0
- core/web/routes/hooks.py +288 -0
- core/web/routes/hub.py +1219 -0
- core/web/routes/logs.py +374 -0
- core/web/routes/metrics.py +43 -0
- core/web/routes/observability.py +400 -0
- core/web/routes/patterns.py +227 -0
- core/web/routes/presence.py +609 -0
- core/web/routes/roles.py +446 -0
- core/web/routes/scheduled.py +261 -0
- core/web/routes/search.py +238 -0
- core/web/routes/sessions.py +220 -0
- core/web/routes/settings.py +363 -0
- core/web/routes/stream.py +547 -0
- core/web/security.py +157 -0
- core/web/server.py +283 -0
- graph_os/__init__.py +29 -0
- graph_os/backend.py +233 -0
- graph_os/backends/__init__.py +13 -0
- graph_os/backends/sqlite_backend.py +1053 -0
- graph_os/communities.py +410 -0
- graph_os/enterprise.py +218 -0
- graph_os/entry_points.py +226 -0
- graph_os/extractors/__init__.py +25 -0
- graph_os/extractors/code_generic.py +914 -0
- graph_os/extractors/code_go.py +1422 -0
- graph_os/extractors/code_json.py +340 -0
- graph_os/extractors/code_php.py +979 -0
- graph_os/extractors/code_python.py +1454 -0
- graph_os/extractors/code_shell.py +538 -0
- graph_os/extractors/code_toml.py +302 -0
- graph_os/extractors/code_ts.py +1665 -0
- graph_os/extractors/code_yaml.py +394 -0
- graph_os/extractors/contracts.py +1592 -0
- graph_os/extractors/md_links.py +890 -0
- graph_os/extractors/task_deps.py +345 -0
- graph_os/groups/__init__.py +22 -0
- graph_os/groups/cross_repo.py +156 -0
- graph_os/groups/manifest.py +141 -0
- graph_os/ingest/__init__.py +19 -0
- graph_os/ingest/base.py +306 -0
- graph_os/ingest/github.py +112 -0
- graph_os/ingest/zip.py +95 -0
- graph_os/toolchain.py +393 -0
- graph_os/tools/__init__.py +9 -0
- graph_os/tools/graph.py +5573 -0
- graph_os/tools/reindex_dispatch.py +730 -0
- graph_os/tree_sitter_overlay.py +235 -0
- graph_os/types.py +252 -0
- graph_os/vec_index.py +277 -0
- graph_os/viewer/__init__.py +12 -0
- graph_os/viewer/exporter.py +93 -0
- graph_os/viewer/template.py +189 -0
- scheduled/__init__.py +0 -0
- scheduled/_activity.py +126 -0
- scheduled/_state.py +113 -0
- scheduled/config.py +86 -0
- scheduled/dep_reconcile.py +135 -0
- scheduled/error_sweep.py +137 -0
- scheduled/nightly.py +930 -0
- scheduled/responsive_extract.py +65 -0
- scripts/__init__.py +4 -0
- scripts/_commit_msg_body.sh +31 -0
- scripts/_post_commit_body.sh +49 -0
- scripts/_pre_commit_body.sh +121 -0
- scripts/_prepare_commit_msg_body.sh +53 -0
- scripts/audit_mcp_tools.py +693 -0
- scripts/bench_sdk_dispatcher.py +177 -0
- scripts/capture_golden.py +169 -0
- scripts/check_graph_phantoms.py +75 -0
- scripts/dev/audit_doc_links.py +359 -0
- scripts/dev/audit_scaffold_module_tags.py +107 -0
- scripts/dev/backfill_doc_headers.py +328 -0
- scripts/dev/backfill_nav_lines.py +119 -0
- scripts/dev/fix_nav_placement.py +106 -0
- scripts/dev/inspect_sdk_options.py +45 -0
- scripts/dev/migrate_check_ids.py +170 -0
- scripts/dev/strip_purpose_blocks.py +154 -0
- scripts/dump_openapi.py +66 -0
- scripts/e2e_dispatch_tool.py +195 -0
- scripts/generate_manifest.py +166 -0
- scripts/golden_sections.py +20 -0
- scripts/graph_demo.py +161 -0
- scripts/install-git-hooks.sh +47 -0
- scripts/migrate_embeddings_minilm_to_bge_m3.py +84 -0
- scripts/operational_eval.py +445 -0
- scripts/probe_agent_session_resolver.py +59 -0
- scripts/prune_deleted_path.py +127 -0
- scripts/refactor_agent_dual_mode.py +171 -0
- scripts/refresh_skill_versions.py +302 -0
- scripts/regen_doc_index.py +209 -0
- scripts/regen_doctor_schema.py +62 -0
- scripts/regen_rules.py +94 -0
- scripts/rename_formulas_to_semantic.py +241 -0
- scripts/smoke_db_connections.py +183 -0
- scripts/smoke_doc_header.py +72 -0
- scripts/smoke_graph_e2e.py +374 -0
- scripts/smoke_sdk_dispatch.py +84 -0
- scripts/smoke_uid_resolver.py +164 -0
- scripts/verify_dispatchers.py +244 -0
- scripts/verify_phase_c_e2e.py +436 -0
- templates/__init__.py +6 -0
- templates/_base/Makefile.base +353 -0
- templates/_base/base.yaml +59 -0
- templates/_base/coding-os.yaml.template +39 -0
- templates/_base/dimension-registry.template.md +68 -0
- templates/_base/domain-config.template.json +46 -0
- templates/_base/fragments/anatomy-map.md.tmpl +11 -0
- templates/_base/fragments/context-discipline.md.tmpl +3 -0
- templates/_base/fragments/core-loop.md.tmpl +52 -0
- templates/_base/fragments/engineering-routing.md.tmpl +3 -0
- templates/_base/fragments/header.md.tmpl +6 -0
- templates/_base/fragments/identity.md.tmpl +3 -0
- templates/_base/fragments/principles.md.tmpl +3 -0
- templates/_base/fragments/retrieval-routing.md.tmpl +23 -0
- templates/_base/fragments/session-handoff.md.tmpl +3 -0
- templates/_base/fragments/skills.md.tmpl +3 -0
- templates/_base/fragments/ssot-map.md.tmpl +3 -0
- templates/_base/fragments/stop-conditions.md.tmpl +3 -0
- templates/_base/fragments/subagent-dispatch.md.tmpl +3 -0
- templates/_base/fragments/task-authoring.md.tmpl +69 -0
- templates/_base/fragments/task-logging.md.tmpl +8 -0
- templates/_base/fragments/tool-routing.md.tmpl +9 -0
- templates/_base/fragments/verification-matrix.md.tmpl +12 -0
- templates/_base/lang/dart/analysis_options.yaml +7 -0
- templates/_base/lang/php/phpcs.xml.dist +10 -0
- templates/_base/lang/python/pyproject.toml +19 -0
- templates/_base/lang/rust/clippy.toml +5 -0
- templates/_base/lang/rust/rustfmt.toml +3 -0
- templates/_base/lang/typescript/eslint.config.js +26 -0
- templates/_base/lang/typescript/tsconfig.json +15 -0
- templates/_base/lang/typescript/vitest.config.ts +10 -0
- templates/_base/scaffold/changes.log +1 -0
- templates/_base/scaffold/docs/00-index.md +55 -0
- templates/_base/scaffold/docs/_meta/feature-dependency-tree.md +30 -0
- templates/_base/scaffold/docs/_meta/foundation-map.md +56 -0
- templates/_base/scaffold/docs/_meta/questions.md +8 -0
- templates/_base/scaffold/docs/_meta/roadmap.md +33 -0
- templates/_base/scaffold/docs/api-contracts/00-index.md +58 -0
- templates/_base/scaffold/docs/api-contracts/error-format.md +58 -0
- templates/_base/scaffold/docs/architecture/00-index.md +42 -0
- templates/_base/scaffold/docs/architecture/adr/00-index.md +39 -0
- templates/_base/scaffold/docs/engineering/00-index.md +9 -0
- templates/_base/scaffold/docs/governance/00-index.md +55 -0
- templates/_base/scaffold/docs/governance/_templates/doc-cheat-sheet.md +202 -0
- templates/_base/scaffold/docs/governance/_templates/playbook-template.md +81 -0
- templates/_base/scaffold/docs/governance/_templates/post-mortem-template.md +85 -0
- templates/_base/scaffold/docs/governance/_templates/runbook-template.md +88 -0
- templates/_base/scaffold/docs/governance/_templates/security-review-template.md +111 -0
- templates/_base/scaffold/docs/governance/_templates/task-detail.md +61 -0
- templates/_base/scaffold/docs/governance/agent-workflow.md +101 -0
- templates/_base/scaffold/docs/governance/anatomy-contract.md +150 -0
- templates/_base/scaffold/docs/governance/critical-rules.md +224 -0
- templates/_base/scaffold/docs/governance/decision-records.md +58 -0
- templates/_base/scaffold/docs/governance/docs-first-protocol.md +157 -0
- templates/_base/scaffold/docs/governance/docs-system.md +151 -0
- templates/_base/scaffold/docs/governance/gdpr-compliance.md +66 -0
- templates/_base/scaffold/docs/governance/mcp-tool-inventory.md +112 -0
- templates/_base/scaffold/docs/governance/risk-register.md +26 -0
- templates/_base/scaffold/docs/governance/scaffold-boundary-contract.md +161 -0
- templates/_base/scaffold/docs/governance/task-lifecycle.md +125 -0
- templates/_base/scaffold/docs/governance/wrapper-derivation.md +50 -0
- templates/_base/scaffold/docs/insights/00-index.md +17 -0
- templates/_base/scaffold/docs/ops/00-index.md +59 -0
- templates/_base/scaffold/docs/ops/runbooks/00-index.md +9 -0
- templates/_base/scaffold/docs/playbooks/00-index.md +12 -0
- templates/_base/scaffold/docs/playbooks/research-validation.md +29 -0
- templates/_base/scaffold/docs/playbooks/security-review.md +41 -0
- templates/_base/scaffold/docs/prd/00-index.md +43 -0
- templates/_base/scaffold/docs/prd/01-snapshot-vision.md +56 -0
- templates/_base/scaffold/docs/workflow/workflow-guide.md +138 -0
- templates/_base/scaffold/src/shared/README.md +23 -0
- templates/_base/skill-enforcement.template.md +14 -0
- templates/_base/task-detail.template.md +61 -0
- templates/_presets/ai-saas.yaml +9 -0
- templates/_presets/django-next.yaml +8 -0
- templates/_presets/dotnet-react.yaml +8 -0
- templates/_presets/flutter-baas.yaml +8 -0
- templates/_presets/go-react.yaml +8 -0
- templates/_presets/hexagonal-product.yaml +16 -0
- templates/_presets/jamstack.yaml +8 -0
- templates/_presets/laravel-vue.yaml +8 -0
- templates/_presets/mean.yaml +8 -0
- templates/_presets/mern.yaml +9 -0
- templates/_presets/nest-angular.yaml +8 -0
- templates/_presets/nextjs-fastapi.yaml +10 -0
- templates/_presets/nuxt-fullstack.yaml +9 -0
- templates/_presets/pern.yaml +9 -0
- templates/_presets/rails-react.yaml +8 -0
- templates/_presets/rn-api.yaml +8 -0
- templates/_presets/rust-svelte.yaml +8 -0
- templates/_presets/spring-react.yaml +8 -0
- templates/_presets/t3-style.yaml +9 -0
- templates/_presets/tall.yaml +8 -0
- templates/_presets/wordpress-cms.yaml +8 -0
- templates/angular/rules/frontend.md +19 -0
- templates/angular/scaffold/docs/engineering/accessibility.md +46 -0
- templates/angular/scaffold/docs/engineering/angular-rules.md +35 -0
- templates/angular/scaffold/docs/playbooks/angular-app.md +42 -0
- templates/angular/scaffold/src/frontend/angular.json +52 -0
- templates/angular/scaffold/src/frontend/package.json +28 -0
- templates/angular/scaffold/src/frontend/src/app/app.component.ts +19 -0
- templates/angular/scaffold/src/frontend/src/app/app.config.ts +22 -0
- templates/angular/scaffold/src/frontend/src/app/app.routes.ts +8 -0
- templates/angular/scaffold/src/frontend/src/app/core/global-error-handler.ts +12 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.component.ts +14 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.spec.ts +16 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.ts +10 -0
- templates/angular/scaffold/src/frontend/src/index.html +11 -0
- templates/angular/scaffold/src/frontend/src/main.ts +9 -0
- templates/angular/scaffold/src/frontend/src/styles.css +14 -0
- templates/angular/scaffold/src/frontend/tsconfig.app.json +8 -0
- templates/angular/scaffold/src/frontend/tsconfig.json +27 -0
- templates/angular/scaffold/src/frontend/tsconfig.spec.json +8 -0
- templates/angular/scaffold-boundary.yaml +27 -0
- templates/angular/skills/angular/SKILL.md +79 -0
- templates/angular/skills/angular/references/anatomy.md +69 -0
- templates/angular/stack.yaml +75 -0
- templates/aspnet-core/rules/backend.md +20 -0
- templates/aspnet-core/scaffold/docs/engineering/aspnet-core-rules.md +36 -0
- templates/aspnet-core/scaffold/docs/playbooks/aspnet-core-service.md +40 -0
- templates/aspnet-core/scaffold/src/backend/Backend.csproj +11 -0
- templates/aspnet-core/scaffold/src/backend/Backend.sln +27 -0
- templates/aspnet-core/scaffold/src/backend/Common/ExceptionHandlingMiddleware.cs +35 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthEndpoints.cs +10 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthService.cs +9 -0
- templates/aspnet-core/scaffold/src/backend/Program.cs +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/Backend.Tests.csproj +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/HealthServiceTests.cs +15 -0
- templates/aspnet-core/scaffold-boundary.yaml +24 -0
- templates/aspnet-core/skills/aspnet-core/SKILL.md +80 -0
- templates/aspnet-core/skills/aspnet-core/references/anatomy.md +65 -0
- templates/aspnet-core/stack.yaml +68 -0
- templates/astro/rules/frontend.md +20 -0
- templates/astro/scaffold/docs/engineering/astro-rules.md +40 -0
- templates/astro/scaffold/docs/playbooks/astro-app.md +57 -0
- templates/astro/scaffold/docs/playbooks/content-seo.md +37 -0
- templates/astro/scaffold/src/frontend/astro.config.mjs +10 -0
- templates/astro/scaffold/src/frontend/package.json +23 -0
- templates/astro/scaffold/src/frontend/src/components/Greeting.astro +13 -0
- templates/astro/scaffold/src/frontend/src/content/posts/hello.md +10 -0
- templates/astro/scaffold/src/frontend/src/content.config.ts +19 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.test.ts +39 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.ts +30 -0
- templates/astro/scaffold/src/frontend/src/pages/api/health.ts +13 -0
- templates/astro/scaffold/src/frontend/src/pages/index.astro +21 -0
- templates/astro/scaffold/src/frontend/tsconfig.json +9 -0
- templates/astro/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/astro/scaffold-boundary.yaml +28 -0
- templates/astro/skills/astro/SKILL.md +71 -0
- templates/astro/skills/astro/references/anatomy.md +66 -0
- templates/astro/stack.yaml +75 -0
- templates/csharp-plain/scaffold/src/backend/Backend.csproj +12 -0
- templates/csharp-plain/scaffold/src/backend/Program.cs +1 -0
- templates/csharp-plain/scaffold-boundary.yaml +23 -0
- templates/csharp-plain/stack.yaml +50 -0
- templates/django/rules/backend.md +18 -0
- templates/django/scaffold/docs/engineering/anti-ambiguity.md +74 -0
- templates/django/scaffold/docs/engineering/backend-rules.md +133 -0
- templates/django/scaffold/docs/engineering/glossary.md +51 -0
- templates/django/scaffold/docs/engineering/logging-standards.md +107 -0
- templates/django/scaffold/docs/engineering/naming-conventions.md +68 -0
- templates/django/scaffold/docs/engineering/secrets-rotation-runbook.md +142 -0
- templates/django/scaffold/docs/playbooks/backend-api.md +119 -0
- templates/django/scaffold/src/backend/config/__init__.py +0 -0
- templates/django/scaffold/src/backend/config/settings.py +31 -0
- templates/django/scaffold/src/backend/config/urls.py +11 -0
- templates/django/scaffold/src/backend/config/wsgi.py +6 -0
- templates/django/scaffold/src/backend/manage.py +14 -0
- templates/django/scaffold/src/backend/pyproject.toml +37 -0
- templates/django/scaffold/src/backend/tests/test_health.py +4 -0
- templates/django/scaffold-boundary.yaml +25 -0
- templates/django/skills/python-django/SKILL.md +450 -0
- templates/django/skills/python-django/references/anatomy.md +117 -0
- templates/django/skills/python-django/scripts/new_endpoint.py +89 -0
- templates/django/stack.yaml +73 -0
- templates/fastapi/rules/backend.md +18 -0
- templates/fastapi/scaffold/docs/engineering/fastapi-rules.md +37 -0
- templates/fastapi/scaffold/docs/playbooks/fastapi-service.md +30 -0
- templates/fastapi/scaffold/src/backend/app/__init__.py +0 -0
- templates/fastapi/scaffold/src/backend/app/main.py +8 -0
- templates/fastapi/scaffold/src/backend/pyproject.toml +39 -0
- templates/fastapi/scaffold/src/backend/tests/test_health.py +11 -0
- templates/fastapi/scaffold-boundary.yaml +25 -0
- templates/fastapi/skills/python-fastapi/SKILL.md +75 -0
- templates/fastapi/skills/python-fastapi/references/anatomy.md +117 -0
- templates/fastapi/skills/python-fastapi/scripts/new_endpoint.py +101 -0
- templates/fastapi/stack.yaml +57 -0
- templates/flutter/rules/mobile.md +26 -0
- templates/flutter/scaffold/docs/engineering/flutter-rules.md +35 -0
- templates/flutter/scaffold/docs/playbooks/flutter-app.md +45 -0
- templates/flutter/scaffold/src/mobile/lib/core/error_mapper.dart +17 -0
- templates/flutter/scaffold/src/mobile/lib/core/router.dart +13 -0
- templates/flutter/scaffold/src/mobile/lib/main.dart +22 -0
- templates/flutter/scaffold/src/mobile/lib/screens/health_screen.dart +32 -0
- templates/flutter/scaffold/src/mobile/lib/services/health_service.dart +11 -0
- templates/flutter/scaffold/src/mobile/lib/state/health_provider.dart +13 -0
- templates/flutter/scaffold/src/mobile/pubspec.yaml +22 -0
- templates/flutter/scaffold/src/mobile/test/health_provider_test.dart +90 -0
- templates/flutter/scaffold-boundary.yaml +28 -0
- templates/flutter/skills/flutter/SKILL.md +76 -0
- templates/flutter/skills/flutter/references/anatomy.md +64 -0
- templates/flutter/stack.yaml +70 -0
- templates/go/rules/backend.md +19 -0
- templates/go/scaffold/docs/engineering/go-rules.md +45 -0
- templates/go/scaffold/docs/playbooks/go-service.md +30 -0
- templates/go/scaffold/src/backend/cmd/api/main.go +22 -0
- templates/go/scaffold/src/backend/cmd/api/main_test.go +20 -0
- templates/go/scaffold/src/backend/go.mod +3 -0
- templates/go/scaffold-boundary.yaml +25 -0
- templates/go/skills/go-patterns/SKILL.md +68 -0
- templates/go/skills/go-patterns/assets/go-checklist.md +29 -0
- templates/go/skills/go-patterns/references/anatomy.md +115 -0
- templates/go/skills/go-patterns/references/go-2026-idioms.md +92 -0
- templates/go/skills/go-patterns/scripts/new_endpoint.py +117 -0
- templates/go/skills/go-patterns/versions.json +16 -0
- templates/go/stack.yaml +54 -0
- templates/go-fiber/rules/backend.md +20 -0
- templates/go-fiber/scaffold/docs/engineering/fiber-rules.md +97 -0
- templates/go-fiber/scaffold/docs/playbooks/fiber-service.md +149 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main.go +21 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main_test.go +17 -0
- templates/go-fiber/scaffold/src/backend/go.mod +23 -0
- templates/go-fiber/scaffold/src/backend/go.sum +49 -0
- templates/go-fiber/scaffold-boundary.yaml +26 -0
- templates/go-fiber/skills/go-fiber/SKILL.md +203 -0
- templates/go-fiber/skills/go-fiber/assets/fiber-checklist.md +26 -0
- templates/go-fiber/skills/go-fiber/references/anatomy.md +116 -0
- templates/go-fiber/skills/go-fiber/references/fiber-v3-patterns.md +89 -0
- templates/go-fiber/skills/go-fiber/scripts/new_endpoint.py +107 -0
- templates/go-fiber/skills/go-fiber/versions.json +16 -0
- templates/go-fiber/stack.yaml +59 -0
- templates/go-plain/scaffold/src/backend/go.mod +3 -0
- templates/go-plain/scaffold/src/backend/main.go +7 -0
- templates/go-plain/scaffold-boundary.yaml +22 -0
- templates/go-plain/stack.yaml +51 -0
- templates/java-plain/scaffold/src/backend/mvnw +302 -0
- templates/java-plain/scaffold/src/backend/pom.xml +41 -0
- templates/java-plain/scaffold/src/backend/src/main/java/com/example/app/Main.java +10 -0
- templates/java-plain/scaffold-boundary.yaml +23 -0
- templates/java-plain/stack.yaml +51 -0
- templates/laravel/rules/backend.md +20 -0
- templates/laravel/scaffold/docs/engineering/laravel-rules.md +28 -0
- templates/laravel/scaffold/docs/playbooks/laravel-service.md +28 -0
- templates/laravel/scaffold/src/backend/app/Exceptions/Handler.php +27 -0
- templates/laravel/scaffold/src/backend/app/Http/Controllers/HealthController.php +15 -0
- templates/laravel/scaffold/src/backend/app/Support/HealthStatus.php +14 -0
- templates/laravel/scaffold/src/backend/composer.json +24 -0
- templates/laravel/scaffold/src/backend/phpunit.xml +10 -0
- templates/laravel/scaffold/src/backend/public/index.php +8 -0
- templates/laravel/scaffold/src/backend/routes/api.php +7 -0
- templates/laravel/scaffold/src/backend/tests/Unit/HealthStatusTest.php +16 -0
- templates/laravel/scaffold-boundary.yaml +23 -0
- templates/laravel/skills/laravel/SKILL.md +56 -0
- templates/laravel/skills/laravel/references/anatomy.md +65 -0
- templates/laravel/stack.yaml +66 -0
- templates/meta/rules/graph-first.md +27 -0
- templates/meta/rules/hook-author.md +19 -0
- templates/meta/rules/mcp-tool-author.md +18 -0
- templates/meta/rules/meta-engineering.md +17 -0
- templates/meta/scaffold-boundary.yaml +55 -0
- templates/meta/skills/claude-sdk-integration/SKILL.md +163 -0
- templates/meta/skills/claude-sdk-integration/assets/sdk-checklist.md +27 -0
- templates/meta/skills/claude-sdk-integration/scripts/check_model_ids.py +97 -0
- templates/meta/skills/graph-os-authoring/SKILL.md +278 -0
- templates/meta/skills/graph-os-authoring/assets/graph-os-checklist.md +25 -0
- templates/meta/skills/graph-os-authoring/scripts/new_extractor.py +76 -0
- templates/meta/skills/hook-authoring/SKILL.md +292 -0
- templates/meta/skills/hook-authoring/assets/hook-checklist.md +30 -0
- templates/meta/skills/hook-authoring/scripts/new_hook.sh +75 -0
- templates/meta/skills/mcp-tool-authoring/SKILL.md +301 -0
- templates/meta/skills/mcp-tool-authoring/assets/mcp-tool-checklist.md +29 -0
- templates/meta/skills/mcp-tool-authoring/scripts/new_tool.py +74 -0
- templates/meta/skills/meta-engineering/SKILL.md +151 -0
- templates/meta/skills/meta-engineering/assets/meta-edit-checklist.md +28 -0
- templates/meta/skills/meta-engineering/scripts/which_layer.py +61 -0
- templates/meta/skills/python-meta-server/SKILL.md +162 -0
- templates/meta/skills/python-meta-server/assets/meta-server-checklist.md +28 -0
- templates/meta/skills/python-meta-server/scripts/check_envelope.py +91 -0
- templates/meta/skills/react-vite-hub/SKILL.md +140 -0
- templates/meta/skills/react-vite-hub/assets/hub-ui-checklist.md +23 -0
- templates/meta/skills/react-vite-hub/scripts/check_vite_env.py +73 -0
- templates/meta/stack.yaml +119 -0
- templates/nestjs/rules/backend.md +20 -0
- templates/nestjs/scaffold/docs/engineering/nestjs-rules.md +33 -0
- templates/nestjs/scaffold/docs/playbooks/nestjs-service.md +39 -0
- templates/nestjs/scaffold/src/backend/nest-cli.json +5 -0
- templates/nestjs/scaffold/src/backend/package.json +28 -0
- templates/nestjs/scaffold/src/backend/src/app.module.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/common/all-exceptions.filter.ts +59 -0
- templates/nestjs/scaffold/src/backend/src/health/health.controller.ts +14 -0
- templates/nestjs/scaffold/src/backend/src/health/health.module.ts +10 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.spec.ts +21 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/main.ts +26 -0
- templates/nestjs/scaffold/src/backend/tsconfig.json +16 -0
- templates/nestjs/scaffold/src/backend/vitest.config.ts +9 -0
- templates/nestjs/scaffold-boundary.yaml +25 -0
- templates/nestjs/skills/nestjs/SKILL.md +67 -0
- templates/nestjs/skills/nestjs/references/anatomy.md +65 -0
- templates/nestjs/stack.yaml +68 -0
- templates/nextjs/rules/frontend.md +18 -0
- templates/nextjs/scaffold/docs/design/00-index.md +23 -0
- templates/nextjs/scaffold/docs/design/colors-tokens.md +141 -0
- templates/nextjs/scaffold/docs/design/components-patterns.md +159 -0
- templates/nextjs/scaffold/docs/design/motion-accessibility.md +137 -0
- templates/nextjs/scaffold/docs/design/typography-spacing.md +107 -0
- templates/nextjs/scaffold/docs/engineering/accessibility-web.md +56 -0
- templates/nextjs/scaffold/docs/engineering/copywriting-standard.md +102 -0
- templates/nextjs/scaffold/docs/engineering/formatting-rules.md +89 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rendering-rules.md +80 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rules.md +183 -0
- templates/nextjs/scaffold/docs/engineering/i18n-policy.md +99 -0
- templates/nextjs/scaffold/docs/pages-content-spec/00-index.md +78 -0
- templates/nextjs/scaffold/docs/playbooks/content-seo.md +55 -0
- templates/nextjs/scaffold/docs/playbooks/docs-governance.md +51 -0
- templates/nextjs/scaffold/docs/playbooks/frontend-ui.md +63 -0
- templates/nextjs/scaffold/src/frontend/app/layout.tsx +14 -0
- templates/nextjs/scaffold/src/frontend/app/page.tsx +3 -0
- templates/nextjs/scaffold/src/frontend/eslint.config.js +17 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.test.ts +9 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.ts +3 -0
- templates/nextjs/scaffold/src/frontend/package.json +28 -0
- templates/nextjs/scaffold/src/frontend/tsconfig.json +18 -0
- templates/nextjs/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/nextjs/scaffold-boundary.yaml +30 -0
- templates/nextjs/skills/nextjs-react/SKILL.md +485 -0
- templates/nextjs/skills/nextjs-react/references/anatomy.md +116 -0
- templates/nextjs/skills/nextjs-react/scripts/new_component.py +72 -0
- templates/nextjs/stack.yaml +81 -0
- templates/node-express/rules/backend.md +20 -0
- templates/node-express/scaffold/docs/engineering/express-rules.md +30 -0
- templates/node-express/scaffold/docs/playbooks/express-service.md +35 -0
- templates/node-express/scaffold/src/backend/package.json +24 -0
- templates/node-express/scaffold/src/backend/src/index.ts +17 -0
- templates/node-express/scaffold/src/backend/src/middleware/error-handler.ts +12 -0
- templates/node-express/scaffold/src/backend/src/routes/health.test.ts +33 -0
- templates/node-express/scaffold/src/backend/src/routes/health.ts +7 -0
- templates/node-express/scaffold/src/backend/tsconfig.json +15 -0
- templates/node-express/scaffold/src/backend/types/express-bootstrap.d.ts +21 -0
- templates/node-express/scaffold-boundary.yaml +25 -0
- templates/node-express/skills/node-express/SKILL.md +70 -0
- templates/node-express/skills/node-express/references/anatomy.md +63 -0
- templates/node-express/stack.yaml +63 -0
- templates/python/scaffold/docs/engineering/python-rules.md +27 -0
- templates/python/scaffold/docs/playbooks/python-library.md +35 -0
- templates/python/stack.yaml +60 -0
- templates/rails/rules/backend.md +10 -0
- templates/rails/scaffold/docs/engineering/rails-rules.md +33 -0
- templates/rails/scaffold/docs/playbooks/rails-service.md +42 -0
- templates/rails/scaffold/src/backend/Gemfile +12 -0
- templates/rails/scaffold/src/backend/app/controllers/application_controller.rb +26 -0
- templates/rails/scaffold/src/backend/app/controllers/health_controller.rb +6 -0
- templates/rails/scaffold/src/backend/app/models/health.rb +6 -0
- templates/rails/scaffold/src/backend/config/application.rb +12 -0
- templates/rails/scaffold/src/backend/config/boot.rb +3 -0
- templates/rails/scaffold/src/backend/config/routes.rb +4 -0
- templates/rails/scaffold/src/backend/config.ru +5 -0
- templates/rails/scaffold/src/backend/spec/rails_helper.rb +18 -0
- templates/rails/scaffold/src/backend/spec/requests/health_spec.rb +24 -0
- templates/rails/scaffold-boundary.yaml +25 -0
- templates/rails/skills/rails/SKILL.md +62 -0
- templates/rails/skills/rails/references/anatomy.md +71 -0
- templates/rails/stack.yaml +72 -0
- templates/react-native/rules/mobile.md +26 -0
- templates/react-native/scaffold/docs/engineering/accessibility-mobile.md +95 -0
- templates/react-native/scaffold/docs/engineering/mobile-rules.md +56 -0
- templates/react-native/scaffold/docs/engineering/offline-first.md +61 -0
- templates/react-native/scaffold/docs/playbooks/mobile-app.md +49 -0
- templates/react-native/scaffold/src/mobile/App.tsx +9 -0
- templates/react-native/scaffold/src/mobile/eslint.config.js +17 -0
- templates/react-native/scaffold/src/mobile/package.json +23 -0
- templates/react-native/scaffold/src/mobile/src/greeting.test.ts +9 -0
- templates/react-native/scaffold/src/mobile/src/greeting.ts +3 -0
- templates/react-native/scaffold/src/mobile/tsconfig.json +17 -0
- templates/react-native/scaffold/src/mobile/vitest.config.ts +10 -0
- templates/react-native/scaffold-boundary.yaml +30 -0
- templates/react-native/skills/react-native-mobile/SKILL.md +119 -0
- templates/react-native/skills/react-native-mobile/assets/rn-mobile-checklist.md +28 -0
- templates/react-native/skills/react-native-mobile/references/anatomy.md +140 -0
- templates/react-native/skills/react-native-mobile/references/rn-2026-practices.md +54 -0
- templates/react-native/skills/react-native-mobile/scripts/new_screen.py +73 -0
- templates/react-native/skills/react-native-mobile/versions.json +16 -0
- templates/react-native/skills/react-native-patterns/SKILL.md +512 -0
- templates/react-native/skills/react-native-patterns/assets/rn-review-checklist.md +26 -0
- templates/react-native/skills/react-native-patterns/references/anatomy.md +62 -0
- templates/react-native/skills/react-native-patterns/references/list-performance.md +70 -0
- templates/react-native/skills/react-native-patterns/scripts/scan_rn_perf.py +77 -0
- templates/react-native/stack.yaml +70 -0
- templates/ruby-plain/scaffold/src/backend/Gemfile +8 -0
- templates/ruby-plain/scaffold/src/backend/main.rb +3 -0
- templates/ruby-plain/scaffold-boundary.yaml +23 -0
- templates/ruby-plain/stack.yaml +50 -0
- templates/rust-axum/rules/backend.md +20 -0
- templates/rust-axum/scaffold/docs/engineering/rust-axum-rules.md +35 -0
- templates/rust-axum/scaffold/docs/playbooks/rust-axum-service.md +43 -0
- templates/rust-axum/scaffold/src/backend/Cargo.toml +20 -0
- templates/rust-axum/scaffold/src/backend/src/app.rs +12 -0
- templates/rust-axum/scaffold/src/backend/src/error.rs +48 -0
- templates/rust-axum/scaffold/src/backend/src/main.rs +24 -0
- templates/rust-axum/scaffold/src/backend/src/routes/health.rs +37 -0
- templates/rust-axum/scaffold/src/backend/src/routes/mod.rs +2 -0
- templates/rust-axum/scaffold-boundary.yaml +25 -0
- templates/rust-axum/skills/rust/SKILL.md +73 -0
- templates/rust-axum/skills/rust/references/anatomy.md +63 -0
- templates/rust-axum/stack.yaml +65 -0
- templates/rust-plain/scaffold/src/backend/Cargo.toml +6 -0
- templates/rust-plain/scaffold/src/backend/src/main.rs +3 -0
- templates/rust-plain/scaffold-boundary.yaml +23 -0
- templates/rust-plain/stack.yaml +51 -0
- templates/spring-boot/rules/backend.md +20 -0
- templates/spring-boot/scaffold/docs/engineering/spring-boot-rules.md +35 -0
- templates/spring-boot/scaffold/docs/playbooks/spring-boot-service.md +45 -0
- templates/spring-boot/scaffold/src/backend/mvnw +302 -0
- templates/spring-boot/scaffold/src/backend/pom.xml +69 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/Application.java +16 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/common/GlobalExceptionHandler.java +31 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthController.java +22 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthService.java +12 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthStatus.java +4 -0
- templates/spring-boot/scaffold/src/backend/src/test/java/com/example/app/health/HealthServiceTest.java +15 -0
- templates/spring-boot/scaffold-boundary.yaml +26 -0
- templates/spring-boot/skills/spring-boot/SKILL.md +84 -0
- templates/spring-boot/skills/spring-boot/references/anatomy.md +63 -0
- templates/spring-boot/stack.yaml +65 -0
- templates/svelte-sveltekit/rules/frontend.md +20 -0
- templates/svelte-sveltekit/scaffold/docs/engineering/svelte-sveltekit-rules.md +37 -0
- templates/svelte-sveltekit/scaffold/docs/playbooks/svelte-sveltekit-app.md +36 -0
- templates/svelte-sveltekit/scaffold/src/frontend/package.json +22 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/app.html +12 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/hooks.server.ts +14 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/components/Greeting.svelte +6 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.test.ts +26 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.ts +4 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+layout.svelte +24 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.svelte +9 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/health/+server.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/svelte.config.js +10 -0
- templates/svelte-sveltekit/scaffold/src/frontend/tsconfig.json +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vite.config.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vitest.config.ts +11 -0
- templates/svelte-sveltekit/scaffold-boundary.yaml +29 -0
- templates/svelte-sveltekit/skills/svelte/SKILL.md +90 -0
- templates/svelte-sveltekit/skills/svelte/references/anatomy.md +62 -0
- templates/svelte-sveltekit/stack.yaml +70 -0
- templates/typescript-plain/scaffold/src/index.ts +3 -0
- templates/typescript-plain/scaffold/tsconfig.json +13 -0
- templates/typescript-plain/scaffold-boundary.yaml +22 -0
- templates/typescript-plain/stack.yaml +44 -0
- templates/vue-nuxt/rules/frontend.md +19 -0
- templates/vue-nuxt/scaffold/docs/engineering/nuxt-rules.md +30 -0
- templates/vue-nuxt/scaffold/docs/playbooks/nuxt-app.md +29 -0
- templates/vue-nuxt/scaffold/src/frontend/app.vue +3 -0
- templates/vue-nuxt/scaffold/src/frontend/nuxt.config.ts +11 -0
- templates/vue-nuxt/scaffold/src/frontend/package.json +20 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.test.ts +19 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.vue +11 -0
- templates/vue-nuxt/scaffold/src/frontend/vitest.config.ts +12 -0
- templates/vue-nuxt/scaffold-boundary.yaml +26 -0
- templates/vue-nuxt/skills/vue-nuxt/SKILL.md +57 -0
- templates/vue-nuxt/skills/vue-nuxt/references/anatomy.md +60 -0
- templates/vue-nuxt/stack.yaml +61 -0
- templates/wordpress/rules/backend.md +19 -0
- templates/wordpress/scaffold/docs/engineering/wordpress-rules.md +28 -0
- templates/wordpress/scaffold/docs/playbooks/wordpress-service.md +29 -0
- templates/wordpress/scaffold/src/backend/composer.json +17 -0
- templates/wordpress/scaffold/src/backend/phpcs.xml.dist +11 -0
- templates/wordpress/scaffold/src/backend/phpunit.xml +10 -0
- templates/wordpress/scaffold/src/backend/plugin/inc/health.php +8 -0
- templates/wordpress/scaffold/src/backend/plugin/plugin.php +28 -0
- templates/wordpress/scaffold/src/backend/tests/HealthStatusTest.php +15 -0
- templates/wordpress/scaffold/src/backend/theme/functions.php +18 -0
- templates/wordpress/scaffold/src/backend/theme/style.css +11 -0
- templates/wordpress/scaffold-boundary.yaml +23 -0
- templates/wordpress/skills/wordpress/SKILL.md +110 -0
- templates/wordpress/skills/wordpress/assets/wp-checklist.md +28 -0
- templates/wordpress/skills/wordpress/references/wp-development.md +75 -0
- templates/wordpress/skills/wordpress/references/wp-security.md +68 -0
- templates/wordpress/skills/wordpress/scripts/scan_wp_smells.py +91 -0
- templates/wordpress/skills/wordpress/versions.json +16 -0
- templates/wordpress/stack.yaml +60 -0
- thinking_os/__init__.py +1 -0
- thinking_os/_agent_markers.py +32 -0
- thinking_os/background.py +405 -0
- thinking_os/bootstrap_outcomes.py +200 -0
- thinking_os/budget.py +302 -0
- thinking_os/capture.py +495 -0
- thinking_os/cognition.py +516 -0
- thinking_os/cognition_schemas.py +517 -0
- thinking_os/compress.py +192 -0
- thinking_os/concepts.py +233 -0
- thinking_os/dashboard.py +159 -0
- thinking_os/database.py +2883 -0
- thinking_os/decay.py +393 -0
- thinking_os/digest.py +295 -0
- thinking_os/dispatcher.py +192 -0
- thinking_os/dispatcher_helpers.py +48 -0
- thinking_os/dispatchers/__init__.py +3 -0
- thinking_os/dispatchers/default.py +47 -0
- thinking_os/distill.py +192 -0
- thinking_os/doc_indexer.py +905 -0
- thinking_os/embeddings.py +943 -0
- thinking_os/formula_composer.py +556 -0
- thinking_os/gate_marker.py +75 -0
- thinking_os/graph.py +296 -0
- thinking_os/graph_indexer.py +360 -0
- thinking_os/health_check.py +517 -0
- thinking_os/impact.py +119 -0
- thinking_os/memory_gc.py +356 -0
- thinking_os/migrator_embeddings.py +318 -0
- thinking_os/precision.py +194 -0
- thinking_os/record_outcome.py +399 -0
- thinking_os/repair.py +105 -0
- thinking_os/retrieval_quality.py +239 -0
- thinking_os/roles_state.py +168 -0
- thinking_os/sanitizer.py +320 -0
- thinking_os/server.py +3160 -0
- thinking_os/session_enrich.py +272 -0
- thinking_os/session_observe_worker.py +111 -0
- thinking_os/session_startup.py +74 -0
- thinking_os/session_summary.py +245 -0
- thinking_os/task_analyzer.py +462 -0
- thinking_os/task_parser.py +342 -0
- thinking_os/task_sync.py +73 -0
- thinking_os/tools/__init__.py +6 -0
- thinking_os/tools/_shared.py +947 -0
- thinking_os/tools/cognition.py +1867 -0
- thinking_os/tools/docs.py +770 -0
- thinking_os/tools/learning.py +2078 -0
- thinking_os/tools/logs.py +79 -0
- thinking_os/tools/memory.py +840 -0
- thinking_os/tools/metrics.py +200 -0
- thinking_os/tools/retrieve.py +415 -0
- thinking_os/tools/routing.py +658 -0
- thinking_os/tools/tasks.py +449 -0
- thinking_os/tools/trajectory.py +181 -0
- thinking_os/tracing.py +235 -0
- web/__init__.py +5 -0
- web/_cache.py +118 -0
- web/_deps.py +56 -0
- web/_envelope.py +85 -0
- web/_project_context.py +140 -0
- web/chat_providers.py +108 -0
- web/init_jobs.py +216 -0
- web/routes/__init__.py +25 -0
- web/routes/_bounded_read.py +76 -0
- web/routes/board.py +1089 -0
- web/routes/cognition.py +1838 -0
- web/routes/config.py +635 -0
- web/routes/graph.py +513 -0
- web/routes/health.py +180 -0
- web/routes/hooks.py +288 -0
- web/routes/hub.py +1219 -0
- web/routes/logs.py +374 -0
- web/routes/metrics.py +43 -0
- web/routes/observability.py +400 -0
- web/routes/patterns.py +227 -0
- web/routes/presence.py +609 -0
- web/routes/roles.py +446 -0
- web/routes/scheduled.py +261 -0
- web/routes/search.py +238 -0
- web/routes/sessions.py +220 -0
- web/routes/settings.py +363 -0
- web/routes/stream.py +547 -0
- web/security.py +157 -0
- web/server.py +283 -0
|
@@ -0,0 +1,1838 @@
|
|
|
1
|
+
"""core.web.routes.cognition — /api/cognition/* HTTP wrappers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import dataclasses
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sqlite3
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
18
|
+
from fastapi.responses import StreamingResponse
|
|
19
|
+
|
|
20
|
+
from .._deps import make_metrics_dep, make_rate_limit_dep
|
|
21
|
+
from .._envelope import ENVELOPE_ERROR_RESPONSES, unwrap
|
|
22
|
+
from ._bounded_read import DEFAULT_WINDOW, tail_lines
|
|
23
|
+
|
|
24
|
+
# A trace jsonl can reach GBs (e.g. a long run_await loop). Read only the tail
|
|
25
|
+
# so the viewer shows the most-recent events without OOMing the server. TASK-225.
|
|
26
|
+
_MAX_TRACE_EVENTS = 2000
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_CORE_DIR = Path(__file__).resolve().parents[3]
|
|
31
|
+
if str(_CORE_DIR) not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(_CORE_DIR))
|
|
33
|
+
|
|
34
|
+
router = APIRouter(prefix="/api/cognition", tags=["cognition"], responses=ENVELOPE_ERROR_RESPONSES)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _state_dir() -> Path:
|
|
38
|
+
"""Resolve the .coding-os state directory.
|
|
39
|
+
|
|
40
|
+
Per-project requests (`/api/p/<slug>/...`) ALWAYS use that project's
|
|
41
|
+
`.coding-os/` — env vars cannot override scope. Otherwise env vars
|
|
42
|
+
win for backwards compatibility with tests + manual overrides.
|
|
43
|
+
"""
|
|
44
|
+
from web._project_context import current_project_root, is_explicit_project_scope
|
|
45
|
+
|
|
46
|
+
if is_explicit_project_scope():
|
|
47
|
+
return current_project_root() / ".coding-os"
|
|
48
|
+
base = os.environ.get("COS_STATE_DIR") or os.environ.get("COS_AGENT_DIR")
|
|
49
|
+
if base:
|
|
50
|
+
return Path(base).resolve()
|
|
51
|
+
return current_project_root() / ".coding-os"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cognition_module():
|
|
55
|
+
"""Lazy import for cognition tools."""
|
|
56
|
+
try:
|
|
57
|
+
tos_dir = _CORE_DIR / "thinking_os"
|
|
58
|
+
if str(tos_dir) not in sys.path:
|
|
59
|
+
sys.path.insert(0, str(tos_dir))
|
|
60
|
+
from tools import cognition as _cog # type: ignore
|
|
61
|
+
|
|
62
|
+
return _cog
|
|
63
|
+
except ImportError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _unavailable(msg: str = "cognition tools not available"):
|
|
68
|
+
return json.dumps(
|
|
69
|
+
{
|
|
70
|
+
"ok": False,
|
|
71
|
+
"error": {"category": "unavailable", "retryable": False, "message": msg},
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _auto_route_model(prompt: str) -> dict:
|
|
77
|
+
"""Deterministic Auto-model triage (hub-architecture.md § Hub settings
|
|
78
|
+
contract): classify the prompt, prefer cos_route_model's empirical pick
|
|
79
|
+
when history exists, else the settings' orchestrator_model."""
|
|
80
|
+
from .settings import _load as _load_hub_settings
|
|
81
|
+
|
|
82
|
+
routing_cfg = _load_hub_settings().get("model_routing") or {}
|
|
83
|
+
if not routing_cfg.get("enabled"):
|
|
84
|
+
return {"error": "model 'auto' requires settings.model_routing.enabled"}
|
|
85
|
+
|
|
86
|
+
cog = _cognition_module()
|
|
87
|
+
complexity = "COMPLICATED"
|
|
88
|
+
if cog is not None and hasattr(cog, "classify_prompt_heuristic"):
|
|
89
|
+
complexity = cog.classify_prompt_heuristic(prompt)["complexity"]
|
|
90
|
+
|
|
91
|
+
routed = ""
|
|
92
|
+
source = "orchestrator_default"
|
|
93
|
+
try:
|
|
94
|
+
from thinking_os.database import resolve_db_path # type: ignore
|
|
95
|
+
from tools.routing import route_model # type: ignore
|
|
96
|
+
|
|
97
|
+
conn = sqlite3.connect(str(resolve_db_path()))
|
|
98
|
+
try:
|
|
99
|
+
recommendation = route_model(conn, complexity=complexity)
|
|
100
|
+
finally:
|
|
101
|
+
conn.close()
|
|
102
|
+
if int(recommendation.get("data_points") or 0) > 0:
|
|
103
|
+
routed = str(recommendation.get("recommended_model") or "")
|
|
104
|
+
source = "empirical"
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
logger.debug("auto-route empirical lookup failed: %s", exc)
|
|
107
|
+
|
|
108
|
+
if not routed:
|
|
109
|
+
routed = str(routing_cfg.get("orchestrator_model") or "")
|
|
110
|
+
source = "orchestrator_default"
|
|
111
|
+
return {"model": routed, "complexity": complexity, "source": source}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _enrich_trace_row(row: dict) -> dict:
|
|
115
|
+
"""Augment a session row with cheap trace stats (event_count, first_kind).
|
|
116
|
+
|
|
117
|
+
Every row returns the same shape — frontend `TraceList` indexes
|
|
118
|
+
`mtime_ts`, `event_count`, `first_event_kind` unconditionally; missing
|
|
119
|
+
fields make the rows render with broken relative-time and "0ev"
|
|
120
|
+
placeholders. Session-only entries (no jsonl yet) get derived stats:
|
|
121
|
+
- mtime_ts = newest activity timestamp from the session.json
|
|
122
|
+
- event_count = 0
|
|
123
|
+
- first_event_kind = None
|
|
124
|
+
"""
|
|
125
|
+
trace_path = row.get("trace_path")
|
|
126
|
+
enriched = dict(row)
|
|
127
|
+
enriched.setdefault("event_count", 0)
|
|
128
|
+
enriched.setdefault("first_event_kind", None)
|
|
129
|
+
|
|
130
|
+
if trace_path:
|
|
131
|
+
p = Path(trace_path)
|
|
132
|
+
event_count = 0
|
|
133
|
+
first_kind: str | None = None
|
|
134
|
+
try:
|
|
135
|
+
with p.open("r", encoding="utf-8", errors="ignore") as fh:
|
|
136
|
+
for i, line in enumerate(fh):
|
|
137
|
+
stripped = line.strip()
|
|
138
|
+
if not stripped:
|
|
139
|
+
continue
|
|
140
|
+
event_count += 1
|
|
141
|
+
if first_kind is None:
|
|
142
|
+
try:
|
|
143
|
+
payload = json.loads(stripped)
|
|
144
|
+
first_kind = payload.get("kind") if isinstance(payload, dict) else None
|
|
145
|
+
except (json.JSONDecodeError, ValueError):
|
|
146
|
+
first_kind = "raw"
|
|
147
|
+
if i > 5000:
|
|
148
|
+
break # cap — prevent pathological scans
|
|
149
|
+
except OSError as exc:
|
|
150
|
+
logger.debug("trace scan skipped %s: %s", p, exc)
|
|
151
|
+
enriched["path"] = trace_path # legacy alias kept for older clients
|
|
152
|
+
enriched["event_count"] = event_count
|
|
153
|
+
enriched["first_event_kind"] = first_kind
|
|
154
|
+
|
|
155
|
+
# Pick the most-recent timestamp available — same rule as the
|
|
156
|
+
# /api/cognition/traces sort key (max, not first-truthy).
|
|
157
|
+
candidates = [
|
|
158
|
+
row.get("modified_ts"),
|
|
159
|
+
row.get("last_tool_at"),
|
|
160
|
+
row.get("last_prompt_at"),
|
|
161
|
+
row.get("last_stop_at"),
|
|
162
|
+
row.get("started_at"),
|
|
163
|
+
]
|
|
164
|
+
fresh = max((float(c) for c in candidates if c is not None), default=0.0)
|
|
165
|
+
enriched["mtime_ts"] = int(fresh) if fresh > 0 else None
|
|
166
|
+
return enriched
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.get("/traces")
|
|
170
|
+
def list_traces(
|
|
171
|
+
agent: str | None = Query(None, description="Agent name (e.g. 'claude')"),
|
|
172
|
+
_rl=Depends(make_rate_limit_dep("cognition.traces")),
|
|
173
|
+
_m=Depends(make_metrics_dep("cognition.traces")),
|
|
174
|
+
):
|
|
175
|
+
"""List trace + session-only entries with activity sort."""
|
|
176
|
+
from web.routes.observability import _scan_sessions # type: ignore
|
|
177
|
+
|
|
178
|
+
state = _state_dir()
|
|
179
|
+
if not state.exists():
|
|
180
|
+
return unwrap(
|
|
181
|
+
json.dumps(
|
|
182
|
+
{
|
|
183
|
+
"ok": True,
|
|
184
|
+
"data": {
|
|
185
|
+
"sessions": [],
|
|
186
|
+
"count": 0,
|
|
187
|
+
"trace_count": 0,
|
|
188
|
+
"session_count": 0,
|
|
189
|
+
"meta": {"layer": "cognition"},
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
rows = _scan_sessions(state, agent_filter=agent)
|
|
196
|
+
enriched = [_enrich_trace_row(r) for r in rows]
|
|
197
|
+
trace_count = sum(1 for r in enriched if r.get("has_trace"))
|
|
198
|
+
session_count = sum(1 for r in enriched if r.get("source") in ("session-only", "trace+session"))
|
|
199
|
+
|
|
200
|
+
return unwrap(
|
|
201
|
+
json.dumps(
|
|
202
|
+
{
|
|
203
|
+
"ok": True,
|
|
204
|
+
"data": {
|
|
205
|
+
"sessions": enriched,
|
|
206
|
+
"count": len(enriched),
|
|
207
|
+
"trace_count": trace_count,
|
|
208
|
+
"session_count": session_count,
|
|
209
|
+
"meta": {"layer": "cognition"},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _safe_seg(seg: str | None) -> bool:
|
|
217
|
+
# A path segment (session_id / agent) is safe iff it is a non-empty run of
|
|
218
|
+
# [A-Za-z0-9_-] — rejects '/', '..', and any traversal before it reaches a
|
|
219
|
+
# filesystem join. Session ids are ses-<agent>-<ts>-<pid>; agents are alnum.
|
|
220
|
+
return bool(seg) and all(c.isalnum() or c in "-_" for c in seg)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _find_trace_file(
|
|
224
|
+
state: Path, session_id: str, agent: str | None
|
|
225
|
+
) -> tuple[Path | None, str | None]:
|
|
226
|
+
"""Locate a session's jsonl trace file across all (or one) agent dir."""
|
|
227
|
+
if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
|
|
228
|
+
return (None, None)
|
|
229
|
+
if agent:
|
|
230
|
+
candidate = state / agent / "traces" / f"{session_id}.jsonl"
|
|
231
|
+
return (candidate, agent) if candidate.exists() else (None, agent)
|
|
232
|
+
if not state.is_dir():
|
|
233
|
+
return (None, None)
|
|
234
|
+
for agent_dir in state.iterdir():
|
|
235
|
+
if not agent_dir.is_dir():
|
|
236
|
+
continue
|
|
237
|
+
candidate = agent_dir / "traces" / f"{session_id}.jsonl"
|
|
238
|
+
if candidate.exists():
|
|
239
|
+
return (candidate, agent_dir.name)
|
|
240
|
+
return (None, None)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _find_session_meta(
|
|
244
|
+
state: Path, session_id: str, agent: str | None
|
|
245
|
+
) -> tuple[dict | None, str | None]:
|
|
246
|
+
"""Locate a session's .json metadata across all (or one) agent dir."""
|
|
247
|
+
if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
|
|
248
|
+
return (None, None)
|
|
249
|
+
if agent:
|
|
250
|
+
p = state / agent / "sessions" / f"{session_id}.json"
|
|
251
|
+
if p.exists():
|
|
252
|
+
try:
|
|
253
|
+
return json.loads(p.read_text(encoding="utf-8")), agent
|
|
254
|
+
except (OSError, json.JSONDecodeError):
|
|
255
|
+
return None, agent
|
|
256
|
+
return None, agent
|
|
257
|
+
if not state.is_dir():
|
|
258
|
+
return None, None
|
|
259
|
+
for agent_dir in state.iterdir():
|
|
260
|
+
if not agent_dir.is_dir():
|
|
261
|
+
continue
|
|
262
|
+
p = agent_dir / "sessions" / f"{session_id}.json"
|
|
263
|
+
if p.exists():
|
|
264
|
+
try:
|
|
265
|
+
return json.loads(p.read_text(encoding="utf-8")), agent_dir.name
|
|
266
|
+
except (OSError, json.JSONDecodeError):
|
|
267
|
+
return None, agent_dir.name
|
|
268
|
+
return None, None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@router.get("/trace/{session_id}")
|
|
272
|
+
def get_trace(
|
|
273
|
+
session_id: str,
|
|
274
|
+
agent: str | None = Query(None),
|
|
275
|
+
_rl=Depends(make_rate_limit_dep("cognition.trace")),
|
|
276
|
+
_m=Depends(make_metrics_dep("cognition.trace")),
|
|
277
|
+
):
|
|
278
|
+
"""Read trace events for a session; fall back to session metadata when no jsonl yet."""
|
|
279
|
+
state = _state_dir()
|
|
280
|
+
target, resolved_agent = _find_trace_file(state, session_id, agent)
|
|
281
|
+
session_meta, meta_agent = _find_session_meta(state, session_id, agent)
|
|
282
|
+
|
|
283
|
+
if target is None and session_meta is None:
|
|
284
|
+
raise HTTPException(
|
|
285
|
+
status_code=404,
|
|
286
|
+
detail=f"trace {session_id!r} not found (no jsonl trace, no session.json metadata)",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
events: list[dict] = []
|
|
290
|
+
trace_truncated = False
|
|
291
|
+
if target is not None:
|
|
292
|
+
# Tail-read only the last _MAX_TRACE_EVENTS lines (≤256KB window) so a
|
|
293
|
+
# multi-GB trace cannot OOM the server or the response.
|
|
294
|
+
lines, trace_truncated = tail_lines(target, max_lines=_MAX_TRACE_EVENTS)
|
|
295
|
+
for line in lines:
|
|
296
|
+
stripped = line.strip()
|
|
297
|
+
if not stripped:
|
|
298
|
+
continue
|
|
299
|
+
try:
|
|
300
|
+
events.append(json.loads(stripped))
|
|
301
|
+
except json.JSONDecodeError:
|
|
302
|
+
events.append({"raw": stripped})
|
|
303
|
+
|
|
304
|
+
return unwrap(
|
|
305
|
+
json.dumps(
|
|
306
|
+
{
|
|
307
|
+
"ok": True,
|
|
308
|
+
"data": {
|
|
309
|
+
"session_id": session_id,
|
|
310
|
+
"agent": resolved_agent or meta_agent,
|
|
311
|
+
"events": events,
|
|
312
|
+
"count": len(events),
|
|
313
|
+
"truncated": trace_truncated,
|
|
314
|
+
"session": session_meta,
|
|
315
|
+
"has_trace": target is not None,
|
|
316
|
+
"source": "trace+session"
|
|
317
|
+
if target and session_meta
|
|
318
|
+
else ("trace-only" if target else "session-only"),
|
|
319
|
+
"meta": {"layer": "cognition"},
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _drain_trace_events(log: Path, pos: int) -> tuple[list[dict[str, Any]], int]:
|
|
327
|
+
# Blocking stat + read (run on a worker thread by the SSE loop) — mirrors
|
|
328
|
+
# hooks.stream so a burst of trace lines never stalls the event loop.
|
|
329
|
+
if not log.exists():
|
|
330
|
+
return [], pos
|
|
331
|
+
size = log.stat().st_size
|
|
332
|
+
if size < pos:
|
|
333
|
+
pos = 0 # rotated
|
|
334
|
+
if size <= pos:
|
|
335
|
+
return [], pos
|
|
336
|
+
with log.open("r", encoding="utf-8", errors="ignore") as fh:
|
|
337
|
+
fh.seek(pos)
|
|
338
|
+
chunk = fh.read()
|
|
339
|
+
pos = fh.tell()
|
|
340
|
+
parsed: list[dict[str, Any]] = []
|
|
341
|
+
for line in chunk.splitlines():
|
|
342
|
+
stripped = line.strip()
|
|
343
|
+
if not stripped:
|
|
344
|
+
continue
|
|
345
|
+
try:
|
|
346
|
+
parsed.append(json.loads(stripped))
|
|
347
|
+
except json.JSONDecodeError:
|
|
348
|
+
parsed.append({"raw": stripped})
|
|
349
|
+
return parsed, pos
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _initial_trace_pos(log: Path, max_bytes: int = DEFAULT_WINDOW) -> int:
|
|
353
|
+
"""Byte offset for a bounded initial replay: the start of the first whole
|
|
354
|
+
line within the last `max_bytes`. A long session's trace jsonl can reach
|
|
355
|
+
many MB; replaying it whole on connect would read the entire file into
|
|
356
|
+
memory (the same DoS the bounded-read helper guards for the non-streaming
|
|
357
|
+
reads). Returns 0 when the file fits the window or cannot be stat'd."""
|
|
358
|
+
try:
|
|
359
|
+
size = log.stat().st_size
|
|
360
|
+
except OSError:
|
|
361
|
+
return 0
|
|
362
|
+
if size <= max_bytes:
|
|
363
|
+
return 0
|
|
364
|
+
try:
|
|
365
|
+
with log.open("rb") as fh:
|
|
366
|
+
fh.seek(size - max_bytes)
|
|
367
|
+
fh.readline() # discard the partial leading line
|
|
368
|
+
return fh.tell()
|
|
369
|
+
except OSError:
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@router.get("/trace/{session_id}/stream")
|
|
374
|
+
async def stream_trace(
|
|
375
|
+
session_id: str,
|
|
376
|
+
agent: str | None = Query(None),
|
|
377
|
+
_rl=Depends(make_rate_limit_dep("cognition.trace_stream")),
|
|
378
|
+
_m=Depends(make_metrics_dep("cognition.trace_stream")),
|
|
379
|
+
):
|
|
380
|
+
"""SSE: tail (and replay from the start) a session's append-only cognition trace jsonl."""
|
|
381
|
+
if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
|
|
382
|
+
raise HTTPException(status_code=400, detail="invalid session_id or agent")
|
|
383
|
+
state = _state_dir()
|
|
384
|
+
target, _resolved_agent = _find_trace_file(state, session_id, agent)
|
|
385
|
+
# The dispatch may not have created the file yet — resolve the canonical
|
|
386
|
+
# path so tailing begins the instant the first event lands. Segments are
|
|
387
|
+
# validated above, so this join cannot escape the state dir.
|
|
388
|
+
log = (
|
|
389
|
+
target
|
|
390
|
+
if target is not None
|
|
391
|
+
else state / (agent or "claude") / "traces" / f"{session_id}.jsonl"
|
|
392
|
+
)
|
|
393
|
+
poll_secs = float(os.environ.get("COS_TRACE_STREAM_POLL_MS", "750")) / 1000.0
|
|
394
|
+
heartbeat_secs = 15.0
|
|
395
|
+
idle_terminate_secs = float(os.environ.get("COS_TRACE_STREAM_IDLE_TERMINATE_S", "30"))
|
|
396
|
+
|
|
397
|
+
async def gen() -> AsyncGenerator[bytes, None]:
|
|
398
|
+
yield f"event: connected\ndata: {json.dumps({'session_id': session_id})}\n\n".encode()
|
|
399
|
+
# Replay from a bounded tail window so a viewer connecting mid-run sees
|
|
400
|
+
# the recent trace, then keeps tailing new lines. Starting at 0 would
|
|
401
|
+
# read a multi-MB trace whole on connect (worker-memory spike).
|
|
402
|
+
pos = _initial_trace_pos(log)
|
|
403
|
+
last_beat = time.monotonic()
|
|
404
|
+
last_event = time.monotonic()
|
|
405
|
+
saw_completed = False
|
|
406
|
+
try:
|
|
407
|
+
while True:
|
|
408
|
+
events, pos = await asyncio.to_thread(_drain_trace_events, log, pos)
|
|
409
|
+
for evt in events:
|
|
410
|
+
yield f"event: trace\ndata: {json.dumps(evt, ensure_ascii=False)}\n\n".encode()
|
|
411
|
+
if events:
|
|
412
|
+
last_event = time.monotonic()
|
|
413
|
+
if any(
|
|
414
|
+
isinstance(e, dict) and e.get("kind") == "dispatch_completed"
|
|
415
|
+
for e in events
|
|
416
|
+
):
|
|
417
|
+
saw_completed = True
|
|
418
|
+
# Self-terminate once a completion has been seen AND the trace has
|
|
419
|
+
# been idle for a grace period. A supervisor session emits one
|
|
420
|
+
# dispatch_completed per role in a chain, so terminating on the
|
|
421
|
+
# FIRST would drop later roles — the idle window ends a lone
|
|
422
|
+
# sub-session promptly while letting a multi-role run keep going.
|
|
423
|
+
elif saw_completed and (time.monotonic() - last_event) > idle_terminate_secs:
|
|
424
|
+
yield b"event: done\ndata: {}\n\n"
|
|
425
|
+
return
|
|
426
|
+
if time.monotonic() - last_beat > heartbeat_secs:
|
|
427
|
+
yield f"event: heartbeat\ndata: {json.dumps({'ts': int(time.time())})}\n\n".encode()
|
|
428
|
+
last_beat = time.monotonic()
|
|
429
|
+
await asyncio.sleep(poll_secs)
|
|
430
|
+
except asyncio.CancelledError:
|
|
431
|
+
raise
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
logger.exception("trace stream failed")
|
|
434
|
+
yield f"event: error\ndata: {json.dumps({'message': str(exc)})}\n\n".encode()
|
|
435
|
+
|
|
436
|
+
return StreamingResponse(
|
|
437
|
+
gen(),
|
|
438
|
+
media_type="text/event-stream",
|
|
439
|
+
headers={
|
|
440
|
+
"Cache-Control": "no-cache",
|
|
441
|
+
"X-Accel-Buffering": "no",
|
|
442
|
+
"Connection": "keep-alive",
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
# T2.4 / T19.1 — Dispatcher cost panel
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _db_path() -> str | None:
|
|
453
|
+
"""Resolve coding-os SQLite DB path via canonical helper.
|
|
454
|
+
|
|
455
|
+
Returns None when nothing exists yet (the route returns a typed
|
|
456
|
+
``unavailable`` envelope in that case).
|
|
457
|
+
"""
|
|
458
|
+
try:
|
|
459
|
+
from thinking_os.database import resolve_db_path # type: ignore
|
|
460
|
+
from web._project_context import current_project_root # type: ignore[import]
|
|
461
|
+
|
|
462
|
+
path = resolve_db_path(current_project_root())
|
|
463
|
+
if path.exists():
|
|
464
|
+
return str(path)
|
|
465
|
+
except Exception as exc:
|
|
466
|
+
logger.debug("project-root db path resolve failed: %s", exc)
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@router.get("/cost")
|
|
471
|
+
def dispatcher_cost_summary(
|
|
472
|
+
formula_id: str | None = Query(None, description="Filter to one formula"),
|
|
473
|
+
limit: int = Query(50, ge=1, le=500),
|
|
474
|
+
_rl=Depends(make_rate_limit_dep("cognition.cost")),
|
|
475
|
+
_m=Depends(make_metrics_dep("cognition.cost")),
|
|
476
|
+
):
|
|
477
|
+
"""Aggregate dispatch cost rolled up by formula and day (T2.4)."""
|
|
478
|
+
db = _db_path()
|
|
479
|
+
if db is None:
|
|
480
|
+
return unwrap(
|
|
481
|
+
json.dumps(
|
|
482
|
+
{
|
|
483
|
+
"ok": True,
|
|
484
|
+
"data": {
|
|
485
|
+
"rows": [],
|
|
486
|
+
"total_usd": 0.0,
|
|
487
|
+
"count": 0,
|
|
488
|
+
"meta": {"layer": "cognition"},
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
params: list = []
|
|
496
|
+
where = "WHERE cost_usd IS NOT NULL"
|
|
497
|
+
if formula_id:
|
|
498
|
+
where += " AND formula_id = ?"
|
|
499
|
+
params.append(formula_id)
|
|
500
|
+
query_sql = (
|
|
501
|
+
f"SELECT formula_id, date(ts) as day, "
|
|
502
|
+
f"SUM(cost_usd) as total_cost_usd, COUNT(*) as count, "
|
|
503
|
+
f"AVG(latency_ms) as avg_latency_ms "
|
|
504
|
+
f"FROM formula_dispatches {where} "
|
|
505
|
+
f"GROUP BY formula_id, day "
|
|
506
|
+
f"ORDER BY day DESC, total_cost_usd DESC "
|
|
507
|
+
f"LIMIT ?"
|
|
508
|
+
)
|
|
509
|
+
params.append(limit)
|
|
510
|
+
with sqlite3.connect(db) as conn:
|
|
511
|
+
conn.row_factory = sqlite3.Row
|
|
512
|
+
rows = [dict(r) for r in conn.execute(query_sql, params).fetchall()]
|
|
513
|
+
total_usd = sum(r["total_cost_usd"] or 0 for r in rows)
|
|
514
|
+
except Exception as exc:
|
|
515
|
+
return unwrap(
|
|
516
|
+
json.dumps(
|
|
517
|
+
{
|
|
518
|
+
"ok": False,
|
|
519
|
+
"error": {"category": "internal", "retryable": False, "message": str(exc)},
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return unwrap(
|
|
525
|
+
json.dumps(
|
|
526
|
+
{
|
|
527
|
+
"ok": True,
|
|
528
|
+
"data": {
|
|
529
|
+
"rows": rows,
|
|
530
|
+
"total_usd": round(total_usd, 6),
|
|
531
|
+
"count": len(rows),
|
|
532
|
+
"meta": {"layer": "cognition"},
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@router.get("/cost/health")
|
|
540
|
+
def dispatcher_cost_health(
|
|
541
|
+
_rl=Depends(make_rate_limit_dep("cognition.cost_health")),
|
|
542
|
+
_m=Depends(make_metrics_dep("cognition.cost_health")),
|
|
543
|
+
):
|
|
544
|
+
"""Cost-health gauges over formula_dispatches: MAD anomaly, burn-rate, budget ladder."""
|
|
545
|
+
empty = {
|
|
546
|
+
"anomaly": {"ok": True, "n": 0, "outliers": []},
|
|
547
|
+
"burn": {"days": 0},
|
|
548
|
+
"budget": {"level": "ok", "cap_usd": None, "spent_usd": 0.0, "allowed": True},
|
|
549
|
+
"overall_ok": True,
|
|
550
|
+
"meta": {"layer": "cognition"},
|
|
551
|
+
}
|
|
552
|
+
db = _db_path()
|
|
553
|
+
if db is None:
|
|
554
|
+
return unwrap(json.dumps({"ok": True, "data": empty}))
|
|
555
|
+
try:
|
|
556
|
+
from thinking_os import budget
|
|
557
|
+
|
|
558
|
+
anomaly = budget.cost_anomaly(db)
|
|
559
|
+
burn = budget.cost_burn_rate(db)
|
|
560
|
+
gate = budget.check(db)
|
|
561
|
+
data = {
|
|
562
|
+
"anomaly": anomaly,
|
|
563
|
+
"burn": burn,
|
|
564
|
+
"budget": {
|
|
565
|
+
"level": gate.level,
|
|
566
|
+
"cap_usd": gate.cap_usd,
|
|
567
|
+
"spent_usd": round(gate.spent_usd, 6),
|
|
568
|
+
"allowed": gate.allowed,
|
|
569
|
+
},
|
|
570
|
+
"overall_ok": bool(anomaly.get("ok", True) and gate.level != "hard_stop"),
|
|
571
|
+
"meta": {"layer": "cognition"},
|
|
572
|
+
}
|
|
573
|
+
return unwrap(json.dumps({"ok": True, "data": data}))
|
|
574
|
+
except Exception as exc:
|
|
575
|
+
logger.debug("cost/health failed, failing open: %s", exc)
|
|
576
|
+
return unwrap(json.dumps({"ok": True, "data": empty}))
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@router.get("/dispatchers")
|
|
580
|
+
def list_dispatchers(
|
|
581
|
+
limit: int = Query(100, ge=1, le=1000),
|
|
582
|
+
status: str | None = Query(None),
|
|
583
|
+
_rl=Depends(make_rate_limit_dep("cognition.dispatchers")),
|
|
584
|
+
_m=Depends(make_metrics_dep("cognition.dispatchers")),
|
|
585
|
+
):
|
|
586
|
+
"""List recent formula dispatches with telemetry (T19.1)."""
|
|
587
|
+
db = _db_path()
|
|
588
|
+
if db is None:
|
|
589
|
+
return unwrap(
|
|
590
|
+
json.dumps(
|
|
591
|
+
{
|
|
592
|
+
"ok": True,
|
|
593
|
+
"data": {"dispatches": [], "count": 0, "meta": {"layer": "cognition"}},
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
params: list = []
|
|
600
|
+
where = "WHERE cost_usd IS NOT NULL"
|
|
601
|
+
if status:
|
|
602
|
+
where += " AND status = ?"
|
|
603
|
+
params.append(status)
|
|
604
|
+
params.append(limit)
|
|
605
|
+
with sqlite3.connect(db) as conn:
|
|
606
|
+
conn.row_factory = sqlite3.Row
|
|
607
|
+
rows = [
|
|
608
|
+
dict(r)
|
|
609
|
+
for r in conn.execute(
|
|
610
|
+
f"SELECT session_id, formula_id, ts, cost_usd, budget_usd, "
|
|
611
|
+
f"status, latency_ms "
|
|
612
|
+
f"FROM formula_dispatches {where} "
|
|
613
|
+
f"ORDER BY ts DESC LIMIT ?",
|
|
614
|
+
params,
|
|
615
|
+
).fetchall()
|
|
616
|
+
]
|
|
617
|
+
except Exception as exc:
|
|
618
|
+
return unwrap(
|
|
619
|
+
json.dumps(
|
|
620
|
+
{
|
|
621
|
+
"ok": False,
|
|
622
|
+
"error": {"category": "internal", "retryable": False, "message": str(exc)},
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
return unwrap(
|
|
628
|
+
json.dumps(
|
|
629
|
+
{
|
|
630
|
+
"ok": True,
|
|
631
|
+
"data": {"dispatches": rows, "count": len(rows), "meta": {"layer": "cognition"}},
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@router.get("/dispatchers/{session_id}/tools")
|
|
638
|
+
def dispatcher_tools(
|
|
639
|
+
session_id: str,
|
|
640
|
+
_rl=Depends(make_rate_limit_dep("cognition.dispatcher_tools")),
|
|
641
|
+
_m=Depends(make_metrics_dep("cognition.dispatcher_tools")),
|
|
642
|
+
):
|
|
643
|
+
"""Parse tool_calls_jsonb for one dispatch session (T19.2)."""
|
|
644
|
+
db = _db_path()
|
|
645
|
+
if db is None:
|
|
646
|
+
raise HTTPException(status_code=503, detail="DB not available")
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
with sqlite3.connect(db) as conn:
|
|
650
|
+
conn.row_factory = sqlite3.Row
|
|
651
|
+
row = conn.execute(
|
|
652
|
+
"SELECT tool_calls_jsonb, tool_failures_jsonb "
|
|
653
|
+
"FROM formula_dispatches WHERE session_id = ? "
|
|
654
|
+
"ORDER BY ts DESC LIMIT 1",
|
|
655
|
+
(session_id,),
|
|
656
|
+
).fetchone()
|
|
657
|
+
except Exception as exc:
|
|
658
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
659
|
+
|
|
660
|
+
if row is None:
|
|
661
|
+
raise HTTPException(status_code=404, detail=f"session {session_id!r} not found")
|
|
662
|
+
|
|
663
|
+
def _parse(col: str | None) -> list:
|
|
664
|
+
if not col:
|
|
665
|
+
return []
|
|
666
|
+
try:
|
|
667
|
+
parsed = json.loads(col)
|
|
668
|
+
return parsed if isinstance(parsed, list) else []
|
|
669
|
+
except (json.JSONDecodeError, TypeError):
|
|
670
|
+
return []
|
|
671
|
+
|
|
672
|
+
tool_calls = _parse(row["tool_calls_jsonb"])
|
|
673
|
+
failures = _parse(row["tool_failures_jsonb"])
|
|
674
|
+
return unwrap(
|
|
675
|
+
json.dumps(
|
|
676
|
+
{
|
|
677
|
+
"ok": True,
|
|
678
|
+
"data": {
|
|
679
|
+
"session_id": session_id,
|
|
680
|
+
"tool_calls": tool_calls,
|
|
681
|
+
"failures": failures,
|
|
682
|
+
"count": len(tool_calls),
|
|
683
|
+
"meta": {"layer": "cognition"},
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@router.get("/analyze")
|
|
691
|
+
def cognition_analyze(
|
|
692
|
+
task_description: str = Query(...),
|
|
693
|
+
complexity_hint: str | None = Query(None),
|
|
694
|
+
_rl=Depends(make_rate_limit_dep("cognition.analyze")),
|
|
695
|
+
_m=Depends(make_metrics_dep("cognition.analyze")),
|
|
696
|
+
):
|
|
697
|
+
"""Analyze a task via cos_analyze_task."""
|
|
698
|
+
cog = _cognition_module()
|
|
699
|
+
if cog is None:
|
|
700
|
+
return unwrap(_unavailable())
|
|
701
|
+
# The cognition module exposes analyze_task directly (not through MCP wrapper).
|
|
702
|
+
try:
|
|
703
|
+
if hasattr(cog, "analyze_task"):
|
|
704
|
+
result = cog.analyze_task(task_description, complexity_hint=complexity_hint)
|
|
705
|
+
return unwrap(result if isinstance(result, str) else json.dumps(result))
|
|
706
|
+
except Exception as exc:
|
|
707
|
+
return unwrap(
|
|
708
|
+
json.dumps(
|
|
709
|
+
{
|
|
710
|
+
"ok": False,
|
|
711
|
+
"error": {"category": "internal", "retryable": False, "message": str(exc)},
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
return unwrap(_unavailable("analyze_task not available in this cognition module version"))
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
# ---------------------------------------------------------------------------
|
|
719
|
+
# Chat surface — Claude Agent SDK transcript browser + resume (TASK-chat)
|
|
720
|
+
# ---------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _claude_sdk():
|
|
724
|
+
"""Lazy import the Claude Agent SDK; return None when missing."""
|
|
725
|
+
try:
|
|
726
|
+
import claude_agent_sdk # type: ignore
|
|
727
|
+
|
|
728
|
+
return claude_agent_sdk
|
|
729
|
+
except ImportError as exc:
|
|
730
|
+
logger.debug("claude_agent_sdk unavailable: %s", exc)
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _project_cwd() -> str:
|
|
735
|
+
from web._project_context import current_project_root
|
|
736
|
+
|
|
737
|
+
return str(current_project_root())
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
_ADAPTER_DISPATCHER_MOD = None
|
|
741
|
+
_ADAPTER_DISPATCHER_TRIED = False
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _adapter_dispatcher():
|
|
745
|
+
"""Load src/adapters/claude/sdk_dispatcher.py once — the adapter SDK-construction
|
|
746
|
+
seam (P8: every ClaudeAgentOptions build crosses this boundary into the adapter)."""
|
|
747
|
+
global _ADAPTER_DISPATCHER_MOD, _ADAPTER_DISPATCHER_TRIED
|
|
748
|
+
if _ADAPTER_DISPATCHER_TRIED:
|
|
749
|
+
return _ADAPTER_DISPATCHER_MOD
|
|
750
|
+
_ADAPTER_DISPATCHER_TRIED = True
|
|
751
|
+
try:
|
|
752
|
+
import importlib.util
|
|
753
|
+
from pathlib import Path
|
|
754
|
+
|
|
755
|
+
path = Path(__file__).resolve().parents[3] / "adapters" / "claude" / "sdk_dispatcher.py"
|
|
756
|
+
spec = importlib.util.spec_from_file_location("cos_adapter_claude_dispatcher", path)
|
|
757
|
+
if spec and spec.loader:
|
|
758
|
+
mod = importlib.util.module_from_spec(spec)
|
|
759
|
+
spec.loader.exec_module(mod)
|
|
760
|
+
_ADAPTER_DISPATCHER_MOD = mod
|
|
761
|
+
except Exception as exc:
|
|
762
|
+
logger.debug("adapter dispatcher load failed: %s", exc)
|
|
763
|
+
return _ADAPTER_DISPATCHER_MOD
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _session_options_builder():
|
|
767
|
+
"""The adapter's profile-based session-options builder (SSOT), or None."""
|
|
768
|
+
mod = _adapter_dispatcher()
|
|
769
|
+
return getattr(mod, "claude_session_options", None) if mod else None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _build_agent_options(**kwargs):
|
|
773
|
+
"""Construct ClaudeAgentOptions via the adapter seam — P8: core never builds the
|
|
774
|
+
SDK type itself. Raises if the adapter dispatcher cannot be loaded."""
|
|
775
|
+
mod = _adapter_dispatcher()
|
|
776
|
+
builder = getattr(mod, "claude_agent_options", None) if mod else None
|
|
777
|
+
if builder is None:
|
|
778
|
+
raise RuntimeError("claude adapter ClaudeAgentOptions seam unavailable")
|
|
779
|
+
return builder(**kwargs)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _chat_session_options(
|
|
783
|
+
profile, *, cwd, model, system_prompt, effort=None, resume=None, fork=False
|
|
784
|
+
):
|
|
785
|
+
"""Build chat ClaudeAgentOptions via the adapter SSOT builder; on builder error
|
|
786
|
+
fall back to the chat-light kwargs, still constructed through the adapter seam."""
|
|
787
|
+
build = _session_options_builder()
|
|
788
|
+
if build is not None:
|
|
789
|
+
try:
|
|
790
|
+
return build(
|
|
791
|
+
profile,
|
|
792
|
+
cwd=cwd,
|
|
793
|
+
model=model,
|
|
794
|
+
system_prompt=system_prompt,
|
|
795
|
+
effort=effort,
|
|
796
|
+
resume=resume,
|
|
797
|
+
fork=fork,
|
|
798
|
+
)
|
|
799
|
+
except Exception as exc:
|
|
800
|
+
logger.debug("session-options builder call failed (%s); generic seam fallback", exc)
|
|
801
|
+
kwargs = dict(
|
|
802
|
+
cwd=cwd,
|
|
803
|
+
model=model,
|
|
804
|
+
permission_mode="dontAsk",
|
|
805
|
+
setting_sources=[],
|
|
806
|
+
include_partial_messages=True,
|
|
807
|
+
system_prompt=system_prompt,
|
|
808
|
+
)
|
|
809
|
+
if effort:
|
|
810
|
+
kwargs["effort"] = effort
|
|
811
|
+
if profile == "chat_resume":
|
|
812
|
+
if resume:
|
|
813
|
+
kwargs["resume"] = resume
|
|
814
|
+
kwargs["fork_session"] = fork
|
|
815
|
+
return _build_agent_options(**kwargs)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
_CHAT_PRESENCE_WRITER = None
|
|
819
|
+
_CHAT_PRESENCE_TRIED = False
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _chat_presence_write(cwd: str, sid: str, event: str) -> None:
|
|
823
|
+
"""Fire-and-forget Hub-chat presence so the chat shows in the Live-agents HUD (P13)."""
|
|
824
|
+
# Reuse the adapter's unified 12-key writer and stamp the long-lived host
|
|
825
|
+
# pid, so the board's glob reader sees the live chat session (the chat path
|
|
826
|
+
# fires no shell hooks, so nothing else writes its presence).
|
|
827
|
+
global _CHAT_PRESENCE_WRITER, _CHAT_PRESENCE_TRIED
|
|
828
|
+
try:
|
|
829
|
+
if not _CHAT_PRESENCE_TRIED:
|
|
830
|
+
_CHAT_PRESENCE_TRIED = True
|
|
831
|
+
import importlib.util
|
|
832
|
+
from pathlib import Path as _Path
|
|
833
|
+
|
|
834
|
+
path = (
|
|
835
|
+
_Path(__file__).resolve().parents[3] / "adapters" / "claude" / "sdk_dispatcher.py"
|
|
836
|
+
)
|
|
837
|
+
spec = importlib.util.spec_from_file_location("cos_adapter_claude_presence", path)
|
|
838
|
+
if spec and spec.loader:
|
|
839
|
+
mod = importlib.util.module_from_spec(spec)
|
|
840
|
+
spec.loader.exec_module(mod)
|
|
841
|
+
_CHAT_PRESENCE_WRITER = getattr(mod, "_presence_write", None)
|
|
842
|
+
if _CHAT_PRESENCE_WRITER is not None:
|
|
843
|
+
import os
|
|
844
|
+
from pathlib import Path as _Path
|
|
845
|
+
|
|
846
|
+
_CHAT_PRESENCE_WRITER(_Path(cwd), "claude", sid, event, pid=os.getpid())
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
logger.debug("chat presence write skipped (%s): %s", event, exc)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _serialize_session_info(info: Any) -> dict:
|
|
852
|
+
return {
|
|
853
|
+
"session_id": getattr(info, "session_id", None),
|
|
854
|
+
"summary": getattr(info, "summary", None),
|
|
855
|
+
"custom_title": getattr(info, "custom_title", None),
|
|
856
|
+
"first_prompt": (getattr(info, "first_prompt", None) or "")[:400] or None,
|
|
857
|
+
"last_modified": getattr(info, "last_modified", None),
|
|
858
|
+
"file_size": getattr(info, "file_size", None),
|
|
859
|
+
"git_branch": getattr(info, "git_branch", None),
|
|
860
|
+
"cwd": getattr(info, "cwd", None),
|
|
861
|
+
"tag": getattr(info, "tag", None),
|
|
862
|
+
"created_at": getattr(info, "created_at", None),
|
|
863
|
+
"agent": "claude",
|
|
864
|
+
"writable": True,
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _coerce_block(block: Any) -> dict:
|
|
869
|
+
if not isinstance(block, dict):
|
|
870
|
+
return {"type": "raw", "value": str(block)[:2000]}
|
|
871
|
+
btype = str(block.get("type") or "unknown")
|
|
872
|
+
out: dict = {"type": btype}
|
|
873
|
+
if btype == "text":
|
|
874
|
+
out["text"] = str(block.get("text") or "")
|
|
875
|
+
elif btype == "thinking":
|
|
876
|
+
out["text"] = str(block.get("thinking") or block.get("text") or "")
|
|
877
|
+
elif btype == "tool_use":
|
|
878
|
+
out["id"] = block.get("id")
|
|
879
|
+
out["name"] = block.get("name")
|
|
880
|
+
inp = block.get("input")
|
|
881
|
+
try:
|
|
882
|
+
out["input"] = (
|
|
883
|
+
inp
|
|
884
|
+
if isinstance(inp, (dict, list, str, int, float, bool, type(None)))
|
|
885
|
+
else str(inp)
|
|
886
|
+
)
|
|
887
|
+
except Exception as exc:
|
|
888
|
+
logger.debug("tool_use input coerce fallback: %s", exc)
|
|
889
|
+
out["input"] = str(inp)[:2000]
|
|
890
|
+
elif btype == "tool_result":
|
|
891
|
+
out["tool_use_id"] = block.get("tool_use_id")
|
|
892
|
+
content = block.get("content")
|
|
893
|
+
if isinstance(content, list):
|
|
894
|
+
out["content"] = [
|
|
895
|
+
c.get("text") if isinstance(c, dict) and c.get("type") == "text" else str(c)[:1500]
|
|
896
|
+
for c in content
|
|
897
|
+
]
|
|
898
|
+
else:
|
|
899
|
+
out["content"] = str(content)[:4000] if content is not None else None
|
|
900
|
+
out["is_error"] = bool(block.get("is_error"))
|
|
901
|
+
elif btype == "image":
|
|
902
|
+
out["source_type"] = (
|
|
903
|
+
block.get("source", {}).get("type") if isinstance(block.get("source"), dict) else None
|
|
904
|
+
)
|
|
905
|
+
else:
|
|
906
|
+
# Catch-all: keep small primitive fields, drop binary noise.
|
|
907
|
+
for k, v in block.items():
|
|
908
|
+
if k == "type":
|
|
909
|
+
continue
|
|
910
|
+
if isinstance(v, (str, int, float, bool, type(None))):
|
|
911
|
+
out[k] = v
|
|
912
|
+
return out
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _serialize_message(msg: Any) -> dict:
|
|
916
|
+
raw = getattr(msg, "message", None)
|
|
917
|
+
if not isinstance(raw, dict):
|
|
918
|
+
raw = {}
|
|
919
|
+
role = raw.get("role") or getattr(msg, "type", None) or "unknown"
|
|
920
|
+
content = raw.get("content")
|
|
921
|
+
blocks: list[dict]
|
|
922
|
+
if isinstance(content, str):
|
|
923
|
+
blocks = [{"type": "text", "text": content}]
|
|
924
|
+
elif isinstance(content, list):
|
|
925
|
+
blocks = [_coerce_block(b) for b in content]
|
|
926
|
+
else:
|
|
927
|
+
blocks = []
|
|
928
|
+
return {
|
|
929
|
+
"uuid": getattr(msg, "uuid", None),
|
|
930
|
+
"session_id": getattr(msg, "session_id", None),
|
|
931
|
+
"type": getattr(msg, "type", None),
|
|
932
|
+
"role": role,
|
|
933
|
+
"model": raw.get("model"),
|
|
934
|
+
"stop_reason": raw.get("stop_reason"),
|
|
935
|
+
"usage": raw.get("usage") if isinstance(raw.get("usage"), dict) else None,
|
|
936
|
+
"blocks": blocks,
|
|
937
|
+
"parent_tool_use_id": getattr(msg, "parent_tool_use_id", None),
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _session_agent_hints(session_id: str) -> set[str]:
|
|
942
|
+
hints: set[str] = set()
|
|
943
|
+
state = _state_dir()
|
|
944
|
+
if not state.is_dir():
|
|
945
|
+
return hints
|
|
946
|
+
for agent_dir in state.iterdir():
|
|
947
|
+
sessions_dir = agent_dir / "sessions"
|
|
948
|
+
if not sessions_dir.is_dir():
|
|
949
|
+
continue
|
|
950
|
+
for path in sessions_dir.glob("*.json"):
|
|
951
|
+
try:
|
|
952
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
953
|
+
except (OSError, json.JSONDecodeError):
|
|
954
|
+
continue
|
|
955
|
+
if payload.get("sdk_uuid") == session_id:
|
|
956
|
+
hints.add(str(payload.get("agent") or agent_dir.name))
|
|
957
|
+
return hints
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@router.get("/chats")
|
|
961
|
+
async def list_chats(
|
|
962
|
+
limit: int = Query(50, ge=1, le=500),
|
|
963
|
+
_rl=Depends(make_rate_limit_dep("cognition.chats")),
|
|
964
|
+
_m=Depends(make_metrics_dep("cognition.chats")),
|
|
965
|
+
):
|
|
966
|
+
"""List chat sessions from every available transcript provider."""
|
|
967
|
+
from web.chat_providers import list_sessions as list_adapter_sessions
|
|
968
|
+
|
|
969
|
+
cwd = _project_cwd()
|
|
970
|
+
adapter_rows, adapter_sources = await list_adapter_sessions(cwd, limit)
|
|
971
|
+
rows_by_id = {str(row["session_id"]): row for row in adapter_rows}
|
|
972
|
+
sources = [f"{agent}_sdk" for agent in adapter_sources]
|
|
973
|
+
sdk = _claude_sdk()
|
|
974
|
+
if sdk is not None:
|
|
975
|
+
try:
|
|
976
|
+
sessions = sdk.list_sessions(directory=cwd, limit=limit)
|
|
977
|
+
except Exception as exc:
|
|
978
|
+
logger.warning("Claude chat list failed: %s", exc)
|
|
979
|
+
else:
|
|
980
|
+
for session in sessions:
|
|
981
|
+
row = _serialize_session_info(session)
|
|
982
|
+
rows_by_id.setdefault(str(row["session_id"]), row)
|
|
983
|
+
sources.append("claude_agent_sdk")
|
|
984
|
+
if not sources:
|
|
985
|
+
return unwrap(_unavailable("no chat transcript provider is available"))
|
|
986
|
+
rows = list(rows_by_id.values())
|
|
987
|
+
rows.sort(key=lambda r: r.get("last_modified") or 0, reverse=True)
|
|
988
|
+
rows = rows[:limit]
|
|
989
|
+
return unwrap(
|
|
990
|
+
json.dumps(
|
|
991
|
+
{
|
|
992
|
+
"ok": True,
|
|
993
|
+
"data": {
|
|
994
|
+
"sessions": rows,
|
|
995
|
+
"count": len(rows),
|
|
996
|
+
"cwd": cwd,
|
|
997
|
+
"meta": {"layer": "cognition", "sources": sources},
|
|
998
|
+
},
|
|
999
|
+
}
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _dispatch_transcript_chat(session_id: str) -> dict | None:
|
|
1005
|
+
# Fall back to a dispatched sub-session's persisted transcript when the live
|
|
1006
|
+
# Claude SDK session no longer exists on disk — resolves the dead sdk_uuid
|
|
1007
|
+
# modal link (TASK-667). Keyed on formula_dispatches.sub_session_id (= the
|
|
1008
|
+
# SDK session_id the UI links from). Read-only, fail-open.
|
|
1009
|
+
db_path = _db_path()
|
|
1010
|
+
if not db_path:
|
|
1011
|
+
return None
|
|
1012
|
+
try:
|
|
1013
|
+
with sqlite3.connect(db_path) as conn:
|
|
1014
|
+
conn.row_factory = sqlite3.Row
|
|
1015
|
+
row = conn.execute(
|
|
1016
|
+
"SELECT formula_id, status, model, raw_transcript "
|
|
1017
|
+
"FROM formula_dispatches "
|
|
1018
|
+
"WHERE sub_session_id = ? AND raw_transcript IS NOT NULL "
|
|
1019
|
+
"ORDER BY ts DESC LIMIT 1",
|
|
1020
|
+
(session_id,),
|
|
1021
|
+
).fetchone()
|
|
1022
|
+
except sqlite3.Error as exc:
|
|
1023
|
+
logger.debug("dispatch transcript fallback query failed: %s", exc)
|
|
1024
|
+
return None
|
|
1025
|
+
if row is None or not row["raw_transcript"]:
|
|
1026
|
+
return None
|
|
1027
|
+
return {
|
|
1028
|
+
"session": {
|
|
1029
|
+
"session_id": session_id,
|
|
1030
|
+
"source": "dispatch_transcript",
|
|
1031
|
+
"formula_id": row["formula_id"],
|
|
1032
|
+
"model": row["model"],
|
|
1033
|
+
"status": row["status"],
|
|
1034
|
+
# Mirror the fields ChatView reads (custom_title ?? summary ?? id;
|
|
1035
|
+
# git_branch/cwd/last_modified in the header) so the fallback renders
|
|
1036
|
+
# a real title instead of the raw session id, and reads no undefined.
|
|
1037
|
+
"custom_title": f"dispatch: {row['formula_id']} ({row['status']})",
|
|
1038
|
+
"summary": None,
|
|
1039
|
+
"first_prompt": None,
|
|
1040
|
+
"last_modified": None,
|
|
1041
|
+
"file_size": None,
|
|
1042
|
+
"git_branch": None,
|
|
1043
|
+
"cwd": None,
|
|
1044
|
+
"tag": None,
|
|
1045
|
+
"created_at": None,
|
|
1046
|
+
},
|
|
1047
|
+
"messages": [
|
|
1048
|
+
{
|
|
1049
|
+
"uuid": None,
|
|
1050
|
+
"session_id": session_id,
|
|
1051
|
+
"type": "assistant",
|
|
1052
|
+
"role": "assistant",
|
|
1053
|
+
"model": row["model"],
|
|
1054
|
+
"stop_reason": None,
|
|
1055
|
+
"usage": None,
|
|
1056
|
+
"blocks": [{"type": "text", "text": row["raw_transcript"]}],
|
|
1057
|
+
"parent_tool_use_id": None,
|
|
1058
|
+
}
|
|
1059
|
+
],
|
|
1060
|
+
"count": 1,
|
|
1061
|
+
"offset": 0,
|
|
1062
|
+
"meta": {"layer": "cognition", "source": "formula_dispatches"},
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
@router.get("/chat/{session_id}")
|
|
1067
|
+
async def get_chat(
|
|
1068
|
+
session_id: str,
|
|
1069
|
+
limit: int = Query(500, ge=1, le=5000),
|
|
1070
|
+
offset: int = Query(0, ge=0),
|
|
1071
|
+
_rl=Depends(make_rate_limit_dep("cognition.chat_get")),
|
|
1072
|
+
_m=Depends(make_metrics_dep("cognition.chat_get")),
|
|
1073
|
+
):
|
|
1074
|
+
"""Return one adapter-normalized transcript."""
|
|
1075
|
+
from web.chat_providers import get_session as get_adapter_session
|
|
1076
|
+
|
|
1077
|
+
cwd = _project_cwd()
|
|
1078
|
+
agent_hints = _session_agent_hints(session_id)
|
|
1079
|
+
if agent_hints and "claude" not in agent_hints:
|
|
1080
|
+
adapter_data = await get_adapter_session(
|
|
1081
|
+
session_id, cwd, limit, offset, agent_hints=agent_hints
|
|
1082
|
+
)
|
|
1083
|
+
if adapter_data is not None:
|
|
1084
|
+
return unwrap(json.dumps({"ok": True, "data": adapter_data}))
|
|
1085
|
+
|
|
1086
|
+
sdk = _claude_sdk()
|
|
1087
|
+
info = None
|
|
1088
|
+
if sdk is not None:
|
|
1089
|
+
try:
|
|
1090
|
+
info = sdk.get_session_info(session_id, directory=cwd)
|
|
1091
|
+
except Exception as exc:
|
|
1092
|
+
logger.warning("get_session_info(%s) failed: %s", session_id, exc)
|
|
1093
|
+
if info is None:
|
|
1094
|
+
adapter_data = await get_adapter_session(
|
|
1095
|
+
session_id,
|
|
1096
|
+
cwd,
|
|
1097
|
+
limit,
|
|
1098
|
+
offset,
|
|
1099
|
+
agent_hints=agent_hints or None,
|
|
1100
|
+
)
|
|
1101
|
+
if adapter_data is not None:
|
|
1102
|
+
return unwrap(json.dumps({"ok": True, "data": adapter_data}))
|
|
1103
|
+
fallback = _dispatch_transcript_chat(session_id)
|
|
1104
|
+
if fallback is not None:
|
|
1105
|
+
return unwrap(json.dumps({"ok": True, "data": fallback}))
|
|
1106
|
+
raise HTTPException(status_code=404, detail=f"chat session {session_id!r} not found")
|
|
1107
|
+
try:
|
|
1108
|
+
messages = sdk.get_session_messages(session_id, directory=cwd, limit=limit, offset=offset)
|
|
1109
|
+
except Exception as exc:
|
|
1110
|
+
return unwrap(
|
|
1111
|
+
json.dumps(
|
|
1112
|
+
{
|
|
1113
|
+
"ok": False,
|
|
1114
|
+
"error": {
|
|
1115
|
+
"category": "internal",
|
|
1116
|
+
"retryable": False,
|
|
1117
|
+
"message": f"get_session_messages failed: {exc}",
|
|
1118
|
+
},
|
|
1119
|
+
}
|
|
1120
|
+
)
|
|
1121
|
+
)
|
|
1122
|
+
return unwrap(
|
|
1123
|
+
json.dumps(
|
|
1124
|
+
{
|
|
1125
|
+
"ok": True,
|
|
1126
|
+
"data": {
|
|
1127
|
+
"session": _serialize_session_info(info),
|
|
1128
|
+
"messages": [_serialize_message(m) for m in messages],
|
|
1129
|
+
"count": len(messages),
|
|
1130
|
+
"offset": offset,
|
|
1131
|
+
"meta": {"layer": "cognition", "source": "claude_agent_sdk"},
|
|
1132
|
+
},
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
# SDK content-block dataclasses don't carry a `type` discriminator
|
|
1139
|
+
# field — they're disambiguated by Python class. The frontend renders
|
|
1140
|
+
# blocks by `b.type === 'text' | 'thinking' | 'tool_use' | …`, so
|
|
1141
|
+
# without this map every block round-trips as a typeless dict and the
|
|
1142
|
+
# Cognition / Chats panel shows an empty assistant pill (TASK 2026-05-20
|
|
1143
|
+
# UI audit). Names map to the wire-format discriminators that
|
|
1144
|
+
# ChatView.tsx already understands.
|
|
1145
|
+
_BLOCK_TYPE_BY_CLASS = {
|
|
1146
|
+
"TextBlock": "text",
|
|
1147
|
+
"ThinkingBlock": "thinking",
|
|
1148
|
+
"ToolUseBlock": "tool_use",
|
|
1149
|
+
"ToolResultBlock": "tool_result",
|
|
1150
|
+
"ServerToolUseBlock": "server_tool_use",
|
|
1151
|
+
"ServerToolResultBlock": "server_tool_result",
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _safe_serialize(obj: Any) -> Any:
|
|
1156
|
+
"""Best-effort recursive serializer for SDK dataclass events."""
|
|
1157
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
1158
|
+
cls_name = type(obj).__name__
|
|
1159
|
+
# Recurse field-by-field via getattr — NOT dataclasses.asdict, which
|
|
1160
|
+
# pre-flattens the whole tree so a nested TextBlock arrives here as a
|
|
1161
|
+
# plain dict and never gets its `type` stamped (the streamed
|
|
1162
|
+
# AssistantMessage.content[] blocks then lack `type` and the UI drops
|
|
1163
|
+
# them → "agent draft shows nothing").
|
|
1164
|
+
out: dict[str, Any] = {
|
|
1165
|
+
f.name: _safe_serialize(getattr(obj, f.name)) for f in dataclasses.fields(obj)
|
|
1166
|
+
}
|
|
1167
|
+
block_type = _BLOCK_TYPE_BY_CLASS.get(cls_name)
|
|
1168
|
+
if block_type is not None:
|
|
1169
|
+
out["type"] = block_type
|
|
1170
|
+
# ThinkingBlock stores its text under `.thinking`; the UI
|
|
1171
|
+
# reads `.text` for both text + thinking blocks, so mirror
|
|
1172
|
+
# the field rather than forking the frontend.
|
|
1173
|
+
if block_type == "thinking" and "text" not in out and "thinking" in out:
|
|
1174
|
+
out["text"] = out["thinking"]
|
|
1175
|
+
return out
|
|
1176
|
+
if isinstance(obj, dict):
|
|
1177
|
+
return {str(k): _safe_serialize(v) for k, v in obj.items()}
|
|
1178
|
+
if isinstance(obj, (list, tuple)):
|
|
1179
|
+
return [_safe_serialize(v) for v in obj]
|
|
1180
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
1181
|
+
return obj
|
|
1182
|
+
return str(obj)[:4000]
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _sse_chunk(event: str, payload: dict) -> bytes:
|
|
1186
|
+
return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n".encode()
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _role_system_prompt(role: str | None):
|
|
1190
|
+
"""Load a role's agent prompt as a claude_code system-prompt append, if valid."""
|
|
1191
|
+
import re as _re
|
|
1192
|
+
|
|
1193
|
+
if not role or not _re.match(r"^[a-z_]+$", role):
|
|
1194
|
+
return None
|
|
1195
|
+
agent_md = Path(__file__).resolve().parents[2] / "thinking_os" / "agents" / f"{role}.md"
|
|
1196
|
+
try:
|
|
1197
|
+
if agent_md.exists():
|
|
1198
|
+
return {
|
|
1199
|
+
"type": "preset",
|
|
1200
|
+
"preset": "claude_code",
|
|
1201
|
+
"append": agent_md.read_text(encoding="utf-8"),
|
|
1202
|
+
}
|
|
1203
|
+
except OSError:
|
|
1204
|
+
pass
|
|
1205
|
+
return None
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def _role_names(agents_dir: Path) -> list[str]:
|
|
1209
|
+
import re as _re
|
|
1210
|
+
|
|
1211
|
+
try:
|
|
1212
|
+
return sorted(
|
|
1213
|
+
p.stem
|
|
1214
|
+
for p in agents_dir.glob("*.md")
|
|
1215
|
+
if _re.match(r"^[a-z_]+$", p.stem) and not p.stem.startswith("_")
|
|
1216
|
+
)
|
|
1217
|
+
except OSError as exc:
|
|
1218
|
+
logger.debug("roles scan skipped %s: %s", agents_dir, exc)
|
|
1219
|
+
return []
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
@router.get("/roles")
|
|
1223
|
+
def list_roles(
|
|
1224
|
+
_rl=Depends(make_rate_limit_dep("cognition.roles")),
|
|
1225
|
+
_m=Depends(make_metrics_dep("cognition.roles")),
|
|
1226
|
+
):
|
|
1227
|
+
"""List the semantic roles a chat session can adopt (producer: thinking_os/agents/*.md)."""
|
|
1228
|
+
roles = _role_names(Path(__file__).resolve().parents[2] / "thinking_os" / "agents")
|
|
1229
|
+
return unwrap(
|
|
1230
|
+
json.dumps(
|
|
1231
|
+
{
|
|
1232
|
+
"ok": True,
|
|
1233
|
+
"data": {"roles": roles, "count": len(roles), "meta": {"layer": "cognition"}},
|
|
1234
|
+
}
|
|
1235
|
+
)
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
@router.post("/chat")
|
|
1240
|
+
async def chat_new(
|
|
1241
|
+
body: dict = Body(...),
|
|
1242
|
+
_rl=Depends(make_rate_limit_dep("cognition.chat_new")),
|
|
1243
|
+
_m=Depends(make_metrics_dep("cognition.chat_new")),
|
|
1244
|
+
):
|
|
1245
|
+
"""Start a FRESH Claude session from a prompt (no resume); stream SSE.
|
|
1246
|
+
|
|
1247
|
+
Body: ``{"prompt": str, "model": str|null, "role": str|null}``. Emits a
|
|
1248
|
+
``session`` event carrying the SDK-resolved session id so the UI can open
|
|
1249
|
+
the chat under the id that get_session_info / list_sessions actually use.
|
|
1250
|
+
Claude-only — returns an ``unavailable`` envelope without the SDK.
|
|
1251
|
+
"""
|
|
1252
|
+
prompt = str(body.get("prompt") or "").strip()
|
|
1253
|
+
if not prompt:
|
|
1254
|
+
return unwrap(
|
|
1255
|
+
json.dumps(
|
|
1256
|
+
{
|
|
1257
|
+
"ok": False,
|
|
1258
|
+
"error": {
|
|
1259
|
+
"category": "validation",
|
|
1260
|
+
"retryable": False,
|
|
1261
|
+
"message": "prompt must be non-empty",
|
|
1262
|
+
},
|
|
1263
|
+
}
|
|
1264
|
+
)
|
|
1265
|
+
)
|
|
1266
|
+
sdk = _claude_sdk()
|
|
1267
|
+
if sdk is None:
|
|
1268
|
+
return unwrap(_unavailable("claude_agent_sdk not installed"))
|
|
1269
|
+
|
|
1270
|
+
import secrets
|
|
1271
|
+
import time as _time
|
|
1272
|
+
|
|
1273
|
+
model = body.get("model") or None
|
|
1274
|
+
routing_decision: dict | None = None
|
|
1275
|
+
if model == "auto":
|
|
1276
|
+
routing_decision = _auto_route_model(prompt)
|
|
1277
|
+
if "error" in routing_decision:
|
|
1278
|
+
return unwrap(
|
|
1279
|
+
json.dumps(
|
|
1280
|
+
{
|
|
1281
|
+
"ok": False,
|
|
1282
|
+
"error": {
|
|
1283
|
+
"category": "validation",
|
|
1284
|
+
"retryable": False,
|
|
1285
|
+
"message": routing_decision["error"],
|
|
1286
|
+
},
|
|
1287
|
+
}
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
model = routing_decision["model"] or None
|
|
1291
|
+
role = (str(body.get("role") or "")).strip() or None
|
|
1292
|
+
effort = (str(body.get("effort") or "")).strip() or None
|
|
1293
|
+
if effort not in (None, "low", "medium", "high", "xhigh", "max"):
|
|
1294
|
+
effort = None # ignore unknown levels rather than failing the turn
|
|
1295
|
+
cwd = _project_cwd()
|
|
1296
|
+
system_prompt = _role_system_prompt(role) or _chat_system_prompt(model)
|
|
1297
|
+
system_prompt = _prime_with_project_description(system_prompt, cwd)
|
|
1298
|
+
new_session_id = f"ses-claude-ui-{int(_time.time())}-{secrets.token_hex(3)}"
|
|
1299
|
+
# SSOT builder (chat profile): setting_sources=[] (no ~40s SessionStart
|
|
1300
|
+
# suite) + programmatic coding-os MCP (cos_* capability) + base-tool
|
|
1301
|
+
# allow-list (no Write/Edit → chat can't mutate code) + destructive-Bash
|
|
1302
|
+
# deny floor. No session_id: the CLI rejects non-UUID ids; the SDK mints
|
|
1303
|
+
# its own UUID, surfaced below from the stream as the `session` event.
|
|
1304
|
+
options = _chat_session_options(
|
|
1305
|
+
"chat", cwd=cwd, model=model, system_prompt=system_prompt, effort=effort
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
async def event_gen():
|
|
1309
|
+
yield _sse_chunk(
|
|
1310
|
+
"started",
|
|
1311
|
+
{"session_id": new_session_id, "prompt": prompt[:200], "model": model, "role": role},
|
|
1312
|
+
)
|
|
1313
|
+
if routing_decision is not None:
|
|
1314
|
+
yield _sse_chunk("routing", routing_decision)
|
|
1315
|
+
# The Claude SDK rekeys the minted ses-claude-ui-* id to its OWN
|
|
1316
|
+
# transcript uuid, so the minted id 404s on get_session_info and never
|
|
1317
|
+
# appears in list_sessions. Emit the SDK-resolved id the moment the
|
|
1318
|
+
# stream reveals it (SDK messages carry .session_id) so the UI opens /
|
|
1319
|
+
# lists the chat under the id that actually resolves.
|
|
1320
|
+
resolved_id = new_session_id
|
|
1321
|
+
emitted_session = False
|
|
1322
|
+
try:
|
|
1323
|
+
async for event in sdk.query(prompt=prompt, options=options):
|
|
1324
|
+
if not emitted_session:
|
|
1325
|
+
real_id = getattr(event, "session_id", None)
|
|
1326
|
+
if real_id:
|
|
1327
|
+
resolved_id = str(real_id)
|
|
1328
|
+
logger.info(
|
|
1329
|
+
"chat_new resolved session id=%s (minted=%s)",
|
|
1330
|
+
resolved_id,
|
|
1331
|
+
new_session_id,
|
|
1332
|
+
)
|
|
1333
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1334
|
+
emitted_session = True
|
|
1335
|
+
_chat_presence_write(cwd, resolved_id, "prompt")
|
|
1336
|
+
kind = type(event).__name__.lower().replace("message", "") or "event"
|
|
1337
|
+
yield _sse_chunk(kind, _safe_serialize(event))
|
|
1338
|
+
except asyncio.CancelledError:
|
|
1339
|
+
raise
|
|
1340
|
+
except Exception as exc:
|
|
1341
|
+
logger.exception("chat_new stream failed")
|
|
1342
|
+
yield _sse_chunk("error", {"message": str(exc)})
|
|
1343
|
+
if not emitted_session:
|
|
1344
|
+
# No event ever carried a session_id — the turn produced no resolvable
|
|
1345
|
+
# session. Emitting the minted ses-claude-ui-* id strands the UI on an
|
|
1346
|
+
# id that 404s on get_chat (the "session vanished" report), so log it
|
|
1347
|
+
# loudly. The fallback id keeps the UI from hanging with no handle.
|
|
1348
|
+
logger.warning(
|
|
1349
|
+
"chat_new: stream produced no SDK session_id (minted=%s) — UI will 404 on this id",
|
|
1350
|
+
new_session_id,
|
|
1351
|
+
)
|
|
1352
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1353
|
+
_chat_presence_write(cwd, resolved_id, "stop")
|
|
1354
|
+
yield _sse_chunk("done", {"session_id": resolved_id})
|
|
1355
|
+
|
|
1356
|
+
return StreamingResponse(
|
|
1357
|
+
event_gen(),
|
|
1358
|
+
media_type="text/event-stream",
|
|
1359
|
+
headers={
|
|
1360
|
+
"Cache-Control": "no-cache",
|
|
1361
|
+
"X-Accel-Buffering": "no",
|
|
1362
|
+
"Connection": "keep-alive",
|
|
1363
|
+
},
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
_CHAT_SYSTEM = (
|
|
1368
|
+
"You are the coding-os Hub chat assistant — a direct, helpful conversational "
|
|
1369
|
+
"agent for this project. Answer the user's message conversationally in Markdown. "
|
|
1370
|
+
"Do NOT prepend the transparency banner (the line starting with the bell emoji) "
|
|
1371
|
+
"and skip any cognitive-state / gate / work-log ceremony — that protocol is for "
|
|
1372
|
+
"terminal sessions, not Hub chat; just answer. You MAY use the cos_* tools "
|
|
1373
|
+
"(memory, graph, docs, board) to ground an answer when it genuinely helps, but "
|
|
1374
|
+
"keep replies focused and readable rather than running a full work protocol. "
|
|
1375
|
+
"When you commit code for a specific task, include its id like `(TASK-NNN)` in "
|
|
1376
|
+
"the commit subject so the board links the commit to that task."
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _prime_with_project_description(system_prompt: dict, cwd: str) -> dict:
|
|
1381
|
+
"""Append the onboarding intake (docs/_meta/project-description.md) to the
|
|
1382
|
+
chat system prompt so the first session knows what the project IS (TASK-364).
|
|
1383
|
+
Fail-open: missing/unreadable intake leaves the prompt untouched."""
|
|
1384
|
+
try:
|
|
1385
|
+
intake = Path(cwd) / "docs" / "_meta" / "project-description.md"
|
|
1386
|
+
if not intake.is_file():
|
|
1387
|
+
return system_prompt
|
|
1388
|
+
text = intake.read_text(encoding="utf-8").strip()[:2000]
|
|
1389
|
+
if not text or not isinstance(system_prompt, dict) or "append" not in system_prompt:
|
|
1390
|
+
return system_prompt
|
|
1391
|
+
return {
|
|
1392
|
+
**system_prompt,
|
|
1393
|
+
"append": system_prompt["append"]
|
|
1394
|
+
+ "\n\n## Project context (onboarding intake)\n"
|
|
1395
|
+
+ text,
|
|
1396
|
+
}
|
|
1397
|
+
except OSError:
|
|
1398
|
+
return system_prompt
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _chat_system_prompt(model: str | None) -> dict:
|
|
1402
|
+
"""claude_code preset + the chat framing, pinning the model name when known."""
|
|
1403
|
+
append = _CHAT_SYSTEM
|
|
1404
|
+
if model:
|
|
1405
|
+
append = (
|
|
1406
|
+
f"{_CHAT_SYSTEM}\n\nYou are answering as the `{model}` model. If the user "
|
|
1407
|
+
f"asks which model you are, tell them exactly `{model}`."
|
|
1408
|
+
)
|
|
1409
|
+
return {"type": "preset", "preset": "claude_code", "append": append}
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
_TASK_AUTHOR_SYSTEM = (
|
|
1413
|
+
"You are a task-authoring agent for coding-os. Using ONLY cos_* tools, "
|
|
1414
|
+
"research the codebase (cos_graph_query/context, cos_doc_search, "
|
|
1415
|
+
"cos_task_search/board) and then create EXACTLY ONE well-formed Scrumban "
|
|
1416
|
+
"task with cos_task_create: choose the correct swimlane and kind, write a "
|
|
1417
|
+
"one-sentence Outcome and a Given/When/Then Acceptance, and list 1-4 Read "
|
|
1418
|
+
"First files. Reconcile against the existing board first and reuse a task "
|
|
1419
|
+
"instead of duplicating when appropriate. Do NOT write or edit any code or "
|
|
1420
|
+
"files. After creating or identifying the task, state its id and stop."
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
@router.post("/author-task")
|
|
1425
|
+
async def author_task(
|
|
1426
|
+
body: dict = Body(...),
|
|
1427
|
+
_rl=Depends(make_rate_limit_dep("cognition.author_task")),
|
|
1428
|
+
_m=Depends(make_metrics_dep("cognition.author_task")),
|
|
1429
|
+
):
|
|
1430
|
+
"""Headless research+author session that creates one task via cos_task_create. Claude-only."""
|
|
1431
|
+
prompt = str(body.get("prompt") or "").strip()
|
|
1432
|
+
if not prompt:
|
|
1433
|
+
return unwrap(
|
|
1434
|
+
json.dumps(
|
|
1435
|
+
{
|
|
1436
|
+
"ok": False,
|
|
1437
|
+
"error": {
|
|
1438
|
+
"category": "validation",
|
|
1439
|
+
"retryable": False,
|
|
1440
|
+
"message": "prompt must be non-empty",
|
|
1441
|
+
},
|
|
1442
|
+
}
|
|
1443
|
+
)
|
|
1444
|
+
)
|
|
1445
|
+
sdk = _claude_sdk()
|
|
1446
|
+
if sdk is None:
|
|
1447
|
+
return unwrap(_unavailable("claude_agent_sdk not installed"))
|
|
1448
|
+
|
|
1449
|
+
import secrets
|
|
1450
|
+
import time as _time
|
|
1451
|
+
|
|
1452
|
+
model = body.get("model") or None
|
|
1453
|
+
cwd = _project_cwd()
|
|
1454
|
+
sid = f"ses-claude-author-{int(_time.time())}-{secrets.token_hex(3)}"
|
|
1455
|
+
options = _build_agent_options(
|
|
1456
|
+
cwd=cwd,
|
|
1457
|
+
model=model,
|
|
1458
|
+
permission_mode="dontAsk",
|
|
1459
|
+
setting_sources=["project"],
|
|
1460
|
+
# No session_id — claude CLI requires a UUID; SDK mints its own (emitted below).
|
|
1461
|
+
# cos_* only — no Write/Edit/Bash, so it can research + author but never touch code.
|
|
1462
|
+
allowed_tools=["mcp__coding-os__*"],
|
|
1463
|
+
disallowed_tools=["Write", "Edit", "MultiEdit", "Bash"],
|
|
1464
|
+
system_prompt={"type": "preset", "preset": "claude_code", "append": _TASK_AUTHOR_SYSTEM},
|
|
1465
|
+
max_turns=30,
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
async def event_gen():
|
|
1469
|
+
yield _sse_chunk("started", {"session_id": sid, "prompt": prompt[:200], "model": model})
|
|
1470
|
+
resolved_id = sid
|
|
1471
|
+
emitted_session = False
|
|
1472
|
+
try:
|
|
1473
|
+
async for event in sdk.query(prompt=prompt, options=options):
|
|
1474
|
+
if not emitted_session:
|
|
1475
|
+
real_id = getattr(event, "session_id", None)
|
|
1476
|
+
if real_id:
|
|
1477
|
+
resolved_id = str(real_id)
|
|
1478
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1479
|
+
emitted_session = True
|
|
1480
|
+
kind = type(event).__name__.lower().replace("message", "") or "event"
|
|
1481
|
+
yield _sse_chunk(kind, _safe_serialize(event))
|
|
1482
|
+
except asyncio.CancelledError:
|
|
1483
|
+
raise
|
|
1484
|
+
except Exception as exc:
|
|
1485
|
+
logger.exception("author_task stream failed")
|
|
1486
|
+
yield _sse_chunk("error", {"message": str(exc)})
|
|
1487
|
+
if not emitted_session:
|
|
1488
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1489
|
+
yield _sse_chunk("done", {"session_id": resolved_id})
|
|
1490
|
+
|
|
1491
|
+
return StreamingResponse(
|
|
1492
|
+
event_gen(),
|
|
1493
|
+
media_type="text/event-stream",
|
|
1494
|
+
headers={
|
|
1495
|
+
"Cache-Control": "no-cache",
|
|
1496
|
+
"X-Accel-Buffering": "no",
|
|
1497
|
+
"Connection": "keep-alive",
|
|
1498
|
+
},
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
# ---------------------------------------------------------------------------
|
|
1503
|
+
# Onboarding — docs-scoped session (TASK-246)
|
|
1504
|
+
# ---------------------------------------------------------------------------
|
|
1505
|
+
|
|
1506
|
+
_ONBOARD_WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit", "NotebookEdit"})
|
|
1507
|
+
_ONBOARD_ALLOWED_TOOLS = [
|
|
1508
|
+
"mcp__coding-os__*",
|
|
1509
|
+
"Write",
|
|
1510
|
+
"Edit",
|
|
1511
|
+
"MultiEdit",
|
|
1512
|
+
"Read",
|
|
1513
|
+
"Glob",
|
|
1514
|
+
"Grep",
|
|
1515
|
+
"TodoWrite",
|
|
1516
|
+
"WebFetch",
|
|
1517
|
+
"WebSearch",
|
|
1518
|
+
]
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _is_path_under_docs(file_path: str, project_root: Path) -> bool:
|
|
1522
|
+
"""True when file_path resolves to <project_root>/docs (or below)."""
|
|
1523
|
+
if not file_path:
|
|
1524
|
+
return False
|
|
1525
|
+
try:
|
|
1526
|
+
p = Path(file_path)
|
|
1527
|
+
if not p.is_absolute():
|
|
1528
|
+
p = project_root / p
|
|
1529
|
+
p = p.resolve()
|
|
1530
|
+
docs = (project_root / "docs").resolve()
|
|
1531
|
+
return p == docs or docs in p.parents
|
|
1532
|
+
except (OSError, ValueError, RuntimeError):
|
|
1533
|
+
return False
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _onboard_write_allowed(tool_input: dict, project_root: Path) -> bool:
|
|
1537
|
+
"""Permission contract for the onboard session: a write tool may only target
|
|
1538
|
+
a path under docs/. Non-dict input or a missing path denies (fail-closed)."""
|
|
1539
|
+
if not isinstance(tool_input, dict):
|
|
1540
|
+
return False
|
|
1541
|
+
path = tool_input.get("file_path") or tool_input.get("path") or tool_input.get("notebook_path")
|
|
1542
|
+
return _is_path_under_docs(str(path or ""), project_root)
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _count_placeholder_todos(project_root: Path) -> tuple[int, bool]:
|
|
1546
|
+
"""Scan docs/prd/*.md for scaffold `_TODO:` markers.
|
|
1547
|
+
|
|
1548
|
+
Returns (todo_count, prd_exists). prd_exists=False means there is no PRD
|
|
1549
|
+
scaffold at all → nothing to onboard."""
|
|
1550
|
+
prd_dir = project_root / "docs" / "prd"
|
|
1551
|
+
if not prd_dir.is_dir():
|
|
1552
|
+
return 0, False
|
|
1553
|
+
total = 0
|
|
1554
|
+
found_any = False
|
|
1555
|
+
for md in prd_dir.glob("*.md"):
|
|
1556
|
+
found_any = True
|
|
1557
|
+
try:
|
|
1558
|
+
total += md.read_text(encoding="utf-8").count("_TODO:")
|
|
1559
|
+
except OSError as exc:
|
|
1560
|
+
logger.debug("onboarding scan skipped %s: %s", md, exc)
|
|
1561
|
+
continue
|
|
1562
|
+
return total, found_any
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _onboarding_state(project_root: Path, state_dir: Path) -> dict:
|
|
1566
|
+
"""Resolve onboarding completeness: onboarding.json override, else _TODO scan."""
|
|
1567
|
+
marker = state_dir / "onboarding.json"
|
|
1568
|
+
if marker.exists():
|
|
1569
|
+
try:
|
|
1570
|
+
data = json.loads(marker.read_text(encoding="utf-8"))
|
|
1571
|
+
if isinstance(data, dict) and data.get("completed") is True:
|
|
1572
|
+
return {"complete": True, "source": "onboarding_json", "placeholders_remaining": 0}
|
|
1573
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1574
|
+
logger.debug("onboarding.json unreadable: %s", exc)
|
|
1575
|
+
todos, prd_exists = _count_placeholder_todos(project_root)
|
|
1576
|
+
if not prd_exists:
|
|
1577
|
+
return {
|
|
1578
|
+
"complete": True,
|
|
1579
|
+
"source": "no_prd",
|
|
1580
|
+
"placeholders_remaining": 0,
|
|
1581
|
+
"reason": "no PRD scaffold to onboard",
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
"complete": todos == 0,
|
|
1585
|
+
"source": "placeholder_scan",
|
|
1586
|
+
"placeholders_remaining": todos,
|
|
1587
|
+
"reason": (
|
|
1588
|
+
"PRD still has scaffold _TODO markers" if todos else "PRD placeholders authored"
|
|
1589
|
+
),
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
@router.get("/onboarding-status")
|
|
1594
|
+
def onboarding_status(
|
|
1595
|
+
_rl=Depends(make_rate_limit_dep("cognition.onboarding_status")),
|
|
1596
|
+
_m=Depends(make_metrics_dep("cognition.onboarding_status")),
|
|
1597
|
+
):
|
|
1598
|
+
"""Whether the project still needs onboarding (placeholder-scan first, onboarding.json override)."""
|
|
1599
|
+
from web._project_context import current_project_root # type: ignore
|
|
1600
|
+
|
|
1601
|
+
project = current_project_root()
|
|
1602
|
+
state = _state_dir()
|
|
1603
|
+
payload = _onboarding_state(project, state)
|
|
1604
|
+
payload["meta"] = {"layer": "cognition"}
|
|
1605
|
+
return unwrap(json.dumps({"ok": True, "data": payload}))
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
@router.post("/onboarding-status/dismiss")
|
|
1609
|
+
def onboarding_dismiss(
|
|
1610
|
+
_rl=Depends(make_rate_limit_dep("cognition.onboarding_dismiss")),
|
|
1611
|
+
_m=Depends(make_metrics_dep("cognition.onboarding_dismiss")),
|
|
1612
|
+
):
|
|
1613
|
+
"""Persist the onboarding hero dismissal so it stops reappearing on reload."""
|
|
1614
|
+
state = _state_dir()
|
|
1615
|
+
marker = state / "onboarding.json"
|
|
1616
|
+
try:
|
|
1617
|
+
state.mkdir(parents=True, exist_ok=True)
|
|
1618
|
+
marker.write_text(
|
|
1619
|
+
json.dumps({"completed": True, "source": "dismissed"}, indent=2) + "\n",
|
|
1620
|
+
encoding="utf-8",
|
|
1621
|
+
)
|
|
1622
|
+
except OSError as exc:
|
|
1623
|
+
return unwrap(
|
|
1624
|
+
json.dumps(
|
|
1625
|
+
{
|
|
1626
|
+
"ok": False,
|
|
1627
|
+
"error": {
|
|
1628
|
+
"category": "internal",
|
|
1629
|
+
"retryable": False,
|
|
1630
|
+
"message": f"could not write onboarding marker: {exc}",
|
|
1631
|
+
},
|
|
1632
|
+
}
|
|
1633
|
+
)
|
|
1634
|
+
)
|
|
1635
|
+
return unwrap(
|
|
1636
|
+
json.dumps({"ok": True, "data": {"complete": True, "meta": {"layer": "cognition"}}})
|
|
1637
|
+
)
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
@router.post("/onboard")
|
|
1641
|
+
async def onboard(
|
|
1642
|
+
body: dict = Body(...),
|
|
1643
|
+
_rl=Depends(make_rate_limit_dep("cognition.onboard")),
|
|
1644
|
+
_m=Depends(make_metrics_dep("cognition.onboard")),
|
|
1645
|
+
):
|
|
1646
|
+
"""Run the onboarder role with Write/Edit confined to docs/ (PreToolUse-gated). Claude-only."""
|
|
1647
|
+
prompt = str(body.get("prompt") or "").strip()
|
|
1648
|
+
if not prompt:
|
|
1649
|
+
return unwrap(
|
|
1650
|
+
json.dumps(
|
|
1651
|
+
{
|
|
1652
|
+
"ok": False,
|
|
1653
|
+
"error": {
|
|
1654
|
+
"category": "validation",
|
|
1655
|
+
"retryable": False,
|
|
1656
|
+
"message": "prompt must be non-empty",
|
|
1657
|
+
},
|
|
1658
|
+
}
|
|
1659
|
+
)
|
|
1660
|
+
)
|
|
1661
|
+
sdk = _claude_sdk()
|
|
1662
|
+
if sdk is None:
|
|
1663
|
+
return unwrap(_unavailable("claude_agent_sdk not installed"))
|
|
1664
|
+
|
|
1665
|
+
import secrets
|
|
1666
|
+
import time as _time
|
|
1667
|
+
|
|
1668
|
+
model = body.get("model") or None
|
|
1669
|
+
cwd = _project_cwd()
|
|
1670
|
+
project_root = Path(cwd)
|
|
1671
|
+
sid = f"ses-claude-onboard-{int(_time.time())}-{secrets.token_hex(3)}"
|
|
1672
|
+
system_prompt = _role_system_prompt("onboarder") or {
|
|
1673
|
+
"type": "preset",
|
|
1674
|
+
"preset": "claude_code",
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
async def _deny_non_docs_write(input_data: dict, _tool_use_id, _ctx) -> dict:
|
|
1678
|
+
# PreToolUse is evaluated FIRST and honored even under dontAsk (where
|
|
1679
|
+
# can_use_tool is skipped) — the only reliable place to path-scope writes.
|
|
1680
|
+
try:
|
|
1681
|
+
if input_data.get("tool_name") in _ONBOARD_WRITE_TOOLS and not _onboard_write_allowed(
|
|
1682
|
+
input_data.get("tool_input") or {}, project_root
|
|
1683
|
+
):
|
|
1684
|
+
return {
|
|
1685
|
+
"hookSpecificOutput": {
|
|
1686
|
+
"hookEventName": "PreToolUse",
|
|
1687
|
+
"permissionDecision": "deny",
|
|
1688
|
+
"permissionDecisionReason": "onboard sessions may only write under docs/",
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
except Exception as exc: # never raise from a hook — would kill the stream
|
|
1692
|
+
logger.debug("onboard PreToolUse gate error: %s", exc)
|
|
1693
|
+
return {}
|
|
1694
|
+
|
|
1695
|
+
options = _build_agent_options(
|
|
1696
|
+
cwd=cwd,
|
|
1697
|
+
model=model,
|
|
1698
|
+
permission_mode="dontAsk",
|
|
1699
|
+
setting_sources=["project"],
|
|
1700
|
+
# No session_id — claude CLI requires a UUID; SDK mints its own (emitted below).
|
|
1701
|
+
include_partial_messages=True, # token-by-token streaming for the live UI
|
|
1702
|
+
allowed_tools=list(_ONBOARD_ALLOWED_TOOLS),
|
|
1703
|
+
disallowed_tools=["Bash"], # deny wins even over the allow-list
|
|
1704
|
+
system_prompt=system_prompt,
|
|
1705
|
+
# HookMatcher is the adapter SDK's type, constructed here because the hook
|
|
1706
|
+
# closure is core-local; ClaudeAgentOptions itself still routes through the
|
|
1707
|
+
# adapter seam. Migrating HookMatcher is tracked separately (out of scope).
|
|
1708
|
+
hooks={
|
|
1709
|
+
"PreToolUse": [
|
|
1710
|
+
sdk.HookMatcher(
|
|
1711
|
+
matcher="Write|Edit|MultiEdit|NotebookEdit", hooks=[_deny_non_docs_write]
|
|
1712
|
+
)
|
|
1713
|
+
]
|
|
1714
|
+
},
|
|
1715
|
+
max_turns=40,
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
async def event_gen():
|
|
1719
|
+
yield _sse_chunk("started", {"session_id": sid, "prompt": prompt[:200], "model": model})
|
|
1720
|
+
resolved_id = sid
|
|
1721
|
+
emitted_session = False
|
|
1722
|
+
try:
|
|
1723
|
+
async for event in sdk.query(prompt=prompt, options=options):
|
|
1724
|
+
if not emitted_session:
|
|
1725
|
+
real_id = getattr(event, "session_id", None)
|
|
1726
|
+
if real_id:
|
|
1727
|
+
resolved_id = str(real_id)
|
|
1728
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1729
|
+
emitted_session = True
|
|
1730
|
+
kind = type(event).__name__.lower().replace("message", "") or "event"
|
|
1731
|
+
yield _sse_chunk(kind, _safe_serialize(event))
|
|
1732
|
+
except asyncio.CancelledError:
|
|
1733
|
+
raise
|
|
1734
|
+
except Exception as exc:
|
|
1735
|
+
logger.exception("onboard stream failed")
|
|
1736
|
+
yield _sse_chunk("error", {"message": str(exc)})
|
|
1737
|
+
if not emitted_session:
|
|
1738
|
+
yield _sse_chunk("session", {"session_id": resolved_id})
|
|
1739
|
+
yield _sse_chunk("done", {"session_id": resolved_id})
|
|
1740
|
+
|
|
1741
|
+
return StreamingResponse(
|
|
1742
|
+
event_gen(),
|
|
1743
|
+
media_type="text/event-stream",
|
|
1744
|
+
headers={
|
|
1745
|
+
"Cache-Control": "no-cache",
|
|
1746
|
+
"X-Accel-Buffering": "no",
|
|
1747
|
+
"Connection": "keep-alive",
|
|
1748
|
+
},
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
@router.post("/chat/{session_id}/send")
|
|
1753
|
+
async def chat_send(
|
|
1754
|
+
session_id: str,
|
|
1755
|
+
body: dict = Body(...),
|
|
1756
|
+
_rl=Depends(make_rate_limit_dep("cognition.chat_send")),
|
|
1757
|
+
_m=Depends(make_metrics_dep("cognition.chat_send")),
|
|
1758
|
+
):
|
|
1759
|
+
"""Resume a Claude session with a new prompt; stream events as SSE.
|
|
1760
|
+
|
|
1761
|
+
Body: ``{"prompt": str, "fork": bool=false, "model": str|null}``.
|
|
1762
|
+
Each SSE event ``data`` payload is a serialized SDK message; ``event``
|
|
1763
|
+
field is one of ``user``, ``assistant``, ``system``, ``result``,
|
|
1764
|
+
``stream``, ``rate_limit``, ``error``, ``done``.
|
|
1765
|
+
"""
|
|
1766
|
+
prompt = str(body.get("prompt") or "").strip()
|
|
1767
|
+
if not prompt:
|
|
1768
|
+
return unwrap(
|
|
1769
|
+
json.dumps(
|
|
1770
|
+
{
|
|
1771
|
+
"ok": False,
|
|
1772
|
+
"error": {
|
|
1773
|
+
"category": "validation",
|
|
1774
|
+
"retryable": False,
|
|
1775
|
+
"message": "prompt must be non-empty",
|
|
1776
|
+
},
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
sdk = _claude_sdk()
|
|
1781
|
+
if sdk is None:
|
|
1782
|
+
return unwrap(_unavailable("claude_agent_sdk not installed"))
|
|
1783
|
+
|
|
1784
|
+
cwd = _project_cwd()
|
|
1785
|
+
fork = bool(body.get("fork"))
|
|
1786
|
+
model = body.get("model") or None
|
|
1787
|
+
# SSOT builder (chat_resume profile) — same chat-light policy as chat_new
|
|
1788
|
+
# (mcp + deny floor + no Write/Edit), plus resume/fork for the follow-up turn.
|
|
1789
|
+
options = _chat_session_options(
|
|
1790
|
+
"chat_resume",
|
|
1791
|
+
cwd=cwd,
|
|
1792
|
+
model=model,
|
|
1793
|
+
system_prompt=_chat_system_prompt(model),
|
|
1794
|
+
resume=session_id,
|
|
1795
|
+
fork=fork,
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
async def event_gen():
|
|
1799
|
+
yield _sse_chunk(
|
|
1800
|
+
"started", {"session_id": session_id, "prompt": prompt[:200], "fork": fork}
|
|
1801
|
+
)
|
|
1802
|
+
_chat_presence_write(cwd, session_id, "prompt")
|
|
1803
|
+
emitted_kinds: list[str] = []
|
|
1804
|
+
try:
|
|
1805
|
+
async for event in sdk.query(prompt=prompt, options=options):
|
|
1806
|
+
kind = type(event).__name__.lower().replace("message", "")
|
|
1807
|
+
if not kind:
|
|
1808
|
+
kind = "event"
|
|
1809
|
+
emitted_kinds.append(kind)
|
|
1810
|
+
logger.info(
|
|
1811
|
+
"chat_send stream: session=%s kind=%s class=%s",
|
|
1812
|
+
session_id,
|
|
1813
|
+
kind,
|
|
1814
|
+
type(event).__name__,
|
|
1815
|
+
)
|
|
1816
|
+
yield _sse_chunk(kind, _safe_serialize(event))
|
|
1817
|
+
except asyncio.CancelledError:
|
|
1818
|
+
raise
|
|
1819
|
+
except Exception as exc:
|
|
1820
|
+
logger.exception("chat resume stream failed")
|
|
1821
|
+
yield _sse_chunk("error", {"message": str(exc)})
|
|
1822
|
+
logger.info(
|
|
1823
|
+
"chat_send stream done: session=%s emitted=%s",
|
|
1824
|
+
session_id,
|
|
1825
|
+
",".join(emitted_kinds) or "(none)",
|
|
1826
|
+
)
|
|
1827
|
+
_chat_presence_write(cwd, session_id, "stop")
|
|
1828
|
+
yield _sse_chunk("done", {"session_id": session_id})
|
|
1829
|
+
|
|
1830
|
+
return StreamingResponse(
|
|
1831
|
+
event_gen(),
|
|
1832
|
+
media_type="text/event-stream",
|
|
1833
|
+
headers={
|
|
1834
|
+
"Cache-Control": "no-cache",
|
|
1835
|
+
"X-Accel-Buffering": "no",
|
|
1836
|
+
"Connection": "keep-alive",
|
|
1837
|
+
},
|
|
1838
|
+
)
|