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
cli/main.py
ADDED
|
@@ -0,0 +1,3070 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Coding OS — CLI tool for installing and managing the cognitive operating system.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
coding-os init --agent claude,codex [--template django]
|
|
7
|
+
coding-os add-adapter codex
|
|
8
|
+
coding-os health
|
|
9
|
+
coding-os adopt # overlay onto an existing repo
|
|
10
|
+
coding-os eject # remove coding-os, keep your code/docs
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import functools
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import shlex
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
from cli._data_types import AggregatedWorld
|
|
30
|
+
from cli._resources import (
|
|
31
|
+
adapters_dir,
|
|
32
|
+
core_dir,
|
|
33
|
+
data_root,
|
|
34
|
+
overlay_adapter_dirs,
|
|
35
|
+
overlay_template_dirs,
|
|
36
|
+
templates_dir,
|
|
37
|
+
)
|
|
38
|
+
from cli._init_helpers import (
|
|
39
|
+
InitError,
|
|
40
|
+
ensure_agents_md,
|
|
41
|
+
ensure_gitignore,
|
|
42
|
+
install_consumer_git_hooks,
|
|
43
|
+
materialize_ci_workflow,
|
|
44
|
+
materialize_dockerfiles,
|
|
45
|
+
materialize_makefile_targets,
|
|
46
|
+
maybe_git_init,
|
|
47
|
+
maybe_initial_commit,
|
|
48
|
+
resolve_init_target,
|
|
49
|
+
)
|
|
50
|
+
from cli.adapter_registry import load_adapter_registry
|
|
51
|
+
from cli.add_stack import add_stack as add_stack_cmd
|
|
52
|
+
from cli.remove_stack import remove_stack as remove_stack_cmd
|
|
53
|
+
from cli.config_composer import COMPOSED_FILENAMES, compose_coding_os_configs
|
|
54
|
+
from cli.aggregator import aggregate, today_iso
|
|
55
|
+
from cli.brain_commands import (
|
|
56
|
+
brain_decay as brain_decay_cmd,
|
|
57
|
+
brain_gc as brain_gc_cmd,
|
|
58
|
+
brain_sweep_changelog as brain_sweep_changelog_cmd,
|
|
59
|
+
docs_index as docs_index_cmd,
|
|
60
|
+
reindex as reindex_cmd,
|
|
61
|
+
task_sync as task_sync_cmd,
|
|
62
|
+
)
|
|
63
|
+
from cli.core_version import stamp_core_version
|
|
64
|
+
from cli.doctor import doctor as doctor_cmd
|
|
65
|
+
from cli.materialize_file import materialize_file as materialize_file_cmd
|
|
66
|
+
from cli.list_adapters import list_adapters as list_adapters_cmd
|
|
67
|
+
from cli.list_stacks import list_stacks as list_stacks_cmd
|
|
68
|
+
from cli.setup import setup as setup_cmd
|
|
69
|
+
from cli.skills_list import skills_list as skills_list_cmd
|
|
70
|
+
from cli.stack_registry import (
|
|
71
|
+
load_base_profile,
|
|
72
|
+
load_stack_registry,
|
|
73
|
+
resolve_relocated_profiles,
|
|
74
|
+
service_relocations,
|
|
75
|
+
)
|
|
76
|
+
from cli.tail_command import tail_cmd
|
|
77
|
+
from cli.update import update as update_cmd
|
|
78
|
+
|
|
79
|
+
# CODING_OS_ROOT is the source-checkout root — kept for dev-only operations; it
|
|
80
|
+
# is meaningless under a wheel install. The bundled DATA trees resolve via
|
|
81
|
+
# importlib so they are found under both src-layout and wheel installs (TASK-219).
|
|
82
|
+
CODING_OS_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
83
|
+
ADAPTERS_DIR = adapters_dir()
|
|
84
|
+
CORE_DIR = core_dir()
|
|
85
|
+
TEMPLATES_DIR = templates_dir()
|
|
86
|
+
|
|
87
|
+
CONFIG_FILE = ".coding-os.yaml"
|
|
88
|
+
STATE_DIR = ".coding-os"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _discover_valid_agents() -> list[str]:
|
|
92
|
+
"""Read adapter ids from adapters/*/adapter.yaml at CLI startup.
|
|
93
|
+
|
|
94
|
+
Deliberately separate from `_get_adapter_registry()` because click
|
|
95
|
+
needs a plain list at decorator evaluation time, before module
|
|
96
|
+
initialization has completed. Returns a conservative fallback on
|
|
97
|
+
any load error so the CLI stays bootable.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
return sorted(
|
|
101
|
+
load_adapter_registry(ADAPTERS_DIR, overlay_dirs=overlay_adapter_dirs()).keys()
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _discover_valid_templates() -> list[str]:
|
|
108
|
+
"""Read stack ids from templates/*/stack.yaml (+ community overlay) at CLI startup."""
|
|
109
|
+
try:
|
|
110
|
+
return sorted(
|
|
111
|
+
load_stack_registry(TEMPLATES_DIR, overlay_dirs=overlay_template_dirs()).keys()
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
VALID_AGENTS: list[str] = _discover_valid_agents()
|
|
118
|
+
VALID_TEMPLATES: list[str] = _discover_valid_templates()
|
|
119
|
+
|
|
120
|
+
# Stack and adapter metadata live in templates/*/stack.yaml and
|
|
121
|
+
# adapters/*/adapter.yaml. Adding a new stack or adapter is a pure data-file
|
|
122
|
+
# change — never touch this module.
|
|
123
|
+
#
|
|
124
|
+
# The caches below memoize registry loads within a single CLI invocation so
|
|
125
|
+
# cos init/doctor/add-stack don't re-parse YAML repeatedly. Tests can reset
|
|
126
|
+
# them via _reset_registries_for_tests().
|
|
127
|
+
|
|
128
|
+
_base_cache = None
|
|
129
|
+
_stack_cache = None
|
|
130
|
+
_adapter_cache = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_base_profile():
|
|
134
|
+
global _base_cache
|
|
135
|
+
if _base_cache is None:
|
|
136
|
+
_base_cache = load_base_profile(TEMPLATES_DIR / "_base")
|
|
137
|
+
return _base_cache
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _get_stack_registry():
|
|
141
|
+
global _stack_cache
|
|
142
|
+
if _stack_cache is None:
|
|
143
|
+
# Consumer-discovery path: include out-of-tree community stacks
|
|
144
|
+
# ($COS_USER_TEMPLATES_DIR). The meta-repo SSOT regen/lint scripts load
|
|
145
|
+
# the registry bundled-only (no overlay) so a community stack never leaks
|
|
146
|
+
# into scaffold_manifest.json / dimension-registry.md (TASK-458/471).
|
|
147
|
+
_stack_cache = load_stack_registry(TEMPLATES_DIR, overlay_dirs=overlay_template_dirs())
|
|
148
|
+
return _stack_cache
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_adapter_registry():
|
|
152
|
+
global _adapter_cache
|
|
153
|
+
if _adapter_cache is None:
|
|
154
|
+
_adapter_cache = load_adapter_registry(ADAPTERS_DIR, overlay_dirs=overlay_adapter_dirs())
|
|
155
|
+
return _adapter_cache
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _reset_registries_for_tests() -> None:
|
|
159
|
+
"""Clear cached registry state. Call from test fixtures that mutate
|
|
160
|
+
templates/ or adapters/ between invocations within a single process."""
|
|
161
|
+
global _base_cache, _stack_cache, _adapter_cache
|
|
162
|
+
_base_cache = None
|
|
163
|
+
_stack_cache = None
|
|
164
|
+
_adapter_cache = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_world(
|
|
168
|
+
agent: str,
|
|
169
|
+
templates: tuple[str, ...],
|
|
170
|
+
project: Path,
|
|
171
|
+
*,
|
|
172
|
+
today: str | None = None,
|
|
173
|
+
) -> AggregatedWorld:
|
|
174
|
+
"""Load base + requested stacks + adapter and aggregate into a world.
|
|
175
|
+
|
|
176
|
+
`today` is an optional ISO-8601 override for deterministic fixtures
|
|
177
|
+
(golden parity tests). Production callers leave it None so the
|
|
178
|
+
current wall-clock date is used.
|
|
179
|
+
"""
|
|
180
|
+
base = _get_base_profile()
|
|
181
|
+
stack_registry = _get_stack_registry()
|
|
182
|
+
adapter_registry = _get_adapter_registry()
|
|
183
|
+
|
|
184
|
+
if agent not in adapter_registry:
|
|
185
|
+
raise click.ClickException(f"adapter '{agent}' not found in {ADAPTERS_DIR}")
|
|
186
|
+
adapter_profile = adapter_registry[agent]
|
|
187
|
+
|
|
188
|
+
for t in templates:
|
|
189
|
+
if t not in stack_registry:
|
|
190
|
+
raise click.ClickException(
|
|
191
|
+
f"stack '{t}' not found — available: {sorted(stack_registry.keys())}"
|
|
192
|
+
)
|
|
193
|
+
# Colliding structure.roots are relocated to src/services/<id> BEFORE
|
|
194
|
+
# aggregation so every derived artifact is service-scoped
|
|
195
|
+
# (project-anatomy.md § Glob/verify propagation, TASK-355).
|
|
196
|
+
stack_profiles = resolve_relocated_profiles(stack_registry, templates)
|
|
197
|
+
|
|
198
|
+
return aggregate(
|
|
199
|
+
base,
|
|
200
|
+
stack_profiles,
|
|
201
|
+
adapter_profile,
|
|
202
|
+
project.name,
|
|
203
|
+
today=today or today_iso(),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _load_config(project_dir: Path) -> dict:
|
|
208
|
+
"""Load .coding-os.yaml from project directory."""
|
|
209
|
+
config_path = project_dir / CONFIG_FILE
|
|
210
|
+
if config_path.exists():
|
|
211
|
+
with open(config_path) as f:
|
|
212
|
+
return yaml.safe_load(f) or {}
|
|
213
|
+
return {}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _save_config(project_dir: Path, config: dict) -> None:
|
|
217
|
+
"""Save config to .coding-os.yaml."""
|
|
218
|
+
config_path = project_dir / CONFIG_FILE
|
|
219
|
+
with open(config_path, "w") as f:
|
|
220
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _detect_existing_install(path: Path) -> dict | None:
|
|
224
|
+
"""Return install snapshot dict if `path` has a coding-os config, else None.
|
|
225
|
+
|
|
226
|
+
Used by `cos init` to pivot into idempotent sync mode when the user
|
|
227
|
+
accidentally re-runs init in an already-initialized project.
|
|
228
|
+
"""
|
|
229
|
+
cfg = path / CONFIG_FILE
|
|
230
|
+
if not cfg.exists():
|
|
231
|
+
return None
|
|
232
|
+
try:
|
|
233
|
+
data = yaml.safe_load(cfg.read_text(encoding="utf-8")) or {}
|
|
234
|
+
except yaml.YAMLError:
|
|
235
|
+
return None
|
|
236
|
+
return {
|
|
237
|
+
"agents": list(data.get("agents") or []),
|
|
238
|
+
"templates": list(data.get("templates") or []),
|
|
239
|
+
"version": data.get("version"),
|
|
240
|
+
"state_dir": data.get("state_dir", STATE_DIR),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _sync_missing(project: Path, *, output_format: str = "text") -> None:
|
|
245
|
+
"""Re-link any missing adapter hooks/rules/commands/skills for a project.
|
|
246
|
+
|
|
247
|
+
Non-destructive: existing files and symlinks are left alone; only gaps
|
|
248
|
+
are filled. Used both by `cos init` on an already-initialized project
|
|
249
|
+
and (in D.3) by `cos update`.
|
|
250
|
+
"""
|
|
251
|
+
config = _load_config(project) or {}
|
|
252
|
+
agents = config.get("agents") or []
|
|
253
|
+
templates = tuple(config.get("templates") or [])
|
|
254
|
+
added: list[str] = []
|
|
255
|
+
|
|
256
|
+
for agent in agents:
|
|
257
|
+
_run_adapter_install(agent, project)
|
|
258
|
+
if templates:
|
|
259
|
+
_link_stack_skills(agent, templates, project)
|
|
260
|
+
added.append(agent)
|
|
261
|
+
|
|
262
|
+
if output_format == "text":
|
|
263
|
+
click.echo(f" Synced adapters: {', '.join(added) if added else '(none)'}")
|
|
264
|
+
click.echo(" (idempotent — existing files untouched)")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _prompt_templates() -> tuple[str, ...]:
|
|
268
|
+
"""Ask the user which stack templates to apply. Returns a tuple of IDs.
|
|
269
|
+
|
|
270
|
+
Stacks render grouped by language (template-authoring.md § Language
|
|
271
|
+
layer): the user can answer with a stack id, a number, OR a bare
|
|
272
|
+
language name — the latter resolves to that language's plain stack.
|
|
273
|
+
"""
|
|
274
|
+
from cli.stack_registry import group_stacks_by_language, plain_stack_by_language
|
|
275
|
+
|
|
276
|
+
registry = _get_stack_registry()
|
|
277
|
+
if not registry.keys():
|
|
278
|
+
return ()
|
|
279
|
+
profiles = {sid: registry[sid] for sid in registry.keys()}
|
|
280
|
+
groups = group_stacks_by_language(profiles)
|
|
281
|
+
language_to_plain = plain_stack_by_language(profiles)
|
|
282
|
+
|
|
283
|
+
available: list[str] = []
|
|
284
|
+
click.echo("\nAvailable stacks (a bare language name picks its plain stack):")
|
|
285
|
+
for language, members in groups.items():
|
|
286
|
+
click.echo(f" [{language}]")
|
|
287
|
+
for profile in members:
|
|
288
|
+
available.append(profile.id)
|
|
289
|
+
click.echo(f" {len(available)}. {profile.id:17s} — {profile.label}")
|
|
290
|
+
click.echo(" 0. none")
|
|
291
|
+
click.echo(" (ready-made compositions: cos init --preset <id> — list with `cos list-stacks`)")
|
|
292
|
+
raw = click.prompt(
|
|
293
|
+
"Select stacks (numbers, names, or a language — e.g. '1,4', 'django,nextjs', 'go')",
|
|
294
|
+
default="0",
|
|
295
|
+
show_default=False,
|
|
296
|
+
)
|
|
297
|
+
tokens = [t.strip() for t in raw.split(",") if t.strip()]
|
|
298
|
+
chosen: list[str] = []
|
|
299
|
+
for tok in tokens:
|
|
300
|
+
if tok == "0":
|
|
301
|
+
return ()
|
|
302
|
+
if tok.isdigit():
|
|
303
|
+
i = int(tok) - 1
|
|
304
|
+
if 0 <= i < len(available):
|
|
305
|
+
chosen.append(available[i])
|
|
306
|
+
elif tok in registry:
|
|
307
|
+
chosen.append(tok)
|
|
308
|
+
elif tok in language_to_plain:
|
|
309
|
+
chosen.append(language_to_plain[tok])
|
|
310
|
+
# Deduplicate while preserving order.
|
|
311
|
+
seen = set()
|
|
312
|
+
result: list[str] = []
|
|
313
|
+
for s in chosen:
|
|
314
|
+
if s not in seen:
|
|
315
|
+
seen.add(s)
|
|
316
|
+
result.append(s)
|
|
317
|
+
return tuple(result)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _parse_agents(raw: str) -> list[str]:
|
|
321
|
+
"""Parse and validate a comma-separated agent string (e.g. 'claude,codex').
|
|
322
|
+
|
|
323
|
+
Raises click.ClickException if any token is not a known adapter.
|
|
324
|
+
"""
|
|
325
|
+
tokens = [t.strip() for t in raw.split(",") if t.strip()]
|
|
326
|
+
if not tokens:
|
|
327
|
+
raise click.ClickException("--agent value is empty")
|
|
328
|
+
invalid = [t for t in tokens if t not in VALID_AGENTS]
|
|
329
|
+
if invalid:
|
|
330
|
+
raise click.ClickException(
|
|
331
|
+
f"unknown agent(s): {', '.join(invalid)} — available: {', '.join(VALID_AGENTS)}"
|
|
332
|
+
)
|
|
333
|
+
# Deduplicate preserving order.
|
|
334
|
+
seen: set[str] = set()
|
|
335
|
+
result: list[str] = []
|
|
336
|
+
for t in tokens:
|
|
337
|
+
if t not in seen:
|
|
338
|
+
seen.add(t)
|
|
339
|
+
result.append(t)
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _prompt_agents() -> list[str]:
|
|
344
|
+
"""Interactively prompt for one or more agents (comma-separated)."""
|
|
345
|
+
label = ", ".join(VALID_AGENTS)
|
|
346
|
+
raw = click.prompt(
|
|
347
|
+
f"Agent(s) — comma-separated ({label})",
|
|
348
|
+
default=VALID_AGENTS[0] if VALID_AGENTS else None,
|
|
349
|
+
)
|
|
350
|
+
return _parse_agents(raw)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _prompt_name_and_location(shell_cwd: Path) -> tuple[str | None, str | None]:
|
|
354
|
+
"""Decide whether to use the current dir or create a subdir.
|
|
355
|
+
|
|
356
|
+
Returns (name, project_dir) — either may be None to fall back to the
|
|
357
|
+
resolver default. `project_dir` is returned as a string path to match
|
|
358
|
+
the CLI option type.
|
|
359
|
+
"""
|
|
360
|
+
default_name = shell_cwd.name
|
|
361
|
+
use_current = click.confirm(
|
|
362
|
+
f"Use current directory ({shell_cwd})?",
|
|
363
|
+
default=True,
|
|
364
|
+
)
|
|
365
|
+
if use_current:
|
|
366
|
+
return None, str(shell_cwd)
|
|
367
|
+
name = click.prompt("Project name (subdirectory)", default=default_name)
|
|
368
|
+
return name, str(shell_cwd)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _derive_verify_from_world(world: AggregatedWorld) -> dict[str, str]:
|
|
372
|
+
"""Extract domain → command mapping from aggregated VERIFY_* substitutions.
|
|
373
|
+
|
|
374
|
+
Looks for keys of the form `VERIFY_<DOMAIN>` (exact — not `_GLOB`, not
|
|
375
|
+
`_SUITES`) and maps them to lowercased domain names. Strips surrounding
|
|
376
|
+
backticks from values so `.coding-os.yaml.verify` stores raw shell
|
|
377
|
+
commands, not display-formatted ones.
|
|
378
|
+
"""
|
|
379
|
+
result: dict[str, str] = {}
|
|
380
|
+
for key, value in world.substitutions.items():
|
|
381
|
+
if not key.startswith("VERIFY_"):
|
|
382
|
+
continue
|
|
383
|
+
if key.endswith("_GLOB") or key.endswith("_SUITES"):
|
|
384
|
+
continue
|
|
385
|
+
domain = key.removeprefix("VERIFY_").lower()
|
|
386
|
+
cleaned = value.strip().strip("`").strip()
|
|
387
|
+
if not cleaned or cleaned == "(none)":
|
|
388
|
+
continue
|
|
389
|
+
result[domain] = cleaned
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _link_stack_skills(
|
|
394
|
+
agent: str,
|
|
395
|
+
templates: tuple[str, ...],
|
|
396
|
+
project_dir: Path,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Symlink each applied stack's skills into the agent's skills_dir.
|
|
399
|
+
|
|
400
|
+
No-op for adapters whose skills_dir is null (e.g. Codex). Delegates the
|
|
401
|
+
filesystem work to src/core/scripts/link-stack-skills.sh so the same logic
|
|
402
|
+
is callable from Make targets / cos update.
|
|
403
|
+
"""
|
|
404
|
+
registry = _get_adapter_registry()
|
|
405
|
+
if agent not in registry:
|
|
406
|
+
return
|
|
407
|
+
skills_dir = registry[agent].skills_dir
|
|
408
|
+
if not skills_dir:
|
|
409
|
+
return
|
|
410
|
+
linker = CORE_DIR / "scripts" / "link-stack-skills.sh"
|
|
411
|
+
if not linker.exists():
|
|
412
|
+
click.echo(f" WARN: stack-skill linker missing: {linker}", err=True)
|
|
413
|
+
return
|
|
414
|
+
agent_skills_abs = str(project_dir / skills_dir)
|
|
415
|
+
result = subprocess.run(
|
|
416
|
+
["bash", str(linker), agent_skills_abs, str(data_root()), *templates],
|
|
417
|
+
capture_output=True,
|
|
418
|
+
text=True,
|
|
419
|
+
)
|
|
420
|
+
if result.returncode != 0:
|
|
421
|
+
click.echo(
|
|
422
|
+
f" WARN: stack-skill linking failed:\n{result.stderr}",
|
|
423
|
+
err=True,
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
linked = []
|
|
427
|
+
for t in templates:
|
|
428
|
+
stack_skills = templates_dir(t, "skills")
|
|
429
|
+
if stack_skills.exists():
|
|
430
|
+
linked.extend(sorted(p.name for p in stack_skills.iterdir() if p.is_dir()))
|
|
431
|
+
if linked:
|
|
432
|
+
click.echo(f" Linked stack skills: {', '.join(linked)}")
|
|
433
|
+
|
|
434
|
+
# Community-overlay stacks live outside the bundled tree the shell linker
|
|
435
|
+
# scans (data_root), so link their skills here from the resolved source_dir
|
|
436
|
+
# (TASK-479). Only fires for a stack whose source_dir is NOT under TEMPLATES_DIR.
|
|
437
|
+
stack_registry = _get_stack_registry()
|
|
438
|
+
bundled_root = TEMPLATES_DIR.resolve()
|
|
439
|
+
community: list[str] = []
|
|
440
|
+
for t in templates:
|
|
441
|
+
if t not in stack_registry:
|
|
442
|
+
continue
|
|
443
|
+
source_dir = stack_registry[t].source_dir.resolve()
|
|
444
|
+
try:
|
|
445
|
+
source_dir.relative_to(bundled_root)
|
|
446
|
+
continue # bundled — already linked by the shell
|
|
447
|
+
except ValueError:
|
|
448
|
+
pass # community overlay stack
|
|
449
|
+
skills_src = source_dir / "skills"
|
|
450
|
+
if not skills_src.is_dir():
|
|
451
|
+
continue
|
|
452
|
+
for skill_dir in sorted(p for p in skills_src.iterdir() if p.is_dir()):
|
|
453
|
+
src_md = skill_dir / "SKILL.md"
|
|
454
|
+
dest = project_dir / skills_dir / skill_dir.name / "SKILL.md"
|
|
455
|
+
if not src_md.is_file() or dest.exists():
|
|
456
|
+
continue
|
|
457
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
dest.symlink_to(src_md)
|
|
459
|
+
community.append(skill_dir.name)
|
|
460
|
+
if community:
|
|
461
|
+
click.echo(f" Linked community stack skills: {', '.join(community)}")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _run_adapter_install(agent: str, project_dir: Path) -> None:
|
|
465
|
+
"""Run the adapter's declared install script.
|
|
466
|
+
|
|
467
|
+
Uses adapter.yaml::install_script so a new adapter is pure data — no
|
|
468
|
+
hardcoded path assumption.
|
|
469
|
+
"""
|
|
470
|
+
registry = _get_adapter_registry()
|
|
471
|
+
if agent not in registry:
|
|
472
|
+
click.echo(
|
|
473
|
+
f" ERROR: Unknown adapter '{agent}' — available: {sorted(registry.keys())}",
|
|
474
|
+
err=True,
|
|
475
|
+
)
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
install_script = registry[agent].install_script
|
|
478
|
+
if not install_script.exists():
|
|
479
|
+
click.echo(
|
|
480
|
+
f" ERROR: Adapter install script not found: {install_script}",
|
|
481
|
+
err=True,
|
|
482
|
+
)
|
|
483
|
+
sys.exit(1)
|
|
484
|
+
|
|
485
|
+
result = subprocess.run(
|
|
486
|
+
["bash", str(install_script)],
|
|
487
|
+
cwd=str(project_dir),
|
|
488
|
+
capture_output=True,
|
|
489
|
+
text=True,
|
|
490
|
+
)
|
|
491
|
+
if result.stdout:
|
|
492
|
+
click.echo(result.stdout)
|
|
493
|
+
if result.returncode != 0:
|
|
494
|
+
click.echo(f" ERROR: Adapter install failed:\n{result.stderr}", err=True)
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _apply_template(
|
|
499
|
+
template_name: str,
|
|
500
|
+
project_dir: Path,
|
|
501
|
+
agent: str | None = None,
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Apply a stack template to the project.
|
|
504
|
+
|
|
505
|
+
1. Copies `src/templates/<name>/{rules,skills,playbooks,hooks}/` to
|
|
506
|
+
`<project>/.coding-os/templates/<name>/…` for browsing.
|
|
507
|
+
2. If `agent` is provided and that adapter supports path-scoped rules
|
|
508
|
+
(`adapter.rules_dir` != null), also copies every
|
|
509
|
+
`src/templates/<name>/rules/*.md` into the adapter's rules dir with a
|
|
510
|
+
`<stack>-<filename>` prefix so multiple stacks coexist.
|
|
511
|
+
"""
|
|
512
|
+
stack_registry = _get_stack_registry()
|
|
513
|
+
if template_name not in stack_registry:
|
|
514
|
+
click.echo(
|
|
515
|
+
f" WARN: Template '{template_name}' not in registry "
|
|
516
|
+
f"(available: {sorted(stack_registry.keys())})",
|
|
517
|
+
err=True,
|
|
518
|
+
)
|
|
519
|
+
return
|
|
520
|
+
stack_profile = stack_registry[template_name]
|
|
521
|
+
template_dir = stack_profile.source_dir
|
|
522
|
+
|
|
523
|
+
# 1. Mirror every subdir into .coding-os/templates/<name>/
|
|
524
|
+
for subdir in ("rules", "skills", "playbooks", "hooks"):
|
|
525
|
+
src = template_dir / subdir
|
|
526
|
+
if src.exists():
|
|
527
|
+
dest = project_dir / STATE_DIR / "src" / "templates" / template_name / subdir
|
|
528
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
529
|
+
for item in src.iterdir():
|
|
530
|
+
if item.is_file():
|
|
531
|
+
shutil.copy2(item, dest / item.name)
|
|
532
|
+
elif item.is_dir():
|
|
533
|
+
shutil.copytree(item, dest / item.name, dirs_exist_ok=True)
|
|
534
|
+
|
|
535
|
+
# 2. Copy path-scoped rules into the adapter's rules_dir (if supported).
|
|
536
|
+
if agent is not None:
|
|
537
|
+
adapters = _get_adapter_registry()
|
|
538
|
+
if agent in adapters:
|
|
539
|
+
adapter_profile = adapters[agent]
|
|
540
|
+
if adapter_profile.supports_rules and adapter_profile.rules_dir:
|
|
541
|
+
rules_src = template_dir / "rules"
|
|
542
|
+
if rules_src.exists():
|
|
543
|
+
rules_dest = project_dir / adapter_profile.rules_dir
|
|
544
|
+
rules_dest.mkdir(parents=True, exist_ok=True)
|
|
545
|
+
for rule_file in sorted(rules_src.glob("*.md")):
|
|
546
|
+
# Prefix with stack id to avoid collisions between stacks.
|
|
547
|
+
out = rules_dest / f"{template_name}-{rule_file.name}"
|
|
548
|
+
shutil.copy2(rule_file, out)
|
|
549
|
+
elif not adapter_profile.supports_rules:
|
|
550
|
+
click.echo(
|
|
551
|
+
f" INFO: adapter '{agent}' does not support path-scoped "
|
|
552
|
+
f"rules — skipping rules copy for '{template_name}'",
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
click.echo(f" Template '{template_name}' applied.")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# _merge_profiles, _build_substitutions, _list_installed_skills have all
|
|
559
|
+
# been replaced by the aggregator pipeline. See _build_world() above and
|
|
560
|
+
# src/cli/aggregator.py::aggregate() for the data-driven replacement.
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _resolve_placeholders(text: str, substitutions: dict[str, str]) -> str:
|
|
564
|
+
"""Replace `{{KEY}}` placeholders. Unknown keys are left intact for later overlay."""
|
|
565
|
+
result = text
|
|
566
|
+
for key, value in substitutions.items():
|
|
567
|
+
result = result.replace(f"{{{{{key}}}}}", value)
|
|
568
|
+
return result
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# Tag-driven docs composition (TASK-360):
|
|
572
|
+
# - file-level: a `module:<id>` token in the first-line header comment skips
|
|
573
|
+
# the whole doc when that module is disabled;
|
|
574
|
+
# - block-level: `<!-- if-stack:a,b -->` / `<!-- if-module:docs -->` ...
|
|
575
|
+
# `<!-- end-if -->` keep the block only when ANY listed stack is installed /
|
|
576
|
+
# the module is enabled. Markers and tags are stripped from the copy, so a
|
|
577
|
+
# fully-default project's output is byte-identical to untagged sources.
|
|
578
|
+
_DOC_MODULE_TAG_RE = re.compile(r"\s*\|\s*module:([a-z][a-z0-9_-]*)")
|
|
579
|
+
_DOC_IF_RE = re.compile(r"^<!--\s*if-(stack|module):([a-z0-9_,-]+)\s*-->\s*$")
|
|
580
|
+
_DOC_ENDIF_RE = re.compile(r"^<!--\s*end-if\s*-->\s*$")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _apply_doc_conditions(
|
|
584
|
+
text: str, disabled_modules: set[str], active_stacks: set[str]
|
|
585
|
+
) -> tuple[bool, str]:
|
|
586
|
+
"""(skip_file, transformed_text) — see the marker contract above."""
|
|
587
|
+
lines = text.split("\n")
|
|
588
|
+
if lines:
|
|
589
|
+
tag = _DOC_MODULE_TAG_RE.search(lines[0])
|
|
590
|
+
if tag and lines[0].lstrip().startswith("<!--"):
|
|
591
|
+
if tag.group(1) in disabled_modules:
|
|
592
|
+
return True, ""
|
|
593
|
+
lines[0] = _DOC_MODULE_TAG_RE.sub("", lines[0], count=1)
|
|
594
|
+
|
|
595
|
+
out: list[str] = []
|
|
596
|
+
keeping = True
|
|
597
|
+
in_block = False
|
|
598
|
+
for line in lines:
|
|
599
|
+
opener = _DOC_IF_RE.match(line)
|
|
600
|
+
if opener and not in_block:
|
|
601
|
+
in_block = True
|
|
602
|
+
kind, raw_ids = opener.group(1), opener.group(2)
|
|
603
|
+
wanted = {x for x in raw_ids.split(",") if x}
|
|
604
|
+
if kind == "stack":
|
|
605
|
+
keeping = bool(wanted & active_stacks)
|
|
606
|
+
else:
|
|
607
|
+
keeping = not (wanted & disabled_modules)
|
|
608
|
+
continue
|
|
609
|
+
if _DOC_ENDIF_RE.match(line) and in_block:
|
|
610
|
+
in_block = False
|
|
611
|
+
keeping = True
|
|
612
|
+
continue
|
|
613
|
+
if keeping:
|
|
614
|
+
out.append(line)
|
|
615
|
+
return False, "\n".join(out)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def module_scaffold_doc_rels(templates: tuple[str, ...], module_id: str) -> list[str]:
|
|
619
|
+
"""Consumer-relative paths of scaffold `.md` docs tagged `| module:<module_id>`.
|
|
620
|
+
|
|
621
|
+
Reuses the source-root + relocation mapping of the scaffold overlay so the
|
|
622
|
+
result matches what init composed. Shared by the toggle doc-sync (prune /
|
|
623
|
+
restore, TASK-813) and cos doctor's modules.doc_drift backstop — the consumer
|
|
624
|
+
copy has its tag STRIPPED at init, so drift/prune must map via the tagged
|
|
625
|
+
SOURCE, never the untagged destination."""
|
|
626
|
+
registry = _get_stack_registry()
|
|
627
|
+
relocations = _service_relocations(templates)
|
|
628
|
+
sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
|
|
629
|
+
for name in templates:
|
|
630
|
+
stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
|
|
631
|
+
candidate = stack_root / "scaffold"
|
|
632
|
+
if candidate.exists():
|
|
633
|
+
sources.append((candidate, name))
|
|
634
|
+
rels: set[str] = set()
|
|
635
|
+
for src_root, stack_id in sources:
|
|
636
|
+
if not src_root.exists():
|
|
637
|
+
continue
|
|
638
|
+
relocated_root = relocations.get(stack_id) if stack_id else None
|
|
639
|
+
declared_root = (
|
|
640
|
+
(registry[stack_id].structure or {}).get("root", "").rstrip("/")
|
|
641
|
+
if stack_id and stack_id in registry.keys()
|
|
642
|
+
else ""
|
|
643
|
+
)
|
|
644
|
+
for src_file in src_root.rglob("*.md"):
|
|
645
|
+
if not src_file.is_file():
|
|
646
|
+
continue
|
|
647
|
+
try:
|
|
648
|
+
first = src_file.read_text(encoding="utf-8").split("\n", 1)[0]
|
|
649
|
+
except OSError:
|
|
650
|
+
continue
|
|
651
|
+
if not first.lstrip().startswith("<!--"):
|
|
652
|
+
continue
|
|
653
|
+
tag = _DOC_MODULE_TAG_RE.search(first)
|
|
654
|
+
if not (tag and tag.group(1) == module_id):
|
|
655
|
+
continue
|
|
656
|
+
rel = src_file.relative_to(src_root)
|
|
657
|
+
if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
|
|
658
|
+
rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
|
|
659
|
+
rels.add(str(rel))
|
|
660
|
+
return sorted(rels)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _dry_config_preview(templates: tuple[str, ...], output_format: str) -> None:
|
|
664
|
+
"""`cos init --dry-config` — merged .coding-os preview, zero writes."""
|
|
665
|
+
from cli.config_composer import preview_coding_os_configs
|
|
666
|
+
|
|
667
|
+
merged, conflicts = preview_coding_os_configs(list(templates), templates_dir=TEMPLATES_DIR)
|
|
668
|
+
if output_format == "json":
|
|
669
|
+
click.echo(
|
|
670
|
+
json.dumps(
|
|
671
|
+
{"stacks": list(templates), "configs": merged, "conflicts": conflicts},
|
|
672
|
+
indent=2,
|
|
673
|
+
ensure_ascii=False,
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
return
|
|
677
|
+
scrumban = merged.get("scrumban-config.yaml") or {}
|
|
678
|
+
lanes = [lane.get("id") for lane in scrumban.get("swimlanes") or [] if isinstance(lane, dict)]
|
|
679
|
+
click.echo(f"Merge preview for stacks: {', '.join(templates) or '(base only)'}")
|
|
680
|
+
click.echo(f" swimlanes: {', '.join(lanes) or '(none)'}")
|
|
681
|
+
for filename in merged:
|
|
682
|
+
click.echo(f" composed: {filename}")
|
|
683
|
+
if conflicts:
|
|
684
|
+
click.echo(f" conflicts ({len(conflicts)} — later wins):")
|
|
685
|
+
for line in conflicts:
|
|
686
|
+
click.echo(f" WARN: {line}")
|
|
687
|
+
else:
|
|
688
|
+
click.echo(" conflicts: none")
|
|
689
|
+
click.echo("(dry-config — nothing written)")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _scaffold_tree_preview(
|
|
693
|
+
templates: tuple[str, ...], disabled_modules: tuple[str, ...] = ()
|
|
694
|
+
) -> tuple[list[str], list[str]]:
|
|
695
|
+
"""Relative paths `cos init` WOULD create — zero reads of the target, zero writes.
|
|
696
|
+
|
|
697
|
+
Returns (sorted paths, config-merge conflicts). Mirrors the source roots,
|
|
698
|
+
service-relocation logic AND the `<!-- module:X -->` doc-skip of
|
|
699
|
+
`_overlay_scaffold` / `_run_scaffold_phase` so the preview matches what an
|
|
700
|
+
actual init writes (audit INIT-4). disabled_modules is taken LITERALLY from the
|
|
701
|
+
validated `--disable-module` flags; the real init additionally resolves
|
|
702
|
+
dependency-refusal (e.g. `docs` stays enabled while `tasks` depends on it) and
|
|
703
|
+
preset-declared disables at scaffold time, so for those two cases the preview
|
|
704
|
+
is a best-effort upper bound on what gets dropped, not byte-exact (pass-3).
|
|
705
|
+
"""
|
|
706
|
+
from cli.config_composer import preview_coding_os_configs
|
|
707
|
+
|
|
708
|
+
relocations = _service_relocations(templates)
|
|
709
|
+
registry = _get_stack_registry()
|
|
710
|
+
disabled_set = {m.strip() for m in disabled_modules if m.strip()}
|
|
711
|
+
active_set = set(templates)
|
|
712
|
+
paths: set[str] = set()
|
|
713
|
+
|
|
714
|
+
sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
|
|
715
|
+
for name in templates:
|
|
716
|
+
# Community stacks resolve from source_dir, not the bundled tree (TASK-479).
|
|
717
|
+
stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
|
|
718
|
+
candidate = stack_root / "scaffold"
|
|
719
|
+
if candidate.exists():
|
|
720
|
+
sources.append((candidate, name))
|
|
721
|
+
|
|
722
|
+
for src_root, stack_id in sources:
|
|
723
|
+
if not src_root.exists():
|
|
724
|
+
continue
|
|
725
|
+
relocated_root = relocations.get(stack_id) if stack_id else None
|
|
726
|
+
declared_root = (
|
|
727
|
+
(registry[stack_id].structure or {}).get("root", "").rstrip("/")
|
|
728
|
+
if stack_id and stack_id in registry.keys()
|
|
729
|
+
else ""
|
|
730
|
+
)
|
|
731
|
+
for src_file in src_root.rglob("*"):
|
|
732
|
+
if not src_file.is_file() or src_file.name == ".gitkeep":
|
|
733
|
+
continue
|
|
734
|
+
rel = src_file.relative_to(src_root)
|
|
735
|
+
# A module-tagged doc the actual --disable-module init would drop
|
|
736
|
+
# must not appear in the preview (INIT-4: preview == actual). Only
|
|
737
|
+
# .md files carry the marker, mirroring the overlay's own scope.
|
|
738
|
+
if disabled_set and src_file.suffix == ".md":
|
|
739
|
+
try:
|
|
740
|
+
skip_file, _ = _apply_doc_conditions(
|
|
741
|
+
src_file.read_text(encoding="utf-8"), disabled_set, active_set
|
|
742
|
+
)
|
|
743
|
+
if skip_file:
|
|
744
|
+
continue
|
|
745
|
+
except OSError as exc:
|
|
746
|
+
logging.getLogger(__name__).debug(
|
|
747
|
+
"doc-condition preview check skipped for %s: %s", src_file, exc
|
|
748
|
+
)
|
|
749
|
+
if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
|
|
750
|
+
rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
|
|
751
|
+
# Composed configs come from the merge step below, not the overlay.
|
|
752
|
+
if rel.parent.name == ".coding-os" and rel.name in COMPOSED_FILENAMES:
|
|
753
|
+
continue
|
|
754
|
+
paths.add(str(rel))
|
|
755
|
+
|
|
756
|
+
merged, conflicts = preview_coding_os_configs(list(templates), templates_dir=TEMPLATES_DIR)
|
|
757
|
+
for filename in merged:
|
|
758
|
+
paths.add(f"{STATE_DIR}/{filename}")
|
|
759
|
+
|
|
760
|
+
# Generated artifacts the scaffold phase always writes (not under scaffold/).
|
|
761
|
+
paths.update(
|
|
762
|
+
{
|
|
763
|
+
CONFIG_FILE,
|
|
764
|
+
"AGENTS.md",
|
|
765
|
+
"Makefile",
|
|
766
|
+
f"{STATE_DIR}/coding-os.db",
|
|
767
|
+
f"{STATE_DIR}/Makefile.base",
|
|
768
|
+
}
|
|
769
|
+
)
|
|
770
|
+
return sorted(paths), conflicts
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _dry_run_preview(
|
|
774
|
+
templates: tuple[str, ...], output_format: str, disabled_modules: tuple[str, ...] = ()
|
|
775
|
+
) -> None:
|
|
776
|
+
"""`cos init --dry-run` — preview the scaffold tree with ZERO writes."""
|
|
777
|
+
paths, conflicts = _scaffold_tree_preview(templates, disabled_modules)
|
|
778
|
+
if output_format == "json":
|
|
779
|
+
click.echo(
|
|
780
|
+
json.dumps(
|
|
781
|
+
{
|
|
782
|
+
"stacks": list(templates),
|
|
783
|
+
"disabled_modules": [m for m in disabled_modules if m],
|
|
784
|
+
"files": paths,
|
|
785
|
+
"conflicts": conflicts,
|
|
786
|
+
"note": "the .claude/ agent surface (hooks/skills/commands/rules) is installed by the adapter and NOT previewed here",
|
|
787
|
+
},
|
|
788
|
+
indent=2,
|
|
789
|
+
ensure_ascii=False,
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
return
|
|
793
|
+
click.echo(f"Scaffold preview for stacks: {', '.join(templates) or '(base only)'}")
|
|
794
|
+
click.echo(f" {len(paths)} file(s) would be created:")
|
|
795
|
+
for path in paths:
|
|
796
|
+
click.echo(f" {path}")
|
|
797
|
+
if conflicts:
|
|
798
|
+
click.echo(f" config conflicts ({len(conflicts)} — later wins):")
|
|
799
|
+
for line in conflicts:
|
|
800
|
+
click.echo(f" WARN: {line}")
|
|
801
|
+
click.echo(
|
|
802
|
+
" note: the .claude/ agent surface (hooks · skills · commands · rules) is "
|
|
803
|
+
"installed by the adapter and NOT previewed here."
|
|
804
|
+
)
|
|
805
|
+
click.echo("(dry-run — nothing written)")
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _registered_slug(project: Path) -> str:
|
|
809
|
+
# The hub slug the Composer navigates by; "" under --no-register or a failed
|
|
810
|
+
# registry write, both of which are non-fatal for init itself.
|
|
811
|
+
import logging
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
from cli.registry import load_registry
|
|
815
|
+
|
|
816
|
+
# add_project matches on the unresolved path, so match both forms —
|
|
817
|
+
# resolving only would miss a symlinked entry it had just written.
|
|
818
|
+
wanted = {str(project), str(project.resolve())}
|
|
819
|
+
for entry in load_registry().projects:
|
|
820
|
+
stored = Path(entry.path)
|
|
821
|
+
if str(stored) in wanted or str(stored.resolve()) in wanted:
|
|
822
|
+
return entry.slug
|
|
823
|
+
except Exception as exc:
|
|
824
|
+
logging.getLogger(__name__).debug("slug lookup skipped: %s", exc)
|
|
825
|
+
return ""
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@functools.lru_cache(maxsize=1)
|
|
829
|
+
def _subsystem_help_lists() -> tuple[str, str]:
|
|
830
|
+
# Rule 11 — the ids come from subsystems.yaml, never a literal that rots as
|
|
831
|
+
# modules are added (hidden ones are not user-selectable, so they stay out).
|
|
832
|
+
# Cached: these run at decoration time on every `cos` invocation, and both
|
|
833
|
+
# init and adopt declare the flags, so an uncached read costs 4 yaml loads.
|
|
834
|
+
fallback = "see src/core/subsystems.yaml"
|
|
835
|
+
try:
|
|
836
|
+
from cli.subsystems import load_profiles, load_subsystems
|
|
837
|
+
|
|
838
|
+
ids = [m.id for m in load_subsystems().values() if not m.kernel and not m.hidden]
|
|
839
|
+
profiles, default_profile = load_profiles()
|
|
840
|
+
except Exception:
|
|
841
|
+
return fallback, fallback
|
|
842
|
+
return (
|
|
843
|
+
", ".join(ids) or fallback,
|
|
844
|
+
", ".join(f"{n} (default)" if n == default_profile else n for n in profiles) or fallback,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _module_flag_help() -> str:
|
|
849
|
+
return (
|
|
850
|
+
f"Subsystem module to disable at create (repeatable): {_subsystem_help_lists()[0]}. "
|
|
851
|
+
"kernel can't be disabled; modules that depend on it are disabled with it. "
|
|
852
|
+
"Wizard parity with the Composer module toggles."
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _profile_flag_help() -> str:
|
|
857
|
+
return (
|
|
858
|
+
f"Module profile curating the agent's MCP tool surface: {_subsystem_help_lists()[1]}. "
|
|
859
|
+
"UNIONED with --disable-module (a profile can only remove modules, never "
|
|
860
|
+
"re-add one — use --enable-module to keep one on); omit to use the "
|
|
861
|
+
"registry default."
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _enable_flag_help() -> str:
|
|
866
|
+
return (
|
|
867
|
+
"Force-enable a module after the profile union (repeatable) — the escape "
|
|
868
|
+
"from profile+--disable-module union semantics, which can only remove. "
|
|
869
|
+
"Pulls the module's depends_on chain in with it; combining with "
|
|
870
|
+
"--disable-module of the same id is an error."
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _validated_disabled_modules(disable_module: tuple[str, ...]) -> list[str]:
|
|
875
|
+
# Validate --disable-module up-front so BOTH the dry-run preview and the real
|
|
876
|
+
# init reject the same ids (pass-3 review: dry-run returned before validation,
|
|
877
|
+
# so a typo'd module gave a false all-clear). kernel ids are non-disableable.
|
|
878
|
+
if not disable_module:
|
|
879
|
+
return []
|
|
880
|
+
from cli.subsystems import close_over_dependents, load_subsystems
|
|
881
|
+
|
|
882
|
+
registry_modules = load_subsystems()
|
|
883
|
+
disabled = list(dict.fromkeys(m.strip() for m in disable_module if m.strip()))
|
|
884
|
+
unknown = [m for m in disabled if m not in registry_modules]
|
|
885
|
+
if unknown:
|
|
886
|
+
click.echo(
|
|
887
|
+
f"ERROR: unknown module(s) {unknown} — available: {sorted(registry_modules)}.",
|
|
888
|
+
err=True,
|
|
889
|
+
)
|
|
890
|
+
sys.exit(2)
|
|
891
|
+
kernel = [m for m in disabled if registry_modules[m].kernel]
|
|
892
|
+
if kernel:
|
|
893
|
+
click.echo(f"ERROR: module(s) {kernel} are kernel and cannot be disabled.", err=True)
|
|
894
|
+
sys.exit(2)
|
|
895
|
+
closed = close_over_dependents(disabled, registry_modules)
|
|
896
|
+
added = [m for m in closed if m not in disabled]
|
|
897
|
+
if added:
|
|
898
|
+
# stderr: this runs before the json-mode stdout redirect, and a progress
|
|
899
|
+
# line on stdout makes `cos init --format json | jq` a parse error.
|
|
900
|
+
click.echo(f" Also disabling dependent module(s): {', '.join(sorted(added))}", err=True)
|
|
901
|
+
return closed
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def _apply_enable_modules(
|
|
905
|
+
disabled: list[str],
|
|
906
|
+
enable_module: tuple[str, ...],
|
|
907
|
+
explicit_disable: tuple[str, ...],
|
|
908
|
+
) -> list[str]:
|
|
909
|
+
# The escape from union semantics: --enable-module wins over a profile's
|
|
910
|
+
# disable, but contradicting an explicit --disable-module is an error, not
|
|
911
|
+
# a merge. Dependencies come along so the final set stays closed.
|
|
912
|
+
if not enable_module:
|
|
913
|
+
return disabled
|
|
914
|
+
from cli.subsystems import load_subsystems
|
|
915
|
+
|
|
916
|
+
registry_modules = load_subsystems()
|
|
917
|
+
enabled = list(dict.fromkeys(m.strip() for m in enable_module if m.strip()))
|
|
918
|
+
unknown = [m for m in enabled if m not in registry_modules]
|
|
919
|
+
if unknown:
|
|
920
|
+
click.echo(
|
|
921
|
+
f"ERROR: unknown module(s) {unknown} — available: {sorted(registry_modules)}.",
|
|
922
|
+
err=True,
|
|
923
|
+
)
|
|
924
|
+
sys.exit(2)
|
|
925
|
+
conflict = sorted(set(enabled) & {m.strip() for m in explicit_disable if m.strip()})
|
|
926
|
+
if conflict:
|
|
927
|
+
click.echo(
|
|
928
|
+
f"ERROR: module(s) {conflict} passed to both --enable-module and --disable-module.",
|
|
929
|
+
err=True,
|
|
930
|
+
)
|
|
931
|
+
sys.exit(2)
|
|
932
|
+
keep: set[str] = set()
|
|
933
|
+
frontier = list(enabled)
|
|
934
|
+
while frontier:
|
|
935
|
+
module_id = frontier.pop()
|
|
936
|
+
if module_id in keep:
|
|
937
|
+
continue
|
|
938
|
+
keep.add(module_id)
|
|
939
|
+
frontier.extend(d for d in registry_modules[module_id].depends_on if d in registry_modules)
|
|
940
|
+
re_enabled = sorted(set(disabled) & keep)
|
|
941
|
+
if re_enabled:
|
|
942
|
+
click.echo(f" Re-enabling module(s): {', '.join(re_enabled)}", err=True)
|
|
943
|
+
return [m for m in disabled if m not in keep]
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _service_relocations(templates: tuple[str, ...]) -> dict[str, str]:
|
|
947
|
+
"""stack-id → relocated root for stacks whose structure.root collides.
|
|
948
|
+
|
|
949
|
+
Thin wrapper over the SSOT in cli.stack_registry (shared with
|
|
950
|
+
cli.update._aggregate_world); see project-anatomy.md.
|
|
951
|
+
"""
|
|
952
|
+
return service_relocations(_get_stack_registry(), templates)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _overlay_scaffold(
|
|
956
|
+
project: Path,
|
|
957
|
+
templates: tuple[str, ...],
|
|
958
|
+
substitutions: dict[str, str],
|
|
959
|
+
) -> int:
|
|
960
|
+
"""Copy `_base/scaffold/` then each template's `scaffold/` into `project/`.
|
|
961
|
+
|
|
962
|
+
Existing project files are NEVER overwritten (idempotent init).
|
|
963
|
+
Markdown files have their `{{KEY}}` placeholders resolved.
|
|
964
|
+
|
|
965
|
+
Returns: count of files copied.
|
|
966
|
+
"""
|
|
967
|
+
# Source roots in overlay order: _base first, then each template overlay.
|
|
968
|
+
# Each entry: (scaffold dir, owning stack id or None for _base). A community
|
|
969
|
+
# stack's scaffold lives at its resolved source_dir, not the bundled tree (TASK-479).
|
|
970
|
+
registry = _get_stack_registry()
|
|
971
|
+
sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
|
|
972
|
+
for name in templates:
|
|
973
|
+
stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
|
|
974
|
+
candidate = stack_root / "scaffold"
|
|
975
|
+
if candidate.exists():
|
|
976
|
+
sources.append((candidate, name))
|
|
977
|
+
|
|
978
|
+
# Per-language toolchain config (ruff/pytest, eslint/prettier/vitest) lives once
|
|
979
|
+
# under _base/lang/<language>/, selected by each active stack's declared language.
|
|
980
|
+
# Overlaid LAST so a stack's own scaffold config wins the idempotent first-write.
|
|
981
|
+
seen_languages: set[str] = set()
|
|
982
|
+
for name in templates:
|
|
983
|
+
language = registry[name].language if name in registry.keys() else ""
|
|
984
|
+
if not language or language in seen_languages:
|
|
985
|
+
continue
|
|
986
|
+
seen_languages.add(language)
|
|
987
|
+
lang_dir = TEMPLATES_DIR / "_base" / "lang" / language
|
|
988
|
+
if lang_dir.exists():
|
|
989
|
+
sources.append((lang_dir, None))
|
|
990
|
+
|
|
991
|
+
relocations = _service_relocations(templates)
|
|
992
|
+
|
|
993
|
+
from cli.subsystems import module_state
|
|
994
|
+
|
|
995
|
+
disabled_modules = {
|
|
996
|
+
module_id for module_id, enabled in module_state(project).items() if not enabled
|
|
997
|
+
}
|
|
998
|
+
active_stacks = set(templates)
|
|
999
|
+
|
|
1000
|
+
copied = 0
|
|
1001
|
+
for src_root, stack_id in sources:
|
|
1002
|
+
if not src_root.exists():
|
|
1003
|
+
continue
|
|
1004
|
+
relocated_root = relocations.get(stack_id) if stack_id else None
|
|
1005
|
+
declared_root = (
|
|
1006
|
+
(registry[stack_id].structure or {}).get("root", "").rstrip("/")
|
|
1007
|
+
if stack_id and stack_id in registry.keys()
|
|
1008
|
+
else ""
|
|
1009
|
+
)
|
|
1010
|
+
for src_file in src_root.rglob("*"):
|
|
1011
|
+
if not src_file.is_file():
|
|
1012
|
+
continue
|
|
1013
|
+
if src_file.name == ".gitkeep":
|
|
1014
|
+
# .gitkeep just ensures the parent dir exists in the copy —
|
|
1015
|
+
# honoring service relocation like any other scaffold path.
|
|
1016
|
+
rel = src_file.relative_to(src_root)
|
|
1017
|
+
if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
|
|
1018
|
+
rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
|
|
1019
|
+
dest = project / rel
|
|
1020
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
rel = src_file.relative_to(src_root)
|
|
1024
|
+
if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
|
|
1025
|
+
rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
|
|
1026
|
+
if rel.parent.name == ".coding-os" and rel.name in COMPOSED_FILENAMES:
|
|
1027
|
+
# These are deep-merged from base + every stack by
|
|
1028
|
+
# compose_coding_os_configs — overlaying base first would
|
|
1029
|
+
# shadow the merge (first-writer-wins). See config-composition.md.
|
|
1030
|
+
continue
|
|
1031
|
+
dest = project / rel
|
|
1032
|
+
if dest.exists():
|
|
1033
|
+
# Idempotent: never overwrite existing project files.
|
|
1034
|
+
continue
|
|
1035
|
+
|
|
1036
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1037
|
+
try:
|
|
1038
|
+
content = src_file.read_text(encoding="utf-8")
|
|
1039
|
+
except (UnicodeDecodeError, ValueError):
|
|
1040
|
+
# Binary asset (image, font, ...) — copy verbatim, never substitute.
|
|
1041
|
+
shutil.copy2(src_file, dest)
|
|
1042
|
+
copied += 1
|
|
1043
|
+
continue
|
|
1044
|
+
content = _resolve_placeholders(content, substitutions)
|
|
1045
|
+
if src_file.suffix == ".md":
|
|
1046
|
+
skip_file, content = _apply_doc_conditions(content, disabled_modules, active_stacks)
|
|
1047
|
+
if skip_file:
|
|
1048
|
+
continue
|
|
1049
|
+
dest.write_text(content, encoding="utf-8")
|
|
1050
|
+
shutil.copymode(src_file, dest)
|
|
1051
|
+
copied += 1
|
|
1052
|
+
|
|
1053
|
+
return copied
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _aggregate_scaffold_boundaries(
|
|
1057
|
+
project: Path,
|
|
1058
|
+
state: Path,
|
|
1059
|
+
templates: list[str],
|
|
1060
|
+
) -> None:
|
|
1061
|
+
"""Merge per-stack `scaffold-boundary.yaml` files into the consumer."""
|
|
1062
|
+
import yaml
|
|
1063
|
+
|
|
1064
|
+
stacks_data: list[dict] = []
|
|
1065
|
+
for stack_id in templates:
|
|
1066
|
+
boundary_src = TEMPLATES_DIR / stack_id / "scaffold-boundary.yaml"
|
|
1067
|
+
if not boundary_src.exists():
|
|
1068
|
+
continue
|
|
1069
|
+
try:
|
|
1070
|
+
data = yaml.safe_load(boundary_src.read_text(encoding="utf-8"))
|
|
1071
|
+
except yaml.YAMLError as exc:
|
|
1072
|
+
raise click.ClickException(
|
|
1073
|
+
f"src/templates/{stack_id}/scaffold-boundary.yaml is not valid YAML: {exc}"
|
|
1074
|
+
)
|
|
1075
|
+
if not isinstance(data, dict):
|
|
1076
|
+
continue
|
|
1077
|
+
stacks_data.append(
|
|
1078
|
+
{
|
|
1079
|
+
"stack": data.get("stack") or stack_id,
|
|
1080
|
+
"roots": list(data.get("roots") or []),
|
|
1081
|
+
"file_patterns": list(data.get("file_patterns") or []),
|
|
1082
|
+
"imports_from": list(data.get("imports_from") or []),
|
|
1083
|
+
"forbids_writing_in": list(data.get("forbids_writing_in") or []),
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
target = state / "scaffold-boundary.yaml"
|
|
1088
|
+
if not stacks_data:
|
|
1089
|
+
if target.exists():
|
|
1090
|
+
target.unlink()
|
|
1091
|
+
return
|
|
1092
|
+
|
|
1093
|
+
# Multi-backend relocation (project-anatomy.md): colliding declared roots
|
|
1094
|
+
# move each stack's boundary to src/services/<stack-id>/ BEFORE the
|
|
1095
|
+
# shared-root invariant — composed backends coexist by design.
|
|
1096
|
+
relocations = _service_relocations(tuple(templates))
|
|
1097
|
+
if relocations:
|
|
1098
|
+
registry = _get_stack_registry()
|
|
1099
|
+
for entry in stacks_data:
|
|
1100
|
+
new_root = relocations.get(entry["stack"])
|
|
1101
|
+
if not new_root or entry["stack"] not in registry.keys():
|
|
1102
|
+
continue
|
|
1103
|
+
declared = (registry[entry["stack"]].structure or {}).get("root", "").rstrip("/")
|
|
1104
|
+
if not declared:
|
|
1105
|
+
continue
|
|
1106
|
+
|
|
1107
|
+
def _remap(path: str) -> str:
|
|
1108
|
+
stripped = path.rstrip("/")
|
|
1109
|
+
if stripped == declared or stripped.startswith(declared + "/"):
|
|
1110
|
+
remapped = new_root + stripped[len(declared) :]
|
|
1111
|
+
return remapped + "/" if path.endswith("/") else remapped
|
|
1112
|
+
return path
|
|
1113
|
+
|
|
1114
|
+
entry["roots"] = [_remap(r) for r in entry["roots"]]
|
|
1115
|
+
entry["file_patterns"] = [_remap(p) for p in entry["file_patterns"]]
|
|
1116
|
+
|
|
1117
|
+
# Cross-service walls: each relocated root becomes forbidden to every
|
|
1118
|
+
# OTHER stack, so an unowned write into a sibling service is flagged
|
|
1119
|
+
# (project-anatomy.md § Glob/verify propagation — parameterized, never
|
|
1120
|
+
# hand-listed in any stack's scaffold-boundary.yaml).
|
|
1121
|
+
for entry in stacks_data:
|
|
1122
|
+
for other_id, other_root in relocations.items():
|
|
1123
|
+
wall = other_root.rstrip("/") + "/"
|
|
1124
|
+
if other_id != entry["stack"] and wall not in entry["forbids_writing_in"]:
|
|
1125
|
+
entry["forbids_writing_in"].append(wall)
|
|
1126
|
+
|
|
1127
|
+
# Invariant 1: no two installed stacks may share a root.
|
|
1128
|
+
seen: dict[str, str] = {}
|
|
1129
|
+
for entry in stacks_data:
|
|
1130
|
+
for root in entry["roots"]:
|
|
1131
|
+
existing = seen.get(root)
|
|
1132
|
+
if existing and existing != entry["stack"]:
|
|
1133
|
+
raise click.ClickException(
|
|
1134
|
+
f"scaffold-boundary aggregation: root '{root}' claimed by "
|
|
1135
|
+
f"both '{existing}' and '{entry['stack']}'. Two installed "
|
|
1136
|
+
f"stacks may not share a root — pick one per project."
|
|
1137
|
+
)
|
|
1138
|
+
seen[root] = entry["stack"]
|
|
1139
|
+
|
|
1140
|
+
# Invariant 2: every forbid references an installed root OR `shared/`.
|
|
1141
|
+
all_roots = {root.rstrip("/") for root in seen}
|
|
1142
|
+
all_roots.add("shared")
|
|
1143
|
+
for entry in stacks_data:
|
|
1144
|
+
for forbidden in entry["forbids_writing_in"]:
|
|
1145
|
+
stripped = forbidden.rstrip("/")
|
|
1146
|
+
if stripped not in all_roots:
|
|
1147
|
+
# Soft: mention but do not fail — a stack may legitimately
|
|
1148
|
+
# forbid a subtree no installed stack owns yet.
|
|
1149
|
+
click.echo(
|
|
1150
|
+
f" WARN: stack '{entry['stack']}' forbids writes in "
|
|
1151
|
+
f"'{forbidden}', but no installed stack owns that root.",
|
|
1152
|
+
err=True,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
aggregated = {
|
|
1156
|
+
"version": 1,
|
|
1157
|
+
"generated_by": "src/cli/_aggregate_scaffold_boundaries",
|
|
1158
|
+
"stacks": stacks_data,
|
|
1159
|
+
}
|
|
1160
|
+
target.write_text(
|
|
1161
|
+
yaml.safe_dump(aggregated, sort_keys=False, default_flow_style=False),
|
|
1162
|
+
encoding="utf-8",
|
|
1163
|
+
)
|
|
1164
|
+
click.echo(
|
|
1165
|
+
f" Aggregated scaffold-boundary for {len(stacks_data)} stack(s) → {target.relative_to(project)}"
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _copy_workflow_docs(project: Path) -> None:
|
|
1170
|
+
"""Copy thinking_os-final-edition.md from src/core/docs/ into project workflow/.
|
|
1171
|
+
|
|
1172
|
+
The full thinking_os reference is too large (57KB, 1439 lines) to duplicate
|
|
1173
|
+
in the scaffold dir. Instead, we copy it from src/core/docs/ at init time.
|
|
1174
|
+
"""
|
|
1175
|
+
src = CORE_DIR / "docs" / "thinking_os-final-edition.md"
|
|
1176
|
+
if not src.exists():
|
|
1177
|
+
return
|
|
1178
|
+
dest = project / "docs" / "workflow" / "thinking_os-final-edition.md"
|
|
1179
|
+
if dest.exists():
|
|
1180
|
+
return
|
|
1181
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1182
|
+
shutil.copy2(src, dest)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _bootstrap_hub_dir_if_first_run() -> None:
|
|
1186
|
+
"""Seed ~/.coding-os/ the very first time the CLI is invoked."""
|
|
1187
|
+
import os as _os
|
|
1188
|
+
|
|
1189
|
+
override = _os.environ.get("COS_REGISTRY_PATH")
|
|
1190
|
+
hub_dir = Path(override).parent if override else Path.home() / ".coding-os"
|
|
1191
|
+
try:
|
|
1192
|
+
if not hub_dir.exists():
|
|
1193
|
+
hub_dir.mkdir(parents=True, exist_ok=True)
|
|
1194
|
+
registry_file = hub_dir / "registry.json"
|
|
1195
|
+
if not registry_file.exists():
|
|
1196
|
+
# Same shape save_registry() writes — keep in sync with
|
|
1197
|
+
# cli.registry.Registry.to_dict().
|
|
1198
|
+
registry_file.write_text(
|
|
1199
|
+
'{\n "version": 1,\n "projects": []\n}\n',
|
|
1200
|
+
encoding="utf-8",
|
|
1201
|
+
)
|
|
1202
|
+
except OSError as exc:
|
|
1203
|
+
import logging as _logging
|
|
1204
|
+
|
|
1205
|
+
_logging.getLogger("cli.main").debug("hub-dir bootstrap skipped: %s", exc)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
from importlib.metadata import (
|
|
1209
|
+
PackageNotFoundError as _PackageNotFoundError,
|
|
1210
|
+
version as _pkg_version,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _resolve_cli_version() -> str:
|
|
1215
|
+
try:
|
|
1216
|
+
return _pkg_version("coding-os")
|
|
1217
|
+
except _PackageNotFoundError:
|
|
1218
|
+
return "unknown"
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _warn_dangling_agent_links() -> None:
|
|
1222
|
+
"""One stderr nudge when the cwd project's agent symlinks dangle (moved meta-repo).
|
|
1223
|
+
|
|
1224
|
+
Fail-open by contract: the probe must never break or slow a command —
|
|
1225
|
+
hub-architecture.md § Symlink health is the spec for this passive layer.
|
|
1226
|
+
"""
|
|
1227
|
+
try:
|
|
1228
|
+
project = Path.cwd()
|
|
1229
|
+
if not (project / CONFIG_FILE).exists():
|
|
1230
|
+
return
|
|
1231
|
+
from cli.sync_all import _dangling, _iter_symlinks
|
|
1232
|
+
|
|
1233
|
+
for link in _iter_symlinks(project):
|
|
1234
|
+
if _dangling(link):
|
|
1235
|
+
click.echo(
|
|
1236
|
+
"WARN: dangling coding-os symlinks detected (meta-repo moved or removed?) "
|
|
1237
|
+
"— run: cos sync-doctor --repair",
|
|
1238
|
+
err=True,
|
|
1239
|
+
)
|
|
1240
|
+
return
|
|
1241
|
+
except Exception as exc:
|
|
1242
|
+
import logging
|
|
1243
|
+
|
|
1244
|
+
logging.getLogger(__name__).debug("dangling-link probe skipped: %s", exc)
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
@click.group()
|
|
1248
|
+
@click.version_option(version=_resolve_cli_version(), prog_name="coding-os")
|
|
1249
|
+
def cli() -> None:
|
|
1250
|
+
"""Coding OS — the cognitive operating system that gives AI agents memory, structure, and discipline."""
|
|
1251
|
+
_warn_dangling_agent_links()
|
|
1252
|
+
# Route every stdlib logger.error from doctor /
|
|
1253
|
+
# health / any cos command into logging_os so the CLI process is no longer
|
|
1254
|
+
# blind to its own failures. Idempotent — install_bridge() removes a prior
|
|
1255
|
+
# bridge handler before adding. See docs/engineering/observability-eye.md §1.
|
|
1256
|
+
try:
|
|
1257
|
+
from core.logging_os import setup as _logging_os_setup
|
|
1258
|
+
|
|
1259
|
+
_logging_os_setup(level="info")
|
|
1260
|
+
except Exception as _bridge_exc: # pragma: no cover — never block the CLI on logging setup
|
|
1261
|
+
import logging as _logging
|
|
1262
|
+
|
|
1263
|
+
_logging.getLogger("coding_os.cli").debug("logging_os bridge unavailable: %s", _bridge_exc)
|
|
1264
|
+
|
|
1265
|
+
_bootstrap_hub_dir_if_first_run()
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
cli.add_command(doctor_cmd)
|
|
1269
|
+
cli.add_command(list_stacks_cmd)
|
|
1270
|
+
cli.add_command(list_adapters_cmd)
|
|
1271
|
+
cli.add_command(add_stack_cmd)
|
|
1272
|
+
cli.add_command(remove_stack_cmd)
|
|
1273
|
+
cli.add_command(docs_index_cmd)
|
|
1274
|
+
cli.add_command(task_sync_cmd)
|
|
1275
|
+
cli.add_command(reindex_cmd)
|
|
1276
|
+
cli.add_command(brain_decay_cmd)
|
|
1277
|
+
cli.add_command(brain_gc_cmd)
|
|
1278
|
+
cli.add_command(brain_sweep_changelog_cmd)
|
|
1279
|
+
cli.add_command(update_cmd)
|
|
1280
|
+
cli.add_command(setup_cmd)
|
|
1281
|
+
cli.add_command(materialize_file_cmd)
|
|
1282
|
+
cli.add_command(tail_cmd)
|
|
1283
|
+
cli.add_command(skills_list_cmd)
|
|
1284
|
+
|
|
1285
|
+
from cli.module_commands import module_group as module_group_cmd # noqa: E402
|
|
1286
|
+
from cli.preset_commands import preset_group as preset_group_cmd # noqa: E402
|
|
1287
|
+
from cli.skill_commands import skill_group as skill_group_cmd # noqa: E402
|
|
1288
|
+
from cli.stack_lint import stack_lint as stack_lint_cmd # noqa: E402
|
|
1289
|
+
|
|
1290
|
+
cli.add_command(module_group_cmd)
|
|
1291
|
+
cli.add_command(preset_group_cmd)
|
|
1292
|
+
cli.add_command(skill_group_cmd)
|
|
1293
|
+
cli.add_command(stack_lint_cmd)
|
|
1294
|
+
|
|
1295
|
+
# Durable error/log query CLI (cos errors / cos logs).
|
|
1296
|
+
try:
|
|
1297
|
+
from cli.logs_commands import errors_cmd as _errors_cmd
|
|
1298
|
+
from cli.logs_commands import logs_cmd as _logs_cmd
|
|
1299
|
+
|
|
1300
|
+
cli.add_command(_logs_cmd)
|
|
1301
|
+
cli.add_command(_errors_cmd)
|
|
1302
|
+
except ImportError as _logs_cli_exc: # pragma: no cover — defensive
|
|
1303
|
+
import logging as _logging
|
|
1304
|
+
|
|
1305
|
+
_logging.getLogger("coding_os.cli").debug("logs CLI unavailable: %s", _logs_cli_exc)
|
|
1306
|
+
|
|
1307
|
+
# Doc lifecycle CLI (cos doc-new / doc-history / doc-lint).
|
|
1308
|
+
try:
|
|
1309
|
+
from cli.doc_commands import doc_history_cmd as _doc_history_cmd
|
|
1310
|
+
from cli.doc_commands import doc_lint_cmd as _doc_lint_cmd
|
|
1311
|
+
from cli.doc_commands import doc_new_cmd as _doc_new_cmd
|
|
1312
|
+
|
|
1313
|
+
cli.add_command(_doc_new_cmd)
|
|
1314
|
+
cli.add_command(_doc_history_cmd)
|
|
1315
|
+
cli.add_command(_doc_lint_cmd)
|
|
1316
|
+
except ImportError as _doc_cli_exc: # pragma: no cover — defensive
|
|
1317
|
+
import logging as _logging
|
|
1318
|
+
|
|
1319
|
+
_logging.getLogger("coding_os.cli").debug("doc CLI unavailable: %s", _doc_cli_exc)
|
|
1320
|
+
|
|
1321
|
+
# Fast scope-aware verification: `cos verify --since-edit`.
|
|
1322
|
+
try:
|
|
1323
|
+
from cli.verify_since_edit import verify_since_edit_cmd as _verify_cmd
|
|
1324
|
+
|
|
1325
|
+
cli.add_command(_verify_cmd)
|
|
1326
|
+
except ImportError as _verify_exc: # pragma: no cover — defensive
|
|
1327
|
+
import logging as _logging
|
|
1328
|
+
|
|
1329
|
+
_logging.getLogger("coding_os.cli").debug("verify CLI unavailable: %s", _verify_exc)
|
|
1330
|
+
|
|
1331
|
+
# Hub propagation: push meta-repo edits to every registered
|
|
1332
|
+
# project via symlink re-link + DB migration. Lives in src/cli/sync_all.py
|
|
1333
|
+
# so registry.py stays focused on the JSON CRUD.
|
|
1334
|
+
try:
|
|
1335
|
+
from cli.sync_all import sync_all_cmd, sync_doctor_cmd
|
|
1336
|
+
|
|
1337
|
+
cli.add_command(sync_all_cmd)
|
|
1338
|
+
cli.add_command(sync_doctor_cmd)
|
|
1339
|
+
except ImportError as _e:
|
|
1340
|
+
import logging as _logging
|
|
1341
|
+
|
|
1342
|
+
_logging.getLogger("cli.main").debug("sync_all unavailable: %s", _e)
|
|
1343
|
+
|
|
1344
|
+
# board_os CLI surface (16 commands).
|
|
1345
|
+
try:
|
|
1346
|
+
from cli.board_commands import BOARD_COMMANDS
|
|
1347
|
+
|
|
1348
|
+
for _bc in BOARD_COMMANDS:
|
|
1349
|
+
cli.add_command(_bc)
|
|
1350
|
+
except ImportError:
|
|
1351
|
+
pass # board_os optional — don't break `cos` if deps missing.
|
|
1352
|
+
|
|
1353
|
+
# pr-mode multi-agent git executor (cos pr open/submit/status/cleanup/preflight).
|
|
1354
|
+
try:
|
|
1355
|
+
from cli.pr_commands import pr_group as _pr_group
|
|
1356
|
+
|
|
1357
|
+
cli.add_command(_pr_group)
|
|
1358
|
+
except ImportError as _pr_cli_exc: # pragma: no cover — defensive
|
|
1359
|
+
import logging as _logging
|
|
1360
|
+
|
|
1361
|
+
_logging.getLogger("coding_os.cli").debug("pr CLI unavailable: %s", _pr_cli_exc)
|
|
1362
|
+
|
|
1363
|
+
# Scheduled jobs (CRON A/B).
|
|
1364
|
+
try:
|
|
1365
|
+
from cli.cron_commands import cron_cmd
|
|
1366
|
+
|
|
1367
|
+
cli.add_command(cron_cmd)
|
|
1368
|
+
except ImportError as _e:
|
|
1369
|
+
import logging as _logging
|
|
1370
|
+
|
|
1371
|
+
_logging.getLogger("cli.main").debug("cron CLI unavailable: %s", _e)
|
|
1372
|
+
|
|
1373
|
+
# cognition CLI (formula dispatches, persona selections, backtracks).
|
|
1374
|
+
try:
|
|
1375
|
+
from cli.cognition import COGNITION_COMMANDS
|
|
1376
|
+
|
|
1377
|
+
for _cc in COGNITION_COMMANDS:
|
|
1378
|
+
cli.add_command(_cc)
|
|
1379
|
+
except ImportError:
|
|
1380
|
+
pass # cognition optional — don't break `cos` if click missing.
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _resolve_project_dir(raw: str) -> Path:
|
|
1384
|
+
"""Resolve the `--project-dir` value to an absolute path.
|
|
1385
|
+
|
|
1386
|
+
Handles the `uv run --directory <coding-os>` invocation pattern
|
|
1387
|
+
correctly: when uv changes cwd to the coding-os repo before launching
|
|
1388
|
+
Python, a default `.` would resolve to coding-os itself, silently
|
|
1389
|
+
initializing the coding-os repo instead of the user's project.
|
|
1390
|
+
|
|
1391
|
+
Resolution order:
|
|
1392
|
+
1. If the raw value is NOT exactly "." (user passed an explicit path)
|
|
1393
|
+
→ resolve relative to the current Python cwd.
|
|
1394
|
+
2. Otherwise, prefer the shell's `$PWD` env var (uv and most shells
|
|
1395
|
+
preserve it — it's the original invocation directory).
|
|
1396
|
+
3. Fall back to `os.getcwd()` for non-uv invocations.
|
|
1397
|
+
|
|
1398
|
+
This is defensive — `Path(".").resolve()` alone is dangerous under
|
|
1399
|
+
`uv --directory` because uv rewrites cwd before Python starts.
|
|
1400
|
+
"""
|
|
1401
|
+
if raw != ".":
|
|
1402
|
+
return Path(raw).resolve()
|
|
1403
|
+
|
|
1404
|
+
shell_pwd = os.environ.get("PWD")
|
|
1405
|
+
if shell_pwd and Path(shell_pwd).is_dir():
|
|
1406
|
+
return Path(shell_pwd).resolve()
|
|
1407
|
+
return Path.cwd().resolve()
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def _refuse_coding_os_self_init(project: Path) -> None:
|
|
1411
|
+
"""Block init from running inside the coding-os repo itself.
|
|
1412
|
+
|
|
1413
|
+
The coding-os source tree already contains `AGENTS.md`, `Makefile`,
|
|
1414
|
+
`docs/`, `core/` etc — running `init` against it scatters scaffold
|
|
1415
|
+
files across the repo and can overwrite real development docs.
|
|
1416
|
+
Detect this by checking for the telltale `src/core/thinking_os/server.py`
|
|
1417
|
+
file and refuse.
|
|
1418
|
+
"""
|
|
1419
|
+
from cli._init_helpers import is_coding_os_source_tree
|
|
1420
|
+
|
|
1421
|
+
if is_coding_os_source_tree(project):
|
|
1422
|
+
click.echo(
|
|
1423
|
+
f"\nERROR: Refusing to init inside the coding-os repo itself ({project}).\n"
|
|
1424
|
+
f" This path contains src/core/thinking_os/server.py — it is the source tree.\n"
|
|
1425
|
+
f" Initializing here would scatter scaffold files into the repo.\n\n"
|
|
1426
|
+
f" Fix:\n"
|
|
1427
|
+
f" cd /path/to/your/actual-project\n"
|
|
1428
|
+
f" uv run --directory {project} python -m cli.main init \\\n"
|
|
1429
|
+
f' --agent claude --project-dir "$(pwd)"\n\n'
|
|
1430
|
+
f" Or use the alias:\n"
|
|
1431
|
+
f" alias cos-init='uv run --directory {project} python -m cli.main init'\n"
|
|
1432
|
+
f' cos-init --agent claude --project-dir "$(pwd)"\n',
|
|
1433
|
+
err=True,
|
|
1434
|
+
)
|
|
1435
|
+
sys.exit(1)
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
@cli.command()
|
|
1439
|
+
@click.option(
|
|
1440
|
+
"--agent",
|
|
1441
|
+
"-a",
|
|
1442
|
+
default=None,
|
|
1443
|
+
help="Agent adapter(s) to install, comma-separated (e.g. 'claude,codex'). Prompted if omitted (unless --yes).",
|
|
1444
|
+
)
|
|
1445
|
+
@click.option("--template", "-t", multiple=True, help="Stack template(s) to apply")
|
|
1446
|
+
@click.option(
|
|
1447
|
+
"--preset",
|
|
1448
|
+
"preset_id",
|
|
1449
|
+
default=None,
|
|
1450
|
+
help="Named stack composition from templates/_presets/ (mutually exclusive with --template). Discover with `cos list-stacks`.",
|
|
1451
|
+
)
|
|
1452
|
+
@click.option(
|
|
1453
|
+
"--dry-config",
|
|
1454
|
+
is_flag=True,
|
|
1455
|
+
default=False,
|
|
1456
|
+
help="Print the merged .coding-os config preview (swimlane union + conflicts) for the requested stacks/preset and exit without writing anything.",
|
|
1457
|
+
)
|
|
1458
|
+
@click.option(
|
|
1459
|
+
"--dry-run",
|
|
1460
|
+
is_flag=True,
|
|
1461
|
+
default=False,
|
|
1462
|
+
help="Preview the would-be scaffold tree (files + composed configs) for the requested stacks/preset and exit without writing anything.",
|
|
1463
|
+
)
|
|
1464
|
+
@click.option(
|
|
1465
|
+
"--skills",
|
|
1466
|
+
"extra_skills_csv",
|
|
1467
|
+
default=None,
|
|
1468
|
+
help="Extra core skills beyond the stacks' own, comma-separated (wizard parity). Validated against the skill registry.",
|
|
1469
|
+
)
|
|
1470
|
+
@click.option(
|
|
1471
|
+
"--summary",
|
|
1472
|
+
"project_summary",
|
|
1473
|
+
default=None,
|
|
1474
|
+
help="1-2 paragraph project description; seeds docs/_meta/project-description.md (wizard parity, TASK-364 intake).",
|
|
1475
|
+
)
|
|
1476
|
+
@click.option(
|
|
1477
|
+
"--project-dir",
|
|
1478
|
+
"-d",
|
|
1479
|
+
default=None,
|
|
1480
|
+
help="Parent directory for the project (default: shell cwd). Mutually exclusive with --debug.",
|
|
1481
|
+
)
|
|
1482
|
+
@click.option(
|
|
1483
|
+
"--name",
|
|
1484
|
+
"-n",
|
|
1485
|
+
default=None,
|
|
1486
|
+
help="Create a new directory with this name inside --project-dir (or cwd). Validated: ^[a-z0-9][a-z0-9._-]{0,63}$",
|
|
1487
|
+
)
|
|
1488
|
+
@click.option(
|
|
1489
|
+
"--debug",
|
|
1490
|
+
is_flag=True,
|
|
1491
|
+
default=False,
|
|
1492
|
+
help="Scaffold into <coding-os>/.build/debug/<name>/ (or 'the-script-output'). Requires running inside the coding-os repo.",
|
|
1493
|
+
)
|
|
1494
|
+
@click.option(
|
|
1495
|
+
"--git/--no-git",
|
|
1496
|
+
default=True,
|
|
1497
|
+
help="Run `git init` in the new project (default: --git). Skipped silently if target is nested in an existing git repo.",
|
|
1498
|
+
)
|
|
1499
|
+
@click.option(
|
|
1500
|
+
"--force",
|
|
1501
|
+
is_flag=True,
|
|
1502
|
+
default=False,
|
|
1503
|
+
help="Overwrite target directory if it already exists and is non-empty.",
|
|
1504
|
+
)
|
|
1505
|
+
@click.option(
|
|
1506
|
+
"--adopt",
|
|
1507
|
+
is_flag=True,
|
|
1508
|
+
default=False,
|
|
1509
|
+
help="Overlay onto an existing non-empty repo in place — never wipe user files (brownfield). Prefer `cos adopt`.",
|
|
1510
|
+
)
|
|
1511
|
+
@click.option(
|
|
1512
|
+
"--yes",
|
|
1513
|
+
"-y",
|
|
1514
|
+
is_flag=True,
|
|
1515
|
+
default=False,
|
|
1516
|
+
help="Non-interactive: use defaults for anything not passed via flags. Required in CI / non-TTY.",
|
|
1517
|
+
)
|
|
1518
|
+
@click.option(
|
|
1519
|
+
"--format",
|
|
1520
|
+
"output_format",
|
|
1521
|
+
type=click.Choice(["text", "json"]),
|
|
1522
|
+
default="text",
|
|
1523
|
+
help="Output format.",
|
|
1524
|
+
)
|
|
1525
|
+
@click.option(
|
|
1526
|
+
"--today",
|
|
1527
|
+
"today_override",
|
|
1528
|
+
default=None,
|
|
1529
|
+
help="ISO-8601 date to use for {{DATE}} substitutions (default: today). Deterministic fixture for golden tests.",
|
|
1530
|
+
)
|
|
1531
|
+
@click.option(
|
|
1532
|
+
"--no-register",
|
|
1533
|
+
is_flag=True,
|
|
1534
|
+
default=False,
|
|
1535
|
+
help="Skip writing this project to the global ~/.coding-os/registry.json. Used by sandbox fixtures (manifest-regen, golden tests) so disposable temp dirs don't pollute the hub registry.",
|
|
1536
|
+
)
|
|
1537
|
+
@click.option(
|
|
1538
|
+
"--index/--no-index",
|
|
1539
|
+
"do_index",
|
|
1540
|
+
default=True,
|
|
1541
|
+
help="Seed the doc-search index after scaffold (loads the embedding model, ~15s). --no-index skips it for fast / CI / fixture scaffolds — the index lives in the gitignored runtime DB, so golden captures never need it.",
|
|
1542
|
+
)
|
|
1543
|
+
@click.option(
|
|
1544
|
+
"--graph-index/--no-graph-index",
|
|
1545
|
+
"graph_index",
|
|
1546
|
+
default=False,
|
|
1547
|
+
help="Build the knowledge graph even under --no-index (AST walk, no embedding model). The Hub Composer passes this so a fast --no-index create still gets a populated Graph tab; default off keeps CI/fixture scaffolds (which pass --no-index) graph-free.",
|
|
1548
|
+
)
|
|
1549
|
+
@click.option(
|
|
1550
|
+
"--disable-module",
|
|
1551
|
+
"disable_module",
|
|
1552
|
+
multiple=True,
|
|
1553
|
+
help=_module_flag_help(),
|
|
1554
|
+
)
|
|
1555
|
+
@click.option(
|
|
1556
|
+
"--profile",
|
|
1557
|
+
"profile",
|
|
1558
|
+
default=None,
|
|
1559
|
+
help=_profile_flag_help(),
|
|
1560
|
+
)
|
|
1561
|
+
@click.option(
|
|
1562
|
+
"--enable-module",
|
|
1563
|
+
"enable_module",
|
|
1564
|
+
multiple=True,
|
|
1565
|
+
help=_enable_flag_help(),
|
|
1566
|
+
)
|
|
1567
|
+
def init(
|
|
1568
|
+
agent: str | None,
|
|
1569
|
+
template: tuple[str, ...],
|
|
1570
|
+
preset_id: str | None,
|
|
1571
|
+
dry_config: bool,
|
|
1572
|
+
dry_run: bool,
|
|
1573
|
+
extra_skills_csv: str | None,
|
|
1574
|
+
project_summary: str | None,
|
|
1575
|
+
project_dir: str | None,
|
|
1576
|
+
name: str | None,
|
|
1577
|
+
debug: bool,
|
|
1578
|
+
git: bool,
|
|
1579
|
+
force: bool,
|
|
1580
|
+
adopt: bool,
|
|
1581
|
+
yes: bool,
|
|
1582
|
+
output_format: str,
|
|
1583
|
+
today_override: str | None,
|
|
1584
|
+
no_register: bool,
|
|
1585
|
+
do_index: bool,
|
|
1586
|
+
graph_index: bool,
|
|
1587
|
+
disable_module: tuple[str, ...],
|
|
1588
|
+
profile: str | None,
|
|
1589
|
+
enable_module: tuple[str, ...],
|
|
1590
|
+
) -> None:
|
|
1591
|
+
"""Initialize coding-os in a project.
|
|
1592
|
+
|
|
1593
|
+
Interactive by default — prompts for missing agent/template/name when a
|
|
1594
|
+
TTY is attached. Pass --yes for fully non-interactive runs (CI) using
|
|
1595
|
+
whatever flags are provided plus sensible defaults.
|
|
1596
|
+
"""
|
|
1597
|
+
shell_cwd_raw = os.environ.get("PWD") or os.getcwd()
|
|
1598
|
+
shell_cwd = Path(shell_cwd_raw).resolve()
|
|
1599
|
+
|
|
1600
|
+
# --preset expands to its stack list before anything else touches
|
|
1601
|
+
# `template` (config-composition.md § Presets).
|
|
1602
|
+
active_preset = None
|
|
1603
|
+
if preset_id:
|
|
1604
|
+
if template:
|
|
1605
|
+
click.echo("ERROR: --preset and --template are mutually exclusive.", err=True)
|
|
1606
|
+
sys.exit(2)
|
|
1607
|
+
from cli.preset_registry import load_preset_registry
|
|
1608
|
+
|
|
1609
|
+
presets = load_preset_registry(
|
|
1610
|
+
TEMPLATES_DIR, known_stacks=set(_get_stack_registry().keys())
|
|
1611
|
+
)
|
|
1612
|
+
for warning in presets.warnings:
|
|
1613
|
+
click.echo(f" WARN: {warning}", err=True)
|
|
1614
|
+
if preset_id not in presets:
|
|
1615
|
+
click.echo(
|
|
1616
|
+
f"ERROR: preset '{preset_id}' not found — available: "
|
|
1617
|
+
f"{sorted(presets.keys()) or '(none)'}",
|
|
1618
|
+
err=True,
|
|
1619
|
+
)
|
|
1620
|
+
sys.exit(2)
|
|
1621
|
+
active_preset = presets[preset_id]
|
|
1622
|
+
template = active_preset.stacks
|
|
1623
|
+
click.echo(f"Preset '{preset_id}' → stacks: {', '.join(template)}")
|
|
1624
|
+
|
|
1625
|
+
# A --profile expands to a curated disabled-module set (subsystems.yaml::
|
|
1626
|
+
# profiles) MERGED with explicit --disable-module flags; omitted → the
|
|
1627
|
+
# registry default_profile. Resolved before validation so the union flows
|
|
1628
|
+
# through the same dependency-checked apply path (TASK-509).
|
|
1629
|
+
from cli.subsystems import load_profiles, resolve_profile
|
|
1630
|
+
|
|
1631
|
+
_chosen_profile = profile or load_profiles()[1]
|
|
1632
|
+
try:
|
|
1633
|
+
_profile_disabled = resolve_profile(_chosen_profile)
|
|
1634
|
+
except ValueError as exc:
|
|
1635
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
1636
|
+
sys.exit(2)
|
|
1637
|
+
_explicit_disable = tuple(disable_module)
|
|
1638
|
+
disable_module = tuple(_profile_disabled) + tuple(disable_module)
|
|
1639
|
+
|
|
1640
|
+
# Validate --disable-module BEFORE the dry-run/real split so the preview and
|
|
1641
|
+
# the real init reject the same ids (pass-3 review).
|
|
1642
|
+
disabled_modules = _validated_disabled_modules(disable_module)
|
|
1643
|
+
disabled_modules = _apply_enable_modules(disabled_modules, enable_module, _explicit_disable)
|
|
1644
|
+
|
|
1645
|
+
if dry_config:
|
|
1646
|
+
_dry_config_preview(template, output_format)
|
|
1647
|
+
return
|
|
1648
|
+
|
|
1649
|
+
if dry_run:
|
|
1650
|
+
_dry_run_preview(template, output_format, tuple(disabled_modules))
|
|
1651
|
+
return
|
|
1652
|
+
|
|
1653
|
+
# --skills validated up-front (fail fast, wizard parity: the wizard only
|
|
1654
|
+
# offers known core skills).
|
|
1655
|
+
extra_skills: list[str] = []
|
|
1656
|
+
if extra_skills_csv:
|
|
1657
|
+
from cli.skill_registry import load_skill_registry
|
|
1658
|
+
from cli.skills_list import CORE_SKILLS_DIR
|
|
1659
|
+
|
|
1660
|
+
known_skills = set(load_skill_registry(CORE_SKILLS_DIR).skills.keys())
|
|
1661
|
+
extra_skills = [s.strip() for s in extra_skills_csv.split(",") if s.strip()]
|
|
1662
|
+
unknown_skills = [s for s in extra_skills if s not in known_skills]
|
|
1663
|
+
if unknown_skills:
|
|
1664
|
+
click.echo(
|
|
1665
|
+
f"ERROR: unknown skill(s) {unknown_skills} — see `cos skills-list`.", err=True
|
|
1666
|
+
)
|
|
1667
|
+
sys.exit(2)
|
|
1668
|
+
|
|
1669
|
+
# Idempotent detection: existing install → offer sync instead of re-init.
|
|
1670
|
+
existing = _detect_existing_install(shell_cwd) if not name and not project_dir else None
|
|
1671
|
+
if existing is not None:
|
|
1672
|
+
if output_format == "text":
|
|
1673
|
+
click.echo(
|
|
1674
|
+
f"Existing coding-os install detected at {shell_cwd}\n"
|
|
1675
|
+
f" agents: {', '.join(existing['agents']) or '(none)'}\n"
|
|
1676
|
+
f" templates: {', '.join(existing['templates']) or '(none)'}"
|
|
1677
|
+
)
|
|
1678
|
+
if yes or click.confirm("Sync missing components (links + config)?", default=True):
|
|
1679
|
+
_sync_missing(shell_cwd, output_format=output_format)
|
|
1680
|
+
return
|
|
1681
|
+
click.echo("Aborted.")
|
|
1682
|
+
sys.exit(0)
|
|
1683
|
+
|
|
1684
|
+
# Non-TTY without --yes: refuse to guess targets silently (TASK-359).
|
|
1685
|
+
# Sits AFTER existing-install detection so the idempotent sync path keeps
|
|
1686
|
+
# working for a bare re-`cos init` inside a project.
|
|
1687
|
+
if not yes and not sys.stdin.isatty():
|
|
1688
|
+
if agent is None:
|
|
1689
|
+
click.echo(
|
|
1690
|
+
"ERROR: non-interactive shell — pass --agent (and --name/--project-dir), "
|
|
1691
|
+
"or use --yes.",
|
|
1692
|
+
err=True,
|
|
1693
|
+
)
|
|
1694
|
+
sys.exit(2)
|
|
1695
|
+
if name is None and project_dir is None and not debug:
|
|
1696
|
+
click.echo(
|
|
1697
|
+
"ERROR: non-interactive shell — pass --name and/or --project-dir "
|
|
1698
|
+
"(or --yes to scaffold into the current directory).",
|
|
1699
|
+
err=True,
|
|
1700
|
+
)
|
|
1701
|
+
sys.exit(2)
|
|
1702
|
+
|
|
1703
|
+
# Prompt for missing inputs. --yes disables all prompting. Prompts that
|
|
1704
|
+
# hit EOF (closed stdin — CI, scaffold tests) fall back to sensible
|
|
1705
|
+
# defaults instead of aborting, so flag-based test helpers that don't
|
|
1706
|
+
# provide stdin keep working.
|
|
1707
|
+
def _safe_prompt(prompt_fn, fallback):
|
|
1708
|
+
try:
|
|
1709
|
+
return prompt_fn()
|
|
1710
|
+
except (click.exceptions.Abort, click.exceptions.UsageError, EOFError):
|
|
1711
|
+
return fallback
|
|
1712
|
+
|
|
1713
|
+
# Parse --agent: accepts comma-separated values (e.g. "claude,codex").
|
|
1714
|
+
agents: list[str] | None = None
|
|
1715
|
+
if agent is not None:
|
|
1716
|
+
agents = _parse_agents(agent)
|
|
1717
|
+
if agents is None:
|
|
1718
|
+
if yes:
|
|
1719
|
+
click.echo("ERROR: --agent is required with --yes.", err=True)
|
|
1720
|
+
sys.exit(2)
|
|
1721
|
+
agents = _safe_prompt(_prompt_agents, None)
|
|
1722
|
+
if agents is None:
|
|
1723
|
+
click.echo("ERROR: --agent is required (no input available).", err=True)
|
|
1724
|
+
sys.exit(2)
|
|
1725
|
+
if not template and not yes:
|
|
1726
|
+
template = _safe_prompt(_prompt_templates, ())
|
|
1727
|
+
if name is None and project_dir is None and not yes:
|
|
1728
|
+
name, project_dir = _safe_prompt(
|
|
1729
|
+
lambda: _prompt_name_and_location(shell_cwd),
|
|
1730
|
+
(None, None),
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
try:
|
|
1734
|
+
target = resolve_init_target(
|
|
1735
|
+
name=name,
|
|
1736
|
+
project_dir=project_dir,
|
|
1737
|
+
debug=debug,
|
|
1738
|
+
force=force,
|
|
1739
|
+
adopt=adopt,
|
|
1740
|
+
cwd=shell_cwd,
|
|
1741
|
+
# Refuse self-init BEFORE resolve_init_target's --force wipe runs.
|
|
1742
|
+
# Fires only when the target already exists and is non-empty.
|
|
1743
|
+
pre_wipe_hook=_refuse_coding_os_self_init,
|
|
1744
|
+
)
|
|
1745
|
+
except InitError as exc:
|
|
1746
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
1747
|
+
sys.exit(exc.exit_code)
|
|
1748
|
+
|
|
1749
|
+
# Also check the (possibly fresh, possibly empty) target path — catches
|
|
1750
|
+
# the case where self-init was called without --force.
|
|
1751
|
+
_refuse_coding_os_self_init(target.path)
|
|
1752
|
+
|
|
1753
|
+
project = target.path
|
|
1754
|
+
_refuse_coding_os_self_init(project)
|
|
1755
|
+
|
|
1756
|
+
# JSON mode keeps stdout pure for programmatic callers, but the scaffold's
|
|
1757
|
+
# progress echoes are what the Hub's job runner scrapes for phase markers —
|
|
1758
|
+
# buffering them into the void left the create progress bar on "validate".
|
|
1759
|
+
# Streaming them to stderr keeps stdout clean AND the progress bar live.
|
|
1760
|
+
_stdout_redirect = (
|
|
1761
|
+
contextlib.redirect_stdout(sys.stderr)
|
|
1762
|
+
if output_format == "json"
|
|
1763
|
+
else contextlib.nullcontext()
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
if output_format == "text":
|
|
1767
|
+
click.echo(f"Initializing coding-os in {project}")
|
|
1768
|
+
click.echo(f" Agents: {', '.join(agents)}")
|
|
1769
|
+
if template:
|
|
1770
|
+
click.echo(f" Templates: {', '.join(template)}")
|
|
1771
|
+
if debug:
|
|
1772
|
+
click.echo(" Mode: debug (under .build/debug/)")
|
|
1773
|
+
if target.forced_empty:
|
|
1774
|
+
click.echo(" Note: target existed and was wiped (--force)")
|
|
1775
|
+
|
|
1776
|
+
with _stdout_redirect:
|
|
1777
|
+
_run_scaffold_phase(
|
|
1778
|
+
agents,
|
|
1779
|
+
template,
|
|
1780
|
+
project,
|
|
1781
|
+
today=today_override,
|
|
1782
|
+
no_register=no_register,
|
|
1783
|
+
do_index=do_index,
|
|
1784
|
+
graph_index=graph_index,
|
|
1785
|
+
active_preset=active_preset,
|
|
1786
|
+
extra_skills=extra_skills,
|
|
1787
|
+
project_summary=project_summary,
|
|
1788
|
+
disabled_modules=disabled_modules,
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
git_result = maybe_git_init(target, enabled=git)
|
|
1792
|
+
# .gitignore whenever git is in play (fresh or nested repo) so the
|
|
1793
|
+
# mutating runtime DB never gets committed; the baseline commit runs
|
|
1794
|
+
# only when init created the repo — never sweep a parent project's tree.
|
|
1795
|
+
if git:
|
|
1796
|
+
ensure_gitignore(project)
|
|
1797
|
+
commit_result = maybe_initial_commit(target, enabled=git and git_result.ran)
|
|
1798
|
+
# Install the human-persona git hooks AFTER the baseline commit so that
|
|
1799
|
+
# tool-generated commit isn't gated by the freshly-installed pre-commit.
|
|
1800
|
+
hooks_result = install_consumer_git_hooks(project, enabled=git and git_result.ran)
|
|
1801
|
+
files_created = sum(1 for _ in project.rglob("*") if _.is_file())
|
|
1802
|
+
|
|
1803
|
+
summary: dict[str, object] = {
|
|
1804
|
+
"status": "ok",
|
|
1805
|
+
"path": str(project),
|
|
1806
|
+
"slug": _registered_slug(project),
|
|
1807
|
+
"agents": agents,
|
|
1808
|
+
"templates": list(template),
|
|
1809
|
+
"debug": debug,
|
|
1810
|
+
"forced_empty": target.forced_empty,
|
|
1811
|
+
"git": {
|
|
1812
|
+
"ran": git_result.ran,
|
|
1813
|
+
"skipped_reason": git_result.skipped_reason,
|
|
1814
|
+
"error": git_result.error,
|
|
1815
|
+
"initial_commit": commit_result.committed,
|
|
1816
|
+
"commit_error": commit_result.error,
|
|
1817
|
+
"hooks_installed": list(hooks_result.installed),
|
|
1818
|
+
"hooks_error": hooks_result.error,
|
|
1819
|
+
},
|
|
1820
|
+
"files_created": files_created,
|
|
1821
|
+
"db_path": str(project / STATE_DIR / "coding-os.db"),
|
|
1822
|
+
"config_file": str(project / CONFIG_FILE),
|
|
1823
|
+
"warnings": (
|
|
1824
|
+
["No stack template selected — AGENTS.md has placeholder routing. Run: cos add-stack"]
|
|
1825
|
+
if not template
|
|
1826
|
+
else []
|
|
1827
|
+
),
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if output_format == "json":
|
|
1831
|
+
click.echo(json.dumps(summary, indent=2))
|
|
1832
|
+
return
|
|
1833
|
+
|
|
1834
|
+
# text mode — final summary
|
|
1835
|
+
if git_result.ran:
|
|
1836
|
+
click.echo(" git: initialized")
|
|
1837
|
+
if commit_result.committed:
|
|
1838
|
+
click.echo(" git: baseline commit created")
|
|
1839
|
+
elif commit_result.error:
|
|
1840
|
+
click.echo(f" git: WARN baseline commit failed — {commit_result.error}")
|
|
1841
|
+
if hooks_result.installed:
|
|
1842
|
+
click.echo(f" git: hooks installed ({', '.join(hooks_result.installed)})")
|
|
1843
|
+
elif hooks_result.error:
|
|
1844
|
+
click.echo(f" git: WARN hooks not installed — {hooks_result.error}")
|
|
1845
|
+
elif git_result.skipped_reason:
|
|
1846
|
+
click.echo(f" git: skipped ({git_result.skipped_reason})")
|
|
1847
|
+
elif git_result.error:
|
|
1848
|
+
click.echo(f" git: WARN {git_result.error}")
|
|
1849
|
+
|
|
1850
|
+
click.echo("\ncoding-os initialized successfully!")
|
|
1851
|
+
click.echo(f" Path: {project}")
|
|
1852
|
+
click.echo(f" Files: {files_created}")
|
|
1853
|
+
click.echo(f" Config: {CONFIG_FILE}")
|
|
1854
|
+
click.echo(f" State: {STATE_DIR}/")
|
|
1855
|
+
click.echo(" Makefile: make help")
|
|
1856
|
+
click.echo("\nQuick start:")
|
|
1857
|
+
click.echo(" cos daily # Project status + today's tasks")
|
|
1858
|
+
click.echo(" cos task-pick # See next recommended task")
|
|
1859
|
+
click.echo(" cos task-start TASK-001 # Start working")
|
|
1860
|
+
|
|
1861
|
+
if not template:
|
|
1862
|
+
available = sorted(_get_stack_registry().keys())
|
|
1863
|
+
click.echo(
|
|
1864
|
+
"\n WARN: No stack template selected.\n"
|
|
1865
|
+
" AGENTS.md has placeholder routing — agent works but lacks domain rules,\n"
|
|
1866
|
+
" verify commands, and engineering guidelines.\n"
|
|
1867
|
+
f" Add a stack now: cos add-stack <id>\n"
|
|
1868
|
+
f" Available stacks: {', '.join(available)}"
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
import platform as _platform
|
|
1872
|
+
|
|
1873
|
+
if _platform.system() == "Darwin":
|
|
1874
|
+
click.echo("\nNightly maintenance (optional):")
|
|
1875
|
+
click.echo(" cos cron install # launchd job — decay, learn, routing (daily 03:00)")
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
_STACK_MARKER_LANGUAGES: dict[str, str] = {
|
|
1879
|
+
# Build-manifest marker → base language. Resolved to that language's plain
|
|
1880
|
+
# stack via the registry below, so no stack id is ever hardcoded (Rule 11).
|
|
1881
|
+
"pyproject.toml": "python",
|
|
1882
|
+
"setup.py": "python",
|
|
1883
|
+
"requirements.txt": "python",
|
|
1884
|
+
"package.json": "typescript",
|
|
1885
|
+
"go.mod": "go",
|
|
1886
|
+
"Cargo.toml": "rust",
|
|
1887
|
+
"Gemfile": "ruby",
|
|
1888
|
+
"composer.json": "php",
|
|
1889
|
+
"pom.xml": "java",
|
|
1890
|
+
"build.gradle": "java",
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _detect_stacks_from_markers(path: Path) -> list[str]:
|
|
1895
|
+
"""Resolve build-manifest markers in `path` to plain-stack ids via the
|
|
1896
|
+
registry — the brownfield-adopt stack proposal (no anatomy relocation)."""
|
|
1897
|
+
from cli.stack_registry import plain_stack_by_language
|
|
1898
|
+
|
|
1899
|
+
registry = _get_stack_registry()
|
|
1900
|
+
profiles = {sid: registry[sid] for sid in registry.keys()}
|
|
1901
|
+
language_to_plain = plain_stack_by_language(profiles)
|
|
1902
|
+
detected: list[str] = []
|
|
1903
|
+
for marker, language in _STACK_MARKER_LANGUAGES.items():
|
|
1904
|
+
if not (path / marker).exists():
|
|
1905
|
+
continue
|
|
1906
|
+
stack = language_to_plain.get(language)
|
|
1907
|
+
if stack and stack not in detected:
|
|
1908
|
+
detected.append(stack)
|
|
1909
|
+
return detected
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
@cli.command()
|
|
1913
|
+
@click.option(
|
|
1914
|
+
"--agent",
|
|
1915
|
+
"-a",
|
|
1916
|
+
default=None,
|
|
1917
|
+
help="Agent adapter(s) to install, comma-separated (required with --yes).",
|
|
1918
|
+
)
|
|
1919
|
+
@click.option(
|
|
1920
|
+
"--template",
|
|
1921
|
+
"-t",
|
|
1922
|
+
multiple=True,
|
|
1923
|
+
help="Stack template(s) to record — overrides build-marker auto-detection.",
|
|
1924
|
+
)
|
|
1925
|
+
@click.option(
|
|
1926
|
+
"--yes",
|
|
1927
|
+
"-y",
|
|
1928
|
+
is_flag=True,
|
|
1929
|
+
default=False,
|
|
1930
|
+
help="Non-interactive: adopt into the current directory using flags + defaults.",
|
|
1931
|
+
)
|
|
1932
|
+
@click.option("--git/--no-git", default=True, help="Run `git init` if the repo isn't already one.")
|
|
1933
|
+
@click.option(
|
|
1934
|
+
"--format",
|
|
1935
|
+
"output_format",
|
|
1936
|
+
type=click.Choice(["text", "json"]),
|
|
1937
|
+
default="text",
|
|
1938
|
+
help="Output format.",
|
|
1939
|
+
)
|
|
1940
|
+
@click.option(
|
|
1941
|
+
"--no-register",
|
|
1942
|
+
is_flag=True,
|
|
1943
|
+
default=False,
|
|
1944
|
+
help="Skip writing this project to the global registry (sandbox fixtures).",
|
|
1945
|
+
)
|
|
1946
|
+
@click.option(
|
|
1947
|
+
"--index/--no-index",
|
|
1948
|
+
"do_index",
|
|
1949
|
+
default=True,
|
|
1950
|
+
help="Seed the doc-search index after adopt (--no-index for fast / CI runs).",
|
|
1951
|
+
)
|
|
1952
|
+
@click.option(
|
|
1953
|
+
"--disable-module",
|
|
1954
|
+
"disable_module",
|
|
1955
|
+
multiple=True,
|
|
1956
|
+
help=_module_flag_help(),
|
|
1957
|
+
)
|
|
1958
|
+
@click.option(
|
|
1959
|
+
"--profile",
|
|
1960
|
+
"profile",
|
|
1961
|
+
default=None,
|
|
1962
|
+
help=_profile_flag_help(),
|
|
1963
|
+
)
|
|
1964
|
+
@click.option(
|
|
1965
|
+
"--enable-module",
|
|
1966
|
+
"enable_module",
|
|
1967
|
+
multiple=True,
|
|
1968
|
+
help=_enable_flag_help(),
|
|
1969
|
+
)
|
|
1970
|
+
@click.pass_context
|
|
1971
|
+
def adopt(
|
|
1972
|
+
ctx: click.Context,
|
|
1973
|
+
agent: str | None,
|
|
1974
|
+
template: tuple[str, ...],
|
|
1975
|
+
yes: bool,
|
|
1976
|
+
git: bool,
|
|
1977
|
+
output_format: str,
|
|
1978
|
+
no_register: bool,
|
|
1979
|
+
do_index: bool,
|
|
1980
|
+
disable_module: tuple[str, ...],
|
|
1981
|
+
profile: str | None,
|
|
1982
|
+
enable_module: tuple[str, ...],
|
|
1983
|
+
) -> None:
|
|
1984
|
+
"""Overlay coding-os onto an existing repo without touching user code.
|
|
1985
|
+
|
|
1986
|
+
Adds .coding-os/ state, adapter dirs and AGENTS.md in place, auto-detecting
|
|
1987
|
+
stacks from build markers (pyproject.toml / package.json / go.mod / …). An
|
|
1988
|
+
already-adopted repo pivots to the idempotent sync path instead of re-installing.
|
|
1989
|
+
"""
|
|
1990
|
+
target = Path(os.environ.get("PWD") or os.getcwd()).resolve()
|
|
1991
|
+
|
|
1992
|
+
# Already adopted → idempotent sync (same path as a bare re-`cos init`).
|
|
1993
|
+
if _detect_existing_install(target) is not None:
|
|
1994
|
+
if output_format == "text":
|
|
1995
|
+
click.echo(f"coding-os already present at {target} — syncing missing components.")
|
|
1996
|
+
_sync_missing(target, output_format=output_format)
|
|
1997
|
+
return
|
|
1998
|
+
|
|
1999
|
+
# Propose stacks from build markers unless the caller pinned --template.
|
|
2000
|
+
if not template:
|
|
2001
|
+
detected = _detect_stacks_from_markers(target)
|
|
2002
|
+
if detected and output_format == "text":
|
|
2003
|
+
click.echo(f"Detected stacks: {', '.join(detected)}")
|
|
2004
|
+
template = tuple(detected)
|
|
2005
|
+
|
|
2006
|
+
# Reuse the init scaffold in place — name/project_dir unset ⇒ current dir,
|
|
2007
|
+
# force unset ⇒ no wipe, so pre-existing user files are never overwritten.
|
|
2008
|
+
ctx.invoke(
|
|
2009
|
+
init,
|
|
2010
|
+
agent=agent,
|
|
2011
|
+
template=template,
|
|
2012
|
+
adopt=True,
|
|
2013
|
+
yes=yes,
|
|
2014
|
+
git=git,
|
|
2015
|
+
output_format=output_format,
|
|
2016
|
+
no_register=no_register,
|
|
2017
|
+
do_index=do_index,
|
|
2018
|
+
disable_module=disable_module,
|
|
2019
|
+
profile=profile,
|
|
2020
|
+
enable_module=enable_module,
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
def _run_scaffold_phase(
|
|
2025
|
+
agents: list[str],
|
|
2026
|
+
template: tuple[str, ...],
|
|
2027
|
+
project: Path,
|
|
2028
|
+
*,
|
|
2029
|
+
today: str | None = None,
|
|
2030
|
+
no_register: bool = False,
|
|
2031
|
+
do_index: bool = True,
|
|
2032
|
+
graph_index: bool = False,
|
|
2033
|
+
active_preset=None,
|
|
2034
|
+
extra_skills: list[str] | None = None,
|
|
2035
|
+
project_summary: str | None = None,
|
|
2036
|
+
disabled_modules: list[str] | None = None,
|
|
2037
|
+
) -> None:
|
|
2038
|
+
"""Original scaffolding body — extracted so it can be redirected in JSON mode.
|
|
2039
|
+
|
|
2040
|
+
`today` is an optional ISO-8601 override for {{DATE}} substitution
|
|
2041
|
+
in scaffolded files (used by golden parity tests for determinism).
|
|
2042
|
+
|
|
2043
|
+
`no_register` skips the global registry write (step 12). Sandbox
|
|
2044
|
+
fixtures (manifest-regen, golden parity tests) pass it so disposable
|
|
2045
|
+
temp dirs don't pollute ~/.coding-os/registry.json.
|
|
2046
|
+
"""
|
|
2047
|
+
|
|
2048
|
+
# 1. Create state directory
|
|
2049
|
+
state = project / STATE_DIR
|
|
2050
|
+
state.mkdir(parents=True, exist_ok=True)
|
|
2051
|
+
click.echo(f" Created {STATE_DIR}/")
|
|
2052
|
+
stamp_core_version(state)
|
|
2053
|
+
# First-edit grace marker (TASK-372): lets the agent's first legitimate code
|
|
2054
|
+
# edit in a brand-new project skip the doc-anchor BLOCK. enforce-doc-anchor.sh
|
|
2055
|
+
# consumes it on that first edit, so the grace is exactly one edit, bounded.
|
|
2056
|
+
(state / ".fresh-init").touch()
|
|
2057
|
+
|
|
2058
|
+
# 2. Initialize DB directory
|
|
2059
|
+
db_path = state / "coding-os.db"
|
|
2060
|
+
if not db_path.exists():
|
|
2061
|
+
# Initialize the database
|
|
2062
|
+
brain_dir = str(CORE_DIR / "thinking_os")
|
|
2063
|
+
init_code = (
|
|
2064
|
+
"import sys; "
|
|
2065
|
+
f"sys.path.insert(0, {brain_dir!r}); "
|
|
2066
|
+
"from database import init_db; "
|
|
2067
|
+
f"init_db({str(db_path)!r})"
|
|
2068
|
+
)
|
|
2069
|
+
env = os.environ.copy()
|
|
2070
|
+
env["COS_DB_PATH"] = str(db_path)
|
|
2071
|
+
proc = subprocess.run(
|
|
2072
|
+
[sys.executable, "-c", init_code],
|
|
2073
|
+
env=env,
|
|
2074
|
+
capture_output=True,
|
|
2075
|
+
text=True,
|
|
2076
|
+
)
|
|
2077
|
+
if proc.returncode != 0:
|
|
2078
|
+
click.echo(" ERROR: failed to initialize thinking_os database", err=True)
|
|
2079
|
+
if proc.stderr:
|
|
2080
|
+
click.echo(proc.stderr.strip(), err=True)
|
|
2081
|
+
click.echo(
|
|
2082
|
+
" HINT: missing Python deps are the usual cause — run "
|
|
2083
|
+
"`uv sync --extra rag` in the coding-os checkout, then re-run `cos init` "
|
|
2084
|
+
"(machine prerequisites: `cos doctor --bootstrap`)",
|
|
2085
|
+
err=True,
|
|
2086
|
+
)
|
|
2087
|
+
raise SystemExit(1)
|
|
2088
|
+
click.echo(" Initialized thinking_os database")
|
|
2089
|
+
|
|
2090
|
+
# 3. Generate config
|
|
2091
|
+
config = {
|
|
2092
|
+
"version": "1.0",
|
|
2093
|
+
"agents": agents,
|
|
2094
|
+
"templates": list(template),
|
|
2095
|
+
"state_dir": STATE_DIR,
|
|
2096
|
+
"code_extensions": ["py", "ts", "tsx", "js", "jsx"],
|
|
2097
|
+
"verify": {},
|
|
2098
|
+
"protected_files": [],
|
|
2099
|
+
}
|
|
2100
|
+
if active_preset is not None:
|
|
2101
|
+
# Provenance + pass-through for later layers: extra-skill linking is
|
|
2102
|
+
# TASK-370, module toggle behavior is TASK-349.
|
|
2103
|
+
config["preset"] = active_preset.id
|
|
2104
|
+
if active_preset.skills:
|
|
2105
|
+
config["extra_skills"] = list(active_preset.skills)
|
|
2106
|
+
if active_preset.modules:
|
|
2107
|
+
config["modules"] = dict(active_preset.modules)
|
|
2108
|
+
# CLI / wizard --disable-module entries merge on top of preset-declared
|
|
2109
|
+
# module state; the module_toggles pass below disables them in dependency
|
|
2110
|
+
# order (set_module_enabled refuses invalid chains, e.g. docs while tasks on).
|
|
2111
|
+
if disabled_modules:
|
|
2112
|
+
merged_modules = dict(config.get("modules") or {})
|
|
2113
|
+
for module_id in disabled_modules:
|
|
2114
|
+
merged_modules[module_id] = False
|
|
2115
|
+
config["modules"] = merged_modules
|
|
2116
|
+
if extra_skills:
|
|
2117
|
+
# --skills / wizard extras merge on top of preset-declared ones.
|
|
2118
|
+
config["extra_skills"] = list(
|
|
2119
|
+
dict.fromkeys([*(config.get("extra_skills") or []), *extra_skills])
|
|
2120
|
+
)
|
|
2121
|
+
_save_config(project, config)
|
|
2122
|
+
# Preset/wizard module toggles land in project state BEFORE the scaffold
|
|
2123
|
+
# copy so tag-driven docs composition sees them (TASK-360). Disable order:
|
|
2124
|
+
# dependents first (the registry refuses chains, e.g. docs before tasks).
|
|
2125
|
+
module_toggles = {k: v for k, v in (config.get("modules") or {}).items() if v is False}
|
|
2126
|
+
if module_toggles:
|
|
2127
|
+
from cli.subsystems import load_subsystems, set_module_enabled
|
|
2128
|
+
|
|
2129
|
+
registry_modules = load_subsystems()
|
|
2130
|
+
|
|
2131
|
+
def _dependents_being_disabled(module_id: str) -> int:
|
|
2132
|
+
# Dependents disable BEFORE their dependencies (the registry
|
|
2133
|
+
# refuses e.g. docs-off while tasks is still enabled).
|
|
2134
|
+
return sum(
|
|
2135
|
+
1
|
|
2136
|
+
for other in module_toggles
|
|
2137
|
+
if other in registry_modules and module_id in registry_modules[other].depends_on
|
|
2138
|
+
)
|
|
2139
|
+
|
|
2140
|
+
ordered = sorted(module_toggles, key=_dependents_being_disabled)
|
|
2141
|
+
for module_id in ordered:
|
|
2142
|
+
toggle = set_module_enabled(project, module_id, False)
|
|
2143
|
+
if not toggle.ok:
|
|
2144
|
+
click.echo(f" WARN: module '{module_id}': {toggle.reason}", err=True)
|
|
2145
|
+
else:
|
|
2146
|
+
click.echo(f" Module disabled per preset: {module_id}")
|
|
2147
|
+
# SI-1 (TASK-439): route init through the SAME runtime-allowlist path
|
|
2148
|
+
# as `cos module disable`. set_module_enabled alone only flips state;
|
|
2149
|
+
# without this, .coding-os/disabled-hook-scripts is never written at
|
|
2150
|
+
# init time and the disabled modules' hooks keep firing. AGENTS.md is
|
|
2151
|
+
# written fresh by the scaffold copy below, so only the allowlist needs
|
|
2152
|
+
# regenerating here (not the full toggle_and_regen).
|
|
2153
|
+
from cli.project_overrides import write_runtime_allowlist
|
|
2154
|
+
|
|
2155
|
+
allowlist = write_runtime_allowlist(project)
|
|
2156
|
+
click.echo(f" Runtime hook allowlist → {allowlist.relative_to(project)}")
|
|
2157
|
+
if project_summary and project_summary.strip():
|
|
2158
|
+
# Onboarding intake — consumed by the description→PRD pipeline (TASK-364).
|
|
2159
|
+
meta_dir = project / "docs" / "_meta"
|
|
2160
|
+
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
2161
|
+
(meta_dir / "project-description.md").write_text(
|
|
2162
|
+
"# Project Description (onboarding intake)\n\n" + project_summary.strip() + "\n",
|
|
2163
|
+
encoding="utf-8",
|
|
2164
|
+
)
|
|
2165
|
+
click.echo(" Seeded docs/_meta/project-description.md")
|
|
2166
|
+
# Docs-module gate: preset/wizard module toggles are stored in config
|
|
2167
|
+
# (behavior SSOT lands with TASK-349); docs defaults ON.
|
|
2168
|
+
if (config.get("modules") or {}).get("docs", True):
|
|
2169
|
+
from cli.setup import seed_prd_from_text
|
|
2170
|
+
|
|
2171
|
+
seeded = seed_prd_from_text(project, project_summary, date=today)
|
|
2172
|
+
if seeded:
|
|
2173
|
+
click.echo(f" Seeded {len(seeded)} PRD doc(s): {', '.join(seeded)}")
|
|
2174
|
+
click.echo(f" Generated {CONFIG_FILE}")
|
|
2175
|
+
|
|
2176
|
+
# 4. Run adapter install for each agent
|
|
2177
|
+
for agent in agents:
|
|
2178
|
+
click.echo(f"\nInstalling {agent} adapter...")
|
|
2179
|
+
_run_adapter_install(agent, project)
|
|
2180
|
+
|
|
2181
|
+
# 5. Apply templates (agent-agnostic content first)
|
|
2182
|
+
for t in template:
|
|
2183
|
+
click.echo(f"\nApplying template: {t}")
|
|
2184
|
+
# Pass first agent for path-scoped rules; additional agents get
|
|
2185
|
+
# rules via their own adapter install or add-adapter.
|
|
2186
|
+
_apply_template(t, project, agent=agents[0])
|
|
2187
|
+
|
|
2188
|
+
# 5b. Link stack-scoped skills into each agent's skills_dir.
|
|
2189
|
+
if template:
|
|
2190
|
+
for agent in agents:
|
|
2191
|
+
_link_stack_skills(agent, template, project)
|
|
2192
|
+
|
|
2193
|
+
# 5c. A module disabled above must shed its owned skills too (audit D2-1):
|
|
2194
|
+
# the adapter install (step 4) links every core skill, so init reaches the
|
|
2195
|
+
# same skill-parity `cos module disable` has at runtime by running the same
|
|
2196
|
+
# ref-counted cascade (a skill another enabled module owns is kept).
|
|
2197
|
+
if module_toggles:
|
|
2198
|
+
from cli.module_commands import cascade_module_commands
|
|
2199
|
+
from cli.skill_commands import cascade_module_skills
|
|
2200
|
+
|
|
2201
|
+
for module_id in module_toggles:
|
|
2202
|
+
try:
|
|
2203
|
+
cascade = cascade_module_skills(project, module_id, enabled=False)
|
|
2204
|
+
except Exception as exc: # noqa: BLE001 — best-effort; `cos doctor` reconciles drift
|
|
2205
|
+
click.echo(f" WARN: skill cascade for '{module_id}' skipped ({exc})", err=True)
|
|
2206
|
+
else:
|
|
2207
|
+
if cascade["unlinked"]:
|
|
2208
|
+
click.echo(
|
|
2209
|
+
f" Skills unlinked ({module_id} off): {', '.join(cascade['unlinked'])}"
|
|
2210
|
+
)
|
|
2211
|
+
try:
|
|
2212
|
+
cmd_cascade = cascade_module_commands(project, module_id, enabled=False)
|
|
2213
|
+
except Exception as exc: # noqa: BLE001 — best-effort; `cos doctor` reconciles drift
|
|
2214
|
+
click.echo(f" WARN: command cascade for '{module_id}' skipped ({exc})", err=True)
|
|
2215
|
+
else:
|
|
2216
|
+
if cmd_cascade["unlinked"]:
|
|
2217
|
+
click.echo(
|
|
2218
|
+
f" Commands unlinked ({module_id} off): "
|
|
2219
|
+
f"{', '.join(cmd_cascade['unlinked'])}"
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
# 6. Aggregate base + stacks + adapter into a world.
|
|
2223
|
+
# Use the first agent for world building (substitutions, AGENTS.md).
|
|
2224
|
+
# All adapters share the same core content; adapter-specific setup
|
|
2225
|
+
# was handled in step 4.
|
|
2226
|
+
for w in _get_stack_registry().warnings:
|
|
2227
|
+
click.echo(f" WARN: {w}", err=True)
|
|
2228
|
+
world = _build_world(agents[0], template, project, today=today)
|
|
2229
|
+
for msg in world.conflicts:
|
|
2230
|
+
click.echo(f" WARN: {msg}", err=True)
|
|
2231
|
+
substitutions = world.substitutions
|
|
2232
|
+
if project_summary and project_summary.strip():
|
|
2233
|
+
# The user's own words replace the generic default everywhere the
|
|
2234
|
+
# {{PROJECT_DESCRIPTION}} placeholder appears (TASK-364).
|
|
2235
|
+
substitutions = {
|
|
2236
|
+
**substitutions,
|
|
2237
|
+
"PROJECT_DESCRIPTION": " ".join(project_summary.split()),
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
# 6b. Patch .coding-os.yaml.verify with derived commands.
|
|
2241
|
+
# step 3 wrote an empty dict because the world is only available here.
|
|
2242
|
+
# enforce-verify.sh reads this map to know which suite to require per
|
|
2243
|
+
# changed-file glob, so we must populate it before any hook runs.
|
|
2244
|
+
verify_map = _derive_verify_from_world(world)
|
|
2245
|
+
if verify_map:
|
|
2246
|
+
existing = _load_config(project) or {}
|
|
2247
|
+
existing["verify"] = verify_map
|
|
2248
|
+
_save_config(project, existing)
|
|
2249
|
+
click.echo(f" Populated verify config: {', '.join(sorted(verify_map))}")
|
|
2250
|
+
|
|
2251
|
+
# 7. Overlay scaffold files (_base + each template overlay) with placeholder resolution
|
|
2252
|
+
copied = _overlay_scaffold(project, template, substitutions)
|
|
2253
|
+
if copied:
|
|
2254
|
+
click.echo(f" Copied {copied} scaffold file(s) (docs/, governance/, playbooks/, ...)")
|
|
2255
|
+
|
|
2256
|
+
# 7b. Compose .coding-os/ configs (rag/scrumban/domain) from base + every
|
|
2257
|
+
# installed stack — deep-merged, multi-stack-correct. The overlay (step 7)
|
|
2258
|
+
# deliberately skips these. SSOT: docs/engineering/config-composition.md.
|
|
2259
|
+
config_conflicts: list[str] = []
|
|
2260
|
+
composed = compose_coding_os_configs(
|
|
2261
|
+
project, state, list(template), templates_dir=TEMPLATES_DIR, conflicts=config_conflicts
|
|
2262
|
+
)
|
|
2263
|
+
if composed:
|
|
2264
|
+
click.echo(f" Composed {len(composed)} .coding-os config(s): {', '.join(composed)}")
|
|
2265
|
+
for line in config_conflicts:
|
|
2266
|
+
click.echo(f" WARN: config conflict (later wins) — {line}", err=True)
|
|
2267
|
+
|
|
2268
|
+
# 8. Copy thinking_os reference doc from src/core/docs/
|
|
2269
|
+
_copy_workflow_docs(project)
|
|
2270
|
+
|
|
2271
|
+
# 9. Copy Makefile.base verbatim. The `cos` CLI binary (installed
|
|
2272
|
+
# via `uv tool install`) owns path discovery — Makefile.base calls
|
|
2273
|
+
# `cos docs-index`, `cos task-sync`, etc. and stays fully portable.
|
|
2274
|
+
makefile_src = TEMPLATES_DIR / "_base" / "Makefile.base"
|
|
2275
|
+
if makefile_src.exists():
|
|
2276
|
+
makefile_dest = state / "Makefile.base"
|
|
2277
|
+
shutil.copy2(makefile_src, makefile_dest)
|
|
2278
|
+
click.echo(f" Copied Makefile.base to {STATE_DIR}/")
|
|
2279
|
+
|
|
2280
|
+
# Materialize stack-contributed targets (lint-backend, test-backend-<id>,
|
|
2281
|
+
# …) into a generated include so the suites named in AGENTS.md are
|
|
2282
|
+
# runnable. Writes .coding-os/Makefile.stacks; the project Makefile (if
|
|
2283
|
+
# it already exists) gets the `-include` wired in idempotently.
|
|
2284
|
+
materialize_makefile_targets(project, state, world)
|
|
2285
|
+
|
|
2286
|
+
# Create a project Makefile if none exists
|
|
2287
|
+
project_makefile = project / "Makefile"
|
|
2288
|
+
if not project_makefile.exists():
|
|
2289
|
+
project_makefile.write_text(
|
|
2290
|
+
f"# Project Makefile\n"
|
|
2291
|
+
f"# coding-os universal targets\n"
|
|
2292
|
+
f"include {STATE_DIR}/Makefile.base\n"
|
|
2293
|
+
f"-include {STATE_DIR}/Makefile.stacks\n\n"
|
|
2294
|
+
f"# Add your project-specific targets below:\n\n"
|
|
2295
|
+
)
|
|
2296
|
+
click.echo(" Generated Makefile")
|
|
2297
|
+
|
|
2298
|
+
# CI workflow + backend Dockerfiles — gated behind the `cicd` module (off in
|
|
2299
|
+
# lean profiles), independent of the Makefile.base copy so init mirrors the
|
|
2300
|
+
# update.py materialize step. Both delegate to generated artifacts.
|
|
2301
|
+
from cli.subsystems import module_state
|
|
2302
|
+
|
|
2303
|
+
if module_state(project).get("cicd", True):
|
|
2304
|
+
if materialize_ci_workflow(project, world):
|
|
2305
|
+
click.echo(" Generated .github/workflows/ci.yml")
|
|
2306
|
+
if materialize_dockerfiles(project, world):
|
|
2307
|
+
click.echo(" Generated backend Dockerfile(s)")
|
|
2308
|
+
|
|
2309
|
+
# 9b. Aggregate scaffold-boundary.yaml from every installed stack so the
|
|
2310
|
+
# consumer-side enforce-scaffold-boundary.sh hook can enforce subtree
|
|
2311
|
+
# isolation at runtime. SSOT spec: docs/governance/scaffold-boundary-contract.md.
|
|
2312
|
+
_aggregate_scaffold_boundaries(project, state, template)
|
|
2313
|
+
|
|
2314
|
+
# 10. Generate AGENTS.md by composing fragments from base + stacks.
|
|
2315
|
+
# No template file is read; the content is assembled by render_agents_md()
|
|
2316
|
+
# from the fragments registered in base.yaml::agents_md_sections (and any
|
|
2317
|
+
# fragments stacks contribute via their own stack.yaml::agents_md_sections).
|
|
2318
|
+
if ensure_agents_md(project, world):
|
|
2319
|
+
click.echo(" Generated AGENTS.md")
|
|
2320
|
+
|
|
2321
|
+
# 11. Initial RAG indexing of the scaffolded docs so `cos_doc_search`
|
|
2322
|
+
# returns hits from the very first session. Without this, the
|
|
2323
|
+
# consumer's document_chunks table is empty until the user runs
|
|
2324
|
+
# `make docs-index` manually — Rule 19 (doc-sync) enforcement is
|
|
2325
|
+
# also effectively off until something hits the FTS index.
|
|
2326
|
+
# Skipped under --no-index: the index lives in the gitignored runtime DB,
|
|
2327
|
+
# so fast/CI/fixture scaffolds (e.g. golden capture) don't pay the
|
|
2328
|
+
# ~15s embedding-model load for output they discard.
|
|
2329
|
+
if do_index:
|
|
2330
|
+
_initial_doc_index(project, state)
|
|
2331
|
+
else:
|
|
2332
|
+
click.echo(" Skipped initial doc index (--no-index)")
|
|
2333
|
+
|
|
2334
|
+
# 11b. Seed the knowledge graph so the Hub Graph tab + cos_graph_* tools work
|
|
2335
|
+
# from the first session with NO manual `cos graph-reindex` (TASK-423). Built
|
|
2336
|
+
# when --index (the default) OR --graph-index is set — the latter lets a fast
|
|
2337
|
+
# --no-index create (the Hub Composer) still get a populated graph (AST walk,
|
|
2338
|
+
# no embedding model), while CI/fixture scaffolds that pass only --no-index
|
|
2339
|
+
# stay graph-free. Inside, it is also gated on the graph module being enabled
|
|
2340
|
+
# — a disabled graph module owns no tools, so building its graph is wasted.
|
|
2341
|
+
if do_index or graph_index:
|
|
2342
|
+
_initial_graph_index(project, state)
|
|
2343
|
+
else:
|
|
2344
|
+
click.echo(" Skipped initial graph index (--no-index)")
|
|
2345
|
+
|
|
2346
|
+
# 12. Register project in the global ~/.coding-os/registry.json so the
|
|
2347
|
+
# Hub web UI (`cos hub`) can enumerate it and serve its sqlite DB.
|
|
2348
|
+
# Skipped when --no-register passed (sandbox fixtures use disposable
|
|
2349
|
+
# temp dirs — registering them creates stale entries doctor then warns
|
|
2350
|
+
# about in hub.project_paths_exist).
|
|
2351
|
+
if no_register:
|
|
2352
|
+
click.echo(" Skipped hub registry write (--no-register)")
|
|
2353
|
+
else:
|
|
2354
|
+
try:
|
|
2355
|
+
from cli.registry import add_project as _registry_add_project
|
|
2356
|
+
|
|
2357
|
+
entry = _registry_add_project(project)
|
|
2358
|
+
click.echo(f" Registered in hub registry: {entry.slug}")
|
|
2359
|
+
except Exception as exc:
|
|
2360
|
+
# Registry is non-fatal — a failed write should not break init.
|
|
2361
|
+
click.echo(f" WARN: could not register project in hub registry: {exc}", err=True)
|
|
2362
|
+
click.echo(
|
|
2363
|
+
" HINT: register later with `cos registry add <project-path>` "
|
|
2364
|
+
"so the hub web UI can see this project",
|
|
2365
|
+
err=True,
|
|
2366
|
+
)
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
def _initial_doc_index(project: Path, state: Path) -> None:
|
|
2370
|
+
"""Seed document_chunks + FTS for a freshly-scaffolded project."""
|
|
2371
|
+
rag_config = state / "rag-config.yaml"
|
|
2372
|
+
if not rag_config.exists():
|
|
2373
|
+
return
|
|
2374
|
+
db_path = state / "coding-os.db"
|
|
2375
|
+
brain_dir = str(CORE_DIR / "thinking_os")
|
|
2376
|
+
code = (
|
|
2377
|
+
"import sys; "
|
|
2378
|
+
f"sys.path.insert(0, {brain_dir!r}); "
|
|
2379
|
+
"from database import init_db; "
|
|
2380
|
+
"from doc_indexer import index_docs; "
|
|
2381
|
+
"from pathlib import Path; "
|
|
2382
|
+
f"conn = init_db({str(db_path)!r}); "
|
|
2383
|
+
f"stats = index_docs(conn, Path({str(rag_config)!r}), Path({str(project)!r})); "
|
|
2384
|
+
"conn.close(); "
|
|
2385
|
+
"print(f\" Indexed {stats['updated_files']} doc(s), {stats['new_chunks']} chunk(s)\")"
|
|
2386
|
+
)
|
|
2387
|
+
env = os.environ.copy()
|
|
2388
|
+
env["COS_DB_PATH"] = str(db_path)
|
|
2389
|
+
result = subprocess.run(
|
|
2390
|
+
[sys.executable, "-c", code],
|
|
2391
|
+
env=env,
|
|
2392
|
+
capture_output=True,
|
|
2393
|
+
text=True,
|
|
2394
|
+
)
|
|
2395
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
2396
|
+
click.echo(result.stdout.rstrip())
|
|
2397
|
+
elif result.returncode != 0:
|
|
2398
|
+
# Non-fatal: missing yaml / embeddings extras shouldn't break init.
|
|
2399
|
+
click.echo(
|
|
2400
|
+
f" WARN: initial doc index skipped: {result.stderr.strip().splitlines()[-1] if result.stderr else 'unknown'}",
|
|
2401
|
+
err=True,
|
|
2402
|
+
)
|
|
2403
|
+
click.echo(
|
|
2404
|
+
" HINT: doc search stays empty until indexed — install extras with "
|
|
2405
|
+
"`uv sync --extra rag` in the coding-os checkout, then run `make docs-index` here",
|
|
2406
|
+
err=True,
|
|
2407
|
+
)
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def _initial_graph_index(project: Path, state: Path) -> None:
|
|
2411
|
+
"""Build the knowledge graph for a fresh project when the graph module is on (TASK-423)."""
|
|
2412
|
+
try:
|
|
2413
|
+
from cli.subsystems import module_state
|
|
2414
|
+
|
|
2415
|
+
if not module_state(project).get("graph", True):
|
|
2416
|
+
click.echo(" Skipped graph index (graph module disabled)")
|
|
2417
|
+
return
|
|
2418
|
+
except Exception:
|
|
2419
|
+
# State unreadable → graph is on by default; fall through and build.
|
|
2420
|
+
pass
|
|
2421
|
+
db_path = state / "coding-os.db"
|
|
2422
|
+
core_path = str(CORE_DIR)
|
|
2423
|
+
brain_dir = str(CORE_DIR / "thinking_os")
|
|
2424
|
+
# include_docs=False: the docs RAG layer was just seeded by
|
|
2425
|
+
# _initial_doc_index; here we want only the graph (AST + doc structure),
|
|
2426
|
+
# which needs no embedding model. Runs in-process python (sys.executable),
|
|
2427
|
+
# NOT the global `cos`, so an env without the graph deps fails fast instead
|
|
2428
|
+
# of doing heavy work (mirrors _initial_doc_index).
|
|
2429
|
+
code = (
|
|
2430
|
+
"import sys; "
|
|
2431
|
+
f"sys.path.insert(0, {core_path!r}); "
|
|
2432
|
+
f"sys.path.insert(0, {brain_dir!r}); "
|
|
2433
|
+
"from graph_os.ingest.base import walk_local; "
|
|
2434
|
+
"from graph_os.tools.reindex_dispatch import dispatch; "
|
|
2435
|
+
f"plan = walk_local({str(project)!r}); "
|
|
2436
|
+
"reports = [dispatch(str(f), project_root="
|
|
2437
|
+
f"{str(project)!r}, db_path={str(db_path)!r}, "
|
|
2438
|
+
"include_docs=False, link_stubs=True) for f in plan.files]; "
|
|
2439
|
+
"ok = sum(1 for r in reports if r.get('status') == 'ok'); "
|
|
2440
|
+
"print(f' Built knowledge graph: {ok}/{len(reports)} file(s) indexed')"
|
|
2441
|
+
)
|
|
2442
|
+
env = os.environ.copy()
|
|
2443
|
+
env["COS_DB_PATH"] = str(db_path)
|
|
2444
|
+
# Bounded so a very large repo never blows the init budget (the Hub Composer
|
|
2445
|
+
# wraps `cos init` in its own timeout). On timeout the graph is left empty —
|
|
2446
|
+
# valid, since cos_graph_export returns ok([]) for an empty graph — with a
|
|
2447
|
+
# clear repair HINT, far better than hard-failing a half-created project.
|
|
2448
|
+
timeout_s = int(os.environ.get("COS_INIT_GRAPH_TIMEOUT", "180"))
|
|
2449
|
+
try:
|
|
2450
|
+
result = subprocess.run(
|
|
2451
|
+
[sys.executable, "-c", code],
|
|
2452
|
+
env=env,
|
|
2453
|
+
capture_output=True,
|
|
2454
|
+
text=True,
|
|
2455
|
+
timeout=timeout_s,
|
|
2456
|
+
)
|
|
2457
|
+
except subprocess.TimeoutExpired:
|
|
2458
|
+
click.echo(
|
|
2459
|
+
f" WARN: initial graph index exceeded {timeout_s}s — graph left empty", err=True
|
|
2460
|
+
)
|
|
2461
|
+
click.echo(" HINT: graph stays empty until built — run `cos graph-reindex` here", err=True)
|
|
2462
|
+
return
|
|
2463
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
2464
|
+
click.echo(result.stdout.rstrip())
|
|
2465
|
+
elif result.returncode != 0:
|
|
2466
|
+
# Non-fatal: missing graph deps shouldn't break init.
|
|
2467
|
+
detail = result.stderr.strip().splitlines()[-1] if result.stderr.strip() else "unknown"
|
|
2468
|
+
click.echo(f" WARN: initial graph index skipped: {detail}", err=True)
|
|
2469
|
+
click.echo(
|
|
2470
|
+
" HINT: graph stays empty until built — run `cos graph-reindex` here",
|
|
2471
|
+
err=True,
|
|
2472
|
+
)
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
@cli.command("add-adapter")
|
|
2476
|
+
@click.argument("agent", type=click.Choice(VALID_AGENTS))
|
|
2477
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2478
|
+
def add_adapter(agent: str, project_dir: str) -> None:
|
|
2479
|
+
"""Add an additional agent adapter to the project."""
|
|
2480
|
+
project = _resolve_project_dir(project_dir)
|
|
2481
|
+
config = _load_config(project)
|
|
2482
|
+
|
|
2483
|
+
if not config:
|
|
2484
|
+
click.echo("ERROR: No .coding-os.yaml found. Run 'coding-os init' first.", err=True)
|
|
2485
|
+
sys.exit(1)
|
|
2486
|
+
|
|
2487
|
+
agents = config.get("agents", [])
|
|
2488
|
+
if agent in agents:
|
|
2489
|
+
click.echo(f"Adapter '{agent}' is already installed.")
|
|
2490
|
+
return
|
|
2491
|
+
|
|
2492
|
+
click.echo(f"Adding {agent} adapter...")
|
|
2493
|
+
_run_adapter_install(agent, project)
|
|
2494
|
+
|
|
2495
|
+
agents.append(agent)
|
|
2496
|
+
config["agents"] = agents
|
|
2497
|
+
_save_config(project, config)
|
|
2498
|
+
click.echo(f" Updated {CONFIG_FILE}")
|
|
2499
|
+
|
|
2500
|
+
# AGENTS.md is the canonical per-project instruction file (read by both
|
|
2501
|
+
# Claude and Codex). `cos init` generates it, but older projects or
|
|
2502
|
+
# partial installs may be missing it — fill the gap so the newly added
|
|
2503
|
+
# adapter has something to read on first session.
|
|
2504
|
+
templates = tuple(config.get("templates", []) or [])
|
|
2505
|
+
world = _build_world(agent, templates, project)
|
|
2506
|
+
if ensure_agents_md(project, world):
|
|
2507
|
+
click.echo(" Generated AGENTS.md")
|
|
2508
|
+
|
|
2509
|
+
|
|
2510
|
+
@cli.command("codex-mcp-install")
|
|
2511
|
+
@click.option(
|
|
2512
|
+
"--config",
|
|
2513
|
+
"config_path",
|
|
2514
|
+
default=None,
|
|
2515
|
+
help="Codex config file (default: ./.codex/config.toml)",
|
|
2516
|
+
)
|
|
2517
|
+
@click.option(
|
|
2518
|
+
"--global",
|
|
2519
|
+
"global_scope",
|
|
2520
|
+
is_flag=True,
|
|
2521
|
+
default=False,
|
|
2522
|
+
help="Write to ~/.codex/config.toml instead of the project-local .codex/config.toml",
|
|
2523
|
+
)
|
|
2524
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Print the snippet without writing")
|
|
2525
|
+
def codex_mcp_install(config_path: str | None, global_scope: bool, dry_run: bool) -> None:
|
|
2526
|
+
"""Register the coding-os MCP server in Codex config.
|
|
2527
|
+
|
|
2528
|
+
Codex CLI supports both user-level ~/.codex/config.toml and trusted
|
|
2529
|
+
project overrides in .codex/config.toml. This command defaults to the
|
|
2530
|
+
project-local config so coding-os MCP stays scoped to the current repo;
|
|
2531
|
+
pass `--global` only when you explicitly want the server available
|
|
2532
|
+
everywhere. Safe to re-run — it repairs or replaces the
|
|
2533
|
+
`[mcp_servers.coding-os]` section idempotently.
|
|
2534
|
+
|
|
2535
|
+
Uses append-based text edits (no TOML parser required) so it works on
|
|
2536
|
+
Python 3.10 and preserves any hand-authored comments in config.toml.
|
|
2537
|
+
"""
|
|
2538
|
+
if config_path and global_scope:
|
|
2539
|
+
raise click.ClickException("use either --config or --global, not both")
|
|
2540
|
+
|
|
2541
|
+
default_path = (
|
|
2542
|
+
Path.home() / ".codex" / "config.toml"
|
|
2543
|
+
if global_scope
|
|
2544
|
+
else Path.cwd() / ".codex" / "config.toml"
|
|
2545
|
+
)
|
|
2546
|
+
target = Path(config_path).expanduser().resolve() if config_path else default_path
|
|
2547
|
+
|
|
2548
|
+
has_cos = shutil.which("cos") is not None
|
|
2549
|
+
if has_cos:
|
|
2550
|
+
snippet = '\n[mcp_servers.coding-os]\ncommand = "cos"\nargs = ["server-start"]\n'
|
|
2551
|
+
command = "cos"
|
|
2552
|
+
args = ["server-start"]
|
|
2553
|
+
else:
|
|
2554
|
+
server_py = core_dir("thinking_os", "server.py").as_posix()
|
|
2555
|
+
python = sys.executable
|
|
2556
|
+
snippet = f'\n[mcp_servers.coding-os]\ncommand = "{python}"\nargs = ["{server_py}"]\n'
|
|
2557
|
+
command = python
|
|
2558
|
+
args = [server_py]
|
|
2559
|
+
|
|
2560
|
+
if dry_run:
|
|
2561
|
+
click.echo(f"# Would append to {target}:")
|
|
2562
|
+
click.echo(snippet.rstrip())
|
|
2563
|
+
return
|
|
2564
|
+
|
|
2565
|
+
# Locate the adapter that ships an MCP-helper script. This is the
|
|
2566
|
+
# codex adapter by design — discovered via registry metadata so the
|
|
2567
|
+
# adapter id is not hardcoded in Python code (tests/test_no_hardcoded_stacks).
|
|
2568
|
+
_helper_profile = next(
|
|
2569
|
+
(p for p in load_adapter_registry(ADAPTERS_DIR).values() if p.mcp_helper),
|
|
2570
|
+
None,
|
|
2571
|
+
)
|
|
2572
|
+
if _helper_profile is None:
|
|
2573
|
+
raise click.ClickException(
|
|
2574
|
+
"no adapter declares mcp_helper in adapter.yaml; cannot install MCP"
|
|
2575
|
+
)
|
|
2576
|
+
helper = _helper_profile.source_dir / _helper_profile.mcp_helper
|
|
2577
|
+
proc = subprocess.run(
|
|
2578
|
+
[sys.executable, str(helper), str(target), command, *args],
|
|
2579
|
+
capture_output=True,
|
|
2580
|
+
text=True,
|
|
2581
|
+
check=False,
|
|
2582
|
+
)
|
|
2583
|
+
if proc.returncode != 0:
|
|
2584
|
+
raise click.ClickException(
|
|
2585
|
+
proc.stderr.strip() or f"failed to configure coding-os MCP in {target}"
|
|
2586
|
+
)
|
|
2587
|
+
|
|
2588
|
+
status = (proc.stdout or "").strip()
|
|
2589
|
+
if status.startswith("already configured"):
|
|
2590
|
+
click.echo(f"Already registered in {target} — no changes made.")
|
|
2591
|
+
return
|
|
2592
|
+
|
|
2593
|
+
click.echo(f"OK: registered coding-os MCP in {target}")
|
|
2594
|
+
click.echo("Reload Codex CLI (or start a new session) to pick up the new server.")
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
@cli.command()
|
|
2598
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2599
|
+
def health(project_dir: str) -> None:
|
|
2600
|
+
"""Check coding-os health status."""
|
|
2601
|
+
project = _resolve_project_dir(project_dir)
|
|
2602
|
+
config = _load_config(project)
|
|
2603
|
+
|
|
2604
|
+
click.echo("Coding OS Health Check")
|
|
2605
|
+
click.echo("=" * 40)
|
|
2606
|
+
|
|
2607
|
+
# Config
|
|
2608
|
+
if config:
|
|
2609
|
+
click.echo(f" Config: OK ({CONFIG_FILE})")
|
|
2610
|
+
click.echo(f" Agents: {', '.join(config.get('agents', []))}")
|
|
2611
|
+
click.echo(f" Templates: {', '.join(config.get('templates', [])) or 'none'}")
|
|
2612
|
+
else:
|
|
2613
|
+
click.echo(" Config: MISSING (run 'coding-os init')")
|
|
2614
|
+
return
|
|
2615
|
+
|
|
2616
|
+
# State dir
|
|
2617
|
+
state = project / config.get("state_dir", STATE_DIR)
|
|
2618
|
+
if state.exists():
|
|
2619
|
+
click.echo(f" State dir: OK ({state.name}/)")
|
|
2620
|
+
else:
|
|
2621
|
+
click.echo(" State dir: MISSING")
|
|
2622
|
+
|
|
2623
|
+
# Database
|
|
2624
|
+
db_path = state / "coding-os.db"
|
|
2625
|
+
if db_path.exists():
|
|
2626
|
+
size_kb = db_path.stat().st_size / 1024
|
|
2627
|
+
click.echo(f" Database: OK ({size_kb:.0f} KB)")
|
|
2628
|
+
else:
|
|
2629
|
+
click.echo(" Database: MISSING")
|
|
2630
|
+
|
|
2631
|
+
# Hooks
|
|
2632
|
+
hooks_dir = CORE_DIR / "hooks"
|
|
2633
|
+
hook_count = len(list(hooks_dir.glob("*.sh"))) if hooks_dir.exists() else 0
|
|
2634
|
+
click.echo(f" Core hooks: {hook_count} scripts")
|
|
2635
|
+
|
|
2636
|
+
# MCP server
|
|
2637
|
+
server_py = CORE_DIR / "thinking_os" / "server.py"
|
|
2638
|
+
if server_py.exists():
|
|
2639
|
+
click.echo(" MCP server: OK")
|
|
2640
|
+
else:
|
|
2641
|
+
click.echo(" MCP server: MISSING")
|
|
2642
|
+
|
|
2643
|
+
click.echo("")
|
|
2644
|
+
click.echo("Run 'coding-os init' to fix any missing components.")
|
|
2645
|
+
|
|
2646
|
+
|
|
2647
|
+
@cli.command()
|
|
2648
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2649
|
+
def materialize(project_dir: str) -> None:
|
|
2650
|
+
"""Convert coding-os symlinks to real files (self-contained project)."""
|
|
2651
|
+
project = _resolve_project_dir(project_dir)
|
|
2652
|
+
materialized = 0
|
|
2653
|
+
|
|
2654
|
+
for root, dirs, files in os.walk(project):
|
|
2655
|
+
for name in files:
|
|
2656
|
+
filepath = Path(root) / name
|
|
2657
|
+
if filepath.is_symlink():
|
|
2658
|
+
target = filepath.resolve()
|
|
2659
|
+
if target.exists():
|
|
2660
|
+
filepath.unlink()
|
|
2661
|
+
shutil.copy2(target, filepath)
|
|
2662
|
+
materialized += 1
|
|
2663
|
+
if materialized % 50 == 0:
|
|
2664
|
+
click.echo(f" … materialized {materialized} symlinks so far", err=True)
|
|
2665
|
+
|
|
2666
|
+
click.echo(f"Materialized {materialized} symlinks to real files.")
|
|
2667
|
+
click.echo("Project is now self-contained.")
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _is_coding_os_symlink(link: Path) -> bool:
|
|
2671
|
+
"""True if `link` is coding-os wiring — dangling or resolving into the
|
|
2672
|
+
meta-repo checkout (a user's own symlink elsewhere is left alone)."""
|
|
2673
|
+
try:
|
|
2674
|
+
real = link.resolve()
|
|
2675
|
+
except OSError:
|
|
2676
|
+
return True # broken/cyclic — it was one of ours
|
|
2677
|
+
if not real.exists():
|
|
2678
|
+
return True # dangling: source moved/removed
|
|
2679
|
+
try:
|
|
2680
|
+
real.relative_to(CODING_OS_ROOT.resolve())
|
|
2681
|
+
return True
|
|
2682
|
+
except ValueError:
|
|
2683
|
+
return False
|
|
2684
|
+
|
|
2685
|
+
|
|
2686
|
+
@cli.command()
|
|
2687
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2688
|
+
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip the confirmation prompt.")
|
|
2689
|
+
def eject(project_dir: str, yes: bool) -> None:
|
|
2690
|
+
"""Remove coding-os from a project, keeping your code and docs.
|
|
2691
|
+
|
|
2692
|
+
Deletes coding-os symlinks, the .coding-os/ state dir and the generated
|
|
2693
|
+
AGENTS.md / .coding-os.yaml entrypoints, then deregisters the project. Real
|
|
2694
|
+
files you authored (source, docs, anything that isn't a managed symlink) are
|
|
2695
|
+
never touched. Re-running on an already-ejected project is a no-op.
|
|
2696
|
+
"""
|
|
2697
|
+
project = _resolve_project_dir(project_dir)
|
|
2698
|
+
state_dir = project / STATE_DIR
|
|
2699
|
+
config = project / CONFIG_FILE
|
|
2700
|
+
coding_os_links = [
|
|
2701
|
+
p
|
|
2702
|
+
for p in (Path(root) / n for root, _d, fs in os.walk(project) for n in fs)
|
|
2703
|
+
if p.is_symlink() and _is_coding_os_symlink(p)
|
|
2704
|
+
]
|
|
2705
|
+
# CLAUDE.md is the generated symlink to AGENTS.md (points at a sibling, so
|
|
2706
|
+
# the meta-repo filter above misses it) — treat any symlinked one as ours.
|
|
2707
|
+
claude_md = project / "CLAUDE.md"
|
|
2708
|
+
generated_entrypoints = [config, project / "AGENTS.md"]
|
|
2709
|
+
if claude_md.is_symlink():
|
|
2710
|
+
generated_entrypoints.append(claude_md)
|
|
2711
|
+
|
|
2712
|
+
present = [f for f in generated_entrypoints if f.exists() or f.is_symlink()]
|
|
2713
|
+
if not coding_os_links and not state_dir.exists() and not present:
|
|
2714
|
+
click.echo("No coding-os install found here — nothing to eject.")
|
|
2715
|
+
return
|
|
2716
|
+
|
|
2717
|
+
if not yes and not click.confirm(
|
|
2718
|
+
f"Remove coding-os from {project}? Your code and docs stay.", default=False
|
|
2719
|
+
):
|
|
2720
|
+
click.echo("Aborted.")
|
|
2721
|
+
return
|
|
2722
|
+
|
|
2723
|
+
for link in coding_os_links:
|
|
2724
|
+
link.unlink()
|
|
2725
|
+
removed_files = 0
|
|
2726
|
+
for f in present:
|
|
2727
|
+
f.unlink()
|
|
2728
|
+
removed_files += 1
|
|
2729
|
+
removed_state = False
|
|
2730
|
+
if state_dir.exists():
|
|
2731
|
+
shutil.rmtree(state_dir)
|
|
2732
|
+
removed_state = True
|
|
2733
|
+
|
|
2734
|
+
# Prune adapter dirs left empty after the symlinks went; keep any that still
|
|
2735
|
+
# hold real (user-authored or materialized) files.
|
|
2736
|
+
kept_dirs: list[str] = []
|
|
2737
|
+
for agent_dir in sorted({p.parent for p in coding_os_links if p.parent.name.startswith(".")}):
|
|
2738
|
+
if agent_dir.is_dir() and not any(agent_dir.iterdir()):
|
|
2739
|
+
agent_dir.rmdir()
|
|
2740
|
+
elif agent_dir.is_dir():
|
|
2741
|
+
kept_dirs.append(agent_dir.name)
|
|
2742
|
+
|
|
2743
|
+
from cli.registry import remove_project
|
|
2744
|
+
|
|
2745
|
+
deregistered = False
|
|
2746
|
+
try:
|
|
2747
|
+
deregistered = remove_project(str(project)) is not None
|
|
2748
|
+
except Exception as exc: # registry is best-effort — never block an eject
|
|
2749
|
+
_logging.getLogger("coding_os.cli").debug("eject: deregister skipped: %s", exc)
|
|
2750
|
+
|
|
2751
|
+
click.echo(f"Ejected coding-os from {project}")
|
|
2752
|
+
click.echo(
|
|
2753
|
+
f" removed: {len(coding_os_links)} symlinks · {removed_files} config file(s)"
|
|
2754
|
+
+ (" · .coding-os/ state" if removed_state else "")
|
|
2755
|
+
+ (" · global-registry entry" if deregistered else "")
|
|
2756
|
+
)
|
|
2757
|
+
kept = "your source, docs, and any files you authored"
|
|
2758
|
+
if kept_dirs:
|
|
2759
|
+
kept += f" (incl. real files under {', '.join(sorted(set(kept_dirs)))})"
|
|
2760
|
+
click.echo(f" kept: {kept}")
|
|
2761
|
+
|
|
2762
|
+
|
|
2763
|
+
@cli.command("hooks-dir")
|
|
2764
|
+
def hooks_dir() -> None:
|
|
2765
|
+
"""Print the path to the core hooks directory."""
|
|
2766
|
+
click.echo(CORE_DIR / "hooks")
|
|
2767
|
+
|
|
2768
|
+
|
|
2769
|
+
@cli.command("hooks-log")
|
|
2770
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2771
|
+
@click.option("-n", "tail_count", default=50, help="Show last N lines (default 50)")
|
|
2772
|
+
@click.option("--follow", "-f", is_flag=True, default=False, help="Follow new entries (tail -f)")
|
|
2773
|
+
@click.option("--agent", type=str, default=None, help="Filter by agent (claude|codex|unknown)")
|
|
2774
|
+
@click.option("--session", type=str, default=None, help="Filter by session id (substring match)")
|
|
2775
|
+
@click.option("--task", type=str, default=None, help="Filter by task name (substring match)")
|
|
2776
|
+
@click.option("--hook", type=str, default=None, help="Filter by hook name (substring match)")
|
|
2777
|
+
@click.option(
|
|
2778
|
+
"--all",
|
|
2779
|
+
"--verbose",
|
|
2780
|
+
"show_all",
|
|
2781
|
+
is_flag=True,
|
|
2782
|
+
default=False,
|
|
2783
|
+
help="Show lifecycle rows (enter/ok) too; default hides them.",
|
|
2784
|
+
)
|
|
2785
|
+
def hooks_log(
|
|
2786
|
+
project_dir: str,
|
|
2787
|
+
tail_count: int,
|
|
2788
|
+
follow: bool,
|
|
2789
|
+
agent: str | None,
|
|
2790
|
+
session: str | None,
|
|
2791
|
+
task: str | None,
|
|
2792
|
+
hook: str | None,
|
|
2793
|
+
show_all: bool,
|
|
2794
|
+
) -> None:
|
|
2795
|
+
"""Show recent hook activity from .coding-os/.hooks.log.
|
|
2796
|
+
|
|
2797
|
+
Hooks call `cos_log_hook` (from src/core/hooks/cos-env.sh) on fire / block /
|
|
2798
|
+
allow / warn. Every line carries `agent=X session=Y task=Z` identity
|
|
2799
|
+
fields so you can filter by any combination:
|
|
2800
|
+
|
|
2801
|
+
cos hooks-log --agent claude # only claude runs
|
|
2802
|
+
cos hooks-log --session ses-20260418-143638-c769
|
|
2803
|
+
cos hooks-log --task governance-mcp-envelope --hook enforce-
|
|
2804
|
+
cos hooks-log --agent codex --follow # live codex stream
|
|
2805
|
+
cos hooks-log --all # include enter/ok noise
|
|
2806
|
+
|
|
2807
|
+
By default only decision-states (fire/block/warn/paths/reminded/full/
|
|
2808
|
+
debounced/skip/bypass) are shown; lifecycle rows ([enter]/[ok]) are hidden
|
|
2809
|
+
behind --all/--verbose. Filters are AND-ed together (case-sensitive
|
|
2810
|
+
substring match).
|
|
2811
|
+
"""
|
|
2812
|
+
project = _resolve_project_dir(project_dir)
|
|
2813
|
+
config = _load_config(project) or {}
|
|
2814
|
+
state = project / config.get("state_dir", STATE_DIR)
|
|
2815
|
+
log_path = state / ".hooks.log"
|
|
2816
|
+
|
|
2817
|
+
if not log_path.exists():
|
|
2818
|
+
click.echo(f"No hook activity yet ({log_path} does not exist).")
|
|
2819
|
+
click.echo(
|
|
2820
|
+
"Hint: hooks log on fire — if you expected events, check"
|
|
2821
|
+
" .claude/settings.json or .codex/hooks.json wiring."
|
|
2822
|
+
)
|
|
2823
|
+
return
|
|
2824
|
+
|
|
2825
|
+
# Lifecycle actions are bookkeeping, not decisions — hide them by default
|
|
2826
|
+
# so `cos hooks-log` surfaces signal (fire/block/warn/...) over noise.
|
|
2827
|
+
lifecycle_actions = {"[enter]", "[ok]"}
|
|
2828
|
+
|
|
2829
|
+
def _is_decision_state(line: str) -> bool:
|
|
2830
|
+
return not any(token in line for token in lifecycle_actions)
|
|
2831
|
+
|
|
2832
|
+
filters: list[str] = []
|
|
2833
|
+
if agent:
|
|
2834
|
+
filters.append(f"agent={agent}")
|
|
2835
|
+
if session:
|
|
2836
|
+
filters.append(f"session={session}")
|
|
2837
|
+
if task:
|
|
2838
|
+
filters.append(f"task={task}")
|
|
2839
|
+
if hook:
|
|
2840
|
+
filters.append(f"[{hook}")
|
|
2841
|
+
|
|
2842
|
+
if follow:
|
|
2843
|
+
tail_cmd = f"tail -f -n {tail_count} {shlex.quote(str(log_path))}"
|
|
2844
|
+
pipe = [tail_cmd]
|
|
2845
|
+
if not show_all:
|
|
2846
|
+
pipe.append("grep --line-buffered -vE '\\[(enter|ok)\\]'")
|
|
2847
|
+
pipe.extend(f"grep -F --line-buffered {shlex.quote(f)}" for f in filters)
|
|
2848
|
+
subprocess.run(["bash", "-c", " | ".join(pipe)])
|
|
2849
|
+
return
|
|
2850
|
+
|
|
2851
|
+
try:
|
|
2852
|
+
lines = log_path.read_text(errors="replace").splitlines()
|
|
2853
|
+
except OSError as exc:
|
|
2854
|
+
click.echo(f"Could not read {log_path}: {exc}", err=True)
|
|
2855
|
+
return
|
|
2856
|
+
matched = [
|
|
2857
|
+
ln for ln in lines if all(f in ln for f in filters) and (show_all or _is_decision_state(ln))
|
|
2858
|
+
]
|
|
2859
|
+
for ln in matched[-tail_count:]:
|
|
2860
|
+
click.echo(ln)
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
@cli.command("hooks-list")
|
|
2864
|
+
@click.option("--agent", type=str, default=None, help="Filter by adapter (claude|codex)")
|
|
2865
|
+
@click.option("--category", type=str, default=None, help="Filter by category")
|
|
2866
|
+
@click.option("--phase", type=str, default=None, help="Filter by phase")
|
|
2867
|
+
def hooks_list(agent: str | None, category: str | None, phase: str | None) -> None:
|
|
2868
|
+
"""List hooks registered in src/core/hooks/registry.yaml with filters.
|
|
2869
|
+
|
|
2870
|
+
Reads the manifest SSOT and prints a summary. With --agent, filters to
|
|
2871
|
+
hooks whose events fit that adapter's declared capabilities — answers
|
|
2872
|
+
"what enforcement is active for Codex?" without grepping settings.
|
|
2873
|
+
"""
|
|
2874
|
+
from cli.hook_renderer import list_hooks_for_agent, load_registry
|
|
2875
|
+
|
|
2876
|
+
registry_path = CORE_DIR / "hooks" / "registry.yaml"
|
|
2877
|
+
if not registry_path.exists():
|
|
2878
|
+
click.echo(f"ERROR: {registry_path} not found", err=True)
|
|
2879
|
+
sys.exit(1)
|
|
2880
|
+
|
|
2881
|
+
entries = load_registry(registry_path)
|
|
2882
|
+
if agent:
|
|
2883
|
+
entries = list_hooks_for_agent(entries, agent, ADAPTERS_DIR)
|
|
2884
|
+
if category:
|
|
2885
|
+
entries = [h for h in entries if h.category == category]
|
|
2886
|
+
if phase:
|
|
2887
|
+
entries = [h for h in entries if str(h.phase) == phase]
|
|
2888
|
+
|
|
2889
|
+
if not entries:
|
|
2890
|
+
click.echo("No hooks match the filters.")
|
|
2891
|
+
return
|
|
2892
|
+
|
|
2893
|
+
by_cat: dict[str, list] = {}
|
|
2894
|
+
for h in entries:
|
|
2895
|
+
by_cat.setdefault(h.category or "uncategorized", []).append(h)
|
|
2896
|
+
|
|
2897
|
+
for cat in sorted(by_cat):
|
|
2898
|
+
click.echo(f"\n[{cat}]")
|
|
2899
|
+
for h in by_cat[cat]:
|
|
2900
|
+
events = ", ".join(
|
|
2901
|
+
f"{e['event']}::{e.get('matcher', '')}".rstrip(":") for e in h.events
|
|
2902
|
+
)
|
|
2903
|
+
click.echo(f" {h.id:30s} phase={h.phase!s:3s} events=[{events}]")
|
|
2904
|
+
if h.description:
|
|
2905
|
+
click.echo(f" {h.description}")
|
|
2906
|
+
click.echo("")
|
|
2907
|
+
|
|
2908
|
+
|
|
2909
|
+
@cli.command("server-start")
|
|
2910
|
+
def server_start() -> None:
|
|
2911
|
+
"""Start the thinking_os MCP server (wrapper used by .mcp.json).
|
|
2912
|
+
|
|
2913
|
+
Projects register `cos server-start` in their .mcp.json so the MCP
|
|
2914
|
+
entry stays portable — coding-os location is resolved at call time by
|
|
2915
|
+
whichever `cos` binary is on PATH, not hardcoded per-install.
|
|
2916
|
+
|
|
2917
|
+
Historically this wrapper re-entered `uv run --directory ...`, which
|
|
2918
|
+
dragged in `~/.cache/uv` at every MCP launch. In sandboxed runtimes
|
|
2919
|
+
that cache path may be unreadable, causing MCP startup to fail before
|
|
2920
|
+
the server process even booted. We already have a Python interpreter
|
|
2921
|
+
available — the one running `cos` itself — so execute `server.py`
|
|
2922
|
+
directly with that interpreter instead.
|
|
2923
|
+
|
|
2924
|
+
We still capture the caller's cwd (the real project root the agent
|
|
2925
|
+
launched us from) and export it as COS_DB_PATH / COS_STATE_DIR so the
|
|
2926
|
+
server reads the right DB regardless of its own source location.
|
|
2927
|
+
"""
|
|
2928
|
+
server_py = CORE_DIR / "thinking_os" / "server.py"
|
|
2929
|
+
if not server_py.exists():
|
|
2930
|
+
click.echo(f"ERROR: MCP server not found at {server_py}", err=True)
|
|
2931
|
+
sys.exit(1)
|
|
2932
|
+
|
|
2933
|
+
caller_cwd = Path.cwd().resolve()
|
|
2934
|
+
env = os.environ.copy()
|
|
2935
|
+
# Only inject if the caller hasn't already set them — respects
|
|
2936
|
+
# explicit overrides for tests / multi-project setups.
|
|
2937
|
+
env.setdefault(
|
|
2938
|
+
"COS_DB_PATH",
|
|
2939
|
+
str(caller_cwd / STATE_DIR / "coding-os.db"),
|
|
2940
|
+
)
|
|
2941
|
+
env.setdefault(
|
|
2942
|
+
"COS_STATE_DIR",
|
|
2943
|
+
str(caller_cwd / STATE_DIR),
|
|
2944
|
+
)
|
|
2945
|
+
|
|
2946
|
+
# Exec so signals / stdio pass through cleanly (MCP is stdio-based).
|
|
2947
|
+
python = sys.executable
|
|
2948
|
+
os.execvpe(
|
|
2949
|
+
python,
|
|
2950
|
+
[
|
|
2951
|
+
python,
|
|
2952
|
+
str(server_py),
|
|
2953
|
+
],
|
|
2954
|
+
env,
|
|
2955
|
+
)
|
|
2956
|
+
|
|
2957
|
+
|
|
2958
|
+
@cli.command("session-state")
|
|
2959
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2960
|
+
def session_state(project_dir: str) -> None:
|
|
2961
|
+
"""Show current session gate, task, and skill state."""
|
|
2962
|
+
import time
|
|
2963
|
+
|
|
2964
|
+
from cli.board_commands import _detect_agent_runtime
|
|
2965
|
+
|
|
2966
|
+
project = Path(project_dir).resolve()
|
|
2967
|
+
agent = os.environ.get("COS_AGENT") or _detect_agent_runtime()
|
|
2968
|
+
if not agent:
|
|
2969
|
+
adapters = sorted(load_adapter_registry(ADAPTERS_DIR).keys())
|
|
2970
|
+
if not adapters:
|
|
2971
|
+
click.echo("No adapters registered under src/adapters/.", err=True)
|
|
2972
|
+
sys.exit(1)
|
|
2973
|
+
agent = adapters[0]
|
|
2974
|
+
agent_dir = project / ".coding-os" / agent
|
|
2975
|
+
|
|
2976
|
+
if not agent_dir.exists():
|
|
2977
|
+
click.echo(f"No session state at {agent_dir}")
|
|
2978
|
+
sys.exit(1)
|
|
2979
|
+
|
|
2980
|
+
session_file = agent_dir / "session-id"
|
|
2981
|
+
current_session = session_file.read_text().strip() if session_file.exists() else ""
|
|
2982
|
+
|
|
2983
|
+
def _read_state(path: Path, max_age: int = 7200) -> tuple[str, str]:
|
|
2984
|
+
if not path.exists():
|
|
2985
|
+
return ("none", "")
|
|
2986
|
+
try:
|
|
2987
|
+
content = path.read_text().splitlines()[0] if path.exists() else ""
|
|
2988
|
+
except OSError:
|
|
2989
|
+
return ("error", "")
|
|
2990
|
+
parts = content.split(" ", 1)
|
|
2991
|
+
file_session = parts[0] if parts else ""
|
|
2992
|
+
value = parts[1] if len(parts) > 1 else ""
|
|
2993
|
+
if current_session and file_session and file_session != current_session:
|
|
2994
|
+
return ("session-mismatch", value)
|
|
2995
|
+
age = int(time.time() - path.stat().st_mtime)
|
|
2996
|
+
if age > max_age:
|
|
2997
|
+
return (f"stale ({age // 60}min old, max {max_age // 60}min)", value)
|
|
2998
|
+
return ("valid", value)
|
|
2999
|
+
|
|
3000
|
+
gate_status, gate_val = _read_state(agent_dir / ".thinking_os-gate")
|
|
3001
|
+
task_status, task_val = _read_state(agent_dir / ".task-current")
|
|
3002
|
+
skill_status, skill_val = _read_state(agent_dir / ".active-skill")
|
|
3003
|
+
zoom_status, _ = _read_state(agent_dir / ".zoom-checkpoint")
|
|
3004
|
+
doc_status, _ = _read_state(agent_dir / ".doc-anchor")
|
|
3005
|
+
|
|
3006
|
+
click.echo(f"Session : {current_session or '(unset)'}")
|
|
3007
|
+
click.echo(f"Agent : {agent}")
|
|
3008
|
+
click.echo(f"Gate : {gate_status:30s} {gate_val}")
|
|
3009
|
+
click.echo(f"Zoom : {zoom_status}")
|
|
3010
|
+
click.echo(f"Task : {task_status:30s} {task_val}")
|
|
3011
|
+
click.echo(f"Skill : {skill_status:30s} {skill_val}")
|
|
3012
|
+
click.echo(f"DocAnchor : {doc_status}")
|
|
3013
|
+
|
|
3014
|
+
if "stale" in gate_status or gate_status == "none":
|
|
3015
|
+
click.echo("")
|
|
3016
|
+
click.echo("Gate not valid — next Write/Edit on .py/.ts/.tsx will BLOCK")
|
|
3017
|
+
click.echo(f' Re-record: bash "{agent_dir}/hooks/write-state.sh" \\')
|
|
3018
|
+
click.echo(' .thinking_os-gate "CLEAR 1"')
|
|
3019
|
+
click.echo(" (bare basename auto-routes to $COS_PANEL_DIR via cos_state_path)")
|
|
3020
|
+
|
|
3021
|
+
|
|
3022
|
+
# ---------------------------------------------------------------------------
|
|
3023
|
+
# graph_os subcommand family (`cos graph-*`).
|
|
3024
|
+
# Registration lives in src/cli/graph_commands.py so the main file stays lean.
|
|
3025
|
+
# ---------------------------------------------------------------------------
|
|
3026
|
+
try:
|
|
3027
|
+
from cli import graph_commands as _graph_commands
|
|
3028
|
+
|
|
3029
|
+
_graph_commands.register(cli)
|
|
3030
|
+
except ImportError as _graph_cli_exc: # pragma: no cover — defensive
|
|
3031
|
+
import logging as _logging
|
|
3032
|
+
|
|
3033
|
+
_logging.getLogger("coding_os.cli").debug("graph_os CLI unavailable: %s", _graph_cli_exc)
|
|
3034
|
+
|
|
3035
|
+
|
|
3036
|
+
# ---------------------------------------------------------------------------
|
|
3037
|
+
# DB lifecycle — `cos db-stats`, `cos db-reset`. Spec: docs/playbooks/db-reset.md.
|
|
3038
|
+
# ---------------------------------------------------------------------------
|
|
3039
|
+
try:
|
|
3040
|
+
from cli import db_reset as _db_reset
|
|
3041
|
+
|
|
3042
|
+
_db_reset.register(cli)
|
|
3043
|
+
except ImportError as _db_reset_exc: # pragma: no cover — defensive
|
|
3044
|
+
import logging as _logging
|
|
3045
|
+
|
|
3046
|
+
_logging.getLogger("coding_os.cli").debug("db_reset CLI unavailable: %s", _db_reset_exc)
|
|
3047
|
+
|
|
3048
|
+
|
|
3049
|
+
# ---------------------------------------------------------------------------
|
|
3050
|
+
# S4/S5 — registry + hub CLI. (`cos web` removed: it duplicated
|
|
3051
|
+
# `cos hub start --foreground` — both just call web.server.run_server. Dev
|
|
3052
|
+
# auto-reload lives in `make ui-dev`.)
|
|
3053
|
+
# ---------------------------------------------------------------------------
|
|
3054
|
+
try:
|
|
3055
|
+
from cli.registry import registry_cli as _registry_cli
|
|
3056
|
+
|
|
3057
|
+
cli.add_command(_registry_cli)
|
|
3058
|
+
|
|
3059
|
+
from cli.hub_commands import hub_cli as _hub_cli, service_cli as _service_cli
|
|
3060
|
+
|
|
3061
|
+
cli.add_command(_hub_cli)
|
|
3062
|
+
cli.add_command(_service_cli)
|
|
3063
|
+
except ImportError as _web_cli_exc: # pragma: no cover — defensive
|
|
3064
|
+
import logging as _logging
|
|
3065
|
+
|
|
3066
|
+
_logging.getLogger("coding_os.cli").debug("web CLI unavailable: %s", _web_cli_exc)
|
|
3067
|
+
|
|
3068
|
+
|
|
3069
|
+
if __name__ == "__main__":
|
|
3070
|
+
cli()
|