coding-os 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adapters/claude/README.md +6 -0
- adapters/claude/_install_helpers/extract_stacks.py +43 -0
- adapters/claude/_install_helpers/update_mcp_json.py +75 -0
- adapters/claude/adapter.yaml +164 -0
- adapters/claude/hooks/README.md +40 -0
- adapters/claude/hooks/agent_memory_sync.py +131 -0
- adapters/claude/hooks/ensure-agent-memory-link.sh +36 -0
- adapters/claude/hooks/sync-agent-memory.sh +21 -0
- adapters/claude/install.sh +86 -0
- adapters/claude/sdk_dispatcher.py +871 -0
- adapters/claude/settings.local.template.json +31 -0
- adapters/claude/settings.template.json +808 -0
- adapters/claude/update_mcp_json.py +85 -0
- adapters/codex/adapter.yaml +254 -0
- adapters/codex/chat_provider.py +230 -0
- adapters/codex/commands/formula-f1.md +129 -0
- adapters/codex/commands/formula-f10.md +100 -0
- adapters/codex/commands/formula-f11.md +123 -0
- adapters/codex/commands/formula-f2.md +139 -0
- adapters/codex/commands/formula-f3.md +127 -0
- adapters/codex/commands/formula-f4.md +101 -0
- adapters/codex/commands/formula-f5.md +135 -0
- adapters/codex/commands/formula-f6.md +147 -0
- adapters/codex/commands/formula-f7.md +111 -0
- adapters/codex/commands/formula-f8.md +133 -0
- adapters/codex/commands/formula-f9.md +112 -0
- adapters/codex/enable_codex_hooks.py +94 -0
- adapters/codex/ensure_codex_mcp.py +124 -0
- adapters/codex/hooks/codex-merge-hook-output.py +72 -0
- adapters/codex/hooks/codex-normalize-edit.py +96 -0
- adapters/codex/hooks/codex-postedit-dispatch.sh +75 -0
- adapters/codex/hooks/codex-posttool-dispatch.sh +70 -0
- adapters/codex/hooks/codex-preedit-dispatch.sh +83 -0
- adapters/codex/hooks/codex-pretool-dispatch.sh +82 -0
- adapters/codex/hooks/codex-sessionend-dispatch.sh +20 -0
- adapters/codex/hooks/codex-sessionstart-dispatch.sh +68 -0
- adapters/codex/hooks/codex-stop-dispatch.sh +73 -0
- adapters/codex/hooks/codex-userpromptsubmit-dispatch.sh +74 -0
- adapters/codex/hooks.template.json +208 -0
- adapters/codex/install.sh +83 -0
- adapters/codex/sdk_dispatcher.py +449 -0
- board_os/__init__.py +39 -0
- board_os/_agent_runtime.py +256 -0
- board_os/config.py +421 -0
- board_os/git_coherence.py +107 -0
- board_os/hub_adapter_manifest.py +140 -0
- board_os/mcp_tools.py +3228 -0
- board_os/migration.py +166 -0
- board_os/parser.py +317 -0
- board_os/presence.py +156 -0
- board_os/sync.py +320 -0
- board_os/transition_gates.py +224 -0
- board_os/transition_gates_cli.py +272 -0
- board_os/transition_gates_validator.py +551 -0
- board_os/verify_suites.py +126 -0
- board_os/verify_suites_cli.py +328 -0
- board_os/workflow.py +967 -0
- cli/__init__.py +0 -0
- cli/_data_types.py +248 -0
- cli/_init_helpers.py +587 -0
- cli/_resources.py +100 -0
- cli/adapter_registry.py +239 -0
- cli/add_stack.py +315 -0
- cli/aggregator.py +438 -0
- cli/board_commands.py +1218 -0
- cli/brain_commands.py +255 -0
- cli/cognition.py +345 -0
- cli/config_composer.py +349 -0
- cli/core_version.py +41 -0
- cli/cron_commands.py +278 -0
- cli/db_reset.py +298 -0
- cli/doc_commands.py +111 -0
- cli/doctor.py +2953 -0
- cli/doctor_board.py +365 -0
- cli/doctor_extras.py +1121 -0
- cli/doctor_graph.py +608 -0
- cli/doctor_tokens.py +254 -0
- cli/graph_commands.py +1265 -0
- cli/hook_renderer.py +393 -0
- cli/hub_commands.py +580 -0
- cli/list_adapters.py +79 -0
- cli/list_stacks.py +105 -0
- cli/logs_commands.py +89 -0
- cli/main.py +3070 -0
- cli/materialize_file.py +65 -0
- cli/mcp_start.py +153 -0
- cli/module_commands.py +513 -0
- cli/pr_commands.py +2024 -0
- cli/preset_commands.py +126 -0
- cli/preset_registry.py +171 -0
- cli/project_overrides.py +119 -0
- cli/registry.py +365 -0
- cli/remove_stack.py +492 -0
- cli/renderer.py +620 -0
- cli/setup.py +464 -0
- cli/skill_commands.py +689 -0
- cli/skill_registry.py +235 -0
- cli/skills_list.py +332 -0
- cli/stack_lint.py +351 -0
- cli/stack_registry.py +688 -0
- cli/subsystems.py +335 -0
- cli/sync_all.py +310 -0
- cli/tail_command.py +410 -0
- cli/update.py +608 -0
- cli/verify_since_edit.py +439 -0
- coding_os-0.3.2.dist-info/METADATA +508 -0
- coding_os-0.3.2.dist-info/RECORD +1304 -0
- coding_os-0.3.2.dist-info/WHEEL +5 -0
- coding_os-0.3.2.dist-info/entry_points.txt +5 -0
- coding_os-0.3.2.dist-info/licenses/LICENSE +201 -0
- coding_os-0.3.2.dist-info/top_level.txt +10 -0
- core/__init__.py +0 -0
- core/board_os/__init__.py +39 -0
- core/board_os/_agent_runtime.py +256 -0
- core/board_os/config.py +421 -0
- core/board_os/git_coherence.py +107 -0
- core/board_os/hub_adapter_manifest.py +140 -0
- core/board_os/mcp_tools.py +3228 -0
- core/board_os/migration.py +166 -0
- core/board_os/parser.py +317 -0
- core/board_os/presence.py +156 -0
- core/board_os/sync.py +320 -0
- core/board_os/transition-gates.yaml +176 -0
- core/board_os/transition_gates.py +224 -0
- core/board_os/transition_gates_cli.py +272 -0
- core/board_os/transition_gates_validator.py +551 -0
- core/board_os/verify-suites.yaml +113 -0
- core/board_os/verify_suites.py +126 -0
- core/board_os/verify_suites_cli.py +328 -0
- core/board_os/workflow.py +967 -0
- core/commands/board.md +27 -0
- core/commands/classify.md +23 -0
- core/commands/compose.md +23 -0
- core/commands/daily.md +31 -0
- core/commands/diagnose.md +7 -0
- core/commands/memory-search.md +23 -0
- core/commands/new-project.md +33 -0
- core/commands/retro.md +38 -0
- core/commands/review.md +14 -0
- core/commands/task.md +17 -0
- core/commands/verify.md +34 -0
- core/docs/thinking_os-final-edition.md +1449 -0
- core/doctor-config.yaml +74 -0
- core/graph_os/__init__.py +29 -0
- core/graph_os/backend.py +233 -0
- core/graph_os/backends/__init__.py +13 -0
- core/graph_os/backends/sqlite_backend.py +1053 -0
- core/graph_os/bench/__init__.py +17 -0
- core/graph_os/bench/fixtures.py +56 -0
- core/graph_os/bench/harness.py +95 -0
- core/graph_os/bench/persian_precision.py +142 -0
- core/graph_os/bench/scale_500k.py +120 -0
- core/graph_os/bench/token_cost.py +170 -0
- core/graph_os/bench/viewer_fps.py +110 -0
- core/graph_os/communities.py +410 -0
- core/graph_os/enterprise.py +218 -0
- core/graph_os/entry_points.py +226 -0
- core/graph_os/extractors/__init__.py +25 -0
- core/graph_os/extractors/code_generic.py +914 -0
- core/graph_os/extractors/code_go.py +1422 -0
- core/graph_os/extractors/code_json.py +340 -0
- core/graph_os/extractors/code_php.py +979 -0
- core/graph_os/extractors/code_python.py +1454 -0
- core/graph_os/extractors/code_shell.py +538 -0
- core/graph_os/extractors/code_toml.py +302 -0
- core/graph_os/extractors/code_ts.py +1665 -0
- core/graph_os/extractors/code_yaml.py +394 -0
- core/graph_os/extractors/contracts.py +1592 -0
- core/graph_os/extractors/md_links.py +890 -0
- core/graph_os/extractors/task_deps.py +345 -0
- core/graph_os/groups/__init__.py +22 -0
- core/graph_os/groups/cross_repo.py +156 -0
- core/graph_os/groups/manifest.py +141 -0
- core/graph_os/ingest/__init__.py +19 -0
- core/graph_os/ingest/base.py +306 -0
- core/graph_os/ingest/github.py +112 -0
- core/graph_os/ingest/zip.py +95 -0
- core/graph_os/toolchain.py +393 -0
- core/graph_os/tools/__init__.py +9 -0
- core/graph_os/tools/graph.py +5573 -0
- core/graph_os/tools/reindex_dispatch.py +730 -0
- core/graph_os/tree_sitter_overlay.py +235 -0
- core/graph_os/types.py +252 -0
- core/graph_os/vec_index.py +277 -0
- core/graph_os/viewer/__init__.py +12 -0
- core/graph_os/viewer/exporter.py +93 -0
- core/graph_os/viewer/template.py +189 -0
- core/hooks/_helpers/_paths.py +40 -0
- core/hooks/_helpers/advance_role.py +72 -0
- core/hooks/_helpers/auto_compose.py +228 -0
- core/hooks/_helpers/auto_validate_lessons.py +55 -0
- core/hooks/_helpers/branch_guard_check.py +796 -0
- core/hooks/_helpers/check_commit_message.py +108 -0
- core/hooks/_helpers/check_dangerous_rm.py +80 -0
- core/hooks/_helpers/check_git_bypass.py +154 -0
- core/hooks/_helpers/check_git_destructive.py +77 -0
- core/hooks/_helpers/check_settings_write.py +97 -0
- core/hooks/_helpers/consume_override.py +51 -0
- core/hooks/_helpers/context_budget.py +77 -0
- core/hooks/_helpers/cos_say_json.py +103 -0
- core/hooks/_helpers/destructive_edit_check.py +163 -0
- core/hooks/_helpers/detect_status_transition.py +82 -0
- core/hooks/_helpers/digest_regen.py +56 -0
- core/hooks/_helpers/doc_sync_check.py +498 -0
- core/hooks/_helpers/drain_embedding_outbox.py +52 -0
- core/hooks/_helpers/extract_additional_context.py +51 -0
- core/hooks/_helpers/extract_commit_msg_arg.py +74 -0
- core/hooks/_helpers/git_command_parse.py +424 -0
- core/hooks/_helpers/git_settings_fields.py +47 -0
- core/hooks/_helpers/graph_context_match.py +37 -0
- core/hooks/_helpers/graph_marker_check.py +70 -0
- core/hooks/_helpers/jit_recall.py +56 -0
- core/hooks/_helpers/json_field.py +41 -0
- core/hooks/_helpers/narrative_signal.py +59 -0
- core/hooks/_helpers/observation_count.py +31 -0
- core/hooks/_helpers/pre_commit_batch.py +177 -0
- core/hooks/_helpers/pre_commit_fake_input.py +42 -0
- core/hooks/_helpers/presence_gc.py +102 -0
- core/hooks/_helpers/presence_write.py +167 -0
- core/hooks/_helpers/recover_indirect.py +35 -0
- core/hooks/_helpers/routing_evolution.py +104 -0
- core/hooks/_helpers/session_recap.py +72 -0
- core/hooks/_helpers/skill_primer.py +229 -0
- core/hooks/_helpers/task_sync.py +59 -0
- core/hooks/_helpers/tool_failure_capture.py +147 -0
- core/hooks/_helpers/trajectory_autosnap.py +278 -0
- core/hooks/_helpers/trajectory_startup.py +62 -0
- core/hooks/_helpers/turn_summary.py +82 -0
- core/hooks/_helpers/validate_task_frontmatter.py +98 -0
- core/hooks/_helpers/wip_limit_check.py +103 -0
- core/hooks/_helpers/wip_lines.py +53 -0
- core/hooks/_helpers/work_log_append.py +89 -0
- core/hooks/_helpers/wrap_dispatch_output.py +82 -0
- core/hooks/advance-role.sh +48 -0
- core/hooks/agent-presence.sh +179 -0
- core/hooks/auto-brain-decay.sh +184 -0
- core/hooks/auto-compose-roles.sh +83 -0
- core/hooks/auto-graph-reconcile-shell.sh +119 -0
- core/hooks/auto-regen-doc-index.sh +120 -0
- core/hooks/auto-reindex-docs.sh +130 -0
- core/hooks/auto-task-sync.sh +56 -0
- core/hooks/auto-trace-rotate.sh +88 -0
- core/hooks/block-bad-patterns.sh +212 -0
- core/hooks/block-dangerous-commands.sh +182 -0
- core/hooks/block-hardcoded-literals.sh +90 -0
- core/hooks/block-migration-conflict.sh +114 -0
- core/hooks/block-protected-files.sh +129 -0
- core/hooks/block-secrets.sh +185 -0
- core/hooks/block-shared-tree-edit.sh +75 -0
- core/hooks/block-uv-heredoc.sh +78 -0
- core/hooks/branch-guard.sh +122 -0
- core/hooks/capture-observation.sh +76 -0
- core/hooks/capture-tool-failure.sh +24 -0
- core/hooks/capture-work-log.sh +89 -0
- core/hooks/check-agents-md-refs.sh +75 -0
- core/hooks/check-agents-md-size.sh +49 -0
- core/hooks/check-capture-worked.sh +148 -0
- core/hooks/check-doc-size.sh +61 -0
- core/hooks/check-mcp-extras.sh +92 -0
- core/hooks/check-state.sh +87 -0
- core/hooks/classify-task-mode.sh +103 -0
- core/hooks/cos-env.sh +1301 -0
- core/hooks/drain-embedding-outbox.sh +24 -0
- core/hooks/enforce-anti-ambiguity.sh +74 -0
- core/hooks/enforce-commit-message.sh +73 -0
- core/hooks/enforce-doc-anchor.sh +224 -0
- core/hooks/enforce-doc-sync.sh +206 -0
- core/hooks/enforce-graph-context.sh +89 -0
- core/hooks/enforce-graph-first-read.sh +94 -0
- core/hooks/enforce-memory-check.sh +128 -0
- core/hooks/enforce-rename-plan.sh +78 -0
- core/hooks/enforce-scaffold-boundary.sh +68 -0
- core/hooks/enforce-skill.sh +125 -0
- core/hooks/enforce-task-body.sh +52 -0
- core/hooks/enforce-task-start.sh +81 -0
- core/hooks/enforce-task-transition.sh +75 -0
- core/hooks/enforce-template.sh +143 -0
- core/hooks/enforce-verify.sh +112 -0
- core/hooks/enforce-wip-limit.sh +43 -0
- core/hooks/enforce-zoom.sh +69 -0
- core/hooks/ensure-hub-up.sh +67 -0
- core/hooks/inject-mcp-caller-session.sh +70 -0
- core/hooks/jit-recall.sh +65 -0
- core/hooks/link-commit-to-task.sh +143 -0
- core/hooks/lint-task.sh +40 -0
- core/hooks/nudge-docs-first.sh +71 -0
- core/hooks/nudge-git-mode.sh +29 -0
- core/hooks/nudge-graph-os.sh +118 -0
- core/hooks/nudge-learn-narrative.sh +36 -0
- core/hooks/nudge-model-routing.sh +32 -0
- core/hooks/nudge-reentry.sh +101 -0
- core/hooks/nudge-reuse-first.sh +68 -0
- core/hooks/nudge-task-discovery.sh +81 -0
- core/hooks/nudge-thinking-os.sh +109 -0
- core/hooks/pr-reap.sh +23 -0
- core/hooks/reclaim-sweep.sh +58 -0
- core/hooks/record-verify-auto.sh +77 -0
- core/hooks/record-verify.sh +74 -0
- core/hooks/regen-reminder.sh +104 -0
- core/hooks/registry.yaml +1262 -0
- core/hooks/remind-daily.sh +27 -0
- core/hooks/remind-dogfood.sh +70 -0
- core/hooks/remind-learn-validate.sh +94 -0
- core/hooks/rules-primer.sh +50 -0
- core/hooks/search-enforce-inventory.sh +108 -0
- core/hooks/search-verify-remaining.sh +132 -0
- core/hooks/session-context.sh +729 -0
- core/hooks/session-end.sh +145 -0
- core/hooks/session-skill-primer.sh +43 -0
- core/hooks/snapshot-transcript.sh +56 -0
- core/hooks/sync-task-current.sh +85 -0
- core/hooks/test-first-reminder.sh +120 -0
- core/hooks/test-governor.sh +173 -0
- core/hooks/thinking_os-gate.sh +52 -0
- core/hooks/track-backtrack.sh +35 -0
- core/hooks/track-discovery.sh +121 -0
- core/hooks/track-skill.sh +53 -0
- core/hooks/validate-task-frontmatter.sh +49 -0
- core/hooks/verify-rename-callers.sh +119 -0
- core/hooks/warn-abandoned-task.sh +99 -0
- core/hooks/warn-destructive-edit.sh +64 -0
- core/hooks/warn-diff-size.sh +43 -0
- core/hooks/warn-graph-empty.sh +81 -0
- core/hooks/warn-mcp-down.sh +190 -0
- core/hooks/write-state.sh +55 -0
- core/logging_os/__init__.py +33 -0
- core/logging_os/api.py +127 -0
- core/logging_os/bridge.py +80 -0
- core/logging_os/config.py +172 -0
- core/logging_os/fingerprint.py +25 -0
- core/logging_os/redact.py +53 -0
- core/logging_os/render.py +83 -0
- core/logging_os/sinks.py +164 -0
- core/rules/anti-overengineering.md +44 -0
- core/rules/api-contract-discipline.md +41 -0
- core/rules/dimension-registry.md +155 -0
- core/rules/git-workflow.md +57 -0
- core/rules/memory.md +46 -0
- core/rules/model-routing.md +22 -0
- core/rules/skill-enforcement.md +75 -0
- core/rules/test-discipline.md +38 -0
- core/rules/thinking_os.md +48 -0
- core/rules/transparency-banner.md +37 -0
- core/runtime_paths.yaml +36 -0
- core/scaffold_manifest.json +14430 -0
- core/scheduled/__init__.py +0 -0
- core/scheduled/_activity.py +126 -0
- core/scheduled/_state.py +113 -0
- core/scheduled/config.py +86 -0
- core/scheduled/dep_reconcile.py +135 -0
- core/scheduled/error_sweep.py +137 -0
- core/scheduled/nightly.py +930 -0
- core/scheduled/responsive_extract.py +65 -0
- core/schemas/adapter.schema.json +269 -0
- core/schemas/preset.schema.json +50 -0
- core/schemas/skill.schema.json +81 -0
- core/schemas/stack.schema.json +404 -0
- core/scripts/_lib.sh +9 -0
- core/scripts/docs-lint.sh +228 -0
- core/scripts/docs-nav-fix.sh +133 -0
- core/scripts/docs-staleness-check.sh +154 -0
- core/scripts/install-adapter.sh +266 -0
- core/scripts/link-stack-skills.sh +52 -0
- core/scripts/log-latest.sh +106 -0
- core/scripts/log-search.sh +89 -0
- core/scripts/log-write.sh +134 -0
- core/scripts/ref-resolve.sh +71 -0
- core/skills/a11y/SKILL.md +305 -0
- core/skills/a11y/assets/a11y-checklist.md +137 -0
- core/skills/a11y/references/aria-and-focus.md +247 -0
- core/skills/a11y/references/rn-accessibility.md +343 -0
- core/skills/a11y/references/screen-reader-testing.md +190 -0
- core/skills/agent-memory/SKILL.md +191 -0
- core/skills/agent-memory/assets/memory-checklist.md +21 -0
- core/skills/agent-memory/references/memory-recipes.md +57 -0
- core/skills/api-design/SKILL.md +232 -0
- core/skills/api-design/assets/api-design-checklist.md +110 -0
- core/skills/api-design/references/error-envelope.md +381 -0
- core/skills/api-design/references/idempotency-pagination.md +312 -0
- core/skills/api-design/references/rest-contracts.md +426 -0
- core/skills/auth-patterns/SKILL.md +352 -0
- core/skills/auth-patterns/assets/auth-checklist.md +118 -0
- core/skills/auth-patterns/references/jwt-and-service-tokens.md +343 -0
- core/skills/auth-patterns/references/passkeys-2fa.md +289 -0
- core/skills/auth-patterns/references/sessions-vs-jwt.md +230 -0
- core/skills/auth-patterns/scripts/cookie-flag-check.py +146 -0
- core/skills/backend-fundamentals/SKILL.md +238 -0
- core/skills/backend-fundamentals/assets/backend-checklist.md +27 -0
- core/skills/backend-fundamentals/references/backend-patterns.md +56 -0
- core/skills/backend-fundamentals/scripts/check_layering.py +83 -0
- core/skills/clean-code/SKILL.md +642 -0
- core/skills/clean-code/scripts/audit-fail-closed.py +167 -0
- core/skills/codebase-explorer/SKILL.md +89 -0
- core/skills/codebase-explorer/assets/reading-checklist.md +24 -0
- core/skills/codebase-explorer/references/reading-strategies.md +52 -0
- core/skills/codebase-explorer/scripts/outline.py +99 -0
- core/skills/db-design/SKILL.md +327 -0
- core/skills/db-design/assets/migration-template.sql +49 -0
- core/skills/db-design/references/migration-discipline.md +290 -0
- core/skills/db-design/references/postgres-patterns.md +340 -0
- core/skills/db-design/scripts/migration-safety.sh +150 -0
- core/skills/deployment-cicd/SKILL.md +260 -0
- core/skills/deployment-cicd/assets/deploy-checklist.md +26 -0
- core/skills/deployment-cicd/references/pipeline-and-release.md +54 -0
- core/skills/deployment-cicd/scripts/lint_workflow.py +79 -0
- core/skills/docker/SKILL.md +114 -0
- core/skills/docker/assets/dockerfile-checklist.md +31 -0
- core/skills/docker/references/compose-patterns.md +66 -0
- core/skills/docker/references/dockerfile-optimization.md +64 -0
- core/skills/docker/scripts/lint_dockerfile.sh +48 -0
- core/skills/docker/versions.json +16 -0
- core/skills/end-to-end-testing/SKILL.md +101 -0
- core/skills/end-to-end-testing/assets/e2e-checklist.md +23 -0
- core/skills/end-to-end-testing/references/maestro.md +63 -0
- core/skills/end-to-end-testing/references/playwright.md +68 -0
- core/skills/end-to-end-testing/scripts/lint_e2e.py +88 -0
- core/skills/end-to-end-testing/versions.json +16 -0
- core/skills/frontend-design/SKILL.md +76 -0
- core/skills/frontend-design/assets/design-checklist.md +29 -0
- core/skills/frontend-design/references/design-principles.md +65 -0
- core/skills/frontend-design/scripts/check_contrast.py +89 -0
- core/skills/frontend-fundamentals/SKILL.md +213 -0
- core/skills/frontend-fundamentals/assets/frontend-checklist.md +25 -0
- core/skills/frontend-fundamentals/references/rendering-and-state.md +66 -0
- core/skills/frontend-fundamentals/scripts/check_frontend.py +86 -0
- core/skills/graph-explorer/SKILL.md +215 -0
- core/skills/graph-explorer/scripts/explain-impact.sh +64 -0
- core/skills/graphql/SKILL.md +187 -0
- core/skills/grpc-microservices/SKILL.md +174 -0
- core/skills/hexagonal-architecture/SKILL.md +199 -0
- core/skills/hexagonal-architecture/assets/folder-scaffold.md +233 -0
- core/skills/hexagonal-architecture/references/anti-patterns.md +129 -0
- core/skills/hexagonal-architecture/references/go-fiber-layout.md +429 -0
- core/skills/hexagonal-architecture/references/python-fastapi-layout.md +453 -0
- core/skills/hexagonal-architecture/references/react-native-layout.md +428 -0
- core/skills/i18n/SKILL.md +126 -0
- core/skills/incident-response/SKILL.md +225 -0
- core/skills/incident-response/assets/incident-checklist.md +29 -0
- core/skills/incident-response/references/severity-and-runbook.md +53 -0
- core/skills/incident-response/scripts/classify_severity.py +86 -0
- core/skills/linux-sysadmin/SKILL.md +115 -0
- core/skills/linux-sysadmin/assets/hardening-checklist.md +29 -0
- core/skills/linux-sysadmin/references/ssh-hardening.md +62 -0
- core/skills/linux-sysadmin/references/systemd-and-services.md +77 -0
- core/skills/linux-sysadmin/scripts/triage.sh +50 -0
- core/skills/linux-sysadmin/versions.json +17 -0
- core/skills/llm-patterns/SKILL.md +410 -0
- core/skills/llm-patterns/assets/llm-feature-checklist.md +26 -0
- core/skills/llm-patterns/references/rag-and-evals.md +59 -0
- core/skills/llm-patterns/scripts/estimate_tokens.py +75 -0
- core/skills/messaging-queues/SKILL.md +142 -0
- core/skills/mobile-fundamentals/SKILL.md +406 -0
- core/skills/mobile-fundamentals/assets/mobile-launch-checklist.md +130 -0
- core/skills/mobile-fundamentals/references/navigation-and-deep-links.md +337 -0
- core/skills/mobile-fundamentals/references/offline-sync.md +339 -0
- core/skills/node-backend/SKILL.md +114 -0
- core/skills/node-backend/assets/node-checklist.md +25 -0
- core/skills/node-backend/references/async-and-errors.md +67 -0
- core/skills/node-backend/references/event-loop.md +65 -0
- core/skills/node-backend/scripts/check_package.py +78 -0
- core/skills/node-backend/versions.json +17 -0
- core/skills/observability/SKILL.md +289 -0
- core/skills/observability/assets/observability-checklist.md +27 -0
- core/skills/observability/references/instrumentation.md +57 -0
- core/skills/observability/scripts/lint_logging.py +77 -0
- core/skills/payments/SKILL.md +102 -0
- core/skills/performance/SKILL.md +305 -0
- core/skills/performance/assets/perf-checklist.md +143 -0
- core/skills/performance/references/mobile-performance.md +249 -0
- core/skills/performance/references/web-vitals.md +209 -0
- core/skills/php/SKILL.md +116 -0
- core/skills/php/assets/php-checklist.md +26 -0
- core/skills/php/references/modern-php.md +64 -0
- core/skills/php/references/security.md +72 -0
- core/skills/php/scripts/scan_php_smells.py +99 -0
- core/skills/php/versions.json +9 -0
- core/skills/pr-mode-driver/SKILL.md +65 -0
- core/skills/realtime-websockets/SKILL.md +152 -0
- core/skills/redis/SKILL.md +105 -0
- core/skills/redis/assets/redis-checklist.md +27 -0
- core/skills/redis/references/operations.md +66 -0
- core/skills/redis/references/patterns.md +62 -0
- core/skills/redis/scripts/analyze_info.py +101 -0
- core/skills/redis/versions.json +9 -0
- core/skills/search/SKILL.md +91 -0
- core/skills/search/references/grep.md +76 -0
- core/skills/search/scripts/verify-count.sh +73 -0
- core/skills/search-infra/SKILL.md +109 -0
- core/skills/security-mobile/SKILL.md +394 -0
- core/skills/security-mobile/assets/mobile-security-checklist.md +117 -0
- core/skills/security-mobile/references/masvs-l1-checklist.md +127 -0
- core/skills/security-web/SKILL.md +217 -0
- core/skills/security-web/assets/security-web-checklist.md +167 -0
- core/skills/security-web/references/owasp-top-10.md +551 -0
- core/skills/security-web/references/supply-chain.md +179 -0
- core/skills/security-web/scripts/csp-check.sh +153 -0
- core/skills/shell-scripting/SKILL.md +128 -0
- core/skills/shell-scripting/assets/script-checklist.md +33 -0
- core/skills/shell-scripting/references/argument-parsing.md +84 -0
- core/skills/shell-scripting/references/bash-robustness.md +78 -0
- core/skills/shell-scripting/scripts/lint_script.sh +53 -0
- core/skills/shell-scripting/scripts/new_script.py +131 -0
- core/skills/shell-scripting/versions.json +23 -0
- core/skills/sql-authoring/SKILL.md +104 -0
- core/skills/sql-authoring/assets/query-review-checklist.md +26 -0
- core/skills/sql-authoring/references/query-patterns.md +89 -0
- core/skills/sql-authoring/references/reading-explain.md +52 -0
- core/skills/sql-authoring/scripts/analyze_plan.py +100 -0
- core/skills/sql-authoring/versions.json +17 -0
- core/skills/state-management/SKILL.md +428 -0
- core/skills/state-management/references/tanstack-query-recipes.md +301 -0
- core/skills/state-management/references/zustand-recipes.md +364 -0
- core/skills/supabase/SKILL.md +108 -0
- core/skills/supabase/assets/supabase-checklist.md +26 -0
- core/skills/supabase/references/realtime-and-storage.md +55 -0
- core/skills/supabase/references/rls-and-auth.md +65 -0
- core/skills/supabase/scripts/check_rls.py +88 -0
- core/skills/supabase/versions.json +9 -0
- core/skills/task-driver/SKILL.md +272 -0
- core/skills/task-driver/scripts/task-lint.sh +163 -0
- core/skills/technical-writing/SKILL.md +85 -0
- core/skills/technical-writing/assets/doc-checklist.md +29 -0
- core/skills/technical-writing/references/doc-anatomy.md +53 -0
- core/skills/technical-writing/references/writing-craft.md +59 -0
- core/skills/technical-writing/scripts/new_doc.py +81 -0
- core/skills/terraform-k8s/SKILL.md +133 -0
- core/skills/testing-strategy/SKILL.md +266 -0
- core/skills/testing-strategy/assets/test-review-checklist.md +25 -0
- core/skills/testing-strategy/references/test-types.md +52 -0
- core/skills/testing-strategy/scripts/coverage_gate.py +79 -0
- core/skills/thinking_os/SKILL.md +288 -0
- core/skills/thinking_os/scripts/classify.sh +122 -0
- core/skills/typescript/SKILL.md +110 -0
- core/skills/typescript/assets/typescript-checklist.md +24 -0
- core/skills/typescript/references/strictness.md +53 -0
- core/skills/typescript/references/type-system.md +86 -0
- core/skills/typescript/scripts/check_tsconfig.py +92 -0
- core/skills/typescript/versions.json +9 -0
- core/subsystems.yaml +202 -0
- core/thinking_os/__init__.py +1 -0
- core/thinking_os/_agent_markers.py +32 -0
- core/thinking_os/agents/README.md +71 -0
- core/thinking_os/agents/analyst.md +139 -0
- core/thinking_os/agents/architect.md +127 -0
- core/thinking_os/agents/debugger.md +111 -0
- core/thinking_os/agents/deployer.md +112 -0
- core/thinking_os/agents/distiller.md +28 -0
- core/thinking_os/agents/documenter.md +101 -0
- core/thinking_os/agents/implementer.md +135 -0
- core/thinking_os/agents/internal/session_observer.md +39 -0
- core/thinking_os/agents/observer.md +100 -0
- core/thinking_os/agents/onboarder.md +81 -0
- core/thinking_os/agents/refactorer.md +123 -0
- core/thinking_os/agents/repairer.md +47 -0
- core/thinking_os/agents/researcher.md +129 -0
- core/thinking_os/agents/reviewer.md +147 -0
- core/thinking_os/agents/security_auditor.md +133 -0
- core/thinking_os/background.py +405 -0
- core/thinking_os/bootstrap_outcomes.py +200 -0
- core/thinking_os/budget.py +302 -0
- core/thinking_os/capture.py +495 -0
- core/thinking_os/cognition.py +516 -0
- core/thinking_os/cognition_schemas.py +517 -0
- core/thinking_os/compress.py +192 -0
- core/thinking_os/concepts.py +233 -0
- core/thinking_os/dashboard.py +159 -0
- core/thinking_os/database.py +2883 -0
- core/thinking_os/decay.py +393 -0
- core/thinking_os/digest.py +295 -0
- core/thinking_os/dispatcher.py +192 -0
- core/thinking_os/dispatcher_helpers.py +48 -0
- core/thinking_os/dispatchers/__init__.py +3 -0
- core/thinking_os/dispatchers/default.py +47 -0
- core/thinking_os/distill.py +192 -0
- core/thinking_os/doc_indexer.py +905 -0
- core/thinking_os/embeddings.py +943 -0
- core/thinking_os/formula_composer.py +556 -0
- core/thinking_os/gate_marker.py +75 -0
- core/thinking_os/graph.py +296 -0
- core/thinking_os/graph_indexer.py +360 -0
- core/thinking_os/health_check.py +517 -0
- core/thinking_os/impact.py +119 -0
- core/thinking_os/memory_gc.py +356 -0
- core/thinking_os/migrator_embeddings.py +318 -0
- core/thinking_os/precision.py +194 -0
- core/thinking_os/presets/registry.yaml +127 -0
- core/thinking_os/record_outcome.py +399 -0
- core/thinking_os/repair.py +105 -0
- core/thinking_os/retrieval_quality.py +239 -0
- core/thinking_os/roles/analyst.yaml +83 -0
- core/thinking_os/roles/architect.yaml +88 -0
- core/thinking_os/roles/debugger.yaml +71 -0
- core/thinking_os/roles/deployer.yaml +71 -0
- core/thinking_os/roles/documenter.yaml +72 -0
- core/thinking_os/roles/implementer.yaml +83 -0
- core/thinking_os/roles/observer.yaml +70 -0
- core/thinking_os/roles/refactorer.yaml +71 -0
- core/thinking_os/roles/researcher.yaml +72 -0
- core/thinking_os/roles/reviewer.yaml +79 -0
- core/thinking_os/roles/security_auditor.yaml +83 -0
- core/thinking_os/roles_state.py +168 -0
- core/thinking_os/sanitizer.py +320 -0
- core/thinking_os/server.py +3160 -0
- core/thinking_os/session_enrich.py +272 -0
- core/thinking_os/session_observe_worker.py +111 -0
- core/thinking_os/session_startup.py +74 -0
- core/thinking_os/session_summary.py +245 -0
- core/thinking_os/situations/registry.yaml +99 -0
- core/thinking_os/task_analyzer.py +462 -0
- core/thinking_os/task_parser.py +342 -0
- core/thinking_os/task_sync.py +73 -0
- core/thinking_os/tools/__init__.py +6 -0
- core/thinking_os/tools/_shared.py +947 -0
- core/thinking_os/tools/cognition.py +1867 -0
- core/thinking_os/tools/docs.py +770 -0
- core/thinking_os/tools/learning.py +2078 -0
- core/thinking_os/tools/logs.py +79 -0
- core/thinking_os/tools/memory.py +840 -0
- core/thinking_os/tools/metrics.py +200 -0
- core/thinking_os/tools/retrieve.py +415 -0
- core/thinking_os/tools/routing.py +658 -0
- core/thinking_os/tools/tasks.py +449 -0
- core/thinking_os/tools/trajectory.py +181 -0
- core/thinking_os/tracing.py +235 -0
- core/web/__init__.py +5 -0
- core/web/_cache.py +118 -0
- core/web/_deps.py +56 -0
- core/web/_envelope.py +85 -0
- core/web/_project_context.py +140 -0
- core/web/chat_providers.py +108 -0
- core/web/init_jobs.py +216 -0
- core/web/routes/__init__.py +25 -0
- core/web/routes/_bounded_read.py +76 -0
- core/web/routes/board.py +1089 -0
- core/web/routes/cognition.py +1838 -0
- core/web/routes/config.py +635 -0
- core/web/routes/graph.py +513 -0
- core/web/routes/health.py +180 -0
- core/web/routes/hooks.py +288 -0
- core/web/routes/hub.py +1219 -0
- core/web/routes/logs.py +374 -0
- core/web/routes/metrics.py +43 -0
- core/web/routes/observability.py +400 -0
- core/web/routes/patterns.py +227 -0
- core/web/routes/presence.py +609 -0
- core/web/routes/roles.py +446 -0
- core/web/routes/scheduled.py +261 -0
- core/web/routes/search.py +238 -0
- core/web/routes/sessions.py +220 -0
- core/web/routes/settings.py +363 -0
- core/web/routes/stream.py +547 -0
- core/web/security.py +157 -0
- core/web/server.py +283 -0
- graph_os/__init__.py +29 -0
- graph_os/backend.py +233 -0
- graph_os/backends/__init__.py +13 -0
- graph_os/backends/sqlite_backend.py +1053 -0
- graph_os/communities.py +410 -0
- graph_os/enterprise.py +218 -0
- graph_os/entry_points.py +226 -0
- graph_os/extractors/__init__.py +25 -0
- graph_os/extractors/code_generic.py +914 -0
- graph_os/extractors/code_go.py +1422 -0
- graph_os/extractors/code_json.py +340 -0
- graph_os/extractors/code_php.py +979 -0
- graph_os/extractors/code_python.py +1454 -0
- graph_os/extractors/code_shell.py +538 -0
- graph_os/extractors/code_toml.py +302 -0
- graph_os/extractors/code_ts.py +1665 -0
- graph_os/extractors/code_yaml.py +394 -0
- graph_os/extractors/contracts.py +1592 -0
- graph_os/extractors/md_links.py +890 -0
- graph_os/extractors/task_deps.py +345 -0
- graph_os/groups/__init__.py +22 -0
- graph_os/groups/cross_repo.py +156 -0
- graph_os/groups/manifest.py +141 -0
- graph_os/ingest/__init__.py +19 -0
- graph_os/ingest/base.py +306 -0
- graph_os/ingest/github.py +112 -0
- graph_os/ingest/zip.py +95 -0
- graph_os/toolchain.py +393 -0
- graph_os/tools/__init__.py +9 -0
- graph_os/tools/graph.py +5573 -0
- graph_os/tools/reindex_dispatch.py +730 -0
- graph_os/tree_sitter_overlay.py +235 -0
- graph_os/types.py +252 -0
- graph_os/vec_index.py +277 -0
- graph_os/viewer/__init__.py +12 -0
- graph_os/viewer/exporter.py +93 -0
- graph_os/viewer/template.py +189 -0
- scheduled/__init__.py +0 -0
- scheduled/_activity.py +126 -0
- scheduled/_state.py +113 -0
- scheduled/config.py +86 -0
- scheduled/dep_reconcile.py +135 -0
- scheduled/error_sweep.py +137 -0
- scheduled/nightly.py +930 -0
- scheduled/responsive_extract.py +65 -0
- scripts/__init__.py +4 -0
- scripts/_commit_msg_body.sh +31 -0
- scripts/_post_commit_body.sh +49 -0
- scripts/_pre_commit_body.sh +121 -0
- scripts/_prepare_commit_msg_body.sh +53 -0
- scripts/audit_mcp_tools.py +693 -0
- scripts/bench_sdk_dispatcher.py +177 -0
- scripts/capture_golden.py +169 -0
- scripts/check_graph_phantoms.py +75 -0
- scripts/dev/audit_doc_links.py +359 -0
- scripts/dev/audit_scaffold_module_tags.py +107 -0
- scripts/dev/backfill_doc_headers.py +328 -0
- scripts/dev/backfill_nav_lines.py +119 -0
- scripts/dev/fix_nav_placement.py +106 -0
- scripts/dev/inspect_sdk_options.py +45 -0
- scripts/dev/migrate_check_ids.py +170 -0
- scripts/dev/strip_purpose_blocks.py +154 -0
- scripts/dump_openapi.py +66 -0
- scripts/e2e_dispatch_tool.py +195 -0
- scripts/generate_manifest.py +166 -0
- scripts/golden_sections.py +20 -0
- scripts/graph_demo.py +161 -0
- scripts/install-git-hooks.sh +47 -0
- scripts/migrate_embeddings_minilm_to_bge_m3.py +84 -0
- scripts/operational_eval.py +445 -0
- scripts/probe_agent_session_resolver.py +59 -0
- scripts/prune_deleted_path.py +127 -0
- scripts/refactor_agent_dual_mode.py +171 -0
- scripts/refresh_skill_versions.py +302 -0
- scripts/regen_doc_index.py +209 -0
- scripts/regen_doctor_schema.py +62 -0
- scripts/regen_rules.py +94 -0
- scripts/rename_formulas_to_semantic.py +241 -0
- scripts/smoke_db_connections.py +183 -0
- scripts/smoke_doc_header.py +72 -0
- scripts/smoke_graph_e2e.py +374 -0
- scripts/smoke_sdk_dispatch.py +84 -0
- scripts/smoke_uid_resolver.py +164 -0
- scripts/verify_dispatchers.py +244 -0
- scripts/verify_phase_c_e2e.py +436 -0
- templates/__init__.py +6 -0
- templates/_base/Makefile.base +353 -0
- templates/_base/base.yaml +59 -0
- templates/_base/coding-os.yaml.template +39 -0
- templates/_base/dimension-registry.template.md +68 -0
- templates/_base/domain-config.template.json +46 -0
- templates/_base/fragments/anatomy-map.md.tmpl +11 -0
- templates/_base/fragments/context-discipline.md.tmpl +3 -0
- templates/_base/fragments/core-loop.md.tmpl +52 -0
- templates/_base/fragments/engineering-routing.md.tmpl +3 -0
- templates/_base/fragments/header.md.tmpl +6 -0
- templates/_base/fragments/identity.md.tmpl +3 -0
- templates/_base/fragments/principles.md.tmpl +3 -0
- templates/_base/fragments/retrieval-routing.md.tmpl +23 -0
- templates/_base/fragments/session-handoff.md.tmpl +3 -0
- templates/_base/fragments/skills.md.tmpl +3 -0
- templates/_base/fragments/ssot-map.md.tmpl +3 -0
- templates/_base/fragments/stop-conditions.md.tmpl +3 -0
- templates/_base/fragments/subagent-dispatch.md.tmpl +3 -0
- templates/_base/fragments/task-authoring.md.tmpl +69 -0
- templates/_base/fragments/task-logging.md.tmpl +8 -0
- templates/_base/fragments/tool-routing.md.tmpl +9 -0
- templates/_base/fragments/verification-matrix.md.tmpl +12 -0
- templates/_base/lang/dart/analysis_options.yaml +7 -0
- templates/_base/lang/php/phpcs.xml.dist +10 -0
- templates/_base/lang/python/pyproject.toml +19 -0
- templates/_base/lang/rust/clippy.toml +5 -0
- templates/_base/lang/rust/rustfmt.toml +3 -0
- templates/_base/lang/typescript/eslint.config.js +26 -0
- templates/_base/lang/typescript/tsconfig.json +15 -0
- templates/_base/lang/typescript/vitest.config.ts +10 -0
- templates/_base/scaffold/changes.log +1 -0
- templates/_base/scaffold/docs/00-index.md +55 -0
- templates/_base/scaffold/docs/_meta/feature-dependency-tree.md +30 -0
- templates/_base/scaffold/docs/_meta/foundation-map.md +56 -0
- templates/_base/scaffold/docs/_meta/questions.md +8 -0
- templates/_base/scaffold/docs/_meta/roadmap.md +33 -0
- templates/_base/scaffold/docs/api-contracts/00-index.md +58 -0
- templates/_base/scaffold/docs/api-contracts/error-format.md +58 -0
- templates/_base/scaffold/docs/architecture/00-index.md +42 -0
- templates/_base/scaffold/docs/architecture/adr/00-index.md +39 -0
- templates/_base/scaffold/docs/engineering/00-index.md +9 -0
- templates/_base/scaffold/docs/governance/00-index.md +55 -0
- templates/_base/scaffold/docs/governance/_templates/doc-cheat-sheet.md +202 -0
- templates/_base/scaffold/docs/governance/_templates/playbook-template.md +81 -0
- templates/_base/scaffold/docs/governance/_templates/post-mortem-template.md +85 -0
- templates/_base/scaffold/docs/governance/_templates/runbook-template.md +88 -0
- templates/_base/scaffold/docs/governance/_templates/security-review-template.md +111 -0
- templates/_base/scaffold/docs/governance/_templates/task-detail.md +61 -0
- templates/_base/scaffold/docs/governance/agent-workflow.md +101 -0
- templates/_base/scaffold/docs/governance/anatomy-contract.md +150 -0
- templates/_base/scaffold/docs/governance/critical-rules.md +224 -0
- templates/_base/scaffold/docs/governance/decision-records.md +58 -0
- templates/_base/scaffold/docs/governance/docs-first-protocol.md +157 -0
- templates/_base/scaffold/docs/governance/docs-system.md +151 -0
- templates/_base/scaffold/docs/governance/gdpr-compliance.md +66 -0
- templates/_base/scaffold/docs/governance/mcp-tool-inventory.md +112 -0
- templates/_base/scaffold/docs/governance/risk-register.md +26 -0
- templates/_base/scaffold/docs/governance/scaffold-boundary-contract.md +161 -0
- templates/_base/scaffold/docs/governance/task-lifecycle.md +125 -0
- templates/_base/scaffold/docs/governance/wrapper-derivation.md +50 -0
- templates/_base/scaffold/docs/insights/00-index.md +17 -0
- templates/_base/scaffold/docs/ops/00-index.md +59 -0
- templates/_base/scaffold/docs/ops/runbooks/00-index.md +9 -0
- templates/_base/scaffold/docs/playbooks/00-index.md +12 -0
- templates/_base/scaffold/docs/playbooks/research-validation.md +29 -0
- templates/_base/scaffold/docs/playbooks/security-review.md +41 -0
- templates/_base/scaffold/docs/prd/00-index.md +43 -0
- templates/_base/scaffold/docs/prd/01-snapshot-vision.md +56 -0
- templates/_base/scaffold/docs/workflow/workflow-guide.md +138 -0
- templates/_base/scaffold/src/shared/README.md +23 -0
- templates/_base/skill-enforcement.template.md +14 -0
- templates/_base/task-detail.template.md +61 -0
- templates/_presets/ai-saas.yaml +9 -0
- templates/_presets/django-next.yaml +8 -0
- templates/_presets/dotnet-react.yaml +8 -0
- templates/_presets/flutter-baas.yaml +8 -0
- templates/_presets/go-react.yaml +8 -0
- templates/_presets/hexagonal-product.yaml +16 -0
- templates/_presets/jamstack.yaml +8 -0
- templates/_presets/laravel-vue.yaml +8 -0
- templates/_presets/mean.yaml +8 -0
- templates/_presets/mern.yaml +9 -0
- templates/_presets/nest-angular.yaml +8 -0
- templates/_presets/nextjs-fastapi.yaml +10 -0
- templates/_presets/nuxt-fullstack.yaml +9 -0
- templates/_presets/pern.yaml +9 -0
- templates/_presets/rails-react.yaml +8 -0
- templates/_presets/rn-api.yaml +8 -0
- templates/_presets/rust-svelte.yaml +8 -0
- templates/_presets/spring-react.yaml +8 -0
- templates/_presets/t3-style.yaml +9 -0
- templates/_presets/tall.yaml +8 -0
- templates/_presets/wordpress-cms.yaml +8 -0
- templates/angular/rules/frontend.md +19 -0
- templates/angular/scaffold/docs/engineering/accessibility.md +46 -0
- templates/angular/scaffold/docs/engineering/angular-rules.md +35 -0
- templates/angular/scaffold/docs/playbooks/angular-app.md +42 -0
- templates/angular/scaffold/src/frontend/angular.json +52 -0
- templates/angular/scaffold/src/frontend/package.json +28 -0
- templates/angular/scaffold/src/frontend/src/app/app.component.ts +19 -0
- templates/angular/scaffold/src/frontend/src/app/app.config.ts +22 -0
- templates/angular/scaffold/src/frontend/src/app/app.routes.ts +8 -0
- templates/angular/scaffold/src/frontend/src/app/core/global-error-handler.ts +12 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.component.ts +14 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.spec.ts +16 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.ts +10 -0
- templates/angular/scaffold/src/frontend/src/index.html +11 -0
- templates/angular/scaffold/src/frontend/src/main.ts +9 -0
- templates/angular/scaffold/src/frontend/src/styles.css +14 -0
- templates/angular/scaffold/src/frontend/tsconfig.app.json +8 -0
- templates/angular/scaffold/src/frontend/tsconfig.json +27 -0
- templates/angular/scaffold/src/frontend/tsconfig.spec.json +8 -0
- templates/angular/scaffold-boundary.yaml +27 -0
- templates/angular/skills/angular/SKILL.md +79 -0
- templates/angular/skills/angular/references/anatomy.md +69 -0
- templates/angular/stack.yaml +75 -0
- templates/aspnet-core/rules/backend.md +20 -0
- templates/aspnet-core/scaffold/docs/engineering/aspnet-core-rules.md +36 -0
- templates/aspnet-core/scaffold/docs/playbooks/aspnet-core-service.md +40 -0
- templates/aspnet-core/scaffold/src/backend/Backend.csproj +11 -0
- templates/aspnet-core/scaffold/src/backend/Backend.sln +27 -0
- templates/aspnet-core/scaffold/src/backend/Common/ExceptionHandlingMiddleware.cs +35 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthEndpoints.cs +10 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthService.cs +9 -0
- templates/aspnet-core/scaffold/src/backend/Program.cs +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/Backend.Tests.csproj +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/HealthServiceTests.cs +15 -0
- templates/aspnet-core/scaffold-boundary.yaml +24 -0
- templates/aspnet-core/skills/aspnet-core/SKILL.md +80 -0
- templates/aspnet-core/skills/aspnet-core/references/anatomy.md +65 -0
- templates/aspnet-core/stack.yaml +68 -0
- templates/astro/rules/frontend.md +20 -0
- templates/astro/scaffold/docs/engineering/astro-rules.md +40 -0
- templates/astro/scaffold/docs/playbooks/astro-app.md +57 -0
- templates/astro/scaffold/docs/playbooks/content-seo.md +37 -0
- templates/astro/scaffold/src/frontend/astro.config.mjs +10 -0
- templates/astro/scaffold/src/frontend/package.json +23 -0
- templates/astro/scaffold/src/frontend/src/components/Greeting.astro +13 -0
- templates/astro/scaffold/src/frontend/src/content/posts/hello.md +10 -0
- templates/astro/scaffold/src/frontend/src/content.config.ts +19 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.test.ts +39 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.ts +30 -0
- templates/astro/scaffold/src/frontend/src/pages/api/health.ts +13 -0
- templates/astro/scaffold/src/frontend/src/pages/index.astro +21 -0
- templates/astro/scaffold/src/frontend/tsconfig.json +9 -0
- templates/astro/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/astro/scaffold-boundary.yaml +28 -0
- templates/astro/skills/astro/SKILL.md +71 -0
- templates/astro/skills/astro/references/anatomy.md +66 -0
- templates/astro/stack.yaml +75 -0
- templates/csharp-plain/scaffold/src/backend/Backend.csproj +12 -0
- templates/csharp-plain/scaffold/src/backend/Program.cs +1 -0
- templates/csharp-plain/scaffold-boundary.yaml +23 -0
- templates/csharp-plain/stack.yaml +50 -0
- templates/django/rules/backend.md +18 -0
- templates/django/scaffold/docs/engineering/anti-ambiguity.md +74 -0
- templates/django/scaffold/docs/engineering/backend-rules.md +133 -0
- templates/django/scaffold/docs/engineering/glossary.md +51 -0
- templates/django/scaffold/docs/engineering/logging-standards.md +107 -0
- templates/django/scaffold/docs/engineering/naming-conventions.md +68 -0
- templates/django/scaffold/docs/engineering/secrets-rotation-runbook.md +142 -0
- templates/django/scaffold/docs/playbooks/backend-api.md +119 -0
- templates/django/scaffold/src/backend/config/__init__.py +0 -0
- templates/django/scaffold/src/backend/config/settings.py +31 -0
- templates/django/scaffold/src/backend/config/urls.py +11 -0
- templates/django/scaffold/src/backend/config/wsgi.py +6 -0
- templates/django/scaffold/src/backend/manage.py +14 -0
- templates/django/scaffold/src/backend/pyproject.toml +37 -0
- templates/django/scaffold/src/backend/tests/test_health.py +4 -0
- templates/django/scaffold-boundary.yaml +25 -0
- templates/django/skills/python-django/SKILL.md +450 -0
- templates/django/skills/python-django/references/anatomy.md +117 -0
- templates/django/skills/python-django/scripts/new_endpoint.py +89 -0
- templates/django/stack.yaml +73 -0
- templates/fastapi/rules/backend.md +18 -0
- templates/fastapi/scaffold/docs/engineering/fastapi-rules.md +37 -0
- templates/fastapi/scaffold/docs/playbooks/fastapi-service.md +30 -0
- templates/fastapi/scaffold/src/backend/app/__init__.py +0 -0
- templates/fastapi/scaffold/src/backend/app/main.py +8 -0
- templates/fastapi/scaffold/src/backend/pyproject.toml +39 -0
- templates/fastapi/scaffold/src/backend/tests/test_health.py +11 -0
- templates/fastapi/scaffold-boundary.yaml +25 -0
- templates/fastapi/skills/python-fastapi/SKILL.md +75 -0
- templates/fastapi/skills/python-fastapi/references/anatomy.md +117 -0
- templates/fastapi/skills/python-fastapi/scripts/new_endpoint.py +101 -0
- templates/fastapi/stack.yaml +57 -0
- templates/flutter/rules/mobile.md +26 -0
- templates/flutter/scaffold/docs/engineering/flutter-rules.md +35 -0
- templates/flutter/scaffold/docs/playbooks/flutter-app.md +45 -0
- templates/flutter/scaffold/src/mobile/lib/core/error_mapper.dart +17 -0
- templates/flutter/scaffold/src/mobile/lib/core/router.dart +13 -0
- templates/flutter/scaffold/src/mobile/lib/main.dart +22 -0
- templates/flutter/scaffold/src/mobile/lib/screens/health_screen.dart +32 -0
- templates/flutter/scaffold/src/mobile/lib/services/health_service.dart +11 -0
- templates/flutter/scaffold/src/mobile/lib/state/health_provider.dart +13 -0
- templates/flutter/scaffold/src/mobile/pubspec.yaml +22 -0
- templates/flutter/scaffold/src/mobile/test/health_provider_test.dart +90 -0
- templates/flutter/scaffold-boundary.yaml +28 -0
- templates/flutter/skills/flutter/SKILL.md +76 -0
- templates/flutter/skills/flutter/references/anatomy.md +64 -0
- templates/flutter/stack.yaml +70 -0
- templates/go/rules/backend.md +19 -0
- templates/go/scaffold/docs/engineering/go-rules.md +45 -0
- templates/go/scaffold/docs/playbooks/go-service.md +30 -0
- templates/go/scaffold/src/backend/cmd/api/main.go +22 -0
- templates/go/scaffold/src/backend/cmd/api/main_test.go +20 -0
- templates/go/scaffold/src/backend/go.mod +3 -0
- templates/go/scaffold-boundary.yaml +25 -0
- templates/go/skills/go-patterns/SKILL.md +68 -0
- templates/go/skills/go-patterns/assets/go-checklist.md +29 -0
- templates/go/skills/go-patterns/references/anatomy.md +115 -0
- templates/go/skills/go-patterns/references/go-2026-idioms.md +92 -0
- templates/go/skills/go-patterns/scripts/new_endpoint.py +117 -0
- templates/go/skills/go-patterns/versions.json +16 -0
- templates/go/stack.yaml +54 -0
- templates/go-fiber/rules/backend.md +20 -0
- templates/go-fiber/scaffold/docs/engineering/fiber-rules.md +97 -0
- templates/go-fiber/scaffold/docs/playbooks/fiber-service.md +149 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main.go +21 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main_test.go +17 -0
- templates/go-fiber/scaffold/src/backend/go.mod +23 -0
- templates/go-fiber/scaffold/src/backend/go.sum +49 -0
- templates/go-fiber/scaffold-boundary.yaml +26 -0
- templates/go-fiber/skills/go-fiber/SKILL.md +203 -0
- templates/go-fiber/skills/go-fiber/assets/fiber-checklist.md +26 -0
- templates/go-fiber/skills/go-fiber/references/anatomy.md +116 -0
- templates/go-fiber/skills/go-fiber/references/fiber-v3-patterns.md +89 -0
- templates/go-fiber/skills/go-fiber/scripts/new_endpoint.py +107 -0
- templates/go-fiber/skills/go-fiber/versions.json +16 -0
- templates/go-fiber/stack.yaml +59 -0
- templates/go-plain/scaffold/src/backend/go.mod +3 -0
- templates/go-plain/scaffold/src/backend/main.go +7 -0
- templates/go-plain/scaffold-boundary.yaml +22 -0
- templates/go-plain/stack.yaml +51 -0
- templates/java-plain/scaffold/src/backend/mvnw +302 -0
- templates/java-plain/scaffold/src/backend/pom.xml +41 -0
- templates/java-plain/scaffold/src/backend/src/main/java/com/example/app/Main.java +10 -0
- templates/java-plain/scaffold-boundary.yaml +23 -0
- templates/java-plain/stack.yaml +51 -0
- templates/laravel/rules/backend.md +20 -0
- templates/laravel/scaffold/docs/engineering/laravel-rules.md +28 -0
- templates/laravel/scaffold/docs/playbooks/laravel-service.md +28 -0
- templates/laravel/scaffold/src/backend/app/Exceptions/Handler.php +27 -0
- templates/laravel/scaffold/src/backend/app/Http/Controllers/HealthController.php +15 -0
- templates/laravel/scaffold/src/backend/app/Support/HealthStatus.php +14 -0
- templates/laravel/scaffold/src/backend/composer.json +24 -0
- templates/laravel/scaffold/src/backend/phpunit.xml +10 -0
- templates/laravel/scaffold/src/backend/public/index.php +8 -0
- templates/laravel/scaffold/src/backend/routes/api.php +7 -0
- templates/laravel/scaffold/src/backend/tests/Unit/HealthStatusTest.php +16 -0
- templates/laravel/scaffold-boundary.yaml +23 -0
- templates/laravel/skills/laravel/SKILL.md +56 -0
- templates/laravel/skills/laravel/references/anatomy.md +65 -0
- templates/laravel/stack.yaml +66 -0
- templates/meta/rules/graph-first.md +27 -0
- templates/meta/rules/hook-author.md +19 -0
- templates/meta/rules/mcp-tool-author.md +18 -0
- templates/meta/rules/meta-engineering.md +17 -0
- templates/meta/scaffold-boundary.yaml +55 -0
- templates/meta/skills/claude-sdk-integration/SKILL.md +163 -0
- templates/meta/skills/claude-sdk-integration/assets/sdk-checklist.md +27 -0
- templates/meta/skills/claude-sdk-integration/scripts/check_model_ids.py +97 -0
- templates/meta/skills/graph-os-authoring/SKILL.md +278 -0
- templates/meta/skills/graph-os-authoring/assets/graph-os-checklist.md +25 -0
- templates/meta/skills/graph-os-authoring/scripts/new_extractor.py +76 -0
- templates/meta/skills/hook-authoring/SKILL.md +292 -0
- templates/meta/skills/hook-authoring/assets/hook-checklist.md +30 -0
- templates/meta/skills/hook-authoring/scripts/new_hook.sh +75 -0
- templates/meta/skills/mcp-tool-authoring/SKILL.md +301 -0
- templates/meta/skills/mcp-tool-authoring/assets/mcp-tool-checklist.md +29 -0
- templates/meta/skills/mcp-tool-authoring/scripts/new_tool.py +74 -0
- templates/meta/skills/meta-engineering/SKILL.md +151 -0
- templates/meta/skills/meta-engineering/assets/meta-edit-checklist.md +28 -0
- templates/meta/skills/meta-engineering/scripts/which_layer.py +61 -0
- templates/meta/skills/python-meta-server/SKILL.md +162 -0
- templates/meta/skills/python-meta-server/assets/meta-server-checklist.md +28 -0
- templates/meta/skills/python-meta-server/scripts/check_envelope.py +91 -0
- templates/meta/skills/react-vite-hub/SKILL.md +140 -0
- templates/meta/skills/react-vite-hub/assets/hub-ui-checklist.md +23 -0
- templates/meta/skills/react-vite-hub/scripts/check_vite_env.py +73 -0
- templates/meta/stack.yaml +119 -0
- templates/nestjs/rules/backend.md +20 -0
- templates/nestjs/scaffold/docs/engineering/nestjs-rules.md +33 -0
- templates/nestjs/scaffold/docs/playbooks/nestjs-service.md +39 -0
- templates/nestjs/scaffold/src/backend/nest-cli.json +5 -0
- templates/nestjs/scaffold/src/backend/package.json +28 -0
- templates/nestjs/scaffold/src/backend/src/app.module.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/common/all-exceptions.filter.ts +59 -0
- templates/nestjs/scaffold/src/backend/src/health/health.controller.ts +14 -0
- templates/nestjs/scaffold/src/backend/src/health/health.module.ts +10 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.spec.ts +21 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/main.ts +26 -0
- templates/nestjs/scaffold/src/backend/tsconfig.json +16 -0
- templates/nestjs/scaffold/src/backend/vitest.config.ts +9 -0
- templates/nestjs/scaffold-boundary.yaml +25 -0
- templates/nestjs/skills/nestjs/SKILL.md +67 -0
- templates/nestjs/skills/nestjs/references/anatomy.md +65 -0
- templates/nestjs/stack.yaml +68 -0
- templates/nextjs/rules/frontend.md +18 -0
- templates/nextjs/scaffold/docs/design/00-index.md +23 -0
- templates/nextjs/scaffold/docs/design/colors-tokens.md +141 -0
- templates/nextjs/scaffold/docs/design/components-patterns.md +159 -0
- templates/nextjs/scaffold/docs/design/motion-accessibility.md +137 -0
- templates/nextjs/scaffold/docs/design/typography-spacing.md +107 -0
- templates/nextjs/scaffold/docs/engineering/accessibility-web.md +56 -0
- templates/nextjs/scaffold/docs/engineering/copywriting-standard.md +102 -0
- templates/nextjs/scaffold/docs/engineering/formatting-rules.md +89 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rendering-rules.md +80 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rules.md +183 -0
- templates/nextjs/scaffold/docs/engineering/i18n-policy.md +99 -0
- templates/nextjs/scaffold/docs/pages-content-spec/00-index.md +78 -0
- templates/nextjs/scaffold/docs/playbooks/content-seo.md +55 -0
- templates/nextjs/scaffold/docs/playbooks/docs-governance.md +51 -0
- templates/nextjs/scaffold/docs/playbooks/frontend-ui.md +63 -0
- templates/nextjs/scaffold/src/frontend/app/layout.tsx +14 -0
- templates/nextjs/scaffold/src/frontend/app/page.tsx +3 -0
- templates/nextjs/scaffold/src/frontend/eslint.config.js +17 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.test.ts +9 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.ts +3 -0
- templates/nextjs/scaffold/src/frontend/package.json +28 -0
- templates/nextjs/scaffold/src/frontend/tsconfig.json +18 -0
- templates/nextjs/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/nextjs/scaffold-boundary.yaml +30 -0
- templates/nextjs/skills/nextjs-react/SKILL.md +485 -0
- templates/nextjs/skills/nextjs-react/references/anatomy.md +116 -0
- templates/nextjs/skills/nextjs-react/scripts/new_component.py +72 -0
- templates/nextjs/stack.yaml +81 -0
- templates/node-express/rules/backend.md +20 -0
- templates/node-express/scaffold/docs/engineering/express-rules.md +30 -0
- templates/node-express/scaffold/docs/playbooks/express-service.md +35 -0
- templates/node-express/scaffold/src/backend/package.json +24 -0
- templates/node-express/scaffold/src/backend/src/index.ts +17 -0
- templates/node-express/scaffold/src/backend/src/middleware/error-handler.ts +12 -0
- templates/node-express/scaffold/src/backend/src/routes/health.test.ts +33 -0
- templates/node-express/scaffold/src/backend/src/routes/health.ts +7 -0
- templates/node-express/scaffold/src/backend/tsconfig.json +15 -0
- templates/node-express/scaffold/src/backend/types/express-bootstrap.d.ts +21 -0
- templates/node-express/scaffold-boundary.yaml +25 -0
- templates/node-express/skills/node-express/SKILL.md +70 -0
- templates/node-express/skills/node-express/references/anatomy.md +63 -0
- templates/node-express/stack.yaml +63 -0
- templates/python/scaffold/docs/engineering/python-rules.md +27 -0
- templates/python/scaffold/docs/playbooks/python-library.md +35 -0
- templates/python/stack.yaml +60 -0
- templates/rails/rules/backend.md +10 -0
- templates/rails/scaffold/docs/engineering/rails-rules.md +33 -0
- templates/rails/scaffold/docs/playbooks/rails-service.md +42 -0
- templates/rails/scaffold/src/backend/Gemfile +12 -0
- templates/rails/scaffold/src/backend/app/controllers/application_controller.rb +26 -0
- templates/rails/scaffold/src/backend/app/controllers/health_controller.rb +6 -0
- templates/rails/scaffold/src/backend/app/models/health.rb +6 -0
- templates/rails/scaffold/src/backend/config/application.rb +12 -0
- templates/rails/scaffold/src/backend/config/boot.rb +3 -0
- templates/rails/scaffold/src/backend/config/routes.rb +4 -0
- templates/rails/scaffold/src/backend/config.ru +5 -0
- templates/rails/scaffold/src/backend/spec/rails_helper.rb +18 -0
- templates/rails/scaffold/src/backend/spec/requests/health_spec.rb +24 -0
- templates/rails/scaffold-boundary.yaml +25 -0
- templates/rails/skills/rails/SKILL.md +62 -0
- templates/rails/skills/rails/references/anatomy.md +71 -0
- templates/rails/stack.yaml +72 -0
- templates/react-native/rules/mobile.md +26 -0
- templates/react-native/scaffold/docs/engineering/accessibility-mobile.md +95 -0
- templates/react-native/scaffold/docs/engineering/mobile-rules.md +56 -0
- templates/react-native/scaffold/docs/engineering/offline-first.md +61 -0
- templates/react-native/scaffold/docs/playbooks/mobile-app.md +49 -0
- templates/react-native/scaffold/src/mobile/App.tsx +9 -0
- templates/react-native/scaffold/src/mobile/eslint.config.js +17 -0
- templates/react-native/scaffold/src/mobile/package.json +23 -0
- templates/react-native/scaffold/src/mobile/src/greeting.test.ts +9 -0
- templates/react-native/scaffold/src/mobile/src/greeting.ts +3 -0
- templates/react-native/scaffold/src/mobile/tsconfig.json +17 -0
- templates/react-native/scaffold/src/mobile/vitest.config.ts +10 -0
- templates/react-native/scaffold-boundary.yaml +30 -0
- templates/react-native/skills/react-native-mobile/SKILL.md +119 -0
- templates/react-native/skills/react-native-mobile/assets/rn-mobile-checklist.md +28 -0
- templates/react-native/skills/react-native-mobile/references/anatomy.md +140 -0
- templates/react-native/skills/react-native-mobile/references/rn-2026-practices.md +54 -0
- templates/react-native/skills/react-native-mobile/scripts/new_screen.py +73 -0
- templates/react-native/skills/react-native-mobile/versions.json +16 -0
- templates/react-native/skills/react-native-patterns/SKILL.md +512 -0
- templates/react-native/skills/react-native-patterns/assets/rn-review-checklist.md +26 -0
- templates/react-native/skills/react-native-patterns/references/anatomy.md +62 -0
- templates/react-native/skills/react-native-patterns/references/list-performance.md +70 -0
- templates/react-native/skills/react-native-patterns/scripts/scan_rn_perf.py +77 -0
- templates/react-native/stack.yaml +70 -0
- templates/ruby-plain/scaffold/src/backend/Gemfile +8 -0
- templates/ruby-plain/scaffold/src/backend/main.rb +3 -0
- templates/ruby-plain/scaffold-boundary.yaml +23 -0
- templates/ruby-plain/stack.yaml +50 -0
- templates/rust-axum/rules/backend.md +20 -0
- templates/rust-axum/scaffold/docs/engineering/rust-axum-rules.md +35 -0
- templates/rust-axum/scaffold/docs/playbooks/rust-axum-service.md +43 -0
- templates/rust-axum/scaffold/src/backend/Cargo.toml +20 -0
- templates/rust-axum/scaffold/src/backend/src/app.rs +12 -0
- templates/rust-axum/scaffold/src/backend/src/error.rs +48 -0
- templates/rust-axum/scaffold/src/backend/src/main.rs +24 -0
- templates/rust-axum/scaffold/src/backend/src/routes/health.rs +37 -0
- templates/rust-axum/scaffold/src/backend/src/routes/mod.rs +2 -0
- templates/rust-axum/scaffold-boundary.yaml +25 -0
- templates/rust-axum/skills/rust/SKILL.md +73 -0
- templates/rust-axum/skills/rust/references/anatomy.md +63 -0
- templates/rust-axum/stack.yaml +65 -0
- templates/rust-plain/scaffold/src/backend/Cargo.toml +6 -0
- templates/rust-plain/scaffold/src/backend/src/main.rs +3 -0
- templates/rust-plain/scaffold-boundary.yaml +23 -0
- templates/rust-plain/stack.yaml +51 -0
- templates/spring-boot/rules/backend.md +20 -0
- templates/spring-boot/scaffold/docs/engineering/spring-boot-rules.md +35 -0
- templates/spring-boot/scaffold/docs/playbooks/spring-boot-service.md +45 -0
- templates/spring-boot/scaffold/src/backend/mvnw +302 -0
- templates/spring-boot/scaffold/src/backend/pom.xml +69 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/Application.java +16 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/common/GlobalExceptionHandler.java +31 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthController.java +22 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthService.java +12 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthStatus.java +4 -0
- templates/spring-boot/scaffold/src/backend/src/test/java/com/example/app/health/HealthServiceTest.java +15 -0
- templates/spring-boot/scaffold-boundary.yaml +26 -0
- templates/spring-boot/skills/spring-boot/SKILL.md +84 -0
- templates/spring-boot/skills/spring-boot/references/anatomy.md +63 -0
- templates/spring-boot/stack.yaml +65 -0
- templates/svelte-sveltekit/rules/frontend.md +20 -0
- templates/svelte-sveltekit/scaffold/docs/engineering/svelte-sveltekit-rules.md +37 -0
- templates/svelte-sveltekit/scaffold/docs/playbooks/svelte-sveltekit-app.md +36 -0
- templates/svelte-sveltekit/scaffold/src/frontend/package.json +22 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/app.html +12 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/hooks.server.ts +14 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/components/Greeting.svelte +6 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.test.ts +26 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.ts +4 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+layout.svelte +24 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.svelte +9 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/health/+server.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/svelte.config.js +10 -0
- templates/svelte-sveltekit/scaffold/src/frontend/tsconfig.json +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vite.config.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vitest.config.ts +11 -0
- templates/svelte-sveltekit/scaffold-boundary.yaml +29 -0
- templates/svelte-sveltekit/skills/svelte/SKILL.md +90 -0
- templates/svelte-sveltekit/skills/svelte/references/anatomy.md +62 -0
- templates/svelte-sveltekit/stack.yaml +70 -0
- templates/typescript-plain/scaffold/src/index.ts +3 -0
- templates/typescript-plain/scaffold/tsconfig.json +13 -0
- templates/typescript-plain/scaffold-boundary.yaml +22 -0
- templates/typescript-plain/stack.yaml +44 -0
- templates/vue-nuxt/rules/frontend.md +19 -0
- templates/vue-nuxt/scaffold/docs/engineering/nuxt-rules.md +30 -0
- templates/vue-nuxt/scaffold/docs/playbooks/nuxt-app.md +29 -0
- templates/vue-nuxt/scaffold/src/frontend/app.vue +3 -0
- templates/vue-nuxt/scaffold/src/frontend/nuxt.config.ts +11 -0
- templates/vue-nuxt/scaffold/src/frontend/package.json +20 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.test.ts +19 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.vue +11 -0
- templates/vue-nuxt/scaffold/src/frontend/vitest.config.ts +12 -0
- templates/vue-nuxt/scaffold-boundary.yaml +26 -0
- templates/vue-nuxt/skills/vue-nuxt/SKILL.md +57 -0
- templates/vue-nuxt/skills/vue-nuxt/references/anatomy.md +60 -0
- templates/vue-nuxt/stack.yaml +61 -0
- templates/wordpress/rules/backend.md +19 -0
- templates/wordpress/scaffold/docs/engineering/wordpress-rules.md +28 -0
- templates/wordpress/scaffold/docs/playbooks/wordpress-service.md +29 -0
- templates/wordpress/scaffold/src/backend/composer.json +17 -0
- templates/wordpress/scaffold/src/backend/phpcs.xml.dist +11 -0
- templates/wordpress/scaffold/src/backend/phpunit.xml +10 -0
- templates/wordpress/scaffold/src/backend/plugin/inc/health.php +8 -0
- templates/wordpress/scaffold/src/backend/plugin/plugin.php +28 -0
- templates/wordpress/scaffold/src/backend/tests/HealthStatusTest.php +15 -0
- templates/wordpress/scaffold/src/backend/theme/functions.php +18 -0
- templates/wordpress/scaffold/src/backend/theme/style.css +11 -0
- templates/wordpress/scaffold-boundary.yaml +23 -0
- templates/wordpress/skills/wordpress/SKILL.md +110 -0
- templates/wordpress/skills/wordpress/assets/wp-checklist.md +28 -0
- templates/wordpress/skills/wordpress/references/wp-development.md +75 -0
- templates/wordpress/skills/wordpress/references/wp-security.md +68 -0
- templates/wordpress/skills/wordpress/scripts/scan_wp_smells.py +91 -0
- templates/wordpress/skills/wordpress/versions.json +16 -0
- templates/wordpress/stack.yaml +60 -0
- thinking_os/__init__.py +1 -0
- thinking_os/_agent_markers.py +32 -0
- thinking_os/background.py +405 -0
- thinking_os/bootstrap_outcomes.py +200 -0
- thinking_os/budget.py +302 -0
- thinking_os/capture.py +495 -0
- thinking_os/cognition.py +516 -0
- thinking_os/cognition_schemas.py +517 -0
- thinking_os/compress.py +192 -0
- thinking_os/concepts.py +233 -0
- thinking_os/dashboard.py +159 -0
- thinking_os/database.py +2883 -0
- thinking_os/decay.py +393 -0
- thinking_os/digest.py +295 -0
- thinking_os/dispatcher.py +192 -0
- thinking_os/dispatcher_helpers.py +48 -0
- thinking_os/dispatchers/__init__.py +3 -0
- thinking_os/dispatchers/default.py +47 -0
- thinking_os/distill.py +192 -0
- thinking_os/doc_indexer.py +905 -0
- thinking_os/embeddings.py +943 -0
- thinking_os/formula_composer.py +556 -0
- thinking_os/gate_marker.py +75 -0
- thinking_os/graph.py +296 -0
- thinking_os/graph_indexer.py +360 -0
- thinking_os/health_check.py +517 -0
- thinking_os/impact.py +119 -0
- thinking_os/memory_gc.py +356 -0
- thinking_os/migrator_embeddings.py +318 -0
- thinking_os/precision.py +194 -0
- thinking_os/record_outcome.py +399 -0
- thinking_os/repair.py +105 -0
- thinking_os/retrieval_quality.py +239 -0
- thinking_os/roles_state.py +168 -0
- thinking_os/sanitizer.py +320 -0
- thinking_os/server.py +3160 -0
- thinking_os/session_enrich.py +272 -0
- thinking_os/session_observe_worker.py +111 -0
- thinking_os/session_startup.py +74 -0
- thinking_os/session_summary.py +245 -0
- thinking_os/task_analyzer.py +462 -0
- thinking_os/task_parser.py +342 -0
- thinking_os/task_sync.py +73 -0
- thinking_os/tools/__init__.py +6 -0
- thinking_os/tools/_shared.py +947 -0
- thinking_os/tools/cognition.py +1867 -0
- thinking_os/tools/docs.py +770 -0
- thinking_os/tools/learning.py +2078 -0
- thinking_os/tools/logs.py +79 -0
- thinking_os/tools/memory.py +840 -0
- thinking_os/tools/metrics.py +200 -0
- thinking_os/tools/retrieve.py +415 -0
- thinking_os/tools/routing.py +658 -0
- thinking_os/tools/tasks.py +449 -0
- thinking_os/tools/trajectory.py +181 -0
- thinking_os/tracing.py +235 -0
- web/__init__.py +5 -0
- web/_cache.py +118 -0
- web/_deps.py +56 -0
- web/_envelope.py +85 -0
- web/_project_context.py +140 -0
- web/chat_providers.py +108 -0
- web/init_jobs.py +216 -0
- web/routes/__init__.py +25 -0
- web/routes/_bounded_read.py +76 -0
- web/routes/board.py +1089 -0
- web/routes/cognition.py +1838 -0
- web/routes/config.py +635 -0
- web/routes/graph.py +513 -0
- web/routes/health.py +180 -0
- web/routes/hooks.py +288 -0
- web/routes/hub.py +1219 -0
- web/routes/logs.py +374 -0
- web/routes/metrics.py +43 -0
- web/routes/observability.py +400 -0
- web/routes/patterns.py +227 -0
- web/routes/presence.py +609 -0
- web/routes/roles.py +446 -0
- web/routes/scheduled.py +261 -0
- web/routes/search.py +238 -0
- web/routes/sessions.py +220 -0
- web/routes/settings.py +363 -0
- web/routes/stream.py +547 -0
- web/security.py +157 -0
- web/server.py +283 -0
|
@@ -0,0 +1,2078 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thinking OS — MCP learning tools.
|
|
3
|
+
|
|
4
|
+
Pattern mining and rule suggestion:
|
|
5
|
+
- cos_learn_extract: discover patterns from task outcomes
|
|
6
|
+
- cos_learn_suggest: return relevant patterns for current context
|
|
7
|
+
- cos_learn_validate: confirm/deny a pattern's usefulness
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sqlite3
|
|
18
|
+
from datetime import datetime, timedelta, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("thinking_os.learning")
|
|
23
|
+
|
|
24
|
+
MIN_DATA_THRESHOLD = 3 # minimum task outcomes before extraction
|
|
25
|
+
|
|
26
|
+
# self-validation throttle window. Same (session, pattern)
|
|
27
|
+
# positive validation is ignored within this window. 1h is long enough to
|
|
28
|
+
# cover a continuous task loop but short enough that legitimate re-use
|
|
29
|
+
# across sessions isn't suppressed.
|
|
30
|
+
_THROTTLE_WINDOW_SECONDS = 3600
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_session_id_for_validate() -> str:
|
|
34
|
+
import os
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
state_dir = Path(os.environ.get("COS_STATE_DIR", ".coding-os"))
|
|
38
|
+
agent_dir_env = os.environ.get("COS_AGENT_DIR")
|
|
39
|
+
if agent_dir_env:
|
|
40
|
+
f = Path(agent_dir_env) / "session-id"
|
|
41
|
+
if f.exists():
|
|
42
|
+
sid = f.read_text().strip()
|
|
43
|
+
if sid:
|
|
44
|
+
return sid
|
|
45
|
+
agent = os.environ.get("COS_AGENT", "")
|
|
46
|
+
if not agent:
|
|
47
|
+
marker = state_dir / ".agent"
|
|
48
|
+
if marker.exists():
|
|
49
|
+
agent = marker.read_text().strip()
|
|
50
|
+
if agent:
|
|
51
|
+
f = state_dir / agent / "session-id"
|
|
52
|
+
if f.exists():
|
|
53
|
+
sid = f.read_text().strip()
|
|
54
|
+
if sid:
|
|
55
|
+
return sid
|
|
56
|
+
flat = state_dir / "session-id"
|
|
57
|
+
if flat.exists():
|
|
58
|
+
sid = flat.read_text().strip()
|
|
59
|
+
if sid:
|
|
60
|
+
return sid
|
|
61
|
+
return "ses-unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_recent_validation(
|
|
65
|
+
conn: sqlite3.Connection,
|
|
66
|
+
session_id: str,
|
|
67
|
+
pattern_id: int,
|
|
68
|
+
) -> bool:
|
|
69
|
+
try:
|
|
70
|
+
row = conn.execute(
|
|
71
|
+
"SELECT 1 FROM pattern_validations "
|
|
72
|
+
"WHERE session_id = ? AND pattern_id = ? AND was_helpful = 1 "
|
|
73
|
+
" AND created_at >= datetime('now', '-' || ? || ' seconds') "
|
|
74
|
+
"LIMIT 1",
|
|
75
|
+
(session_id, pattern_id, _THROTTLE_WINDOW_SECONDS),
|
|
76
|
+
).fetchone()
|
|
77
|
+
except sqlite3.OperationalError:
|
|
78
|
+
return False
|
|
79
|
+
return row is not None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _log_validation(
|
|
83
|
+
conn: sqlite3.Connection,
|
|
84
|
+
*,
|
|
85
|
+
session_id: str,
|
|
86
|
+
pattern_id: int,
|
|
87
|
+
was_helpful: bool,
|
|
88
|
+
was_throttled: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
# Fire-and-forget — never raises (audit row, must not break validation).
|
|
91
|
+
try:
|
|
92
|
+
conn.execute(
|
|
93
|
+
"INSERT INTO pattern_validations "
|
|
94
|
+
"(session_id, pattern_id, was_helpful, was_throttled) "
|
|
95
|
+
"VALUES (?, ?, ?, ?)",
|
|
96
|
+
(session_id, pattern_id, 1 if was_helpful else 0, 1 if was_throttled else 0),
|
|
97
|
+
)
|
|
98
|
+
conn.commit()
|
|
99
|
+
except sqlite3.OperationalError as exc:
|
|
100
|
+
logger.debug("_log_validation skipped: %s", exc)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Confidence formulas (brain-inspired)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def boost_success(conf: float) -> float:
|
|
109
|
+
"""LTP with diminishing returns — validated pattern gets stronger."""
|
|
110
|
+
return min(0.95, conf + 0.1 * (1.0 - conf))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def penalize_failure(conf: float) -> float:
|
|
114
|
+
"""LTD proportional — violated pattern weakens."""
|
|
115
|
+
return max(0.1, conf - 0.15 * conf)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# cos_learn_extract
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def learn_extract(
|
|
124
|
+
conn: sqlite3.Connection,
|
|
125
|
+
*,
|
|
126
|
+
min_occurrences: int = 3,
|
|
127
|
+
) -> dict:
|
|
128
|
+
"""Scan task_outcomes to discover recurring patterns.
|
|
129
|
+
|
|
130
|
+
Detects:
|
|
131
|
+
- domain_rework: domains with high rework rates
|
|
132
|
+
- skill_correlation: skills correlated with success/failure
|
|
133
|
+
- complexity_mismatch: tasks classified too low/high
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
conn: SQLite connection.
|
|
137
|
+
min_occurrences: Minimum occurrences to consider a pattern.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dict with extracted patterns and stats.
|
|
141
|
+
"""
|
|
142
|
+
min_occurrences = max(1, min_occurrences)
|
|
143
|
+
|
|
144
|
+
# Check data threshold
|
|
145
|
+
total_outcomes = conn.execute("SELECT COUNT(*) FROM task_outcomes").fetchone()[0]
|
|
146
|
+
if total_outcomes < MIN_DATA_THRESHOLD:
|
|
147
|
+
return {
|
|
148
|
+
"status": "insufficient_data",
|
|
149
|
+
"message": f"Insufficient data (need {MIN_DATA_THRESHOLD}+ outcomes, have {total_outcomes})",
|
|
150
|
+
"extracted": [],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
extracted: list[dict] = []
|
|
154
|
+
|
|
155
|
+
# Heal any legacy count-snapshot duplicates before mining so the upsert
|
|
156
|
+
# below updates a single survivor row per fact.
|
|
157
|
+
_collapse_duplicate_patterns(conn)
|
|
158
|
+
|
|
159
|
+
# --- Domain rework patterns ---
|
|
160
|
+
domain_rows = conn.execute(
|
|
161
|
+
"SELECT domain, COUNT(*) AS total, "
|
|
162
|
+
"SUM(CASE WHEN outcome = 'rework' THEN 1 ELSE 0 END) AS rework_count "
|
|
163
|
+
"FROM task_outcomes "
|
|
164
|
+
"GROUP BY domain "
|
|
165
|
+
"HAVING rework_count >= ?",
|
|
166
|
+
(min_occurrences,),
|
|
167
|
+
).fetchall()
|
|
168
|
+
|
|
169
|
+
for row in domain_rows:
|
|
170
|
+
d = dict(row)
|
|
171
|
+
rework_rate = d["rework_count"] / d["total"] if d["total"] > 0 else 0
|
|
172
|
+
if rework_rate < 0.2:
|
|
173
|
+
continue # not significant enough
|
|
174
|
+
confidence = min(0.9, d["rework_count"] / (d["total"] * 2))
|
|
175
|
+
pattern_text = (
|
|
176
|
+
f"{d['domain']} domain has {rework_rate:.0%} rework rate "
|
|
177
|
+
f"({d['rework_count']}/{d['total']} tasks)"
|
|
178
|
+
)
|
|
179
|
+
extracted.append(
|
|
180
|
+
_upsert_pattern(
|
|
181
|
+
conn,
|
|
182
|
+
pattern=pattern_text,
|
|
183
|
+
memory_type="pattern",
|
|
184
|
+
domain=d["domain"],
|
|
185
|
+
source="learn_extract",
|
|
186
|
+
confidence=confidence,
|
|
187
|
+
concepts=json.dumps([d["domain"].lower(), "rework", "domain_pattern"]),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# --- Skill correlation patterns ---
|
|
192
|
+
skill_rows = conn.execute(
|
|
193
|
+
"SELECT skills_used, outcome, COUNT(*) AS count "
|
|
194
|
+
"FROM task_outcomes "
|
|
195
|
+
"WHERE skills_used IS NOT NULL AND skills_used != '' "
|
|
196
|
+
"GROUP BY skills_used, outcome "
|
|
197
|
+
"HAVING count >= ?",
|
|
198
|
+
(min_occurrences,),
|
|
199
|
+
).fetchall()
|
|
200
|
+
|
|
201
|
+
for row in skill_rows:
|
|
202
|
+
d = dict(row)
|
|
203
|
+
if d["outcome"] == "rework":
|
|
204
|
+
confidence = min(0.9, d["count"] / 10.0)
|
|
205
|
+
pattern_text = (
|
|
206
|
+
f"Skill '{d['skills_used']}' correlates with rework ({d['count']} occurrences)"
|
|
207
|
+
)
|
|
208
|
+
extracted.append(
|
|
209
|
+
_upsert_pattern(
|
|
210
|
+
conn,
|
|
211
|
+
pattern=pattern_text,
|
|
212
|
+
memory_type="pattern",
|
|
213
|
+
domain=None,
|
|
214
|
+
source="learn_extract",
|
|
215
|
+
confidence=confidence,
|
|
216
|
+
concepts=json.dumps(["skill", d["skills_used"], "rework"]),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# --- Complexity mismatch patterns ---
|
|
221
|
+
mismatch_rows = conn.execute(
|
|
222
|
+
"SELECT complexity, outcome, COUNT(*) AS count "
|
|
223
|
+
"FROM task_outcomes "
|
|
224
|
+
"WHERE outcome = 'rework' "
|
|
225
|
+
"GROUP BY complexity "
|
|
226
|
+
"HAVING count >= ?",
|
|
227
|
+
(min_occurrences,),
|
|
228
|
+
).fetchall()
|
|
229
|
+
|
|
230
|
+
for row in mismatch_rows:
|
|
231
|
+
d = dict(row)
|
|
232
|
+
# Check if CLEAR tasks frequently rework (likely underclassified)
|
|
233
|
+
if d["complexity"] == "CLEAR" and d["count"] >= min_occurrences:
|
|
234
|
+
total_clear = conn.execute(
|
|
235
|
+
"SELECT COUNT(*) FROM task_outcomes WHERE complexity = 'CLEAR'"
|
|
236
|
+
).fetchone()[0]
|
|
237
|
+
if total_clear > 0:
|
|
238
|
+
rate = d["count"] / total_clear
|
|
239
|
+
if rate > 0.3:
|
|
240
|
+
confidence = min(0.9, rate)
|
|
241
|
+
pattern_text = (
|
|
242
|
+
f"CLEAR tasks rework at {rate:.0%} — may be underclassified "
|
|
243
|
+
f"({d['count']}/{total_clear})"
|
|
244
|
+
)
|
|
245
|
+
extracted.append(
|
|
246
|
+
_upsert_pattern(
|
|
247
|
+
conn,
|
|
248
|
+
pattern=pattern_text,
|
|
249
|
+
memory_type="decision",
|
|
250
|
+
domain=None,
|
|
251
|
+
source="learn_extract",
|
|
252
|
+
confidence=confidence,
|
|
253
|
+
concepts=json.dumps(["complexity", "classification", "mismatch"]),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# --- Success baseline patterns (positive-signal mining) ---
|
|
258
|
+
# A healthy success-only history must still yield learnable patterns —
|
|
259
|
+
# without this the loop can ONLY learn from failure, so a project that
|
|
260
|
+
# rarely reworks produces zero patterns forever. Mine per-domain and
|
|
261
|
+
# per-skill success so cos_learn_suggest has positive anchors to rank.
|
|
262
|
+
# Variance gate: a success-rate stat only informs when the corpus has a
|
|
263
|
+
# non-success outcome to contrast against. On a monotone-success corpus
|
|
264
|
+
# every "X succeeds 100%" is a tautology — skip both stat branches.
|
|
265
|
+
# See docs/engineering/learning-extraction.md § Variance gate.
|
|
266
|
+
_has_variance = (
|
|
267
|
+
conn.execute("SELECT COUNT(*) FROM task_outcomes WHERE outcome != 'success'").fetchone()[0]
|
|
268
|
+
> 0
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
success_domain_rows = (
|
|
272
|
+
conn.execute(
|
|
273
|
+
"SELECT domain, COUNT(*) AS total, "
|
|
274
|
+
"SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) AS success_count "
|
|
275
|
+
"FROM task_outcomes WHERE domain IS NOT NULL AND domain != '' "
|
|
276
|
+
"GROUP BY domain HAVING success_count >= ?",
|
|
277
|
+
(min_occurrences,),
|
|
278
|
+
).fetchall()
|
|
279
|
+
if _has_variance
|
|
280
|
+
else []
|
|
281
|
+
)
|
|
282
|
+
for row in success_domain_rows:
|
|
283
|
+
d = dict(row)
|
|
284
|
+
rate = d["success_count"] / d["total"] if d["total"] else 0
|
|
285
|
+
confidence = min(0.85, 0.4 + d["success_count"] / 20.0)
|
|
286
|
+
pattern_text = (
|
|
287
|
+
f"{d['domain']} domain succeeds at {rate:.0%} "
|
|
288
|
+
f"({d['success_count']}/{d['total']} tasks) — reliable baseline"
|
|
289
|
+
)
|
|
290
|
+
extracted.append(
|
|
291
|
+
_upsert_pattern(
|
|
292
|
+
conn,
|
|
293
|
+
pattern=pattern_text,
|
|
294
|
+
# 'stat' (not a belief): a success rate is observability, not a
|
|
295
|
+
# lesson — excluded from the digest + cos_learn_suggest so it
|
|
296
|
+
# never masquerades as a learning. See learning-extraction.md.
|
|
297
|
+
memory_type="stat",
|
|
298
|
+
domain=d["domain"],
|
|
299
|
+
source="learn_extract",
|
|
300
|
+
confidence=confidence,
|
|
301
|
+
concepts=json.dumps([d["domain"].lower(), "success", "baseline", "stat"]),
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
skill_success_rows = (
|
|
306
|
+
conn.execute(
|
|
307
|
+
"SELECT skills_used, COUNT(*) AS count "
|
|
308
|
+
"FROM task_outcomes "
|
|
309
|
+
"WHERE outcome = 'success' AND skills_used IS NOT NULL AND skills_used != '' "
|
|
310
|
+
"GROUP BY skills_used HAVING count >= ?",
|
|
311
|
+
(min_occurrences,),
|
|
312
|
+
).fetchall()
|
|
313
|
+
if _has_variance
|
|
314
|
+
else []
|
|
315
|
+
)
|
|
316
|
+
for row in skill_success_rows:
|
|
317
|
+
d = dict(row)
|
|
318
|
+
confidence = min(0.8, 0.4 + d["count"] / 20.0)
|
|
319
|
+
pattern_text = (
|
|
320
|
+
f"Skill set '{d['skills_used']}' correlates with success ({d['count']} tasks)"
|
|
321
|
+
)
|
|
322
|
+
extracted.append(
|
|
323
|
+
_upsert_pattern(
|
|
324
|
+
conn,
|
|
325
|
+
pattern=pattern_text,
|
|
326
|
+
memory_type="stat", # observability, not a belief — see above
|
|
327
|
+
domain=None,
|
|
328
|
+
source="learn_extract",
|
|
329
|
+
confidence=confidence,
|
|
330
|
+
concepts=json.dumps(["skill", "success", "correlation", "stat"]),
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# --- Failure anatomy patterns (v25) ---
|
|
335
|
+
# Mine structured backtrack_events for recurring root_cause patterns.
|
|
336
|
+
# Only runs when anatomy columns are present (migration v25).
|
|
337
|
+
try:
|
|
338
|
+
from tools.cognition import CANONICAL_REMEDIES
|
|
339
|
+
|
|
340
|
+
anat_rows = conn.execute(
|
|
341
|
+
# Anatomy pairs a recurring cause with its remedy — the one the agent
|
|
342
|
+
# recorded, else the canonical corrective action for that cause. Never
|
|
343
|
+
# a bare count (learning-extraction.md § Anatomy from backtracks).
|
|
344
|
+
"SELECT root_cause, COUNT(*) AS cnt, "
|
|
345
|
+
" GROUP_CONCAT(DISTINCT from_formula) AS formulas, "
|
|
346
|
+
" MAX(corrective_action) AS remedy "
|
|
347
|
+
"FROM backtrack_events "
|
|
348
|
+
"WHERE root_cause IS NOT NULL "
|
|
349
|
+
"GROUP BY root_cause "
|
|
350
|
+
"HAVING cnt >= ?",
|
|
351
|
+
(min_occurrences,),
|
|
352
|
+
).fetchall()
|
|
353
|
+
for row in anat_rows:
|
|
354
|
+
d = dict(row)
|
|
355
|
+
remedy = (d["remedy"] or "").strip() or CANONICAL_REMEDIES.get(d["root_cause"], "")
|
|
356
|
+
if not remedy:
|
|
357
|
+
continue # no recorded nor canonical remedy — skip, never a bare count
|
|
358
|
+
confidence = min(0.85, d["cnt"] / 20.0 + 0.3)
|
|
359
|
+
formulas_str = d["formulas"] or ""
|
|
360
|
+
pattern_text = (
|
|
361
|
+
f"Recurring backtrack root cause '{d['root_cause']}' "
|
|
362
|
+
f"({d['cnt']} occurrences"
|
|
363
|
+
+ (f"; formulas: {formulas_str[:60]}" if formulas_str else "")
|
|
364
|
+
+ f") → {remedy[:160]}"
|
|
365
|
+
)
|
|
366
|
+
extracted.append(
|
|
367
|
+
_upsert_pattern(
|
|
368
|
+
conn,
|
|
369
|
+
pattern=pattern_text,
|
|
370
|
+
memory_type="failure",
|
|
371
|
+
domain=None,
|
|
372
|
+
source="learn_extract",
|
|
373
|
+
confidence=confidence,
|
|
374
|
+
concepts=json.dumps(["failure", d["root_cause"], "backtrack"]),
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
except Exception as exc: # backtrack_events or anatomy columns absent — fire-and-forget
|
|
378
|
+
logger.debug("learn_extract: failure anatomy skipped: %s", exc)
|
|
379
|
+
|
|
380
|
+
# --- Friction lessons ---
|
|
381
|
+
# The abundant, automatic learning signal: hook BLOCKs and tool failures
|
|
382
|
+
# the agent emits every session. Mined into actionable
|
|
383
|
+
# `lesson` patterns so the loop learns from mistakes — not just success
|
|
384
|
+
# statistics. Contract: docs/engineering/learning-extraction.md.
|
|
385
|
+
try:
|
|
386
|
+
distill_budget = int(os.environ.get("COS_DISTILL_MAX_CLUSTERS", "20"))
|
|
387
|
+
except ValueError:
|
|
388
|
+
distill_budget = 20
|
|
389
|
+
distill_state = {"remaining": max(0, distill_budget)}
|
|
390
|
+
extracted.extend(
|
|
391
|
+
_mine_friction_lessons(conn, min_occurrences=min_occurrences, distill_state=distill_state)
|
|
392
|
+
)
|
|
393
|
+
# Hook BLOCKs live in the activity log (not observations) on Claude — mine
|
|
394
|
+
# them too so the richest friction signal becomes a lesson.
|
|
395
|
+
extracted.extend(
|
|
396
|
+
_mine_hook_block_lessons(conn, min_occurrences=min_occurrences, distill_state=distill_state)
|
|
397
|
+
)
|
|
398
|
+
# fix:/revert: commit subjects — the real engineering-lesson signal that
|
|
399
|
+
# reasoning records in git history, not in any friction table (§5).
|
|
400
|
+
extracted.extend(_mine_commit_lessons(conn, min_occurrences=min_occurrences))
|
|
401
|
+
|
|
402
|
+
# Generalize related lessons into human-review drafts (B3). Fire-and-forget;
|
|
403
|
+
# writes only when a NEW cluster forms (deduped). Never blocks extraction.
|
|
404
|
+
try:
|
|
405
|
+
generalize_lessons(conn)
|
|
406
|
+
except Exception as exc:
|
|
407
|
+
logger.debug("generalize_lessons skipped: %s", exc)
|
|
408
|
+
|
|
409
|
+
conn.commit()
|
|
410
|
+
return {
|
|
411
|
+
"status": "ok",
|
|
412
|
+
"total_outcomes_analyzed": total_outcomes,
|
|
413
|
+
"extracted": extracted,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
_SOURCE_TO_PROVENANCE: dict[str, str] = {
|
|
418
|
+
"learn_extract": "extracted_from_outcome",
|
|
419
|
+
"friction": "extracted_from_observation",
|
|
420
|
+
"commit": "extracted_from_commit",
|
|
421
|
+
"breakthrough": "agent_self",
|
|
422
|
+
"manual": "user_directive",
|
|
423
|
+
"import": "imported",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# Volatile counters embedded in mined pattern text — the running task
|
|
428
|
+
# count grows every extraction run, so it must NOT be part of a pattern's
|
|
429
|
+
# identity or each run mints a new snapshot row instead of updating one.
|
|
430
|
+
_IDENTITY_COUNT_RE = re.compile(r"\(\d+(?:/\d+)?\s*(?:tasks?|occurrences?)[^)]*\)", re.IGNORECASE)
|
|
431
|
+
_IDENTITY_RATIO_RE = re.compile(r"\(\d+/\d+\)")
|
|
432
|
+
_IDENTITY_PCT_RE = re.compile(r"\d+(?:\.\d+)?%")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _pattern_identity(text: str) -> str:
|
|
436
|
+
# Count-agnostic dedup key: strip the running counts / percentages so a
|
|
437
|
+
# re-mined fact ("INFRA succeeds … (40/40)" → "(83/83)") maps to the
|
|
438
|
+
# SAME row. The displayed `pattern` keeps the live numbers; only the
|
|
439
|
+
# identity ignores them.
|
|
440
|
+
t = _IDENTITY_COUNT_RE.sub("", text)
|
|
441
|
+
t = _IDENTITY_RATIO_RE.sub("", t)
|
|
442
|
+
t = _IDENTITY_PCT_RE.sub("", t)
|
|
443
|
+
return " ".join(t.split()).lower()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _collapse_duplicate_patterns(conn: sqlite3.Connection) -> int:
|
|
447
|
+
# Self-healing one-shot: merge legacy count-snapshot duplicates that the
|
|
448
|
+
# previously exact-text dedup let accumulate. Idempotent — once each
|
|
449
|
+
# (identity, domain) group is a single row, this is a no-op. Returns the
|
|
450
|
+
# number of rows deleted.
|
|
451
|
+
rows = conn.execute(
|
|
452
|
+
"SELECT id, pattern, domain, confidence, times_seen, times_validated FROM learned_patterns"
|
|
453
|
+
).fetchall()
|
|
454
|
+
groups: dict[tuple[str, object], list] = {}
|
|
455
|
+
for r in rows:
|
|
456
|
+
groups.setdefault((_pattern_identity(r["pattern"]), r["domain"]), []).append(r)
|
|
457
|
+
removed = 0
|
|
458
|
+
for members in groups.values():
|
|
459
|
+
if len(members) < 2:
|
|
460
|
+
continue
|
|
461
|
+
# Survivor = the most-established row by occurrences; fold BOTH counters so
|
|
462
|
+
# neither the occurrence total (times_seen) nor real validations are lost.
|
|
463
|
+
survivor = max(members, key=lambda m: ((m["times_seen"] or 0), m["confidence"], m["id"]))
|
|
464
|
+
losers = [m["id"] for m in members if m["id"] != survivor["id"]]
|
|
465
|
+
conn.execute(
|
|
466
|
+
"UPDATE learned_patterns SET pattern = ?, confidence = ?, times_seen = ?, "
|
|
467
|
+
"times_validated = ?, last_validated = CURRENT_TIMESTAMP WHERE id = ?",
|
|
468
|
+
(
|
|
469
|
+
survivor["pattern"],
|
|
470
|
+
max(m["confidence"] for m in members),
|
|
471
|
+
sum((m["times_seen"] or 0) for m in members) + len(losers),
|
|
472
|
+
sum((m["times_validated"] or 0) for m in members),
|
|
473
|
+
survivor["id"],
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
conn.executemany("DELETE FROM learned_patterns WHERE id = ?", [(i,) for i in losers])
|
|
477
|
+
removed += len(losers)
|
|
478
|
+
return removed
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _consolidate_semantic_duplicates(
|
|
482
|
+
conn: sqlite3.Connection, *, threshold: float = 0.85, dry_run: bool = False
|
|
483
|
+
) -> int:
|
|
484
|
+
# Survivor = highest (confidence, times_seen, oldest id); loser's access_count
|
|
485
|
+
# + times_seen + times_validated fold in before delete. No-op without embeddings.
|
|
486
|
+
try:
|
|
487
|
+
from embeddings import cosine_similarity, is_available
|
|
488
|
+
except ImportError:
|
|
489
|
+
return 0
|
|
490
|
+
if not is_available():
|
|
491
|
+
return 0
|
|
492
|
+
try:
|
|
493
|
+
rows = conn.execute(
|
|
494
|
+
"SELECT lp.id, lp.confidence, lp.times_seen, lp.times_validated, lp.access_count, e.embedding "
|
|
495
|
+
"FROM learned_patterns lp JOIN embeddings e "
|
|
496
|
+
" ON e.source_table = 'learned_patterns' AND e.source_id = lp.id "
|
|
497
|
+
"WHERE lp.promoted_to IS NULL AND lp.archived_at IS NULL"
|
|
498
|
+
).fetchall()
|
|
499
|
+
except sqlite3.OperationalError as exc:
|
|
500
|
+
logger.debug("semantic consolidation skipped: %s", exc)
|
|
501
|
+
return 0
|
|
502
|
+
|
|
503
|
+
items = [dict(r) for r in rows if r["embedding"]]
|
|
504
|
+
if len(items) < 2:
|
|
505
|
+
return 0
|
|
506
|
+
# Stronger row first → it becomes the survivor of any similar pair.
|
|
507
|
+
items.sort(key=lambda x: (-(x["confidence"] or 0.0), -(x["times_seen"] or 0), x["id"]))
|
|
508
|
+
|
|
509
|
+
removed: set[int] = set()
|
|
510
|
+
merged = 0
|
|
511
|
+
for i, survivor in enumerate(items):
|
|
512
|
+
if survivor["id"] in removed:
|
|
513
|
+
continue
|
|
514
|
+
cands = [c for c in items[i + 1 :] if c["id"] not in removed]
|
|
515
|
+
if not cands:
|
|
516
|
+
continue
|
|
517
|
+
scores = cosine_similarity(survivor["embedding"], [c["embedding"] for c in cands])
|
|
518
|
+
for cand, score in zip(cands, scores):
|
|
519
|
+
if score < threshold:
|
|
520
|
+
continue
|
|
521
|
+
if not dry_run:
|
|
522
|
+
conn.execute(
|
|
523
|
+
"UPDATE learned_patterns SET access_count = COALESCE(access_count, 0) + ?, "
|
|
524
|
+
"times_seen = COALESCE(times_seen, 0) + ?, "
|
|
525
|
+
"times_validated = COALESCE(times_validated, 0) + ? WHERE id = ?",
|
|
526
|
+
(
|
|
527
|
+
cand["access_count"] or 0,
|
|
528
|
+
cand["times_seen"] or 0,
|
|
529
|
+
cand["times_validated"] or 0,
|
|
530
|
+
survivor["id"],
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
conn.execute("DELETE FROM learned_patterns WHERE id = ?", (cand["id"],))
|
|
534
|
+
conn.execute(
|
|
535
|
+
"DELETE FROM embeddings WHERE source_table = 'learned_patterns' AND source_id = ?",
|
|
536
|
+
(cand["id"],),
|
|
537
|
+
)
|
|
538
|
+
removed.add(cand["id"])
|
|
539
|
+
merged += 1
|
|
540
|
+
return merged
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _format_generalize_draft(cluster: list[dict]) -> str:
|
|
544
|
+
lines = [
|
|
545
|
+
"---",
|
|
546
|
+
"type: feedback",
|
|
547
|
+
"status: draft",
|
|
548
|
+
f"lessons: {len(cluster)}",
|
|
549
|
+
"---",
|
|
550
|
+
"",
|
|
551
|
+
f"# Generalize {len(cluster)} related lessons",
|
|
552
|
+
"",
|
|
553
|
+
"These lessons recur on a shared theme. Consider distilling ONE general",
|
|
554
|
+
"rule and promoting it — this is a HUMAN-REVIEW draft; the system never",
|
|
555
|
+
"auto-writes rules.",
|
|
556
|
+
"",
|
|
557
|
+
"## Member lessons",
|
|
558
|
+
]
|
|
559
|
+
lines += [f"- (#{c['id']}) {c['pattern']}" for c in cluster]
|
|
560
|
+
lines += [
|
|
561
|
+
"",
|
|
562
|
+
"## Suggested action",
|
|
563
|
+
"- If they share a root cause, write one rule that covers all of them.",
|
|
564
|
+
"- Then `cos_promote(pattern_id=<strongest>, target='feedback'|'rule')`.",
|
|
565
|
+
]
|
|
566
|
+
return "\n".join(lines) + "\n"
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def generalize_lessons(
|
|
570
|
+
conn: sqlite3.Connection, *, min_cluster: int = 3, sim_threshold: float = 0.6
|
|
571
|
+
) -> dict:
|
|
572
|
+
"""Surface generalizable lesson clusters as human-review drafts (B3).
|
|
573
|
+
|
|
574
|
+
Greedily clusters `lesson` patterns by embeddings cosine; when >= min_cluster
|
|
575
|
+
related lessons share a theme, writes a feedback draft to
|
|
576
|
+
`.coding-os/memory/drafts/` suggesting one general rule. NO LLM, NEVER writes
|
|
577
|
+
to rules/docs — abstraction stays human-gated. Deduped by cluster signature.
|
|
578
|
+
Returns {"drafts": [filenames]}. No-op without embeddings / project root.
|
|
579
|
+
"""
|
|
580
|
+
try:
|
|
581
|
+
from embeddings import cosine_similarity, is_available
|
|
582
|
+
except ImportError:
|
|
583
|
+
return {"drafts": []}
|
|
584
|
+
if not is_available():
|
|
585
|
+
return {"drafts": []}
|
|
586
|
+
root = _derive_project_root(conn)
|
|
587
|
+
if root is None:
|
|
588
|
+
return {"drafts": []}
|
|
589
|
+
try:
|
|
590
|
+
rows = conn.execute(
|
|
591
|
+
"SELECT lp.id, lp.pattern, e.embedding FROM learned_patterns lp "
|
|
592
|
+
"JOIN embeddings e ON e.source_table = 'learned_patterns' AND e.source_id = lp.id "
|
|
593
|
+
"WHERE lp.memory_type = 'lesson' AND lp.archived_at IS NULL AND lp.promoted_to IS NULL"
|
|
594
|
+
).fetchall()
|
|
595
|
+
except sqlite3.OperationalError as exc:
|
|
596
|
+
logger.debug("generalize_lessons skipped: %s", exc)
|
|
597
|
+
return {"drafts": []}
|
|
598
|
+
|
|
599
|
+
items = [dict(r) for r in rows if r["embedding"]]
|
|
600
|
+
if len(items) < min_cluster:
|
|
601
|
+
return {"drafts": []}
|
|
602
|
+
|
|
603
|
+
drafts_dir = root / ".coding-os" / "memory" / "drafts"
|
|
604
|
+
clustered: set[int] = set()
|
|
605
|
+
drafts: list[str] = []
|
|
606
|
+
for seed in items:
|
|
607
|
+
if seed["id"] in clustered:
|
|
608
|
+
continue
|
|
609
|
+
rest = [c for c in items if c["id"] != seed["id"] and c["id"] not in clustered]
|
|
610
|
+
if not rest:
|
|
611
|
+
break
|
|
612
|
+
scores = cosine_similarity(seed["embedding"], [c["embedding"] for c in rest])
|
|
613
|
+
cluster = [seed] + [c for c, s in zip(rest, scores) if s >= sim_threshold]
|
|
614
|
+
if len(cluster) < min_cluster:
|
|
615
|
+
continue
|
|
616
|
+
for c in cluster:
|
|
617
|
+
clustered.add(c["id"])
|
|
618
|
+
sig = "-".join(str(c["id"]) for c in sorted(cluster, key=lambda x: x["id"]))
|
|
619
|
+
fname = f"generalize-{hashlib.sha1(sig.encode()).hexdigest()[:10]}.md"
|
|
620
|
+
target = drafts_dir / fname
|
|
621
|
+
if target.exists():
|
|
622
|
+
continue
|
|
623
|
+
try:
|
|
624
|
+
drafts_dir.mkdir(parents=True, exist_ok=True)
|
|
625
|
+
target.write_text(_format_generalize_draft(cluster), encoding="utf-8")
|
|
626
|
+
drafts.append(fname)
|
|
627
|
+
except OSError as exc:
|
|
628
|
+
logger.debug("generalize draft write failed: %s", exc)
|
|
629
|
+
return {"drafts": drafts}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _upsert_pattern(
|
|
633
|
+
conn: sqlite3.Connection,
|
|
634
|
+
*,
|
|
635
|
+
pattern: str,
|
|
636
|
+
memory_type: str,
|
|
637
|
+
domain: str | None,
|
|
638
|
+
source: str,
|
|
639
|
+
confidence: float,
|
|
640
|
+
concepts: str,
|
|
641
|
+
provenance: str | None = None,
|
|
642
|
+
distill_fingerprint: str | None = None,
|
|
643
|
+
evidence_json: str | None = None,
|
|
644
|
+
) -> dict:
|
|
645
|
+
# Sanitizer runs before any DB write; a rejected pattern returns
|
|
646
|
+
# {"action": "rejected", ...} with no row touched. provenance keeps
|
|
647
|
+
# agent_self writes distinguishable from mined data for sycophancy analysis.
|
|
648
|
+
if provenance is None:
|
|
649
|
+
provenance = _SOURCE_TO_PROVENANCE.get(source, "agent_self")
|
|
650
|
+
from sanitizer import sanitize_write
|
|
651
|
+
|
|
652
|
+
p_sr = sanitize_write(
|
|
653
|
+
"pattern",
|
|
654
|
+
pattern,
|
|
655
|
+
actor="learning._upsert_pattern",
|
|
656
|
+
source_table="learned_patterns",
|
|
657
|
+
conn=conn,
|
|
658
|
+
)
|
|
659
|
+
if not p_sr.ok:
|
|
660
|
+
return {
|
|
661
|
+
"id": None,
|
|
662
|
+
"pattern": (pattern or "")[:60],
|
|
663
|
+
"confidence": 0.0,
|
|
664
|
+
"action": "rejected",
|
|
665
|
+
"reason": p_sr.reason,
|
|
666
|
+
}
|
|
667
|
+
pattern = p_sr.cleaned
|
|
668
|
+
|
|
669
|
+
# Match on a count-agnostic identity, not exact text: a re-mined fact
|
|
670
|
+
# whose running count grew ("(40/40)" → "(83/83)") is the SAME pattern
|
|
671
|
+
# and must update its row, not insert a snapshot. The table is small, so
|
|
672
|
+
# canonicalise candidate rows in the same domain.
|
|
673
|
+
identity = _pattern_identity(pattern)
|
|
674
|
+
existing = None
|
|
675
|
+
if distill_fingerprint:
|
|
676
|
+
try:
|
|
677
|
+
existing = conn.execute(
|
|
678
|
+
"SELECT id, pattern, confidence, times_validated FROM learned_patterns "
|
|
679
|
+
"WHERE distill_fingerprint = ?",
|
|
680
|
+
(distill_fingerprint,),
|
|
681
|
+
).fetchone()
|
|
682
|
+
except sqlite3.OperationalError:
|
|
683
|
+
existing = None
|
|
684
|
+
if existing is None:
|
|
685
|
+
for cand in conn.execute(
|
|
686
|
+
"SELECT id, pattern, confidence, times_validated FROM learned_patterns WHERE domain IS ?",
|
|
687
|
+
(domain,),
|
|
688
|
+
):
|
|
689
|
+
if _pattern_identity(cand["pattern"]) == identity:
|
|
690
|
+
existing = cand
|
|
691
|
+
break
|
|
692
|
+
|
|
693
|
+
if existing:
|
|
694
|
+
# Confidence is owned by validation (LTP/LTD), not re-extraction: a
|
|
695
|
+
# re-mine bumps times_seen (the occurrence count) and refreshes the text,
|
|
696
|
+
# but must NOT raise confidence — otherwise re-mining resurrects a belief
|
|
697
|
+
# that learn_validate penalized, and LTD could never lower a bad pattern.
|
|
698
|
+
# First-insert seeds the prior; validation moves it from there.
|
|
699
|
+
new_conf = existing["confidence"]
|
|
700
|
+
# Re-extraction is a positive signal: refresh recency AND revive a row a
|
|
701
|
+
# prior decay run archived. A REAL promotion (promoted_to='rule:…' /
|
|
702
|
+
# 'feedback:…') survives the re-mine — the knowledge now lives in the
|
|
703
|
+
# rule layer, and un-promoting it would put the same fact in two places.
|
|
704
|
+
conn.execute(
|
|
705
|
+
# Refresh memory_type too: a re-mine reclassifies a row whose class
|
|
706
|
+
# changed (e.g. a legacy success baseline minted as 'pattern' becomes
|
|
707
|
+
# 'stat'), so old garbage reclassifies on the next loop run.
|
|
708
|
+
"UPDATE learned_patterns SET pattern = ?, memory_type = ?, confidence = ?, "
|
|
709
|
+
"times_seen = COALESCE(times_seen, 0) + 1, last_validated = CURRENT_TIMESTAMP, "
|
|
710
|
+
"last_accessed_at = CURRENT_TIMESTAMP, "
|
|
711
|
+
"promoted_to = CASE WHEN COALESCE(promoted_to, '') IN ('', 'archived') "
|
|
712
|
+
" THEN NULL ELSE promoted_to END, "
|
|
713
|
+
"archived_at = CASE WHEN COALESCE(promoted_to, '') IN ('', 'archived') "
|
|
714
|
+
" THEN NULL ELSE archived_at END, "
|
|
715
|
+
"distill_fingerprint = COALESCE(?, distill_fingerprint), "
|
|
716
|
+
"evidence_json = COALESCE(?, evidence_json) "
|
|
717
|
+
"WHERE id = ?",
|
|
718
|
+
(pattern, memory_type, new_conf, distill_fingerprint, evidence_json, existing["id"]),
|
|
719
|
+
)
|
|
720
|
+
pattern_id = existing["id"]
|
|
721
|
+
result = {"id": pattern_id, "pattern": pattern, "confidence": new_conf, "action": "updated"}
|
|
722
|
+
else:
|
|
723
|
+
# Stamp last_validated/last_accessed_at at creation so a fresh pattern's age is 0.
|
|
724
|
+
# Otherwise run_decay reads _days_since(NULL)→999d and archives it on the FIRST run.
|
|
725
|
+
cursor = conn.execute(
|
|
726
|
+
"INSERT INTO learned_patterns "
|
|
727
|
+
"(pattern, memory_type, domain, source, confidence, concepts, provenance, "
|
|
728
|
+
"distill_fingerprint, evidence_json, last_validated, last_accessed_at) "
|
|
729
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
|
730
|
+
(
|
|
731
|
+
pattern,
|
|
732
|
+
memory_type,
|
|
733
|
+
domain,
|
|
734
|
+
source,
|
|
735
|
+
confidence,
|
|
736
|
+
concepts,
|
|
737
|
+
provenance,
|
|
738
|
+
distill_fingerprint,
|
|
739
|
+
evidence_json,
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
pattern_id = cursor.lastrowid
|
|
743
|
+
result = {
|
|
744
|
+
"id": pattern_id,
|
|
745
|
+
"pattern": pattern,
|
|
746
|
+
"confidence": confidence,
|
|
747
|
+
"action": "created",
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
# RAG: embed the pattern for semantic search.
|
|
751
|
+
# Suppressed because embeddings are optional enrichment — the upsert
|
|
752
|
+
# itself must succeed even when rag extras / v5 schema are unavailable.
|
|
753
|
+
_embed_pattern_safe(conn, pattern_id, pattern, concepts)
|
|
754
|
+
|
|
755
|
+
return result
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _embed_pattern_safe(
|
|
759
|
+
conn: sqlite3.Connection,
|
|
760
|
+
pattern_id: int,
|
|
761
|
+
pattern: str,
|
|
762
|
+
concepts: str,
|
|
763
|
+
) -> None:
|
|
764
|
+
# Fire-and-forget: embeddings are optional enrichment — never fail the upsert.
|
|
765
|
+
try:
|
|
766
|
+
from embeddings import upsert_embedding
|
|
767
|
+
except ImportError as exc:
|
|
768
|
+
logger.debug("Skipping pattern embedding (module unavailable): %s", exc)
|
|
769
|
+
return
|
|
770
|
+
try:
|
|
771
|
+
text_to_embed = " ".join(filter(None, [pattern, concepts]))
|
|
772
|
+
upsert_embedding(conn, "learned_patterns", pattern_id, text_to_embed)
|
|
773
|
+
except sqlite3.OperationalError as exc:
|
|
774
|
+
logger.debug("Skipping pattern embedding (table missing): %s", exc)
|
|
775
|
+
except Exception as exc: # pragma: no cover
|
|
776
|
+
logger.debug("Skipping pattern embedding (unexpected): %s", exc)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
# ---------------------------------------------------------------------------
|
|
780
|
+
# Friction lesson mining (the real learning signal)
|
|
781
|
+
# ---------------------------------------------------------------------------
|
|
782
|
+
|
|
783
|
+
# A friction event seen this many times is already worth a rule. Lower than the
|
|
784
|
+
# stat threshold (3) because each failure is individually high-value, and never
|
|
785
|
+
# higher than the caller's floor.
|
|
786
|
+
_FRICTION_MIN_OCCURRENCES = 2
|
|
787
|
+
|
|
788
|
+
# Plain-language corrective hint per friction kind — kept beginner-readable.
|
|
789
|
+
_FRICTION_HINTS: dict[str, str] = {
|
|
790
|
+
"hook_block": "satisfy the blocked rule before retrying the action",
|
|
791
|
+
"schema_mismatch": "match the required output schema exactly before resubmitting",
|
|
792
|
+
"error": "fix the failing precondition before retrying",
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# Normalisers that turn a volatile failure message into a stable cluster key:
|
|
796
|
+
# absolute paths → basename, TASK ids and long hashes → placeholders.
|
|
797
|
+
_ABS_PATH_RE = re.compile(r"(?:/[^\s'\":,]+)+/([^\s'\":/,]+)")
|
|
798
|
+
_TASKID_RE = re.compile(r"TASK-\d+", re.IGNORECASE)
|
|
799
|
+
_LONGHEX_RE = re.compile(r"\b[0-9a-f]{8,}\b", re.IGNORECASE)
|
|
800
|
+
_NONWORD_RE = re.compile(r"[^a-z0-9<>_.-]+")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _friction_kind(title: str, narrative: str, memory_type: str) -> str:
|
|
804
|
+
# Most-specific signal first. hook_block is detected by the capture's
|
|
805
|
+
# memory_type or a leading "BLOCKED" — NOT a loose "blocked" substring,
|
|
806
|
+
# which appears in unrelated remediation text (e.g. "--to blocked").
|
|
807
|
+
title_l = (title or "").lower()
|
|
808
|
+
narr_l = (narrative or "").lower()
|
|
809
|
+
if "does not match required schema" in narr_l or ("schema" in narr_l and "property" in narr_l):
|
|
810
|
+
return "schema_mismatch"
|
|
811
|
+
if memory_type == "hook_block" or narr_l.startswith("blocked") or "[blocked]" in title_l:
|
|
812
|
+
return "hook_block"
|
|
813
|
+
return "error"
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _clean_failure_text(text: str) -> str:
|
|
817
|
+
line = (text or "").strip().split("\n", 1)[0]
|
|
818
|
+
line = _ABS_PATH_RE.sub(r"\1", line)
|
|
819
|
+
line = _TASKID_RE.sub("TASK-N", line)
|
|
820
|
+
line = _LONGHEX_RE.sub("<hash>", line)
|
|
821
|
+
return " ".join(line.split())[:200]
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _failure_cluster_key(display: str) -> str:
|
|
825
|
+
norm = re.sub(r"\d+", "N", display.lower())
|
|
826
|
+
words = [w for w in _NONWORD_RE.split(norm) if w]
|
|
827
|
+
return " ".join(words[:8])
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# Substrings that mark an `error` observation as a tool-fumble or expected
|
|
831
|
+
# refusal — the agent tripping over its own tooling, never an engineering lesson.
|
|
832
|
+
# See learning-extraction.md § Noise filter.
|
|
833
|
+
_NOISE_FAILURE_MARKERS: tuple[str, ...] = (
|
|
834
|
+
"eisdir",
|
|
835
|
+
"illegal operation on a directory",
|
|
836
|
+
"file does not exist",
|
|
837
|
+
"no such file or directory",
|
|
838
|
+
"refusing to write through symlink",
|
|
839
|
+
"structuredoutput", # workflow-internal schema fumble, not a code lesson
|
|
840
|
+
"validation error for cos_", # agent mis-called an MCP tool schema — fumble
|
|
841
|
+
"validation errors for cos_",
|
|
842
|
+
"exceeds maximum allowed", # oversized Read/tool payload — operational refusal
|
|
843
|
+
"scrape aborted", # external scraping engine refusal — environment, not code
|
|
844
|
+
"scraping engines failed",
|
|
845
|
+
"mcp error -", # raw MCP transport error — infrastructure, not a lesson
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _is_noise_failure(display: str) -> bool:
|
|
850
|
+
low = display.lower()
|
|
851
|
+
return any(marker in low for marker in _NOISE_FAILURE_MARKERS)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
# Known internal/model jargon → plain language, so a lesson reads for a novice
|
|
855
|
+
# (XAI/PAIR: speak the user's language, not the model's). Applied longest-first.
|
|
856
|
+
_JARGON_TRANSLATIONS: tuple[tuple[str, str], ...] = (
|
|
857
|
+
(
|
|
858
|
+
"predicates_unsatisfied: no evidencebundle for predicates ['coverage_100']",
|
|
859
|
+
"ended a 'fix everything' task without recording proof every case was handled",
|
|
860
|
+
),
|
|
861
|
+
("predicates_unsatisfied", "ended the task without the required proof-of-completion"),
|
|
862
|
+
("no evidencebundle", "no proof-of-completion was recorded"),
|
|
863
|
+
("task_not_closed", "left a task open"),
|
|
864
|
+
("does not match required schema", "the output's shape did not match what was required"),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _humanize_signature(display: str) -> str:
|
|
869
|
+
out = display
|
|
870
|
+
low = out.lower()
|
|
871
|
+
for jargon, plain in _JARGON_TRANSLATIONS:
|
|
872
|
+
idx = low.find(jargon)
|
|
873
|
+
if idx != -1:
|
|
874
|
+
out = out[:idx] + plain + out[idx + len(jargon) :]
|
|
875
|
+
low = out.lower()
|
|
876
|
+
return out
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _normalize_full(text: str) -> str:
|
|
880
|
+
# Lowercase, digits->N, non-word->space — the same rules as the friction
|
|
881
|
+
# cluster key but KEEPING every word, so a failure key stays a contiguous
|
|
882
|
+
# substring of a lesson's full display text.
|
|
883
|
+
return " ".join(_NONWORD_RE.split(re.sub(r"\d+", "N", (text or "").lower())))
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _load_surfaced_suggestions(path: Path) -> list[tuple[int, str]]:
|
|
887
|
+
out: list[tuple[int, str]] = []
|
|
888
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
889
|
+
if "\t" not in line:
|
|
890
|
+
continue
|
|
891
|
+
pid_s, text = line.split("\t", 1)
|
|
892
|
+
try:
|
|
893
|
+
out.append((int(pid_s), text))
|
|
894
|
+
except ValueError:
|
|
895
|
+
continue
|
|
896
|
+
return out
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _distill_fingerprint_safe(kind: str, cluster_key: str) -> str:
|
|
900
|
+
try:
|
|
901
|
+
import distill
|
|
902
|
+
|
|
903
|
+
return distill.cluster_fingerprint(kind, cluster_key)
|
|
904
|
+
except Exception as exc:
|
|
905
|
+
logger.debug("fingerprint skipped: %s", exc)
|
|
906
|
+
return ""
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def pattern_tier(confidence: float, times_validated: int) -> str:
|
|
910
|
+
"""Confidence tier for a learned pattern — the single mapping used by the UI
|
|
911
|
+
and digest. SSOT: learning-extraction.md § Confidence tier mapping.
|
|
912
|
+
|
|
913
|
+
Trusted = confirmed repeatedly · Fading = decaying, up for re-validation ·
|
|
914
|
+
Forming = seen, not yet confirmed.
|
|
915
|
+
"""
|
|
916
|
+
conf = confidence or 0.0
|
|
917
|
+
tv = times_validated or 0
|
|
918
|
+
if conf >= 0.7 and tv >= 3:
|
|
919
|
+
return "Trusted"
|
|
920
|
+
if 0.2 <= conf <= 0.4 and tv >= 1:
|
|
921
|
+
return "Fading"
|
|
922
|
+
return "Forming"
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _distill_safe(**kwargs) -> dict | None:
|
|
926
|
+
# Fire-and-forget: the distiller is optional enrichment — any failure
|
|
927
|
+
# (module missing, dispatcher down, headless without auth) falls back to
|
|
928
|
+
# the template producer.
|
|
929
|
+
try:
|
|
930
|
+
import distill
|
|
931
|
+
|
|
932
|
+
if not distill.enabled():
|
|
933
|
+
return None
|
|
934
|
+
return distill.distill_cluster(**kwargs)
|
|
935
|
+
except Exception as exc:
|
|
936
|
+
logger.debug("distillation skipped: %s", exc)
|
|
937
|
+
return None
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _adopt_legacy_template(conn: sqlite3.Connection, template_text: str, new_id: int) -> None:
|
|
941
|
+
# A distilled lesson supersedes the template row for the same cluster:
|
|
942
|
+
# fold the old counters in, then invalidate (archive), never delete.
|
|
943
|
+
identity = _pattern_identity(template_text)
|
|
944
|
+
for cand in conn.execute(
|
|
945
|
+
"SELECT id, pattern, times_seen, times_validated, access_count FROM learned_patterns "
|
|
946
|
+
"WHERE domain IS NULL AND COALESCE(promoted_to, '') != 'archived'",
|
|
947
|
+
):
|
|
948
|
+
if cand["id"] == new_id or _pattern_identity(cand["pattern"]) != identity:
|
|
949
|
+
continue
|
|
950
|
+
conn.execute(
|
|
951
|
+
"UPDATE learned_patterns SET times_seen = COALESCE(times_seen, 0) + ?, "
|
|
952
|
+
"times_validated = times_validated + ?, access_count = access_count + ? "
|
|
953
|
+
"WHERE id = ?",
|
|
954
|
+
(
|
|
955
|
+
cand["times_seen"] or 0,
|
|
956
|
+
cand["times_validated"] or 0,
|
|
957
|
+
cand["access_count"] or 0,
|
|
958
|
+
new_id,
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
conn.execute(
|
|
962
|
+
"UPDATE learned_patterns SET promoted_to = 'archived', "
|
|
963
|
+
"archived_at = CURRENT_TIMESTAMP WHERE id = ?",
|
|
964
|
+
(cand["id"],),
|
|
965
|
+
)
|
|
966
|
+
break
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _mint_friction_lesson(
|
|
970
|
+
conn: sqlite3.Connection,
|
|
971
|
+
*,
|
|
972
|
+
kind: str,
|
|
973
|
+
cluster_key: str,
|
|
974
|
+
count: int,
|
|
975
|
+
template_text: str,
|
|
976
|
+
concepts: str,
|
|
977
|
+
hook: str = "",
|
|
978
|
+
rule: str = "",
|
|
979
|
+
samples: list[str] | None = None,
|
|
980
|
+
distill_state: dict | None = None,
|
|
981
|
+
) -> dict:
|
|
982
|
+
# One write path for both friction miners: refresh an already-distilled
|
|
983
|
+
# cluster for free, distill a new one under the per-run budget, or fall
|
|
984
|
+
# back to the deterministic template.
|
|
985
|
+
fingerprint = None
|
|
986
|
+
try:
|
|
987
|
+
import distill
|
|
988
|
+
|
|
989
|
+
fingerprint = distill.cluster_fingerprint(kind, cluster_key)
|
|
990
|
+
except Exception as exc:
|
|
991
|
+
logger.debug("fingerprint unavailable: %s", exc)
|
|
992
|
+
|
|
993
|
+
if fingerprint:
|
|
994
|
+
try:
|
|
995
|
+
row = conn.execute(
|
|
996
|
+
"SELECT id, pattern FROM learned_patterns WHERE distill_fingerprint = ?",
|
|
997
|
+
(fingerprint,),
|
|
998
|
+
).fetchone()
|
|
999
|
+
except sqlite3.OperationalError:
|
|
1000
|
+
row = None
|
|
1001
|
+
if row:
|
|
1002
|
+
return _upsert_pattern(
|
|
1003
|
+
conn,
|
|
1004
|
+
pattern=row["pattern"],
|
|
1005
|
+
memory_type="lesson",
|
|
1006
|
+
domain=None,
|
|
1007
|
+
source="friction",
|
|
1008
|
+
confidence=0.5,
|
|
1009
|
+
concepts=concepts,
|
|
1010
|
+
provenance="llm_distilled",
|
|
1011
|
+
distill_fingerprint=fingerprint,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
budget_left = bool(distill_state) and distill_state.get("remaining", 0) > 0
|
|
1015
|
+
if fingerprint and budget_left:
|
|
1016
|
+
distill_state["remaining"] -= 1
|
|
1017
|
+
distilled = _distill_safe(
|
|
1018
|
+
kind=kind, signature=cluster_key, count=count, hook=hook, rule=rule, samples=samples
|
|
1019
|
+
)
|
|
1020
|
+
if distilled:
|
|
1021
|
+
import distill
|
|
1022
|
+
|
|
1023
|
+
result = _upsert_pattern(
|
|
1024
|
+
conn,
|
|
1025
|
+
pattern=distill.lesson_text(distilled),
|
|
1026
|
+
memory_type="lesson",
|
|
1027
|
+
domain=None,
|
|
1028
|
+
source="friction",
|
|
1029
|
+
confidence=0.5,
|
|
1030
|
+
concepts=concepts,
|
|
1031
|
+
provenance="llm_distilled",
|
|
1032
|
+
distill_fingerprint=fingerprint,
|
|
1033
|
+
evidence_json=json.dumps(
|
|
1034
|
+
{"samples": distill.sanitize_samples(samples or []), "recurrences": count}
|
|
1035
|
+
),
|
|
1036
|
+
)
|
|
1037
|
+
if result.get("id"):
|
|
1038
|
+
_adopt_legacy_template(conn, template_text, result["id"])
|
|
1039
|
+
return result
|
|
1040
|
+
|
|
1041
|
+
return _upsert_pattern(
|
|
1042
|
+
conn,
|
|
1043
|
+
pattern=template_text,
|
|
1044
|
+
memory_type="lesson",
|
|
1045
|
+
domain=None,
|
|
1046
|
+
source="friction",
|
|
1047
|
+
confidence=min(0.85, 0.4 + count / 10.0),
|
|
1048
|
+
concepts=concepts,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _mine_friction_lessons(
|
|
1053
|
+
conn: sqlite3.Connection,
|
|
1054
|
+
*,
|
|
1055
|
+
min_occurrences: int = 3,
|
|
1056
|
+
distill_state: dict | None = None,
|
|
1057
|
+
) -> list[dict]:
|
|
1058
|
+
# Fire-and-forget: a missing observations table/column never breaks extraction.
|
|
1059
|
+
floor = max(1, min(min_occurrences, _FRICTION_MIN_OCCURRENCES))
|
|
1060
|
+
try:
|
|
1061
|
+
rows = conn.execute(
|
|
1062
|
+
"SELECT title, narrative, memory_type, files_modified FROM observations "
|
|
1063
|
+
"WHERE memory_type IN ('hook_block', 'error') AND COALESCE(narrative, '') != '' "
|
|
1064
|
+
" AND created_at >= datetime('now', '-' || ? || ' days')",
|
|
1065
|
+
(_LESSON_WINDOW_DAYS,),
|
|
1066
|
+
).fetchall()
|
|
1067
|
+
except sqlite3.OperationalError as exc:
|
|
1068
|
+
logger.debug("friction mining skipped: %s", exc)
|
|
1069
|
+
return []
|
|
1070
|
+
|
|
1071
|
+
clusters: dict[str, dict] = {}
|
|
1072
|
+
for row in rows:
|
|
1073
|
+
d = dict(row)
|
|
1074
|
+
# Screen title AND narrative: a StructuredOutput fumble carries the marker
|
|
1075
|
+
# in the title while the narrative reads like a generic schema error.
|
|
1076
|
+
if _is_noise_failure(f"{d['title'] or ''} {d['narrative'] or ''}"):
|
|
1077
|
+
continue # tool-fumble / expected refusal — never a lesson
|
|
1078
|
+
display = _clean_failure_text(d["narrative"] or d["title"] or "")
|
|
1079
|
+
key = _failure_cluster_key(display)
|
|
1080
|
+
if not key:
|
|
1081
|
+
continue
|
|
1082
|
+
cluster = clusters.setdefault(
|
|
1083
|
+
key,
|
|
1084
|
+
{
|
|
1085
|
+
"count": 0,
|
|
1086
|
+
# store the humanized signature so the minted lesson reads plainly
|
|
1087
|
+
"display": _humanize_signature(display),
|
|
1088
|
+
"kind": _friction_kind(d["title"], d["narrative"], d["memory_type"]),
|
|
1089
|
+
"files": set(), # source-file basenames → concepts, for JIT recall
|
|
1090
|
+
"samples": [],
|
|
1091
|
+
},
|
|
1092
|
+
)
|
|
1093
|
+
cluster["count"] += 1
|
|
1094
|
+
if len(cluster["samples"]) < 3:
|
|
1095
|
+
cluster["samples"].append(display)
|
|
1096
|
+
fm = d.get("files_modified") or ""
|
|
1097
|
+
if fm:
|
|
1098
|
+
cluster["files"].add(fm.rsplit("/", 1)[-1])
|
|
1099
|
+
|
|
1100
|
+
lessons: list[dict] = []
|
|
1101
|
+
for key, cluster in clusters.items():
|
|
1102
|
+
if cluster["count"] < floor:
|
|
1103
|
+
continue
|
|
1104
|
+
hint = _FRICTION_HINTS.get(cluster["kind"], _FRICTION_HINTS["error"])
|
|
1105
|
+
# Count rendered as "(N occurrences)" so _pattern_identity strips it and
|
|
1106
|
+
# a re-mined cluster UPDATES its row instead of inserting a snapshot.
|
|
1107
|
+
pattern_text = (
|
|
1108
|
+
f"Recurring {cluster['kind'].replace('_', ' ')} "
|
|
1109
|
+
f"({cluster['count']} occurrences): {cluster['display']} → {hint}"
|
|
1110
|
+
)
|
|
1111
|
+
lessons.append(
|
|
1112
|
+
_mint_friction_lesson(
|
|
1113
|
+
conn,
|
|
1114
|
+
kind=cluster["kind"],
|
|
1115
|
+
cluster_key=key,
|
|
1116
|
+
count=cluster["count"],
|
|
1117
|
+
template_text=pattern_text,
|
|
1118
|
+
samples=cluster["samples"],
|
|
1119
|
+
distill_state=distill_state,
|
|
1120
|
+
# file:<basename> tokens key JIT recall on the friction's source
|
|
1121
|
+
# file (not basename-in-humanized-text, which never matched).
|
|
1122
|
+
concepts=json.dumps(
|
|
1123
|
+
["lesson", cluster["kind"], "friction"]
|
|
1124
|
+
+ [f"file:{b}" for b in sorted(cluster["files"])[:5] if b]
|
|
1125
|
+
),
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
return lessons
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
# A hook-log block line: "[<ts>] [<hook>] [block] … rule=<rule> …".
|
|
1132
|
+
_BLOCK_LINE_RE = re.compile(
|
|
1133
|
+
r"^\[(?P<ts>[^\]]+)\]\s+\[(?P<hook>[^\]]+)\]\s+\[block\]\s*(?P<rest>.*)$"
|
|
1134
|
+
)
|
|
1135
|
+
_BLOCK_RULE_RE = re.compile(r"\brule=(\S+)")
|
|
1136
|
+
# Recency window shared by both friction miners: a failure/block only counts as
|
|
1137
|
+
# a recurring lesson if it recurs within this window. Old/resolved/renamed-rule
|
|
1138
|
+
# failures age out (stop being re-confirmed) and decay instead of persisting.
|
|
1139
|
+
_LESSON_WINDOW_DAYS = 90
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _hook_log_paths(conn: sqlite3.Connection) -> list[Path]:
|
|
1143
|
+
# Most-durable first: block-only log (survives the main log's cap) then the
|
|
1144
|
+
# main hook log. Env overrides win; otherwise derive from the project root.
|
|
1145
|
+
paths: list[Path] = []
|
|
1146
|
+
blk = os.environ.get("COS_HOOK_BLOCK_LOG")
|
|
1147
|
+
main = os.environ.get("COS_HOOK_LOG")
|
|
1148
|
+
if blk:
|
|
1149
|
+
paths.append(Path(blk))
|
|
1150
|
+
if main:
|
|
1151
|
+
paths.append(Path(main))
|
|
1152
|
+
if not paths:
|
|
1153
|
+
root = _derive_project_root(conn)
|
|
1154
|
+
if root:
|
|
1155
|
+
paths.append(root / ".coding-os" / ".hook-blocks.log")
|
|
1156
|
+
paths.append(root / ".coding-os" / ".hooks.log")
|
|
1157
|
+
return paths
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _mine_hook_block_lessons(
|
|
1161
|
+
conn: sqlite3.Connection,
|
|
1162
|
+
*,
|
|
1163
|
+
min_occurrences: int = 3,
|
|
1164
|
+
distill_state: dict | None = None,
|
|
1165
|
+
) -> list[dict]:
|
|
1166
|
+
# Hook BLOCKs never reach the observations table on Claude (no PostToolUseFailure)
|
|
1167
|
+
# but are in the append-only hook log — mine them there. Fire-and-forget.
|
|
1168
|
+
floor = max(1, min(min_occurrences, _FRICTION_MIN_OCCURRENCES))
|
|
1169
|
+
# Single source, not a merge: every block is mirrored to both logs, so the
|
|
1170
|
+
# block-only log is a strict superset of the main log's surviving blocks.
|
|
1171
|
+
# Read the first existing, non-empty candidate (block log preferred) — this
|
|
1172
|
+
# avoids double-counting a mirrored block while preserving genuine repeats.
|
|
1173
|
+
log_path = None
|
|
1174
|
+
for lp in _hook_log_paths(conn):
|
|
1175
|
+
try:
|
|
1176
|
+
if lp.exists() and lp.stat().st_size > 0:
|
|
1177
|
+
log_path = lp
|
|
1178
|
+
break
|
|
1179
|
+
except OSError:
|
|
1180
|
+
continue
|
|
1181
|
+
if log_path is None:
|
|
1182
|
+
return []
|
|
1183
|
+
try:
|
|
1184
|
+
lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
1185
|
+
except OSError as exc:
|
|
1186
|
+
logger.debug("hook-block mining skipped (read %s): %s", log_path, exc)
|
|
1187
|
+
return []
|
|
1188
|
+
|
|
1189
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=_LESSON_WINDOW_DAYS)
|
|
1190
|
+
clusters: dict[str, dict] = {}
|
|
1191
|
+
for line in lines:
|
|
1192
|
+
match = _BLOCK_LINE_RE.match(line)
|
|
1193
|
+
if not match:
|
|
1194
|
+
continue
|
|
1195
|
+
try:
|
|
1196
|
+
ts = datetime.fromisoformat(match.group("ts").replace("Z", "+00:00"))
|
|
1197
|
+
if ts.tzinfo is None:
|
|
1198
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
1199
|
+
if ts < cutoff:
|
|
1200
|
+
continue
|
|
1201
|
+
except ValueError:
|
|
1202
|
+
continue # unparseable timestamp — skip, don't guess
|
|
1203
|
+
hook = match.group("hook").strip()
|
|
1204
|
+
rest = match.group("rest") or ""
|
|
1205
|
+
rule_match = _BLOCK_RULE_RE.search(rest)
|
|
1206
|
+
rule = rule_match.group(1) if rule_match else ""
|
|
1207
|
+
key = f"{hook}:{rule}"
|
|
1208
|
+
cluster = clusters.setdefault(key, {"count": 0, "hook": hook, "rule": rule, "samples": []})
|
|
1209
|
+
cluster["count"] += 1
|
|
1210
|
+
if rest and len(cluster["samples"]) < 3:
|
|
1211
|
+
cluster["samples"].append(rest)
|
|
1212
|
+
|
|
1213
|
+
lessons: list[dict] = []
|
|
1214
|
+
for key, cluster in clusters.items():
|
|
1215
|
+
if cluster["count"] < floor:
|
|
1216
|
+
continue
|
|
1217
|
+
subject = f"{cluster['hook']} — {cluster['rule']}" if cluster["rule"] else cluster["hook"]
|
|
1218
|
+
pattern_text = (
|
|
1219
|
+
f"Recurring block ({cluster['count']} occurrences): {subject} "
|
|
1220
|
+
f"→ satisfy the blocked rule before retrying the action"
|
|
1221
|
+
)
|
|
1222
|
+
lessons.append(
|
|
1223
|
+
_mint_friction_lesson(
|
|
1224
|
+
conn,
|
|
1225
|
+
kind="hook_block",
|
|
1226
|
+
cluster_key=key,
|
|
1227
|
+
count=cluster["count"],
|
|
1228
|
+
template_text=pattern_text,
|
|
1229
|
+
hook=cluster["hook"],
|
|
1230
|
+
rule=cluster["rule"],
|
|
1231
|
+
samples=cluster["samples"],
|
|
1232
|
+
distill_state=distill_state,
|
|
1233
|
+
concepts=json.dumps(["lesson", "hook_block", cluster["hook"]]),
|
|
1234
|
+
)
|
|
1235
|
+
)
|
|
1236
|
+
return lessons
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
# ---------------------------------------------------------------------------
|
|
1240
|
+
# Commit-history lesson mining (the real engineering-lesson signal)
|
|
1241
|
+
# ---------------------------------------------------------------------------
|
|
1242
|
+
|
|
1243
|
+
# A Conventional-Commit subject whose type means "something was wrong → fixed":
|
|
1244
|
+
# fix:/revert: (optional scope, optional !). The subject IS a recorded lesson.
|
|
1245
|
+
_FIX_COMMIT_RE = re.compile(
|
|
1246
|
+
r"^(?P<type>fix|revert)(?:\([^)]*\))?!?:\s*(?P<subject>.+)$", re.IGNORECASE
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
# A one-off `fix:` subject is terse shorthand with no reusable rule — noise.
|
|
1250
|
+
# Only a fix that RECURS this many times is a systemic-gap signal. Reverts are
|
|
1251
|
+
# minted at any count (a revert is itself a recorded mistake). See §5 of the doc.
|
|
1252
|
+
_COMMIT_FIX_MIN_RECURRENCE = 3
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _commit_subject_key(subject: str) -> str:
|
|
1256
|
+
s = _TASKID_RE.sub("TASK-N", subject)
|
|
1257
|
+
s = _LONGHEX_RE.sub("<hash>", s)
|
|
1258
|
+
s = re.sub(r"\d+", "N", s.lower())
|
|
1259
|
+
words = [w for w in _NONWORD_RE.split(s) if w]
|
|
1260
|
+
return " ".join(words[:8])
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _mine_commit_lessons(conn: sqlite3.Connection, *, min_occurrences: int = 3) -> list[dict]:
|
|
1264
|
+
# A fix:/revert: commit IS a recorded "something was wrong → correction".
|
|
1265
|
+
# Read-only git log, bounded, no-op outside a work-tree.
|
|
1266
|
+
# Contract: docs/engineering/learning-extraction.md §5.
|
|
1267
|
+
import subprocess
|
|
1268
|
+
|
|
1269
|
+
root = _derive_project_root(conn)
|
|
1270
|
+
if root is None:
|
|
1271
|
+
return []
|
|
1272
|
+
try:
|
|
1273
|
+
proc = subprocess.run(
|
|
1274
|
+
[
|
|
1275
|
+
"git",
|
|
1276
|
+
"-C",
|
|
1277
|
+
str(root),
|
|
1278
|
+
"log",
|
|
1279
|
+
f"--since={_LESSON_WINDOW_DAYS} days ago",
|
|
1280
|
+
"--max-count=2000",
|
|
1281
|
+
"--no-merges",
|
|
1282
|
+
"--pretty=format:%s",
|
|
1283
|
+
],
|
|
1284
|
+
capture_output=True,
|
|
1285
|
+
text=True,
|
|
1286
|
+
timeout=10,
|
|
1287
|
+
check=False,
|
|
1288
|
+
)
|
|
1289
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1290
|
+
logger.debug("commit mining skipped: %s", exc)
|
|
1291
|
+
return []
|
|
1292
|
+
if proc.returncode != 0:
|
|
1293
|
+
return []
|
|
1294
|
+
|
|
1295
|
+
clusters: dict[str, dict] = {}
|
|
1296
|
+
for line in proc.stdout.splitlines():
|
|
1297
|
+
match = _FIX_COMMIT_RE.match(line.strip())
|
|
1298
|
+
if not match:
|
|
1299
|
+
continue
|
|
1300
|
+
subject = match.group("subject").strip()
|
|
1301
|
+
key = _commit_subject_key(subject)
|
|
1302
|
+
if not key:
|
|
1303
|
+
continue
|
|
1304
|
+
cluster = clusters.setdefault(key, {"count": 0, "subject": subject, "revert": False})
|
|
1305
|
+
cluster["count"] += 1
|
|
1306
|
+
if match.group("type").lower() == "revert":
|
|
1307
|
+
cluster["revert"] = True
|
|
1308
|
+
|
|
1309
|
+
lessons: list[dict] = []
|
|
1310
|
+
for cluster in clusters.values():
|
|
1311
|
+
is_revert = cluster["revert"]
|
|
1312
|
+
subject = _clean_failure_text(cluster["subject"])
|
|
1313
|
+
if is_revert:
|
|
1314
|
+
# A revert is a recorded "we shipped this and undid it" — real signal.
|
|
1315
|
+
pattern_text = (
|
|
1316
|
+
f"Reverted before: {subject} → reconsider before re-introducing this change."
|
|
1317
|
+
)
|
|
1318
|
+
elif cluster["count"] >= _COMMIT_FIX_MIN_RECURRENCE:
|
|
1319
|
+
# The RECURRENCE is the signal (same thing keeps breaking), not the
|
|
1320
|
+
# subject itself. "(N occurrences)" so _pattern_identity dedups it.
|
|
1321
|
+
pattern_text = (
|
|
1322
|
+
f"Fixed repeatedly ({cluster['count']} occurrences): {subject} "
|
|
1323
|
+
f"→ address the root cause, not the symptom."
|
|
1324
|
+
)
|
|
1325
|
+
else:
|
|
1326
|
+
continue # one-off / 2x fix subject — no reusable lesson, drop it
|
|
1327
|
+
lessons.append(
|
|
1328
|
+
_upsert_pattern(
|
|
1329
|
+
conn,
|
|
1330
|
+
pattern=pattern_text,
|
|
1331
|
+
memory_type="lesson",
|
|
1332
|
+
domain=None,
|
|
1333
|
+
source="commit",
|
|
1334
|
+
confidence=min(0.85, 0.4 + cluster["count"] / 10.0),
|
|
1335
|
+
concepts=json.dumps(["lesson", "commit", "revert" if is_revert else "fix"]),
|
|
1336
|
+
)
|
|
1337
|
+
)
|
|
1338
|
+
return lessons
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
# ---------------------------------------------------------------------------
|
|
1342
|
+
# cos_learn_suggest
|
|
1343
|
+
# ---------------------------------------------------------------------------
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def learn_suggest(
|
|
1347
|
+
conn: sqlite3.Connection,
|
|
1348
|
+
*,
|
|
1349
|
+
domain: str | None = None,
|
|
1350
|
+
complexity: str | None = None,
|
|
1351
|
+
task_type: str | None = None,
|
|
1352
|
+
limit: int = 5,
|
|
1353
|
+
) -> dict:
|
|
1354
|
+
"""Return relevant patterns for the current task context.
|
|
1355
|
+
|
|
1356
|
+
Includes spaced repetition: patterns at 0.2-0.4 confidence that
|
|
1357
|
+
were once validated get priority with "fading" label.
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
conn: SQLite connection.
|
|
1361
|
+
domain: Task domain (e.g. "BACKEND").
|
|
1362
|
+
complexity: Cynefin classification.
|
|
1363
|
+
task_type: Type of task (e.g. "feat", "fix").
|
|
1364
|
+
limit: Max suggestions (1-20, default 5).
|
|
1365
|
+
|
|
1366
|
+
Returns:
|
|
1367
|
+
Dict with suggestions list.
|
|
1368
|
+
"""
|
|
1369
|
+
limit = max(1, min(20, limit))
|
|
1370
|
+
suggestions: list[dict] = []
|
|
1371
|
+
|
|
1372
|
+
# --- Active patterns (confidence > 0.3) ---
|
|
1373
|
+
# Exclude stats — a success-rate baseline is observability, never a
|
|
1374
|
+
# suggestion to act on. See docs/engineering/learning-extraction.md.
|
|
1375
|
+
conditions = [
|
|
1376
|
+
"confidence >= 0.3",
|
|
1377
|
+
"COALESCE(memory_type, '') != 'stat'",
|
|
1378
|
+
"promoted_to IS NULL",
|
|
1379
|
+
]
|
|
1380
|
+
params: list = []
|
|
1381
|
+
if domain:
|
|
1382
|
+
conditions.append("(domain = ? OR domain IS NULL)")
|
|
1383
|
+
params.append(domain)
|
|
1384
|
+
where = " AND ".join(conditions)
|
|
1385
|
+
|
|
1386
|
+
# Relevance boost: complexity + task_type used to be accepted
|
|
1387
|
+
# then ignored, so recall was relevance-blind. There is no per-pattern
|
|
1388
|
+
# complexity/task_type column, so we BOOST (never exclude) patterns whose
|
|
1389
|
+
# concepts/pattern text mention the term — a matching pattern outranks an
|
|
1390
|
+
# equally-confident non-match. Boost params bind first (SELECT precedes WHERE).
|
|
1391
|
+
boost_terms: list[str] = []
|
|
1392
|
+
boost_params: list = []
|
|
1393
|
+
for term in (complexity, task_type):
|
|
1394
|
+
if term:
|
|
1395
|
+
boost_terms.append(
|
|
1396
|
+
"(CASE WHEN LOWER(COALESCE(concepts,'')) LIKE ? "
|
|
1397
|
+
"OR LOWER(pattern) LIKE ? THEN 1 ELSE 0 END)"
|
|
1398
|
+
)
|
|
1399
|
+
like = f"%{term.lower()}%"
|
|
1400
|
+
boost_params += [like, like]
|
|
1401
|
+
relevance = " + ".join(boost_terms) if boost_terms else "0"
|
|
1402
|
+
|
|
1403
|
+
active_rows = conn.execute(
|
|
1404
|
+
f"SELECT id, pattern, memory_type, domain, confidence, impact_score, "
|
|
1405
|
+
f"times_validated, times_violated, ({relevance}) AS relevance "
|
|
1406
|
+
f"FROM learned_patterns WHERE {where} "
|
|
1407
|
+
"ORDER BY relevance DESC, confidence DESC, impact_score DESC LIMIT ?",
|
|
1408
|
+
boost_params + params + [limit],
|
|
1409
|
+
).fetchall()
|
|
1410
|
+
|
|
1411
|
+
for row in active_rows:
|
|
1412
|
+
d = dict(row)
|
|
1413
|
+
suggestions.append(
|
|
1414
|
+
{
|
|
1415
|
+
"id": d["id"],
|
|
1416
|
+
"pattern": d["pattern"],
|
|
1417
|
+
"confidence": d["confidence"],
|
|
1418
|
+
"impact_score": d.get("impact_score", 0.5),
|
|
1419
|
+
"memory_type": d["memory_type"],
|
|
1420
|
+
"reason": "active",
|
|
1421
|
+
}
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
# --- Fading patterns (spaced repetition) ---
|
|
1425
|
+
fading_conditions = [
|
|
1426
|
+
"confidence BETWEEN 0.2 AND 0.4",
|
|
1427
|
+
# Established-ness (occurrence), not validation: after the honest
|
|
1428
|
+
# times_validated reset a "seen" pattern still resurfaces for review.
|
|
1429
|
+
"times_seen >= 1",
|
|
1430
|
+
"COALESCE(memory_type, '') != 'stat'",
|
|
1431
|
+
"promoted_to IS NULL",
|
|
1432
|
+
]
|
|
1433
|
+
fading_params: list = []
|
|
1434
|
+
if domain:
|
|
1435
|
+
fading_conditions.append("(domain = ? OR domain IS NULL)")
|
|
1436
|
+
fading_params.append(domain)
|
|
1437
|
+
fading_where = " AND ".join(fading_conditions)
|
|
1438
|
+
|
|
1439
|
+
fading_rows = conn.execute(
|
|
1440
|
+
f"SELECT id, pattern, memory_type, domain, confidence, impact_score, "
|
|
1441
|
+
f"times_validated, times_violated "
|
|
1442
|
+
f"FROM learned_patterns WHERE {fading_where} "
|
|
1443
|
+
"ORDER BY confidence ASC LIMIT 3",
|
|
1444
|
+
fading_params,
|
|
1445
|
+
).fetchall()
|
|
1446
|
+
|
|
1447
|
+
for row in fading_rows:
|
|
1448
|
+
d = dict(row)
|
|
1449
|
+
suggestions.insert(
|
|
1450
|
+
0,
|
|
1451
|
+
{ # fading patterns go first
|
|
1452
|
+
"id": d["id"],
|
|
1453
|
+
"pattern": d["pattern"],
|
|
1454
|
+
"confidence": d["confidence"],
|
|
1455
|
+
"impact_score": d.get("impact_score", 0.5),
|
|
1456
|
+
"memory_type": d["memory_type"],
|
|
1457
|
+
"reason": "fading",
|
|
1458
|
+
},
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
# --- Breakthrough narratives (high-value lessons from past struggles) ---
|
|
1462
|
+
try:
|
|
1463
|
+
bt_conditions = ["oh.is_breakthrough = 1", "oh.narrative_key_insight IS NOT NULL"]
|
|
1464
|
+
bt_params: list = []
|
|
1465
|
+
if domain:
|
|
1466
|
+
bt_conditions.append("t.domain = ?")
|
|
1467
|
+
bt_params.append(domain)
|
|
1468
|
+
bt_where = " AND ".join(bt_conditions)
|
|
1469
|
+
|
|
1470
|
+
bt_rows = conn.execute(
|
|
1471
|
+
f"SELECT oh.task_id, oh.narrative_key_insight, oh.narrative_what_failed, "
|
|
1472
|
+
f"oh.previous_outcome, t.domain "
|
|
1473
|
+
f"FROM outcome_history oh "
|
|
1474
|
+
f"LEFT JOIN task_outcomes t ON oh.task_id = t.task_id "
|
|
1475
|
+
f"WHERE {bt_where} "
|
|
1476
|
+
"ORDER BY oh.created_at DESC LIMIT 3",
|
|
1477
|
+
bt_params,
|
|
1478
|
+
).fetchall()
|
|
1479
|
+
|
|
1480
|
+
for row in bt_rows:
|
|
1481
|
+
d = dict(row)
|
|
1482
|
+
insight = d["narrative_key_insight"] or ""
|
|
1483
|
+
failed = d.get("narrative_what_failed") or ""
|
|
1484
|
+
label = f"[Breakthrough] {insight}"
|
|
1485
|
+
if failed:
|
|
1486
|
+
label += f" (avoid: {failed[:60]})"
|
|
1487
|
+
suggestions.append(
|
|
1488
|
+
{
|
|
1489
|
+
"id": None,
|
|
1490
|
+
"pattern": label,
|
|
1491
|
+
"confidence": 0.8,
|
|
1492
|
+
"impact_score": 0.9,
|
|
1493
|
+
"memory_type": "breakthrough",
|
|
1494
|
+
"reason": f"breakthrough from {d['task_id']}",
|
|
1495
|
+
}
|
|
1496
|
+
)
|
|
1497
|
+
except Exception:
|
|
1498
|
+
pass # outcome_history may not exist on pre-v4 DBs
|
|
1499
|
+
|
|
1500
|
+
return {"suggestions": suggestions[:limit], "count": min(len(suggestions), limit)}
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
# ---------------------------------------------------------------------------
|
|
1504
|
+
# cos_learn_validate
|
|
1505
|
+
# ---------------------------------------------------------------------------
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def learn_validate(
|
|
1509
|
+
conn: sqlite3.Connection,
|
|
1510
|
+
*,
|
|
1511
|
+
pattern_id: int,
|
|
1512
|
+
was_helpful: bool,
|
|
1513
|
+
) -> dict:
|
|
1514
|
+
"""Record whether a suggested pattern was helpful.
|
|
1515
|
+
|
|
1516
|
+
Applies confidence formulas:
|
|
1517
|
+
- helpful: LTP with diminishing returns + temporal proximity check
|
|
1518
|
+
- not helpful: LTD proportional penalty
|
|
1519
|
+
|
|
1520
|
+
Self-validation throttle:
|
|
1521
|
+
- Every call is logged to `pattern_validations` (INSERT, append-only).
|
|
1522
|
+
- If the same (session_id, pattern_id, was_helpful=True) was already
|
|
1523
|
+
recorded within THROTTLE_WINDOW_SECONDS, the call is marked
|
|
1524
|
+
`was_throttled=1` and confidence is NOT boosted. Violation (negative
|
|
1525
|
+
feedback) is never throttled — agents must always be able to flag
|
|
1526
|
+
bad patterns.
|
|
1527
|
+
|
|
1528
|
+
Args:
|
|
1529
|
+
conn: SQLite connection.
|
|
1530
|
+
pattern_id: ID in learned_patterns table.
|
|
1531
|
+
was_helpful: Whether the pattern was useful.
|
|
1532
|
+
|
|
1533
|
+
Returns:
|
|
1534
|
+
Dict with updated confidence and status.
|
|
1535
|
+
"""
|
|
1536
|
+
row = conn.execute(
|
|
1537
|
+
"SELECT id, confidence, times_validated, times_violated, decay_rate, trust_tier "
|
|
1538
|
+
"FROM learned_patterns WHERE id = ?",
|
|
1539
|
+
(pattern_id,),
|
|
1540
|
+
).fetchone()
|
|
1541
|
+
|
|
1542
|
+
if row is None:
|
|
1543
|
+
return {"error": f"Pattern not found: id={pattern_id}"}
|
|
1544
|
+
|
|
1545
|
+
# guard: locked/core patterns cannot be mutated via this path
|
|
1546
|
+
# even though the trigger would also block it. Return a clean validation
|
|
1547
|
+
# error instead of letting SQLite raise.
|
|
1548
|
+
trust_tier = row["trust_tier"] if "trust_tier" in row.keys() else "volatile"
|
|
1549
|
+
if trust_tier in {"locked", "core"}:
|
|
1550
|
+
return {
|
|
1551
|
+
"error": f"Pattern {pattern_id} is {trust_tier} — immutable via cos_learn_validate",
|
|
1552
|
+
"pattern_id": pattern_id,
|
|
1553
|
+
"trust_tier": trust_tier,
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
# throttle — only applies to positive validations
|
|
1557
|
+
throttled = False
|
|
1558
|
+
session_id = _read_session_id_for_validate()
|
|
1559
|
+
if was_helpful and _has_recent_validation(conn, session_id, pattern_id):
|
|
1560
|
+
throttled = True
|
|
1561
|
+
|
|
1562
|
+
# Always log the attempt (throttled or not) for audit + sycophancy
|
|
1563
|
+
# detection in later phases.
|
|
1564
|
+
_log_validation(
|
|
1565
|
+
conn,
|
|
1566
|
+
session_id=session_id,
|
|
1567
|
+
pattern_id=pattern_id,
|
|
1568
|
+
was_helpful=was_helpful,
|
|
1569
|
+
was_throttled=throttled,
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
if throttled:
|
|
1573
|
+
# Return current state without confidence mutation
|
|
1574
|
+
return {
|
|
1575
|
+
"status": "throttled",
|
|
1576
|
+
"pattern_id": pattern_id,
|
|
1577
|
+
"old_confidence": round(row["confidence"], 4),
|
|
1578
|
+
"new_confidence": round(row["confidence"], 4),
|
|
1579
|
+
"was_helpful": was_helpful,
|
|
1580
|
+
"reason": f"same (session, pattern) validated within {_THROTTLE_WINDOW_SECONDS}s",
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
old_conf = row["confidence"]
|
|
1584
|
+
decay_rate = row["decay_rate"]
|
|
1585
|
+
|
|
1586
|
+
if was_helpful:
|
|
1587
|
+
new_conf = boost_success(old_conf)
|
|
1588
|
+
|
|
1589
|
+
# Temporal proximity check — 2+ validations in 48h
|
|
1590
|
+
recent_count = conn.execute(
|
|
1591
|
+
"SELECT COUNT(*) FROM learned_patterns "
|
|
1592
|
+
"WHERE id = ? AND last_validated >= datetime('now', '-48 hours')",
|
|
1593
|
+
(pattern_id,),
|
|
1594
|
+
).fetchone()[0]
|
|
1595
|
+
|
|
1596
|
+
if recent_count >= 1: # this will be the 2nd+ in 48h
|
|
1597
|
+
new_conf = min(0.95, new_conf + 0.05)
|
|
1598
|
+
decay_rate = decay_rate * 0.7
|
|
1599
|
+
|
|
1600
|
+
conn.execute(
|
|
1601
|
+
"UPDATE learned_patterns SET "
|
|
1602
|
+
"confidence = ?, "
|
|
1603
|
+
"times_validated = times_validated + 1, "
|
|
1604
|
+
"last_validated = CURRENT_TIMESTAMP, "
|
|
1605
|
+
"decay_rate = ? "
|
|
1606
|
+
"WHERE id = ?",
|
|
1607
|
+
(new_conf, decay_rate, pattern_id),
|
|
1608
|
+
)
|
|
1609
|
+
else:
|
|
1610
|
+
new_conf = penalize_failure(old_conf)
|
|
1611
|
+
conn.execute(
|
|
1612
|
+
"UPDATE learned_patterns SET "
|
|
1613
|
+
"confidence = ?, "
|
|
1614
|
+
"times_violated = times_violated + 1, "
|
|
1615
|
+
"last_validated = CURRENT_TIMESTAMP "
|
|
1616
|
+
"WHERE id = ?",
|
|
1617
|
+
(new_conf, pattern_id),
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
conn.commit()
|
|
1621
|
+
return {
|
|
1622
|
+
"status": "validated" if was_helpful else "penalized",
|
|
1623
|
+
"pattern_id": pattern_id,
|
|
1624
|
+
"old_confidence": round(old_conf, 4),
|
|
1625
|
+
"new_confidence": round(new_conf, 4),
|
|
1626
|
+
"was_helpful": was_helpful,
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def validate_surfaced_lessons(
|
|
1631
|
+
conn: sqlite3.Connection,
|
|
1632
|
+
*,
|
|
1633
|
+
session_id: str,
|
|
1634
|
+
suggestions_path: str | Path,
|
|
1635
|
+
) -> dict:
|
|
1636
|
+
"""Close the learn->apply->confirm loop for one completed task: validate each
|
|
1637
|
+
lesson surfaced during Orient against this session's post-recall friction — a
|
|
1638
|
+
lesson whose failure recurred is penalized (LTD), the rest reinforced (LTP).
|
|
1639
|
+
|
|
1640
|
+
The single primitive BOTH the task-done Bash hook and the MCP completion path
|
|
1641
|
+
call; divergence here was why surfaced patterns never reached the Trusted tier.
|
|
1642
|
+
"""
|
|
1643
|
+
sf = Path(suggestions_path)
|
|
1644
|
+
if not session_id or not sf.exists():
|
|
1645
|
+
return {"status": "skipped"}
|
|
1646
|
+
surfaced = _load_surfaced_suggestions(sf)
|
|
1647
|
+
if not surfaced:
|
|
1648
|
+
return {"status": "no_suggestions"}
|
|
1649
|
+
|
|
1650
|
+
# Only failures recorded AT/AFTER the recall (suggestions file mtime) count.
|
|
1651
|
+
recall_at = datetime.fromtimestamp(sf.stat().st_mtime, tz=timezone.utc).strftime(
|
|
1652
|
+
"%Y-%m-%d %H:%M:%S"
|
|
1653
|
+
)
|
|
1654
|
+
rows = conn.execute(
|
|
1655
|
+
"SELECT narrative, title, memory_type FROM observations "
|
|
1656
|
+
"WHERE session_id = ? AND memory_type IN ('hook_block', 'error') "
|
|
1657
|
+
" AND created_at >= ?",
|
|
1658
|
+
(session_id, recall_at),
|
|
1659
|
+
).fetchall()
|
|
1660
|
+
failure_keys: list[str] = []
|
|
1661
|
+
failure_fingerprints: set[str] = set()
|
|
1662
|
+
for r in rows:
|
|
1663
|
+
d = dict(r)
|
|
1664
|
+
key = _failure_cluster_key(_clean_failure_text(d["narrative"] or d["title"] or ""))
|
|
1665
|
+
if not key:
|
|
1666
|
+
continue
|
|
1667
|
+
failure_keys.append(key)
|
|
1668
|
+
fp = _distill_fingerprint_safe(
|
|
1669
|
+
_friction_kind(d["title"], d["narrative"], d["memory_type"]), key
|
|
1670
|
+
)
|
|
1671
|
+
if fp:
|
|
1672
|
+
failure_fingerprints.add(fp)
|
|
1673
|
+
|
|
1674
|
+
# A distilled lesson no longer contains the raw failure text, so matching its
|
|
1675
|
+
# display text alone would always read helpful=True. Match the stored
|
|
1676
|
+
# fingerprint and evidence samples too.
|
|
1677
|
+
lesson_meta: dict[int, tuple[str, str]] = {}
|
|
1678
|
+
try:
|
|
1679
|
+
placeholders = ",".join("?" * len(surfaced))
|
|
1680
|
+
for row in conn.execute(
|
|
1681
|
+
"SELECT id, distill_fingerprint, evidence_json FROM learned_patterns "
|
|
1682
|
+
f"WHERE id IN ({placeholders})",
|
|
1683
|
+
[pid for pid, _ in surfaced],
|
|
1684
|
+
):
|
|
1685
|
+
d = dict(row)
|
|
1686
|
+
lesson_meta[d["id"]] = (
|
|
1687
|
+
d.get("distill_fingerprint") or "",
|
|
1688
|
+
_normalize_full(d.get("evidence_json") or ""),
|
|
1689
|
+
)
|
|
1690
|
+
except sqlite3.Error:
|
|
1691
|
+
lesson_meta = {}
|
|
1692
|
+
|
|
1693
|
+
helpful = unhelpful = 0
|
|
1694
|
+
for pid, text in surfaced:
|
|
1695
|
+
lesson_norm = _normalize_full(text)
|
|
1696
|
+
fingerprint, evidence_norm = lesson_meta.get(pid, ("", ""))
|
|
1697
|
+
recurred = (fingerprint and fingerprint in failure_fingerprints) or any(
|
|
1698
|
+
key in lesson_norm or (evidence_norm and key in evidence_norm) for key in failure_keys
|
|
1699
|
+
)
|
|
1700
|
+
learn_validate(conn, pattern_id=pid, was_helpful=not recurred)
|
|
1701
|
+
if recurred:
|
|
1702
|
+
unhelpful += 1
|
|
1703
|
+
else:
|
|
1704
|
+
helpful += 1
|
|
1705
|
+
return {
|
|
1706
|
+
"status": "ok",
|
|
1707
|
+
"surfaced": len(surfaced),
|
|
1708
|
+
"helpful": helpful,
|
|
1709
|
+
"unhelpful": unhelpful,
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
# ---------------------------------------------------------------------------
|
|
1714
|
+
# Breakthrough narrative capture
|
|
1715
|
+
# ---------------------------------------------------------------------------
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
_GENERIC_INSIGHT_RE = re.compile(
|
|
1719
|
+
r"\b(be careful|be more careful|double[- ]check|pay attention|take care|"
|
|
1720
|
+
r"more thorough|review carefully|test more|don'?t forget)\b",
|
|
1721
|
+
re.IGNORECASE,
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
def _is_low_quality_insight(text: str) -> bool:
|
|
1726
|
+
# Reject ultra-terse / generic "be careful" slop with no transferable rule;
|
|
1727
|
+
# specific-but-short insights like "Money must use Decimal" still pass.
|
|
1728
|
+
t = (text or "").strip()
|
|
1729
|
+
if len(t) < 8:
|
|
1730
|
+
return True
|
|
1731
|
+
return bool(_GENERIC_INSIGHT_RE.search(t))
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def learn_narrative(
|
|
1735
|
+
conn: sqlite3.Connection,
|
|
1736
|
+
*,
|
|
1737
|
+
task_id: str,
|
|
1738
|
+
what_failed: str = "",
|
|
1739
|
+
what_worked: str = "",
|
|
1740
|
+
key_insight: str = "",
|
|
1741
|
+
) -> dict:
|
|
1742
|
+
"""Record a breakthrough narrative and create a high-impact learned pattern.
|
|
1743
|
+
|
|
1744
|
+
Called by the agent after a rework→success breakthrough. Updates the
|
|
1745
|
+
outcome_history narrative fields and creates a learned_pattern with
|
|
1746
|
+
memory_type='error' and high confidence.
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
conn: SQLite connection.
|
|
1750
|
+
task_id: Task identifier (e.g. "TASK-100").
|
|
1751
|
+
what_failed: Approaches that didn't work.
|
|
1752
|
+
what_worked: The solution that resolved the issue.
|
|
1753
|
+
key_insight: Reusable lesson learned.
|
|
1754
|
+
|
|
1755
|
+
Returns:
|
|
1756
|
+
Dict with status, history_id, pattern_id.
|
|
1757
|
+
"""
|
|
1758
|
+
if not task_id:
|
|
1759
|
+
return {"error": "task_id is required"}
|
|
1760
|
+
if not key_insight:
|
|
1761
|
+
return {"error": "key_insight is required — what did you learn?"}
|
|
1762
|
+
|
|
1763
|
+
# sanitize all narrative fields before they enter memory.
|
|
1764
|
+
# Reject on injection patterns; truncate over-length text.
|
|
1765
|
+
# Single-pass: compute cleaned values once so audit log records each
|
|
1766
|
+
# truncation/reject exactly once.
|
|
1767
|
+
from sanitizer import sanitize_write
|
|
1768
|
+
|
|
1769
|
+
_sanitized: dict[str, str] = {}
|
|
1770
|
+
for _field, _value in (
|
|
1771
|
+
("key_insight", key_insight),
|
|
1772
|
+
("what_failed", what_failed),
|
|
1773
|
+
("what_worked", what_worked),
|
|
1774
|
+
):
|
|
1775
|
+
_sr = sanitize_write(
|
|
1776
|
+
_field,
|
|
1777
|
+
_value,
|
|
1778
|
+
actor="learn_narrative",
|
|
1779
|
+
source_table="outcome_history",
|
|
1780
|
+
conn=conn,
|
|
1781
|
+
)
|
|
1782
|
+
if not _sr.ok:
|
|
1783
|
+
return {"error": f"rejected {_field}: {_sr.reason}"}
|
|
1784
|
+
_sanitized[_field] = _sr.cleaned or ""
|
|
1785
|
+
|
|
1786
|
+
key_insight = _sanitized["key_insight"]
|
|
1787
|
+
what_failed = _sanitized["what_failed"]
|
|
1788
|
+
what_worked = _sanitized["what_worked"]
|
|
1789
|
+
|
|
1790
|
+
# Quality bar: a narrative is only worth storing if the insight is specific.
|
|
1791
|
+
# Blocks "be careful"-class slop the nudge could otherwise elicit.
|
|
1792
|
+
if _is_low_quality_insight(key_insight):
|
|
1793
|
+
return {
|
|
1794
|
+
"error": "key_insight too generic — state the specific situation, why the "
|
|
1795
|
+
"naive approach failed, and the rule to apply (not 'be careful')."
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
# Find the most recent breakthrough for this task
|
|
1799
|
+
row = conn.execute(
|
|
1800
|
+
"SELECT id, outcome, previous_outcome FROM outcome_history "
|
|
1801
|
+
"WHERE task_id = ? AND is_breakthrough = 1 "
|
|
1802
|
+
"ORDER BY created_at DESC LIMIT 1",
|
|
1803
|
+
(task_id,),
|
|
1804
|
+
).fetchone()
|
|
1805
|
+
|
|
1806
|
+
if row is None:
|
|
1807
|
+
# No breakthrough found — create a general narrative entry anyway
|
|
1808
|
+
cursor = conn.execute(
|
|
1809
|
+
"INSERT INTO outcome_history "
|
|
1810
|
+
"(task_id, outcome, previous_outcome, is_breakthrough, "
|
|
1811
|
+
"narrative_what_failed, narrative_what_worked, narrative_key_insight, triggered_by) "
|
|
1812
|
+
"VALUES (?, 'success', NULL, 0, ?, ?, ?, 'learn_narrative')",
|
|
1813
|
+
(task_id, what_failed, what_worked, key_insight),
|
|
1814
|
+
)
|
|
1815
|
+
history_id = cursor.lastrowid
|
|
1816
|
+
else:
|
|
1817
|
+
history_id = row["id"]
|
|
1818
|
+
conn.execute(
|
|
1819
|
+
"UPDATE outcome_history SET "
|
|
1820
|
+
"narrative_what_failed = ?, narrative_what_worked = ?, narrative_key_insight = ? "
|
|
1821
|
+
"WHERE id = ?",
|
|
1822
|
+
(what_failed, what_worked, key_insight, history_id),
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
# Get task domain for the pattern; a still-open task has no outcome row
|
|
1826
|
+
# yet, so fall back to the board's tasks table before giving up.
|
|
1827
|
+
task_row = conn.execute(
|
|
1828
|
+
"SELECT domain, complexity FROM task_outcomes WHERE task_id = ?", (task_id,)
|
|
1829
|
+
).fetchone()
|
|
1830
|
+
domain = task_row["domain"] if task_row else None
|
|
1831
|
+
if not domain:
|
|
1832
|
+
try:
|
|
1833
|
+
board_row = conn.execute(
|
|
1834
|
+
"SELECT domain FROM tasks WHERE task_id = ?", (task_id,)
|
|
1835
|
+
).fetchone()
|
|
1836
|
+
domain = board_row[0] if board_row and board_row[0] else None
|
|
1837
|
+
except sqlite3.Error as exc:
|
|
1838
|
+
logger.debug("narrative domain fallback lookup failed: %s", exc)
|
|
1839
|
+
|
|
1840
|
+
# Build concepts from narrative text
|
|
1841
|
+
words = set()
|
|
1842
|
+
for text in (what_failed, what_worked, key_insight):
|
|
1843
|
+
words.update(w.lower() for w in text.split() if len(w) > 3)
|
|
1844
|
+
# Keep only meaningful concept words (no stop words)
|
|
1845
|
+
stop = {"that", "this", "with", "from", "have", "been", "were", "will", "didn't", "wasn't"}
|
|
1846
|
+
concept_list = sorted(words - stop)[:7]
|
|
1847
|
+
if domain:
|
|
1848
|
+
concept_list.insert(0, domain.lower())
|
|
1849
|
+
|
|
1850
|
+
# Create a high-impact learned pattern
|
|
1851
|
+
pattern_text = f"[Breakthrough] {key_insight}"
|
|
1852
|
+
if what_failed:
|
|
1853
|
+
pattern_text += f" (failed: {what_failed[:80]})"
|
|
1854
|
+
|
|
1855
|
+
# evidence-based auto-promote.
|
|
1856
|
+
# Previously this inserted with confidence=0.7 / impact=0.85 /
|
|
1857
|
+
# no provenance, letting the agent self-certify a "breakthrough"
|
|
1858
|
+
# at high trust after a single call (audit finding A7). Now the
|
|
1859
|
+
# row is explicitly volatile/agent_self at moderate confidence;
|
|
1860
|
+
# promotion to `validated` requires external evidence (outcome
|
|
1861
|
+
# history or explicit `cos_promote`), handled elsewhere.
|
|
1862
|
+
# Stamp last_validated/last_accessed_at so a fresh breakthrough has age 0.
|
|
1863
|
+
# Otherwise run_decay reads _days_since(NULL)->999d and archives it on the
|
|
1864
|
+
# FIRST run (the same fix learn_extract's _upsert_pattern already carries).
|
|
1865
|
+
cursor = conn.execute(
|
|
1866
|
+
"INSERT INTO learned_patterns "
|
|
1867
|
+
"(pattern, memory_type, domain, source, confidence, impact_score, "
|
|
1868
|
+
"concepts, trust_tier, provenance, last_validated, last_accessed_at) "
|
|
1869
|
+
"VALUES (?, 'error', ?, 'breakthrough', 0.3, 0.5, ?, "
|
|
1870
|
+
"'volatile', 'agent_self', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
|
1871
|
+
(pattern_text, domain, json.dumps(concept_list)),
|
|
1872
|
+
)
|
|
1873
|
+
pattern_id = cursor.lastrowid
|
|
1874
|
+
|
|
1875
|
+
conn.commit()
|
|
1876
|
+
|
|
1877
|
+
# RAG: embed both the breakthrough narrative (outcome_history)
|
|
1878
|
+
# and the high-impact learned pattern. Errors are intentionally suppressed
|
|
1879
|
+
# because embeddings are an optional enrichment — never fail the narrative
|
|
1880
|
+
# recording itself if rag extras are not installed or v5 not yet applied.
|
|
1881
|
+
_embed_narrative_and_pattern(
|
|
1882
|
+
conn=conn,
|
|
1883
|
+
history_id=history_id,
|
|
1884
|
+
pattern_id=pattern_id,
|
|
1885
|
+
pattern_text=pattern_text,
|
|
1886
|
+
concept_list=concept_list,
|
|
1887
|
+
key_insight=key_insight,
|
|
1888
|
+
what_failed=what_failed,
|
|
1889
|
+
what_worked=what_worked,
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
# Filing-back: write a human-readable markdown file to docs/insights/.
|
|
1893
|
+
# Fire-and-forget — filing failure must never break narrative recording.
|
|
1894
|
+
filed_path = _file_back_narrative_safe(
|
|
1895
|
+
conn=conn,
|
|
1896
|
+
task_id=task_id,
|
|
1897
|
+
domain=domain,
|
|
1898
|
+
key_insight=key_insight,
|
|
1899
|
+
what_failed=what_failed,
|
|
1900
|
+
what_worked=what_worked,
|
|
1901
|
+
history_id=history_id,
|
|
1902
|
+
pattern_id=pattern_id,
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
return {
|
|
1906
|
+
"status": "narrative_recorded",
|
|
1907
|
+
"history_id": history_id,
|
|
1908
|
+
"pattern_id": pattern_id,
|
|
1909
|
+
"task_id": task_id,
|
|
1910
|
+
"domain": domain,
|
|
1911
|
+
"filed_path": str(filed_path) if filed_path else None,
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _embed_narrative_and_pattern(
|
|
1916
|
+
*,
|
|
1917
|
+
conn: sqlite3.Connection,
|
|
1918
|
+
history_id: int,
|
|
1919
|
+
pattern_id: int,
|
|
1920
|
+
pattern_text: str,
|
|
1921
|
+
concept_list: list,
|
|
1922
|
+
key_insight: str,
|
|
1923
|
+
what_failed: str,
|
|
1924
|
+
what_worked: str,
|
|
1925
|
+
) -> None:
|
|
1926
|
+
# Fire-and-forget: embeddings are optional enrichment — never fail the
|
|
1927
|
+
# narrative recording (missing module/table/model load all swallowed).
|
|
1928
|
+
try:
|
|
1929
|
+
from embeddings import upsert_embedding
|
|
1930
|
+
except ImportError as exc:
|
|
1931
|
+
logger.debug("Skipping embedding (module unavailable): %s", exc)
|
|
1932
|
+
return
|
|
1933
|
+
|
|
1934
|
+
try:
|
|
1935
|
+
narrative_text = " ".join(filter(None, [key_insight, what_failed, what_worked]))
|
|
1936
|
+
upsert_embedding(conn, "outcome_history", history_id, narrative_text)
|
|
1937
|
+
pattern_concepts_str = " ".join(concept_list)
|
|
1938
|
+
upsert_embedding(
|
|
1939
|
+
conn,
|
|
1940
|
+
"learned_patterns",
|
|
1941
|
+
pattern_id,
|
|
1942
|
+
f"{pattern_text} {pattern_concepts_str}".strip(),
|
|
1943
|
+
)
|
|
1944
|
+
except sqlite3.OperationalError as exc:
|
|
1945
|
+
logger.debug("Skipping embedding (table missing — pre-v5 DB): %s", exc)
|
|
1946
|
+
except Exception as exc: # pragma: no cover - defensive against model load errors
|
|
1947
|
+
logger.debug("Skipping embedding (unexpected): %s", exc)
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
# ---------------------------------------------------------------------------
|
|
1951
|
+
# Breakthrough narrative filing-back (human-readable markdown artifact)
|
|
1952
|
+
# ---------------------------------------------------------------------------
|
|
1953
|
+
|
|
1954
|
+
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def _slugify(text: str, max_len: int = 50) -> str:
|
|
1958
|
+
slug = _SLUG_RE.sub("-", text.lower()).strip("-")
|
|
1959
|
+
if not slug:
|
|
1960
|
+
return "untitled"
|
|
1961
|
+
return slug[:max_len].rstrip("-") or "untitled"
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def _derive_project_root(conn: sqlite3.Connection) -> Path | None:
|
|
1965
|
+
# Root = parent of the .coding-os/ dir holding the DB. None for in-memory
|
|
1966
|
+
# DBs or any DB outside the expected <root>/.coding-os/coding-os.db layout.
|
|
1967
|
+
rows = conn.execute("PRAGMA database_list").fetchall()
|
|
1968
|
+
for row in rows:
|
|
1969
|
+
db_path_str = row[2] if len(row) > 2 else None
|
|
1970
|
+
if not db_path_str:
|
|
1971
|
+
continue
|
|
1972
|
+
if db_path_str in ("", ":memory:"):
|
|
1973
|
+
continue
|
|
1974
|
+
db_path = Path(db_path_str).resolve()
|
|
1975
|
+
if db_path.parent.name == ".coding-os":
|
|
1976
|
+
return db_path.parent.parent
|
|
1977
|
+
return None
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def _format_narrative_markdown(
|
|
1981
|
+
*,
|
|
1982
|
+
task_id: str,
|
|
1983
|
+
domain: str | None,
|
|
1984
|
+
key_insight: str,
|
|
1985
|
+
what_failed: str,
|
|
1986
|
+
what_worked: str,
|
|
1987
|
+
history_id: int,
|
|
1988
|
+
pattern_id: int,
|
|
1989
|
+
task_file_name: str | None = None,
|
|
1990
|
+
) -> str:
|
|
1991
|
+
date_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
1992
|
+
# The docs-lint hard gate requires `domain:[A-Z_]+` in the header; XXX is
|
|
1993
|
+
# the canonical unknown-placeholder in its enum ("n/a" fails the regex).
|
|
1994
|
+
# The body's **Domain:** line stays human-readable.
|
|
1995
|
+
domain_header = (domain or "").strip().upper().replace("-", "_")
|
|
1996
|
+
if not re.fullmatch(r"[A-Z_]+", domain_header):
|
|
1997
|
+
domain_header = "XXX"
|
|
1998
|
+
domain_line = domain or "n/a"
|
|
1999
|
+
failed_block = what_failed.strip() or "_(not recorded)_"
|
|
2000
|
+
worked_block = what_worked.strip() or "_(not recorded)_"
|
|
2001
|
+
# Task files are slugged (TASK-NNN-<slug>.md) — a guessed TASK-NNN.md link
|
|
2002
|
+
# is always dead and trips the docs-lint hard gate; plain text when unknown.
|
|
2003
|
+
source_line = (
|
|
2004
|
+
f"**Source task:** [{task_id}](../tasks/{task_file_name})\n\n"
|
|
2005
|
+
if task_file_name
|
|
2006
|
+
else f"**Source task:** {task_id}\n\n"
|
|
2007
|
+
)
|
|
2008
|
+
return (
|
|
2009
|
+
f"<!-- domain:{domain_header} | layer:reference | ssot:false | "
|
|
2010
|
+
f"source:outcome_history#{history_id} | updated:{date_iso} -->\n"
|
|
2011
|
+
f"# {task_id}: {key_insight}\n\n"
|
|
2012
|
+
f"**Date:** {date_iso} \n"
|
|
2013
|
+
f"**Domain:** {domain_line} \n"
|
|
2014
|
+
f"{source_line}"
|
|
2015
|
+
f"## Key Insight\n\n{key_insight}\n\n"
|
|
2016
|
+
f"## What Failed\n\n{failed_block}\n\n"
|
|
2017
|
+
f"## What Worked\n\n{worked_block}\n\n"
|
|
2018
|
+
f"## Links\n\n"
|
|
2019
|
+
f"- Pattern: `learned_patterns#{pattern_id}` — retrievable via `cos_details`\n"
|
|
2020
|
+
f"- History: `outcome_history#{history_id}`\n"
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
def _file_back_narrative_safe(
|
|
2025
|
+
*,
|
|
2026
|
+
conn: sqlite3.Connection,
|
|
2027
|
+
task_id: str,
|
|
2028
|
+
domain: str | None,
|
|
2029
|
+
key_insight: str,
|
|
2030
|
+
what_failed: str,
|
|
2031
|
+
what_worked: str,
|
|
2032
|
+
history_id: int,
|
|
2033
|
+
pattern_id: int,
|
|
2034
|
+
) -> Path | None:
|
|
2035
|
+
# Fire-and-forget write to <root>/docs/insights/; returns None (skipped)
|
|
2036
|
+
# for in-memory DBs or when <root>/docs/ is absent. Never breaks recording.
|
|
2037
|
+
try:
|
|
2038
|
+
project_root = _derive_project_root(conn)
|
|
2039
|
+
if project_root is None:
|
|
2040
|
+
logger.debug("Skipping narrative filing (project root not derivable)")
|
|
2041
|
+
return None
|
|
2042
|
+
docs_root = project_root / "docs"
|
|
2043
|
+
if not docs_root.exists():
|
|
2044
|
+
logger.debug("Skipping narrative filing (no docs/ at %s)", project_root)
|
|
2045
|
+
return None
|
|
2046
|
+
|
|
2047
|
+
target_dir = docs_root / "insights"
|
|
2048
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
2049
|
+
|
|
2050
|
+
slug = _slugify(f"{task_id}-{key_insight}")
|
|
2051
|
+
target_path = target_dir / f"{slug}.md"
|
|
2052
|
+
task_file_name: str | None = None
|
|
2053
|
+
try:
|
|
2054
|
+
row = conn.execute(
|
|
2055
|
+
"SELECT file_path FROM tasks WHERE task_id = ?", (task_id,)
|
|
2056
|
+
).fetchone()
|
|
2057
|
+
if row and row[0] and (project_root / str(row[0])).exists():
|
|
2058
|
+
task_file_name = Path(str(row[0])).name
|
|
2059
|
+
except sqlite3.Error as exc:
|
|
2060
|
+
logger.debug("narrative task-file lookup failed: %s", exc)
|
|
2061
|
+
content = _format_narrative_markdown(
|
|
2062
|
+
task_id=task_id,
|
|
2063
|
+
domain=domain,
|
|
2064
|
+
key_insight=key_insight,
|
|
2065
|
+
what_failed=what_failed,
|
|
2066
|
+
what_worked=what_worked,
|
|
2067
|
+
history_id=history_id,
|
|
2068
|
+
pattern_id=pattern_id,
|
|
2069
|
+
task_file_name=task_file_name,
|
|
2070
|
+
)
|
|
2071
|
+
target_path.write_text(content, encoding="utf-8")
|
|
2072
|
+
return target_path
|
|
2073
|
+
except OSError as exc:
|
|
2074
|
+
logger.debug("Skipping narrative filing (OS error): %s", exc)
|
|
2075
|
+
return None
|
|
2076
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
2077
|
+
logger.debug("Skipping narrative filing (unexpected): %s", exc)
|
|
2078
|
+
return None
|