@ccoalm/ccl-skills 0.1.0
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.
- package/LICENSE +201 -0
- package/README.md +49 -0
- package/dist/assets/marketplace/.agents/plugins/marketplace.json +12 -0
- package/dist/assets/marketplace/.claude-plugin/marketplace.json +13 -0
- package/dist/assets/marketplace/marketplace-manifest.json +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/.claude-plugin/marketplace.json +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/.claude-plugin/plugin.json +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/.codex-plugin/plugin.json +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/.worktree-only +3 -0
- package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/agent-context/subagent-start.md +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/AGENTS.md +19 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-delegation-owner.sh +125 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-edit-isolation.sh +102 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-merge-authorization.sh +1156 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +131 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/merge-authorization-prompt.sh +142 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/owner-dispatch-guard.sh +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/owner-dispatch-stop.sh +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-post-merge-cleanup.sh +144 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/session-context.sh +87 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/session-start.sh +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/skill-extraction-gate-stop.sh +69 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/subagent-start.sh +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_delegation_owner.sh +329 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_edit_isolation.sh +322 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_merge_authorization.sh +902 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_merge_authorization_prompt.sh +178 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_post_merge_cleanup.sh +121 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_session_start.sh +170 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/AGENTS.md +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +564 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-install-skills.md +14 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-update-skills.md +44 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-verify-skills.md +109 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-worktree-check.md +36 -0
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/AGENTS.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/README.md +276 -0
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/owner-dispatch.example.json +10 -0
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/owner-dispatch.sh +1307 -0
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/test.sh +941 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/agents-file-coverage-gate/SKILL.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/agents-file-coverage-gate/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +188 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/android-dev.md +92 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/flutter-dev.md +80 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/ios-dev.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/kotlin-multiplatform.md +93 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-platform-boundaries.md +77 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +77 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/source-evidence-map.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +353 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +419 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/manual-invocation-and-prompts.md +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +197 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +179 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/AGENTS.md +98 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +93 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_timeout_exit.sh +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +1438 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +324 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/concern_excerpt.py +295 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/egress_schema.py +214 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/init_policy_matrix.py +642 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_packet_mcp.py +181 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +1165 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +1190 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_cli_review.py +946 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_opencode_review.py +474 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_probe_result.py +1899 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_review_json.py +200 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +2845 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.sh +6 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/run_claude_capture.py +71 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/runtime-surface-verification-design.md +53 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +68 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +2311 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_cli_review_wrappers.sh +1832 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_code_review_identity.sh +73 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_concern_excerpt.sh +245 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_egress_schema.sh +177 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_init_policy_matrix.sh +272 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_kimi_packet_mcp.py +195 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_opencode_review_concurrency.sh +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_opencode_review_retry.sh +1005 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_opencode_review.sh +258 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_probe_result.sh +574 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +349 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +434 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_order.sh +264 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +2412 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/verify_native_skill_binding.py +123 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +153 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/references/diagnosis-playbook.md +54 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/references/prevention-routing.md +36 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/SKILL.md +69 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/references/security-review-gate.md +41 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +165 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/api-security-boundaries.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +160 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/artifact-generation-architecture.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/audit-history-architecture.md +29 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/bulk-workflow-architecture.md +33 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/config-rule-routing-architecture.md +34 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/cross-cutting-concerns.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-modeling-and-migrations.md +79 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +210 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/dependency-platform.md +105 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/developer-tooling-architecture.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/error-contract-architecture.md +36 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +260 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/http-gateway-architecture.md +74 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/mq-consumer-architecture.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +275 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/notification-architecture.md +25 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/ops-checklist.md +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/performance-capacity-architecture.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/protobuf-contract-architecture.md +119 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/redis-cache-coordination.md +93 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/release-runtime-readiness.md +65 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/replay-comparison-architecture.md +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/runtime-observability.md +94 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/service-scaffold.md +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/source-evidence-map.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/workflow-state-architecture.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +159 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/artifact-generation-patterns.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/audit-history-patterns.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/bulk-import-export-patterns.md +56 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/config-rule-routing-patterns.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/data-access-patterns.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/db-schema-and-dal-patterns.md +109 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/dependency-client-patterns.md +130 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/developer-tooling-patterns.md +70 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/domain-feature-patterns.md +78 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/engineering-patterns.md +119 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/error-contract-patterns.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/feature-playbook.md +61 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/http-gateway-client-patterns.md +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/mq-consumer-patterns.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/notification-patterns.md +42 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/observability-implementation-patterns.md +101 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/performance-capacity-patterns.md +44 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/protobuf-contract-patterns.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/public-api-integration-patterns.md +56 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/quality-and-testing-patterns.md +91 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/redis-cache-lock-patterns.md +123 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/release-ops-patterns.md +112 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/reliability-patterns.md +83 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/replay-comparison-patterns.md +32 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/scaffold-and-codegen.md +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/source-evidence-map.md +54 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/state-machine-task-patterns.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/grill-me/SKILL.md +80 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/grill-me/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +117 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-approval-auto-reviewer.md +106 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-command-sandbox.md +441 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-context-freshness.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-credentials-auth.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-extensions-skills.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-file-edit-protocol.md +129 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-ide-integration.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-input-ingestion.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-instruction-composition.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-lifecycle-hooks.md +92 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-messaging.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-runtime-bootstrap.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-session-persistence.md +448 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-task-orchestration.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-tool-dispatch.md +123 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-turn-lifecycle.md +131 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/inference-capacity-operations.md +162 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/llm-client-gateway.md +156 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/model-prompt-evaluation.md +146 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/retrieval-agent-safety.md +273 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +202 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/contracts-and-state.md +62 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/cross-stack-alignment.md +94 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/framework-choice.md +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/online-practice-uptake.md +56 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/platform-capabilities.md +91 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/product-page-checklist.md +40 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/qa-release.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/source-evidence-map.md +82 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/SKILL.md +103 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/agents/openai.yaml +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/references/multi-agent-delegation-playbook.md +100 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/SKILL.md +70 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +549 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +97 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/research-prompts.md +66 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/scripts/AGENTS.md +32 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/scripts/test-public-data-acquisition-recipes.sh +379 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +244 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/alerting-and-on-call.md +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/framework-middleware-checklist.md +142 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/infra-component-deployment.md +268 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/log-correlation-recipe.md +124 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/log-schema-canonical.md +208 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/metrics-conventions.md +105 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/obs-stack-architecture.md +107 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +95 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +303 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/canary-and-rollout-strategy.md +163 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/config-center-via-etcd.md +245 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/custom-control-plane-boundary.md +298 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/deploy-cli-concrete-recipe.md +312 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/deploy-pipeline.md +165 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/env-and-lane-matrix.md +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/lane-orchestration-control-plane.md +383 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/multi-region-and-cluster.md +135 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +149 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/python-package-registry-release.md +462 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/rollback-playbook.md +123 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +231 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/version-authority-and-deprecation.md +21 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/SKILL.md +276 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/dual-sidecar-and-traffic-config-center.md +127 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/framework-middleware.md +143 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/grpc-authority-workaround.md +90 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/http-response-envelope-contract.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/mesh-architecture.md +127 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/multi-env-routing.md +192 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/protobuf-http-contract-signals.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +124 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/rpc-framework-recipe.md +494 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-choice.md +113 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-migration-playbook.md +231 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-recipe.md +131 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +235 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/adr-convention.md +146 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-checklist.md +30 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-evaluation-report-template.md +25 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-execution-spec.md +108 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-sop.md +457 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-templates.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/artifact-egress-confidentiality.md +58 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/code-review-checklist.md +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/cross-repo-coordination.md +46 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +192 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-review-gate-mechanics.md +62 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/diagnostic-spec-match-gate.md +36 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +35 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dormant-code-activation.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/existing-project-assessment-report.md +223 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/external-skill-augmentation.md +46 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/feature-deprecation-cascade.md +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/high-risk-resilience-gates.md +73 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/implementation-completeness-and-minimality.md +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/implementation-entry-reentry-gate.md +122 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/modular-monolith-heuristic.md +105 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/pre-final-continuation-gate.md +115 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +62 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/quality-attributes.md +112 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/quality-remediation-program.md +88 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/rd-standards-doc-family-checklist.md +27 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/refactoring-discipline.md +52 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/review-reception.md +34 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/shared-gate-artifact-classification.md +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/source-evidence-map.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/status-tracker-sync.md +77 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/sync-spec-repo-contract.md +25 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +34 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/worktree-mechanics.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/scripts/AGENTS.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/scripts/check-agent-contract-coverage.sh +213 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +136 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/agents/openai.yaml +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/analytics-visualization-interactions.md +206 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +108 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/complex-creation-interactions.md +194 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +214 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +53 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +129 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +97 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +79 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +63 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +146 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +250 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +237 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +65 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +237 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +324 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-web-desktop-patterns.md +456 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +114 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +79 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/resource-management-interactions.md +113 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/scenario-community-patterns.md +133 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +130 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/trust-sensitive-ai-and-data-patterns.md +96 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +106 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +176 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +111 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +157 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/ai-service-integration-boundaries.md +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/api-contract-and-schema.md +62 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/api-security-boundaries.md +39 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +46 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/async-execution-model.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/background-jobs-and-scheduling.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/batch-and-pipeline-architecture.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/config-secrets-runtime.md +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-modeling-and-migrations.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +211 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +263 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +281 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/observability-and-ops.md +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +20 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/redis-cache-coordination.md +41 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/reliability-and-error-contract.md +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/source-evidence-map.md +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/web-framework-boundaries.md +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +143 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/async-and-worker-patterns.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/dependency-client-patterns.md +39 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/error-handling-patterns.md +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/feature-playbook.md +43 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/observability-implementation-patterns.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/project-structure-and-tooling.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +52 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/redis-cache-lock-patterns.md +78 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/schema-and-validation-patterns.md +23 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/source-evidence-map.md +56 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/sqlalchemy-and-migrations-patterns.md +99 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +61 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/web-framework-patterns.md +35 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +91 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/config-runtime-readback.md +20 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/mr-merge-authorization.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/post-release-env-reset.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/release-closeout-evidence.md +20 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/release-scope-confirmation.md +21 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +20 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/test-scope-prompt.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/watcher-discipline.md +14 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/SKILL.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/comment-safe-release-doc.md +19 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/release-evidence-workflow.md +23 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/release-testing-scope-section.md +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-baseline/SKILL.md +87 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-baseline/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/SKILL.md +130 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/prd-composition-contract.md +35 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/requirement-closure-contract.md +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/security-four-questions.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-intent/SKILL.md +91 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-intent/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-scope/SKILL.md +88 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-scope/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +337 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/analysis-parse-fix-test-challenge-replay.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attribution-verification.md +69 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/bootstrap-slim-c3-obligation-table.md +112 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +162 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +507 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/evidence-card-template.md +51 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/example-domain-preselect.md +79 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-lifecycle-handoff.md +65 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +194 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +75 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +286 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/incident-postmortem-extraction.md +190 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/l0-l1-l2-routing.md +114 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/online-skill-review.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +164 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +90 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +320 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/resume-paused-delivery.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-feedback-mining.md +33 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-finding-standards.md +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-rubric.md +40 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +118 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/skill-listing-budget.md +19 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +254 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +658 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/two-source-extraction-pattern.md +167 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +179 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-routing-map.md +51 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +180 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/AGENTS.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +1452 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-evidence-card-leak.sh +491 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-mr-target-freshness.sh +173 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +488 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-sync-pointers.sh +419 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-golden-trace.rb +197 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +327 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +401 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing.rb +248 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/generic-r0-leak-scan.sh +282 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/governing-chain-diff.py +321 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +964 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +708 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/skill-behavior-eval.py +540 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/source-register-lifecycle.rb +51 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/source-register-pending-status.rb +55 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +829 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +1203 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_r0_status.sh +75 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_register_pending_exclusion.sh +137 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +173 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_route_drift.sh +377 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +833 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +491 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_source_register_lifecycle.sh +114 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_mr_target_freshness.sh +261 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_sync_pointers.sh +538 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_controlled_escalation_pins.sh +154 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +190 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_surface_binding.sh +178 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_prose_target.sh +86 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_generic_r0_leak_scan.sh +131 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_git_identity_predicate_gate.sh +243 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_governing_chain_diff.sh +419 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +724 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +414 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_regression_runner_registration.sh +34 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +205 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +194 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_credential_cwd.sh +61 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +111 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_root_depth.sh +53 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +257 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +98 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/input-state-machines.md +36 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/streaming-rich-output.md +130 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/terminal-side-channels.md +96 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +408 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/AGENTS.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/bitable-setup.md +573 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/README.md +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/github-actions.yml +119 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/gitlab-ci.yml +76 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/jenkins.Jenkinsfile +106 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/classical-test-design-techniques.md +279 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/gen_report.py +2807 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/makefile-template.md +200 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/report-config-schema.md +272 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/run_pytestless.py +475 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/source-to-case-workflows.md +258 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-marker-conventions.md +316 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-review-and-prioritization.md +145 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/AGENTS.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.dart +129 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.go +197 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.py +135 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.ts +285 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/test_gen_report.py +2144 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/update-lifecycle.md +62 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +212 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/ci-fixtures-and-flake-control.md +75 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +50 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/data-and-workflow-testing.md +34 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/design-closed-contract-oracles.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/e2e-real-flow-testing.md +71 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +240 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/integration-contract-testing.md +235 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/non-functional-specialized-scenarios.md +296 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/rd-testing-standard-template.md +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/run-killing-mutation-walk.md +43 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +136 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/source-evidence-map.md +59 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/structured-tc-input-translation.md +67 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +392 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-data-and-determinism.md +39 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-topology-and-commands.md +92 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/unit-testing.md +46 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/vendored-contract-drift-checklist.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/verify-enforcement-mechanisms.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/AGENTS.md +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/client-terminal-ansi-check.py +140 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/client-terminal-ansi-check.test.sh +75 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-ast-check.py +170 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-ast-check.test.sh +87 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-go-check.go +198 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-go-check.test.sh +109 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/test_mutation_backup_recipe.sh +237 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +184 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/comment-safe-feishu.md +93 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/cross-model-co-review.md +3 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +60 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/session-vantage-leakage.md +58 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +47 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/embedded-h5-in-host.md +87 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/react-architecture.md +194 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/source-evidence-map.md +60 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-quality-release.md +190 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-ui-quality.md +83 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/SKILL.md +179 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/references/shared-branch-rebase.md +25 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/AGENTS.md +23 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/test_worktree_status.sh +207 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/test_worktree_sweep.sh +481 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/worktree-status.sh +325 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/worktree-sweep.sh +245 -0
- package/dist/assets/release.json +2797 -0
- package/dist/claude-adapter.d.ts +9 -0
- package/dist/claude-adapter.js +240 -0
- package/dist/cli-worker.d.ts +1 -0
- package/dist/cli-worker.js +32 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +214 -0
- package/dist/codex-host.d.ts +30 -0
- package/dist/codex-host.js +162 -0
- package/dist/fs-safe.d.ts +21 -0
- package/dist/fs-safe.js +241 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +8 -0
- package/dist/manifest.js +135 -0
- package/dist/opencode-adapter.d.ts +10 -0
- package/dist/opencode-adapter.js +416 -0
- package/dist/operations.d.ts +3 -0
- package/dist/operations.js +956 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +4 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/dist/unified.d.ts +4 -0
- package/dist/unified.js +64 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# owner-dispatch: mechanical firing for the product-rd "owner-dispatch firing gate".
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS
|
|
5
|
+
# The owner-invoke rule (invoke the owning stack/design skill at each product-R&D
|
|
6
|
+
# phase transition before producing substance) lived only as prose in SKILL.md +
|
|
7
|
+
# bootstrap salience, and recurringly did not fire — the failing action is
|
|
8
|
+
# agent-initiated and intent-dependent, so a stronger prompt cannot enforce it.
|
|
9
|
+
# Industry practice (Anthropic "gate intermediate steps" / poka-yoke; Claude Code
|
|
10
|
+
# hooks deny/ask/block enforced at the interpreter level; OpenAI Agents-SDK
|
|
11
|
+
# guardrail tripwires; structured-required) is unanimous: encode a must-happen
|
|
12
|
+
# step as a CODE gate that blocks, run as a separate check — never as prose.
|
|
13
|
+
# This engine is that gate. Built on the decision model in
|
|
14
|
+
# skills/llm-inference-integration/references/agent-lifecycle-hooks.md.
|
|
15
|
+
#
|
|
16
|
+
# SAFETY POSTURE (every default is the non-annoying / non-destructive one)
|
|
17
|
+
# - OPT-IN: a repo with no committed `.owner-dispatch.json` (or enabled:false) is a
|
|
18
|
+
# complete no-op. A cheap parent-walk for that file runs BEFORE any git/jq work,
|
|
19
|
+
# so a non-opted repo pays ~nothing and never waits on git discovery.
|
|
20
|
+
# - FAIL-OPEN: any internal error (missing jq/git, parse failure, unwritable/unsafe
|
|
21
|
+
# state dir, missing session id) ALLOWS the action. A broken gate must never brick
|
|
22
|
+
# editing, and no error path may fail-CLOSED.
|
|
23
|
+
# - DEFAULT `ask`, NOT `deny`: hard `deny` is the explicit `strict:true` opt-in, is
|
|
24
|
+
# Claude-Code-only (Codex ignores the decision), applies ONLY to precise
|
|
25
|
+
# Edit/Write/MultiEdit/NotebookEdit file paths (never the heuristic Bash match),
|
|
26
|
+
# and is downgraded to `ask` when the boundary state dir is not safely writable
|
|
27
|
+
# (so strict can never brick a repo whose state dir is unavailable).
|
|
28
|
+
# - Bash write-gating is BEST-EFFORT / ADVISORY only (a shell hook cannot reliably
|
|
29
|
+
# detect every write form — `python -c`, `ed`, `make generate`, rsync, etc. slip
|
|
30
|
+
# through). The host-agnostic un-bypassable gate is the `ci` subcommand.
|
|
31
|
+
# - STATE WRITES are symlink-safe (refuse/replace symlinked state paths; atomic
|
|
32
|
+
# temp+rename) so a crafted repo cannot redirect a write outside the state dir.
|
|
33
|
+
# - STOP enforcement is per-session (keyed by a real session id; fail-open without
|
|
34
|
+
# one), blocks at most once per actor per session, and fails open if the cap stamp
|
|
35
|
+
# cannot be persisted — so it can never trap the user. The SAME engine handles
|
|
36
|
+
# SubagentStop: when stdin carries agent_id (a dispatched subagent) the activity
|
|
37
|
+
# marker, block-once cap, and waiver are scoped to that subagent, so a read-only
|
|
38
|
+
# sibling / the main thread's dirty gated work never blocks an unrelated subagent.
|
|
39
|
+
#
|
|
40
|
+
# SUBCOMMANDS: pretool | stop | record --owners "a,b" | ci [--base REF] | status
|
|
41
|
+
# | verify-invocations --owners "a,b" --transcript PATH
|
|
42
|
+
|
|
43
|
+
set -u
|
|
44
|
+
|
|
45
|
+
# Absolute path to this engine, so remediation messages name a command that works
|
|
46
|
+
# from any product repo's cwd (a product repo only holds `.owner-dispatch.json`).
|
|
47
|
+
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
48
|
+
SID="nosession" # set per-invocation from the hook's session_id (pretool/stop)
|
|
49
|
+
AID="" # agent_id (raw) — non-empty only inside a dispatched subagent (PreToolUse
|
|
50
|
+
# fires in subagents carrying agent_id; SubagentStop carries it too).
|
|
51
|
+
AIDKEY="" # filename-safe, INJECTIVE key for agent_id, computed by jq @base64 from
|
|
52
|
+
# the RAW JSON (NOT a bash var — command substitution would drop NUL /
|
|
53
|
+
# strip trailing newline / mangle non-ASCII and let distinct agent_ids
|
|
54
|
+
# collide on the same marker). Empty iff no agent_id. Scopes the activity
|
|
55
|
+
# marker / block-cap / waiver to ONE subagent.
|
|
56
|
+
|
|
57
|
+
# ----- fail-open helpers ------------------------------------------------------
|
|
58
|
+
allow_pretool() { exit 0; } # no output => allow
|
|
59
|
+
allow_stop() { exit 0; } # no output => allow stop
|
|
60
|
+
emit_pretool_deny() { # $1 reason $2 decision(deny|ask)
|
|
61
|
+
jq -nc --arg r "$1" --arg d "$2" \
|
|
62
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$d,permissionDecisionReason:$r}}'
|
|
63
|
+
exit 0
|
|
64
|
+
}
|
|
65
|
+
emit_stop_block() { jq -nc --arg r "$1" '{decision:"block",reason:$r}'; exit 0; }
|
|
66
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
67
|
+
# ----- cheap opt-in discovery (NO git; runs before any expensive call) ---------
|
|
68
|
+
# Walk up from a start dir looking for `.owner-dispatch.json`. Pure bash, no git/jq,
|
|
69
|
+
# so a NON-opted repo pays only a few stat()s and never waits on git discovery.
|
|
70
|
+
# (The match here is a CANDIDATE; opt_in_root verifies it is a tracked top-level
|
|
71
|
+
# config before the gate activates.)
|
|
72
|
+
find_config_dir() { # $1 start dir -> echo candidate root dir, or fail
|
|
73
|
+
local d="$1"
|
|
74
|
+
[ -d "$d" ] || d=$(dirname -- "$d")
|
|
75
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "." ]; do
|
|
76
|
+
[ -f "$d/.owner-dispatch.json" ] && { printf '%s' "$d"; return 0; }
|
|
77
|
+
d=$(dirname -- "$d")
|
|
78
|
+
done
|
|
79
|
+
[ -f "/.owner-dispatch.json" ] && { printf '%s' "/"; return 0; }
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
# Verified opt-in: the candidate config must sit at the git TOP-LEVEL of the target
|
|
83
|
+
# AND be TRACKED in the repo (a stray untracked file or a parent-directory config
|
|
84
|
+
# must never gate an unrelated/nested repo). Runs git ONLY when a candidate exists.
|
|
85
|
+
opt_in_root() { # $1 start dir -> echo verified root, or fail
|
|
86
|
+
local cand top; cand=$(find_config_dir "$1") || return 1
|
|
87
|
+
cand=$(realpath "$cand" 2>/dev/null) || return 1 # physical, to match git's toplevel
|
|
88
|
+
top=$(git -C "$cand" rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
89
|
+
[ "$top" = "$cand" ] || return 1
|
|
90
|
+
git -C "$cand" ls-files --error-unmatch .owner-dispatch.json >/dev/null 2>&1 || return 1
|
|
91
|
+
printf '%s' "$cand"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ----- config + classification ------------------------------------------------
|
|
95
|
+
config_path() { printf '%s/.owner-dispatch.json' "$1"; }
|
|
96
|
+
# Echo "enabled" iff opted in AND parseable AND enabled; malformed => empty (fail-open).
|
|
97
|
+
cfg_enabled() { # $1 root
|
|
98
|
+
local c; c=$(config_path "$1"); [ -r "$c" ] || return 0
|
|
99
|
+
jq -er 'if (.enabled // false) then "enabled" else empty end' "$c" 2>/dev/null
|
|
100
|
+
}
|
|
101
|
+
cfg_get() { # $1 root $2 jq-filter $3 default
|
|
102
|
+
local c; c=$(config_path "$1"); [ -r "$c" ] || { printf '%s' "$3"; return; }
|
|
103
|
+
jq -r "$2 // \"$3\"" "$c" 2>/dev/null || printf '%s' "$3"
|
|
104
|
+
}
|
|
105
|
+
path_matches_any() { # $1 rel $2.. globs
|
|
106
|
+
local rel="$1"; shift
|
|
107
|
+
shopt -s extglob 2>/dev/null
|
|
108
|
+
local g
|
|
109
|
+
for g in "$@"; do
|
|
110
|
+
[ -z "$g" ] && continue
|
|
111
|
+
# $g INTENTIONALLY unquoted: match as a glob, not a literal.
|
|
112
|
+
# shellcheck disable=SC2254
|
|
113
|
+
case "$rel" in $g) return 0 ;; esac
|
|
114
|
+
# A leading **/ means "zero or more directories" (gitignore/globstar intent).
|
|
115
|
+
# bash `case` is NOT globstar-aware (* already crosses /, so **/PAT collapses
|
|
116
|
+
# to */PAT and only matches paths WITH a slash). Also test the slash-stripped
|
|
117
|
+
# PAT so a top-level (repo-root) file is gated too — otherwise a repo whose
|
|
118
|
+
# product code is top-level + a ["**/*.go"]-style config silently gates nothing.
|
|
119
|
+
case "$g" in
|
|
120
|
+
'**/'*)
|
|
121
|
+
# shellcheck disable=SC2254
|
|
122
|
+
case "$rel" in ${g#'**/'}) return 0 ;; esac
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
done
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
load_globs() { # $1 root -> PRODUCT_GLOBS / EXCLUDE_GLOBS
|
|
129
|
+
local c; c=$(config_path "$1"); PRODUCT_GLOBS=(); EXCLUDE_GLOBS=()
|
|
130
|
+
[ -r "$c" ] || return 0
|
|
131
|
+
local line
|
|
132
|
+
while IFS= read -r line; do [ -n "$line" ] && PRODUCT_GLOBS+=("$line"); done < <(jq -r '(.product_globs // [])[]' "$c" 2>/dev/null)
|
|
133
|
+
while IFS= read -r line; do [ -n "$line" ] && EXCLUDE_GLOBS+=("$line"); done < <(jq -r '(.exclude_globs // [])[]' "$c" 2>/dev/null)
|
|
134
|
+
EXCLUDE_GLOBS+=("**/*.md" ".owner-dispatch.json" ".git/**")
|
|
135
|
+
}
|
|
136
|
+
is_gated_path() { # $1 root $2 rel
|
|
137
|
+
load_globs "$1"
|
|
138
|
+
[ "${#PRODUCT_GLOBS[@]}" -gt 0 ] || return 1
|
|
139
|
+
path_matches_any "$2" "${EXCLUDE_GLOBS[@]}" && return 1
|
|
140
|
+
path_matches_any "$2" "${PRODUCT_GLOBS[@]}" && return 0
|
|
141
|
+
return 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# ----- symlink-safe per-REPOSITORY state --------------------------------------
|
|
145
|
+
# Keyed on the COMMON git dir, not `--absolute-git-dir`. Under a linked worktree the
|
|
146
|
+
# latter is `.git/worktrees/<name>`, so state written while working in a worktree is
|
|
147
|
+
# invisible to a read from the main checkout — and that split is guaranteed by this
|
|
148
|
+
# org's own rules, not an edge case: `worktree-isolation` mandates developing in a
|
|
149
|
+
# worktree, while `cmd_stop` resolves its repo from $PWD, which the harness routinely
|
|
150
|
+
# resets back to the main checkout. The gate then reports "never recorded" on the
|
|
151
|
+
# standard workflow, and a gate that cries wolf every delivery stops being read at all.
|
|
152
|
+
# Worktree cleanup would also delete the record. State is session-scoped and the actor
|
|
153
|
+
# is already isolated by session/agent id inside this dir, so sharing it per repository
|
|
154
|
+
# is the correct scope — one repo, one boundary namespace.
|
|
155
|
+
#
|
|
156
|
+
# `--git-common-dir` is asked for ABSOLUTELY on purpose: plain `--git-common-dir`
|
|
157
|
+
# answers a bare relative `.git` when the caller is the main checkout (absolute only
|
|
158
|
+
# from a linked worktree), which would make the state dir depend on the caller's cwd —
|
|
159
|
+
# the same defect class, re-introduced. `--path-format` needs git >= 2.31; the fallback
|
|
160
|
+
# resolves the relative answer against the root git was pointed at, not against $PWD.
|
|
161
|
+
boundary_dir() { # $1 root (echo <common-git-dir>/owner-dispatch)
|
|
162
|
+
local gd
|
|
163
|
+
gd=$(git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || gd=""
|
|
164
|
+
if [ -z "$gd" ]; then
|
|
165
|
+
gd=$(git -C "$1" rev-parse --git-common-dir 2>/dev/null) || return 1
|
|
166
|
+
[ -n "$gd" ] || return 1
|
|
167
|
+
case "$gd" in /*) ;; *) gd="$1/$gd" ;; esac
|
|
168
|
+
fi
|
|
169
|
+
# Canonicalise. The two branches above disagree on symlinked roots: git answers the
|
|
170
|
+
# PHYSICAL path, while the fallback's `$1/<relative>` keeps whatever logical path the
|
|
171
|
+
# caller passed (macOS /var -> /private/var is the everyday case). Left alone, a host on
|
|
172
|
+
# git < 2.31 would get a different key from the main checkout than from a worktree and
|
|
173
|
+
# this whole bug would come back on exactly the machines that cannot use the fast path.
|
|
174
|
+
# `opt_in_root` already realpath's "to match git's toplevel"; match it here.
|
|
175
|
+
local rp; rp=$(realpath "$gd" 2>/dev/null) && [ -n "$rp" ] && gd="$rp"
|
|
176
|
+
printf '%s/owner-dispatch' "$gd"
|
|
177
|
+
}
|
|
178
|
+
# Resolve + create the state dir, refusing a symlinked dir (a crafted repo could
|
|
179
|
+
# point it elsewhere). Echo the dir on success; fail (=> caller fails open).
|
|
180
|
+
state_dir() { # $1 root
|
|
181
|
+
local d; d=$(boundary_dir "$1") || return 1
|
|
182
|
+
[ -L "$d" ] && return 1
|
|
183
|
+
[ -e "$d" ] && [ ! -d "$d" ] && return 1
|
|
184
|
+
mkdir -p "$d" 2>/dev/null || return 1
|
|
185
|
+
printf '%s' "$d"
|
|
186
|
+
}
|
|
187
|
+
# Symlink-safe writes: a symlink in our own namespace is illegitimate -> drop it,
|
|
188
|
+
# then write via mktemp (O_EXCL, unpredictable name => no predictable-temp race) +
|
|
189
|
+
# atomic rename so the redirect never follows a link. NOTE: the state dir lives
|
|
190
|
+
# inside `<git-dir>/owner-dispatch`; an attacker who can race writes there already
|
|
191
|
+
# has write access to your `.git` (hooks, objects, config) and owns the repo — so
|
|
192
|
+
# residual TOCTOU here is bounded by a trust boundary you have already lost.
|
|
193
|
+
safe_put() { # $1 file $2 content
|
|
194
|
+
[ -L "$1" ] && rm -f "$1" 2>/dev/null
|
|
195
|
+
local dir t; dir=$(dirname -- "$1")
|
|
196
|
+
t=$(mktemp "$dir/.od.XXXXXX" 2>/dev/null) || return 1
|
|
197
|
+
printf '%s' "$2" > "$t" 2>/dev/null && mv -f "$t" "$1" 2>/dev/null || { rm -f "$t" 2>/dev/null; return 1; }
|
|
198
|
+
}
|
|
199
|
+
safe_append() { # $1 file $2 line (atomic read-modify-rewrite via safe_put)
|
|
200
|
+
[ -L "$1" ] && rm -f "$1" 2>/dev/null
|
|
201
|
+
local cur=""; [ -f "$1" ] && cur=$(cat "$1" 2>/dev/null)
|
|
202
|
+
safe_put "$1" "$cur$2"$'\n'
|
|
203
|
+
}
|
|
204
|
+
# Append $2 + a NUL terminator using O_APPEND (`>>`), which is CONCURRENCY-SAFE: each
|
|
205
|
+
# write is positioned at EOF atomically, so two parallel same-agent PreToolUse hooks (the
|
|
206
|
+
# model can emit parallel Edits in one turn) never lose an entry — unlike a read-copy-
|
|
207
|
+
# rename, where both read the same old file and the last rename clobbers the other's add.
|
|
208
|
+
# A single path is far under PIPE_BUF so the write is atomic. NUL-delimited so entries
|
|
209
|
+
# with newlines/spaces are read back by exact byte-equality. Symlink-dropped first (same
|
|
210
|
+
# .git-trust bound as the other state writers; whole-file atomicity isn't needed for an
|
|
211
|
+
# append-only log).
|
|
212
|
+
safe_append_z() { # $1 file $2 value
|
|
213
|
+
[ -L "$1" ] && rm -f "$1" 2>/dev/null
|
|
214
|
+
printf '%s\0' "$2" >> "$1" 2>/dev/null || return 1
|
|
215
|
+
}
|
|
216
|
+
# Atomic, symlink-safe, BINARY/NUL-safe write from stdin (bash vars can't hold NUL,
|
|
217
|
+
# so NUL-delimited path lists must stream straight to the temp file via cat).
|
|
218
|
+
safe_put_stdin() { # $1 file (content on stdin)
|
|
219
|
+
[ -L "$1" ] && rm -f "$1" 2>/dev/null
|
|
220
|
+
local dir t; dir=$(dirname -- "$1")
|
|
221
|
+
t=$(mktemp "$dir/.od.XXXXXX" 2>/dev/null) || return 1
|
|
222
|
+
cat > "$t" 2>/dev/null && mv -f "$t" "$1" 2>/dev/null || { rm -f "$t" 2>/dev/null; return 1; }
|
|
223
|
+
}
|
|
224
|
+
sid_safe() { printf '%s' "${1:-}" | tr -c 'A-Za-z0-9._-' '_'; }
|
|
225
|
+
# Compute the INJECTIVE, filename-safe agent_id key directly from the hook's raw JSON on
|
|
226
|
+
# stdin via jq @base64 — operating on the JSON value, NOT a bash var, so NUL / trailing
|
|
227
|
+
# newline / non-ASCII in agent_id cannot be dropped or mangled into a collision. base64's
|
|
228
|
+
# '+' and '/' are remapped to '-' '_' (base64url) so the key is a safe filename component;
|
|
229
|
+
# '=' padding is filename-safe as-is. Empty agent_id => empty key (base64 of "" is "").
|
|
230
|
+
aid_key_from() { # stdin: raw hook JSON -> echo injective key (empty if no agent_id)
|
|
231
|
+
jq -r '.agent_id // "" | @base64' 2>/dev/null | tr '+/' '-_'
|
|
232
|
+
}
|
|
233
|
+
# In an UNBORN repo `git rev-parse HEAD` prints the literal "HEAD" on stdout AND exits
|
|
234
|
+
# non-zero, so the old `cmd 2>/dev/null || printf NO_HEAD` emitted BOTH — a two-line value
|
|
235
|
+
# that equalled nothing and made every NO_HEAD comparison downstream dead code. Use
|
|
236
|
+
# --verify so the failure path produces no stdout, and the sentinel is the only output.
|
|
237
|
+
current_head() { git -C "$1" rev-parse --verify --quiet HEAD 2>/dev/null || printf 'NO_HEAD'; }
|
|
238
|
+
|
|
239
|
+
# Validity is (repo, TTL, HEAD-continuity): the recorded head must remain an ANCESTOR of the
|
|
240
|
+
# current HEAD, not equal to it.
|
|
241
|
+
#
|
|
242
|
+
# Why equality was wrong: the gate requires recording BEFORE the first gated edit, and a
|
|
243
|
+
# delivery's own commit necessarily lands AFTER that edit — so a HEAD-equality condition
|
|
244
|
+
# invalidated the boundary of every delivery that commits, and Stop then reported it as
|
|
245
|
+
# "never recorded". Worse than the false block: the only strategy that satisfied a
|
|
246
|
+
# HEAD-pinned boundary was re-recording at session end, which turns "record before editing"
|
|
247
|
+
# into post-hoc attestation — the record-field forgery surface the invocation-evidence
|
|
248
|
+
# check exists to close.
|
|
249
|
+
#
|
|
250
|
+
# Why dropping HEAD entirely was ALSO wrong: equality was incidentally catching history
|
|
251
|
+
# discontinuities — `git switch` to an unrelated branch, a reset onto a divergent commit —
|
|
252
|
+
# which genuinely do mean "a different piece of work". A TTL-only rule let the same actor
|
|
253
|
+
# record once, finish that delivery, jump to an unrelated branch inside the TTL, and edit
|
|
254
|
+
# gated code with no gate at either end. Ancestry keeps the commit-forward case working
|
|
255
|
+
# (the actual defect) while re-firing on the discontinuity case.
|
|
256
|
+
#
|
|
257
|
+
# HEAD was ALSO standing in for actor identity ("this boundary is THIS work unit's"), and
|
|
258
|
+
# nothing here replaces that: there is no actor binding. `owners_not_invoked` at Stop
|
|
259
|
+
# verifies only that the recorded owner NAMES appear as invoked skills in the current
|
|
260
|
+
# actor's transcript. That blocks an actor which invoked nothing, or a different owner set
|
|
261
|
+
# — but it clears an actor that invoked the same names, whoever recorded the boundary. So
|
|
262
|
+
# it detects SOME cross-actor reuse as a side effect; it does not bind a boundary to an
|
|
263
|
+
# actor. Do not restate it as identity binding (see the residual note below).
|
|
264
|
+
#
|
|
265
|
+
# Residual (accepted by the risk owner): within TTL, on the same descendant line of work, in
|
|
266
|
+
# the same worktree, a DIFFERENT actor inherits this boundary. PreToolUse never reads the
|
|
267
|
+
# transcript, and Stop clears that actor too whenever it invoked the same owner NAMES
|
|
268
|
+
# (`missing` empty => allow) — the normal case for a repo with a stable owner set, not an
|
|
269
|
+
# edge case. So "record before first edit" is not enforced per-actor. Do not restate this as
|
|
270
|
+
# "Stop still catches it": it does not. Also unenforced: whether the recorded owner set
|
|
271
|
+
# matches the concerns actually touched (needs a glob->owner map; deferred).
|
|
272
|
+
# Parse all gate-critical fields from ONE jq invocation, i.e. one opened version of the file.
|
|
273
|
+
# Sets b_repo / b_exp / b_head in the caller's scope (they are declared `local` there).
|
|
274
|
+
# @tsv escapes any literal tab inside a value, so splitting on real tabs is unambiguous.
|
|
275
|
+
boundary_read() { # $1 boundary.json -> sets b_repo b_exp b_head b_gitdir; fails if unparseable
|
|
276
|
+
local snap rest
|
|
277
|
+
snap=$(jq -r '[(.repo_root // ""), (.expires_at // 0), (.head // ""), (.repo_git_dir // "")] | @tsv' "$1" 2>/dev/null) || return 1
|
|
278
|
+
[ -n "$snap" ] || return 1
|
|
279
|
+
b_repo=${snap%%$'\t'*}; rest=${snap#*$'\t'}
|
|
280
|
+
b_exp=${rest%%$'\t'*}; rest=${rest#*$'\t'}
|
|
281
|
+
# `@tsv` always emits all four fields, so a missing tab here means a malformed
|
|
282
|
+
# snapshot rather than an absent value — treat the git dir as unknown and let the
|
|
283
|
+
# caller fall back, instead of silently aliasing it to the head.
|
|
284
|
+
case "$rest" in
|
|
285
|
+
*$'\t'*) b_head=${rest%%$'\t'*}; b_gitdir=${rest#*$'\t'} ;;
|
|
286
|
+
*) b_head=$rest; b_gitdir="" ;;
|
|
287
|
+
esac
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
boundary_valid() { # $1 root -> echo "valid" iff a non-expired boundary exists
|
|
291
|
+
local d f now b_repo b_exp b_head b_gitdir cur
|
|
292
|
+
d=$(boundary_dir "$1") || return 1
|
|
293
|
+
f="$d/boundary.json"; [ -r "$f" ] && [ ! -L "$f" ] || return 1
|
|
294
|
+
now=$(date +%s)
|
|
295
|
+
# ONE snapshot, one jq. Reading the fields through separate jq processes was a torn-read
|
|
296
|
+
# fail-open: `record` replaces this file by atomic rename, so a concurrent record between
|
|
297
|
+
# two reads let the check combine an unexpired expiry from one record with the head from
|
|
298
|
+
# another and return valid though neither record was. A single invocation sees exactly one
|
|
299
|
+
# version of the file.
|
|
300
|
+
boundary_read "$f" || return 1 # sets b_repo / b_exp / b_head / b_gitdir
|
|
301
|
+
# REPOSITORY identity, not working-tree path. `repo_root` is a per-worktree path, so
|
|
302
|
+
# comparing it literally rejected a boundary recorded in a linked worktree and read
|
|
303
|
+
# from the main checkout — i.e. every delivery that follows `worktree-isolation`.
|
|
304
|
+
# The common git dir is the same for every worktree of one repo AND survives the
|
|
305
|
+
# worktree being removed at cleanup, which the recording path never does. This is a
|
|
306
|
+
# replacement, not a removal: it still refuses a boundary belonging to a different
|
|
307
|
+
# repository (what the path equality was really there for). A legacy record with no
|
|
308
|
+
# repo_git_dir keeps the old path equality — conservative, since that record was
|
|
309
|
+
# written by the pre-fix code and can only have come from this same root anyway.
|
|
310
|
+
if [ -n "$b_gitdir" ]; then
|
|
311
|
+
[ "$b_gitdir" = "$(boundary_dir "$1")" ] || return 1
|
|
312
|
+
else
|
|
313
|
+
[ "$b_repo" = "$1" ] || return 1
|
|
314
|
+
fi
|
|
315
|
+
[ "$now" -lt "$b_exp" ] 2>/dev/null || return 1
|
|
316
|
+
# CONTINUITY, not equality: the recorded head must still be an ancestor of the current
|
|
317
|
+
# HEAD. Committing forward on the same line of work keeps the boundary (that is the whole
|
|
318
|
+
# point of dropping equality), while `git switch` to an unrelated branch, a reset onto a
|
|
319
|
+
# divergent commit, or any other history discontinuity re-fires the gate — those really do
|
|
320
|
+
# mean "a different piece of work", which equality was incidentally catching and a
|
|
321
|
+
# TTL-only rule would silently let through.
|
|
322
|
+
cur=$(current_head "$1")
|
|
323
|
+
# Unborn repo on either side: there is no history, so continuity cannot be established —
|
|
324
|
+
# fail closed. This IS a deliberate behaviour change, not a no-op: the previous equality
|
|
325
|
+
# rule ALLOWED here. `current_head` used to emit the two-line value "HEAD\nNO_HEAD" (the
|
|
326
|
+
# unborn `rev-parse` prints "HEAD" on stdout *and* fails), and since both `record` and the
|
|
327
|
+
# check used the same helper, both sides produced that identical string and compared equal
|
|
328
|
+
# — verified against the old code, which returns allow in an unborn repo where this returns
|
|
329
|
+
# ask. Failing closed is the intended outcome: an unborn repo has no line of work to be
|
|
330
|
+
# continuous with, and switching between orphan branches must not satisfy the gate.
|
|
331
|
+
{ [ "$b_head" = "NO_HEAD" ] || [ "$cur" = "NO_HEAD" ]; } && return 1
|
|
332
|
+
[ -n "$b_head" ] || return 1
|
|
333
|
+
# Both env vars are load-bearing and neither substitutes for the other (verified on
|
|
334
|
+
# git 2.50): GIT_NO_REPLACE_OBJECTS disables `git replace` refs, GIT_GRAFT_FILE disables
|
|
335
|
+
# the legacy `.git/info/grafts` file — with only the former, a grafts file still makes
|
|
336
|
+
# merge-base report an unrelated commit as an ancestor. Not a serious adversary story
|
|
337
|
+
# (anyone deliberately evading can `touch <waiver>`, which is documented), but a repo
|
|
338
|
+
# legitimately carrying grafts or replace refs would otherwise get a silently wrong
|
|
339
|
+
# verdict. True-object ancestry only.
|
|
340
|
+
GIT_NO_REPLACE_OBJECTS=1 GIT_GRAFT_FILE=/dev/null \
|
|
341
|
+
git -C "$1" merge-base --is-ancestor "$b_head" "$cur" 2>/dev/null || return 1
|
|
342
|
+
printf 'valid'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Classify WHY a boundary is unusable. Wording only — never a gate input; every enforcement
|
|
346
|
+
# path keeps calling boundary_valid.
|
|
347
|
+
#
|
|
348
|
+
# Each invalidating reason gets its OWN label, because collapsing them is the exact defect
|
|
349
|
+
# this file was changed to fix: telling an actor it "never recorded" a boundary when a
|
|
350
|
+
# record exists sends diagnosis hunting for a missing file. `expired` and `discontinuous`
|
|
351
|
+
# are both "you recorded, but it no longer applies" — very different from "there is nothing
|
|
352
|
+
# here". An unparseable record stays `absent` (conservative: never imply a usable record was
|
|
353
|
+
# found when the field could not be read).
|
|
354
|
+
boundary_state() { # $1 root -> echo valid|expired|discontinuous|absent
|
|
355
|
+
local d f now b_repo b_exp b_head b_gitdir cur
|
|
356
|
+
boundary_valid "$1" >/dev/null && { printf 'valid'; return 0; }
|
|
357
|
+
d=$(boundary_dir "$1") || { printf 'absent'; return 0; }
|
|
358
|
+
f="$d/boundary.json"; { [ -r "$f" ] && [ ! -L "$f" ]; } || { printf 'absent'; return 0; }
|
|
359
|
+
boundary_read "$f" || { printf 'absent'; return 0; } # one snapshot, same as the gate
|
|
360
|
+
# Same repository-identity rule as the gate. Keeping literal path equality HERE while the
|
|
361
|
+
# gate compares git dirs would rebuild the exact contradiction `cmd_status` already warns
|
|
362
|
+
# about: the diagnostic saying "absent" for a boundary the gate accepts.
|
|
363
|
+
if [ -n "$b_gitdir" ]; then
|
|
364
|
+
[ "$b_gitdir" = "$(boundary_dir "$1")" ] || { printf 'absent'; return 0; }
|
|
365
|
+
else
|
|
366
|
+
[ "$b_repo" = "$1" ] || { printf 'absent'; return 0; }
|
|
367
|
+
fi
|
|
368
|
+
case "$b_exp" in (''|0|*[!0-9]*) printf 'absent'; return 0 ;; esac # non-integer => absent
|
|
369
|
+
now=$(date +%s)
|
|
370
|
+
[ "$now" -lt "$b_exp" ] 2>/dev/null || { printf 'expired'; return 0; }
|
|
371
|
+
# Present, repo-matching, unexpired, yet boundary_valid rejected it. Before calling that a
|
|
372
|
+
# history discontinuity, prove the recorded head IS a real commit here: a well-formed but
|
|
373
|
+
# nonexistent sha (an all-zero value, a gc'd or shallow-clone-missing object) is an
|
|
374
|
+
# unusable record, not evidence of a branch switch — saying "a reset moved HEAD off it"
|
|
375
|
+
# there would be the same false diagnosis this whole change set exists to remove.
|
|
376
|
+
cur=$(current_head "$1")
|
|
377
|
+
case "$b_head" in (''|*[!0-9a-fA-F]*) printf 'absent'; return 0 ;; esac # not sha-shaped
|
|
378
|
+
GIT_NO_REPLACE_OBJECTS=1 GIT_GRAFT_FILE=/dev/null \
|
|
379
|
+
git -C "$1" cat-file -e "$b_head^{commit}" 2>/dev/null || { printf 'absent'; return 0; }
|
|
380
|
+
[ "$b_head" = "$cur" ] && { printf 'absent'; return 0; } # equal yet invalid => not continuity
|
|
381
|
+
printf 'discontinuous'
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# ----- invocation evidence (name != invoke) -----------------------------------
|
|
385
|
+
# The record only stores owner NAMES; this cross-checks them against the skills
|
|
386
|
+
# ACTUALLY invoked (loaded) this session, read from the Claude Code session
|
|
387
|
+
# transcript (a JSONL of message events). A Skill invocation appears as a
|
|
388
|
+
# `tool_use` content block with name=="Skill" and input.skill=="<plugin>:<name>".
|
|
389
|
+
# Empirically verified on the current transcript format; missing/unreadable or
|
|
390
|
+
# malformed or shape-drifted evidence stays FAIL-OPEN. A subagent transcript with a
|
|
391
|
+
# verifiable tool-event shape but zero Skill calls proves the worker never loaded one.
|
|
392
|
+
#
|
|
393
|
+
# Emit the base skill names invoked in a transcript, ONE PER LINE (strip any "prefix:").
|
|
394
|
+
invoked_skills() { # $1 transcript-path
|
|
395
|
+
[ -n "$1" ] && [ -r "$1" ] || return 0
|
|
396
|
+
# Per-line jq (JSONL); tolerate malformed lines. A line's message.content[] may
|
|
397
|
+
# carry tool_use blocks. input.skill is "ccl-skills:foo" or bare "foo".
|
|
398
|
+
jq -rR '
|
|
399
|
+
fromjson?
|
|
400
|
+
| select(.type=="assistant") # real invocations live in assistant events only
|
|
401
|
+
| (.message.content // empty)
|
|
402
|
+
| if type=="array" then .[] else empty end
|
|
403
|
+
| select(type=="object" and .type=="tool_use" and .name=="Skill")
|
|
404
|
+
| (.input.skill)
|
|
405
|
+
| strings # drop non-string skill values (no sub() abort)
|
|
406
|
+
| select(length>0)
|
|
407
|
+
| ascii_downcase
|
|
408
|
+
| if startswith("ccl-skills:") then ltrimstr("ccl-skills:") else . end
|
|
409
|
+
' "$1" 2>/dev/null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# True iff the transcript contains at least one recognized assistant tool_use block and
|
|
413
|
+
# every visible Skill block has the invocation shape parsed by invoked_skills. This lets
|
|
414
|
+
# strict worker verification distinguish "the worker invoked no skills" from "the host
|
|
415
|
+
# transcript or Skill-event shape drifted" without turning valid-JSON drift into a trap.
|
|
416
|
+
transcript_has_verifiable_invocation_shape() { # $1 transcript-path
|
|
417
|
+
[ -n "$1" ] && [ -r "$1" ] || return 1
|
|
418
|
+
jq -seR '
|
|
419
|
+
[ split("\n")[]
|
|
420
|
+
| try fromjson catch empty
|
|
421
|
+
| select(.type=="assistant")
|
|
422
|
+
| (.message.content // empty)
|
|
423
|
+
| if type=="array" then .[] else empty end
|
|
424
|
+
| select(type=="object" and .type=="tool_use")
|
|
425
|
+
] as $tools
|
|
426
|
+
| ($tools | length) > 0
|
|
427
|
+
and ([ $tools[] | select((.name | type) != "string") ] | length) == 0
|
|
428
|
+
and ([ $tools[]
|
|
429
|
+
| select(.name=="Skill"
|
|
430
|
+
and (((.input | type) != "object")
|
|
431
|
+
or ((.input.skill | type) != "string")
|
|
432
|
+
or (.input.skill | length) == 0))
|
|
433
|
+
] | length) == 0
|
|
434
|
+
' "$1" >/dev/null 2>&1
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Echo (comma-joined) the recorded owners that (a) look like a skill name
|
|
438
|
+
# (kebab token, no spaces) and (b) were NEVER invoked in the transcript. Free-text
|
|
439
|
+
# owner labels ("then the touched owner") are skill-name-shaped-filtered out, so
|
|
440
|
+
# they never false-flag. Empty output => nothing to report (all invoked, or
|
|
441
|
+
# unverifiable => fail-open). $3=true makes a shape-verifiable zero-invocation transcript
|
|
442
|
+
# report every skill-shaped owner; this is used for a worker's own transcript only.
|
|
443
|
+
owners_not_invoked() { # $1 boundary.json $2 transcript-path $3 empty-is-missing(true|false)
|
|
444
|
+
local bf="$1" tp="$2" empty_is_missing="${3:-false}"
|
|
445
|
+
[ -r "$bf" ] && [ -n "$tp" ] && [ -r "$tp" ] || return 0
|
|
446
|
+
# newline-separated invoked base-name set for grep -Fxq membership
|
|
447
|
+
local invoked; invoked=$(invoked_skills "$tp")
|
|
448
|
+
# Main sessions preserve the historical fail-open behavior for an empty set. A worker
|
|
449
|
+
# uses its dedicated transcript and strict mode: if that file contains a verifiable
|
|
450
|
+
# tool-event shape but zero Skill calls, the empty set is evidence of a cold worker.
|
|
451
|
+
# Missing, malformed, or Skill-shape-drifted evidence remains unverifiable/fail-open.
|
|
452
|
+
if [ -z "$invoked" ]; then
|
|
453
|
+
[ "$empty_is_missing" = "true" ] && transcript_has_verifiable_invocation_shape "$tp" || return 0
|
|
454
|
+
fi
|
|
455
|
+
local out="" o norm
|
|
456
|
+
while IFS= read -r o; do
|
|
457
|
+
[ -n "$o" ] || continue
|
|
458
|
+
# Normalize the owner token the SAME way invoked skills are: lowercase and strip a
|
|
459
|
+
# leading "ccl-skills:" so a recorded full id (ccl-skills:foo) matches the
|
|
460
|
+
# invoked base name "foo". Other-plugin prefixes are NOT stripped (they keep the ':'
|
|
461
|
+
# and fail the kebab-shape check below), so a same-named skill from an untrusted
|
|
462
|
+
# plugin cannot silently satisfy a CCL owner.
|
|
463
|
+
norm=$(printf '%s' "$o" | tr '[:upper:]' '[:lower:]')
|
|
464
|
+
case "$norm" in ccl-skills:*) norm=${norm#ccl-skills:} ;; esac
|
|
465
|
+
# skill-name shape only: a single kebab token; excludes phrases with spaces/commas
|
|
466
|
+
# and any remaining "plugin:" colon.
|
|
467
|
+
case "$norm" in *[!a-z0-9-]*|"") continue ;; esac
|
|
468
|
+
printf '%s\n' "$invoked" | grep -Fxq "$norm" && continue
|
|
469
|
+
out="${out:+$out,}$o"
|
|
470
|
+
done < <(jq -r '(.touched_owners // [])[]? | gsub("^\\s+|\\s+$";"")' "$bf" 2>/dev/null)
|
|
471
|
+
printf '%s' "$out"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# ----- per-session activity markers + baseline --------------------------------
|
|
475
|
+
# Two activity markers record THAT this session did owner-dispatch-relevant work:
|
|
476
|
+
# .touched — an Edit/Write tool touched a gated path.
|
|
477
|
+
# .bashwrite — the best-effort Bash write-verb heuristic matched (over-matches
|
|
478
|
+
# read-only commands, so it is only an activity hint, never proof).
|
|
479
|
+
# Neither marker's CONTENT is parsed (safe_append concatenates without a reliable
|
|
480
|
+
# separator) — they are existence-only triggers. What a session actually CHANGED is
|
|
481
|
+
# decided at Stop by diffing the current tree against a baseline snapshot (HEAD +
|
|
482
|
+
# product/exclude globs + content-hashed pre-session dirty paths) captured at the FIRST
|
|
483
|
+
# touch (PreToolUse fires before the edit applies, so the baseline is the pre-edit
|
|
484
|
+
# state). That distinguishes real new gated work (committed OR dirty) from a read-only/
|
|
485
|
+
# rejected/reverted session and from untouched pre-existing dirt — without trapping —
|
|
486
|
+
# and resists config mutation (product-shrink / exclude-widen) via baseline∪current
|
|
487
|
+
# classification, with `ci` as the reliable backstop.
|
|
488
|
+
note_touched() { # $1 root $2 rel (per-session, symlink-safe; + per-agent in a subagent)
|
|
489
|
+
local sd; sd=$(state_dir "$1") || return 0
|
|
490
|
+
capture_baseline "$1"
|
|
491
|
+
safe_append "$sd/s.$(sid_safe "$SID").touched" "$2"
|
|
492
|
+
# In a subagent, ALSO record a per-agent marker so SubagentStop can tell THIS subagent
|
|
493
|
+
# did gated work (vs. a read-only sibling or the main thread). NUL-delimited so the path
|
|
494
|
+
# is read back by exact byte-equality (the session marker above is existence-only).
|
|
495
|
+
[ -n "$AID" ] && safe_append_z "$sd/s.$(sid_safe "$SID").a.$AIDKEY.touched" "$2"
|
|
496
|
+
}
|
|
497
|
+
note_bashwrite() { # $1 root (per-session heuristic sentinel, symlink-safe; + per-agent in a subagent)
|
|
498
|
+
local sd; sd=$(state_dir "$1") || return 0
|
|
499
|
+
capture_baseline "$1"
|
|
500
|
+
safe_put "$sd/s.$(sid_safe "$SID").bashwrite" "1"
|
|
501
|
+
[ -n "$AID" ] && safe_put "$sd/s.$(sid_safe "$SID").a.$AIDKEY.bashwrite" "1"
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# Baseline globs captured at first touch (the authoritative classification at the time
|
|
505
|
+
# the work happened), kept separate from the current globs so config mutation can't
|
|
506
|
+
# dodge. Always SET (even empty) so `"${BASE_*[@]}"` is safe under bash 3.2 `set -u`.
|
|
507
|
+
BASE_PRODUCT=(); BASE_EXCLUDE=()
|
|
508
|
+
|
|
509
|
+
# Echo the working-tree content hash of <rel> (or ABSENT if missing/unreadable).
|
|
510
|
+
# Filters are applied identically at capture and at Stop, so the comparison is stable.
|
|
511
|
+
path_hash() { # $1 root $2 rel
|
|
512
|
+
git -C "$1" hash-object -- "$2" 2>/dev/null || printf 'ABSENT'
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# Snapshot, ONCE per session+repo (first touch = PreToolUse, before the edit applies),
|
|
516
|
+
# the pre-session state so Stop can tell what THIS session changed. Writes: .basehead
|
|
517
|
+
# (HEAD SHA, or UNBORN); .baseglobs / .baseexcl (product/exclude globs at capture time,
|
|
518
|
+
# the authoritative classification — used at Stop alongside the current globs so a
|
|
519
|
+
# later product-shrink OR exclude-widen can't hide a gated path); .basepaths (NUL pairs
|
|
520
|
+
# rel\0hash\0 of gated paths ALREADY changed-vs-HEAD before the session, WITH their
|
|
521
|
+
# content hash — so a pre-existing dirty file is excused only while its content is
|
|
522
|
+
# unchanged; editing it further changes the hash and is caught). .basehead is written
|
|
523
|
+
# LAST and is the idempotency guard.
|
|
524
|
+
capture_baseline() { # $1 root
|
|
525
|
+
local sd sidf; sd=$(state_dir "$1") || return 0; sidf=$(sid_safe "$SID")
|
|
526
|
+
[ -e "$sd/s.$sidf.basehead" ] && return 0
|
|
527
|
+
local head; head=$(git -C "$1" rev-parse HEAD 2>/dev/null || printf 'UNBORN')
|
|
528
|
+
load_globs "$1"; BASE_PRODUCT=(); BASE_EXCLUDE=() # baseline classification == current here
|
|
529
|
+
# Write the data files FIRST; .basehead is the success-guard written LAST, so a
|
|
530
|
+
# partial write (disk full, RO fs) leaves no .basehead => the next touch re-captures
|
|
531
|
+
# and meanwhile session_gated_changed falls back to gated_dirty (never a stale baseline).
|
|
532
|
+
{ local g; for g in ${PRODUCT_GLOBS[@]+"${PRODUCT_GLOBS[@]}"}; do [ -n "$g" ] && printf '%s\0' "$g"; done; } \
|
|
533
|
+
| safe_put_stdin "$sd/s.$sidf.baseglobs" || return 0
|
|
534
|
+
{ local x; for x in ${EXCLUDE_GLOBS[@]+"${EXCLUDE_GLOBS[@]}"}; do [ -n "$x" ] && printf '%s\0' "$x"; done; } \
|
|
535
|
+
| safe_put_stdin "$sd/s.$sidf.baseexcl" || return 0
|
|
536
|
+
{ local rel; while IFS= read -r -d '' rel; do
|
|
537
|
+
[ -n "$rel" ] && printf '%s\0%s\0' "$rel" "$(path_hash "$1" "$rel")"
|
|
538
|
+
done < <(emit_changed_z "$1" "$head"); } | safe_put_stdin "$sd/s.$sidf.basepaths" || return 0
|
|
539
|
+
safe_put "$sd/s.$sidf.basehead" "$head"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# Load BASE_PRODUCT / BASE_EXCLUDE from the captured baseline glob files.
|
|
543
|
+
load_base_globs() { # $1 baseglobs-file $2 baseexcl-file
|
|
544
|
+
BASE_PRODUCT=(); BASE_EXCLUDE=(); local g
|
|
545
|
+
[ -r "$1" ] && while IFS= read -r -d '' g; do [ -n "$g" ] && BASE_PRODUCT+=("$g"); done < "$1"
|
|
546
|
+
[ -r "$2" ] && while IFS= read -r -d '' g; do [ -n "$g" ] && BASE_EXCLUDE+=("$g"); done < "$2"
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# True (0) iff <rel> is gated under the CURRENT config OR the BASELINE config (gated ==
|
|
550
|
+
# matches product AND not matches exclude, evaluated per-config) — so neither a
|
|
551
|
+
# product-shrink nor an exclude-widen after the first touch can de-classify it.
|
|
552
|
+
gated_either() { # $1 rel (empty-array-safe expansions for bash 3.2 `set -u`)
|
|
553
|
+
local rel="$1"
|
|
554
|
+
path_matches_any "$rel" ${EXCLUDE_GLOBS[@]+"${EXCLUDE_GLOBS[@]}"} \
|
|
555
|
+
|| { path_matches_any "$rel" ${PRODUCT_GLOBS[@]+"${PRODUCT_GLOBS[@]}"} && return 0; }
|
|
556
|
+
path_matches_any "$rel" ${BASE_EXCLUDE[@]+"${BASE_EXCLUDE[@]}"} \
|
|
557
|
+
|| { path_matches_any "$rel" ${BASE_PRODUCT[@]+"${BASE_PRODUCT[@]}"} && return 0; }
|
|
558
|
+
return 1
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
# Emit (NUL) every path that differs from <head> in the current tree (committed-since-
|
|
562
|
+
# head OR staged OR unstaged — `git diff <head>` compares head to the WORKING TREE, so
|
|
563
|
+
# a committed change still shows) plus untracked files, filtered to gated_either. Caller
|
|
564
|
+
# must have loaded current + baseline globs. head=UNBORN => diff the empty tree. Inner
|
|
565
|
+
# `| while` only printfs (no `return`), so the subshell is harmless.
|
|
566
|
+
emit_changed_z() { # $1 root $2 head
|
|
567
|
+
local root="$1" head="$2" rel diffref="$2"
|
|
568
|
+
[ "$head" = "UNBORN" ] && diffref=$(git -C "$root" hash-object -t tree /dev/null 2>/dev/null)
|
|
569
|
+
{ [ -n "$diffref" ] && git -C "$root" diff --name-only --no-renames -z "$diffref" 2>/dev/null
|
|
570
|
+
git -C "$root" ls-files --others --exclude-standard -z 2>/dev/null; } \
|
|
571
|
+
| while IFS= read -r -d '' rel; do
|
|
572
|
+
[ -n "$rel" ] && gated_either "$rel" && printf '%s\0' "$rel"
|
|
573
|
+
done
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# True (0) iff <rel> is recorded in baseline-paths AND its current content hash matches
|
|
577
|
+
# the recorded one (i.e. a pre-existing change untouched this session). A re-edit changes
|
|
578
|
+
# the hash => not unchanged => the caller blocks. Not recorded => not unchanged.
|
|
579
|
+
baseline_unchanged() { # $1 basepaths-file $2 root $3 rel
|
|
580
|
+
[ -r "$1" ] || return 1
|
|
581
|
+
local p h cur; cur=$(path_hash "$2" "$3")
|
|
582
|
+
while IFS= read -r -d '' p && IFS= read -r -d '' h; do
|
|
583
|
+
[ "$p" = "$3" ] && { [ "$h" = "$cur" ] && return 0; return 1; }
|
|
584
|
+
done < "$1"
|
|
585
|
+
return 1
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# True (0) iff this session changed a gated path that was NOT already changed (same
|
|
589
|
+
# content) at the baseline — i.e. real new gated work, committed or in the worktree.
|
|
590
|
+
# Read-only, rejected, reverted/stashed-to-baseline, and untouched-pre-existing-dirty
|
|
591
|
+
# sessions yield no such path => return 1 (allow). No baseline (state write failed) =>
|
|
592
|
+
# conservative fallback to gated_dirty. `< <(...)` keeps the loop in this shell so
|
|
593
|
+
# `return` exits the function.
|
|
594
|
+
session_gated_changed() { # $1 root $2 state-dir $3 sidf
|
|
595
|
+
local root="$1" sd="$2" sidf="$3" head rel
|
|
596
|
+
[ -r "$sd/s.$sidf.basehead" ] || { gated_dirty "$root"; return; }
|
|
597
|
+
head=$(cat "$sd/s.$sidf.basehead" 2>/dev/null)
|
|
598
|
+
load_globs "$root"
|
|
599
|
+
load_base_globs "$sd/s.$sidf.baseglobs" "$sd/s.$sidf.baseexcl"
|
|
600
|
+
while IFS= read -r -d '' rel; do
|
|
601
|
+
[ -n "$rel" ] || continue
|
|
602
|
+
baseline_unchanged "$sd/s.$sidf.basepaths" "$root" "$rel" && continue
|
|
603
|
+
return 0
|
|
604
|
+
done < <(emit_changed_z "$root" "$head")
|
|
605
|
+
return 1
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# ACTOR-PRECISE variant for SubagentStop: true (0) iff a gated path THIS subagent
|
|
609
|
+
# actually touched (its per-agent .touched list of Edit/Write paths) is among the
|
|
610
|
+
# session's newly-changed gated paths. This is the real isolation — session_gated_changed
|
|
611
|
+
# alone would blame a subagent that merely ATTEMPTED a gated edit for a SIBLING's change.
|
|
612
|
+
# A subagent whose only activity was a Bash write (no .touched paths, sentinel only) is
|
|
613
|
+
# NOT path-attributable, so it is NOT blocked here (the Bash heuristic is advisory by
|
|
614
|
+
# design; the session Stop + `ci` remain the backstop for those). No per-agent touched
|
|
615
|
+
# file => nothing to attribute => allow. No baseline (state write failed) => CANNOT
|
|
616
|
+
# attribute precisely, so ALLOW (return 1) rather than falling back to the session-wide
|
|
617
|
+
# check, which would re-introduce the sibling-blame this function exists to prevent.
|
|
618
|
+
# The touched list is NUL-delimited (paths may contain newlines/spaces) and membership is
|
|
619
|
+
# tested by exact byte-equality, so no substring/line-fragment can cross-attribute.
|
|
620
|
+
agent_touched_has() { # $1 touched-file(NUL-delimited) $2 rel -> 0 iff exact member
|
|
621
|
+
local p
|
|
622
|
+
[ -r "$1" ] || return 1
|
|
623
|
+
while IFS= read -r -d '' p; do [ "$p" = "$2" ] && return 0; done < "$1"
|
|
624
|
+
return 1
|
|
625
|
+
}
|
|
626
|
+
agent_gated_changed() { # $1 root $2 state-dir $3 sidf $4 per-agent-touched-file
|
|
627
|
+
local root="$1" sd="$2" sidf="$3" tf="$4" head rel
|
|
628
|
+
[ -r "$sd/s.$sidf.basehead" ] || return 1 # no baseline => not attributable => allow
|
|
629
|
+
[ -r "$tf" ] || return 1 # only a .bashwrite sentinel (or nothing) => not attributable
|
|
630
|
+
head=$(cat "$sd/s.$sidf.basehead" 2>/dev/null)
|
|
631
|
+
load_globs "$root"
|
|
632
|
+
load_base_globs "$sd/s.$sidf.baseglobs" "$sd/s.$sidf.baseexcl"
|
|
633
|
+
while IFS= read -r -d '' rel; do
|
|
634
|
+
[ -n "$rel" ] || continue
|
|
635
|
+
baseline_unchanged "$sd/s.$sidf.basepaths" "$root" "$rel" && continue
|
|
636
|
+
# rel is a newly-changed gated path this session — attribute it only if THIS
|
|
637
|
+
# subagent touched that EXACT path (NUL-safe byte-equality, not line/substring match).
|
|
638
|
+
agent_touched_has "$tf" "$rel" && return 0
|
|
639
|
+
done < <(emit_changed_z "$root" "$head")
|
|
640
|
+
return 1
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Fallback only (used when no baseline was captured): block iff a gated path is dirty
|
|
644
|
+
# in the current tree, using current globs.
|
|
645
|
+
gated_dirty() { # $1 root
|
|
646
|
+
local root="$1" rel
|
|
647
|
+
load_globs "$root"
|
|
648
|
+
[ "${#PRODUCT_GLOBS[@]}" -gt 0 ] || return 1
|
|
649
|
+
while IFS= read -r -d '' rel; do
|
|
650
|
+
[ -n "$rel" ] || continue
|
|
651
|
+
path_matches_any "$rel" "${EXCLUDE_GLOBS[@]}" && continue
|
|
652
|
+
path_matches_any "$rel" "${PRODUCT_GLOBS[@]}" && return 0
|
|
653
|
+
done < <(git -C "$root" diff --name-only --no-renames -z HEAD 2>/dev/null; \
|
|
654
|
+
git -C "$root" diff --name-only --no-renames -z --cached 2>/dev/null; \
|
|
655
|
+
git -C "$root" ls-files --others --exclude-standard -z 2>/dev/null)
|
|
656
|
+
return 1
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
# =============================================================================
|
|
660
|
+
cmd_pretool() {
|
|
661
|
+
have jq || allow_pretool # can't parse stdin / determine opt-in => silent fail-open
|
|
662
|
+
local input tool fp cmd repo rel
|
|
663
|
+
input=$(cat)
|
|
664
|
+
SID=$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)
|
|
665
|
+
AID=$(printf '%s' "$input" | jq -r '.agent_id // empty' 2>/dev/null) # subagent => per-agent markers
|
|
666
|
+
AIDKEY=$(printf '%s' "$input" | aid_key_from) # injective key (jq, raw JSON)
|
|
667
|
+
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
668
|
+
|
|
669
|
+
case "$tool" in
|
|
670
|
+
Edit|Write|MultiEdit|NotebookEdit)
|
|
671
|
+
fp=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // .tool_input.path // empty' 2>/dev/null)
|
|
672
|
+
[ -n "$fp" ] || allow_pretool
|
|
673
|
+
if [ -e "$fp" ]; then
|
|
674
|
+
local _r; _r=$(realpath "$fp" 2>/dev/null) && [ -n "$_r" ] && fp="$_r"
|
|
675
|
+
else
|
|
676
|
+
local _d; _d=$(realpath "$(dirname -- "$fp")" 2>/dev/null) && [ -n "$_d" ] && fp="$_d/$(basename -- "$fp")"
|
|
677
|
+
fi
|
|
678
|
+
repo=$(opt_in_root "$(dirname -- "$fp")") || allow_pretool # cheap walk, git only if candidate
|
|
679
|
+
[ "$(cfg_enabled "$repo")" = "enabled" ] || allow_pretool
|
|
680
|
+
rel=${fp#"$repo"/}
|
|
681
|
+
is_gated_path "$repo" "$rel" || allow_pretool
|
|
682
|
+
local strict decision hint sidf sdv
|
|
683
|
+
sidf=$(sid_safe "$SID"); sdv=$(state_dir "$repo" 2>/dev/null)
|
|
684
|
+
# Capture the activity marker + baseline for EVERY gated edit — even under a valid
|
|
685
|
+
# boundary — so the Stop invocation-evidence check can run on the proactive-record
|
|
686
|
+
# path (record owners BEFORE any edit). Without this, a boundary short-circuit here
|
|
687
|
+
# left no marker and Stop silently allowed, fully bypassing the name!=invoke check.
|
|
688
|
+
note_touched "$repo" "$rel"
|
|
689
|
+
# A valid boundary satisfies the PRETOOL gate (allow the edit through); Stop still
|
|
690
|
+
# verifies actual-gated-change + invocation evidence from the marker just captured.
|
|
691
|
+
boundary_valid "$repo" >/dev/null && allow_pretool
|
|
692
|
+
# Explicit per-session exemption (the escape hatch, esp. under strict): if the
|
|
693
|
+
# agent recorded a waiver for a genuinely narrow exempt edit, allow.
|
|
694
|
+
[ -n "$sdv" ] && [ -e "$sdv/s.$sidf.waiver" ] && allow_pretool
|
|
695
|
+
strict=$(cfg_get "$repo" '.strict' 'false')
|
|
696
|
+
hint=$(cfg_get "$repo" '.owner_hint' 'the owning product-rd / stack skill')
|
|
697
|
+
decision="ask"
|
|
698
|
+
# strict hard-deny ONLY if we can actually persist state (else the agent could
|
|
699
|
+
# never unblock => brick); otherwise downgrade to ask.
|
|
700
|
+
[ "$strict" = "true" ] && [ -n "$sdv" ] && decision="deny"
|
|
701
|
+
if [ "$decision" = "deny" ]; then
|
|
702
|
+
emit_pretool_deny "owner-dispatch (strict): blocked edit of gated product code ($rel) in [$repo] — no owner-dispatch boundary this session. INVOKE (load) ${hint} for each touched concern, build the owner-dispatch map, then: bash $SELF record --owners \"<owner1,owner2>\". If this is a genuinely narrow exempt edit (single bug/test/doc/config), record an exemption to proceed: touch \"$sdv/s.$sidf.waiver\" (only after classifying this as a genuinely narrow exempt edit; otherwise invoke + record). (naming an owner is not invoking it.) Clearing THIS owner-dispatch boundary is yours, agent — invoke the owners then run record; don't punt it to the user as an approval. It unblocks only this gate: merge/push/destructive/scope/product decisions still require user authorization." "deny"
|
|
703
|
+
else
|
|
704
|
+
emit_pretool_deny "owner-dispatch: about to edit gated product code ($rel) in [$repo] with no owner-dispatch boundary this session. Per the product-rd owner-dispatch firing gate, INVOKE (load) ${hint} for each touched concern and build the owner-dispatch map BEFORE producing substance — naming an owner is not invoking it. Then record it: bash $SELF record --owners \"<owner1,owner2>\". (Narrow self-contained bug/test/doc/config edits are exempt — proceed if so.) Agent: clearing this is yours — invoke the owners + record; don't punt it to the user as an approval. It clears only this owner-dispatch gate; merge/push/destructive/scope/product decisions still require user authorization." "ask"
|
|
705
|
+
fi
|
|
706
|
+
;;
|
|
707
|
+
Bash)
|
|
708
|
+
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
709
|
+
[ -n "$cmd" ] || allow_pretool
|
|
710
|
+
repo=$(opt_in_root "$PWD") || allow_pretool # cheap walk, git only if candidate
|
|
711
|
+
[ "$(cfg_enabled "$repo")" = "enabled" ] || allow_pretool
|
|
712
|
+
[ "$(cfg_get "$repo" '.gate_bash' 'true')" = "true" ] || allow_pretool
|
|
713
|
+
# Best-effort write-verb detection (advisory only — many forms slip through).
|
|
714
|
+
printf '%s' "$cmd" | grep -Eq '(>>?|>\||[[:space:]]tee[[:space:]]|sed -i|perl -i|[[:space:]]ed[[:space:]]|[[:space:]]dd[[:space:]]|[[:space:]]cp[[:space:]]|[[:space:]]mv[[:space:]]|[[:space:]]install[[:space:]]|[[:space:]]rsync[[:space:]]|git (apply|restore|checkout)|[[:space:]]patch[[:space:]]|python[0-9.]* -c|<<)' || allow_pretool
|
|
715
|
+
load_globs "$repo"
|
|
716
|
+
[ "${#PRODUCT_GLOBS[@]}" -gt 0 ] || allow_pretool # empty/malformed globs => clean fail-open
|
|
717
|
+
local matched="" g stem
|
|
718
|
+
for g in "${PRODUCT_GLOBS[@]}"; do
|
|
719
|
+
stem="${g%%/\**}"; stem="${stem%%\**}"
|
|
720
|
+
[ -n "$stem" ] && printf '%s' "$cmd" | grep -Fq "$stem" && { matched=1; break; }
|
|
721
|
+
done
|
|
722
|
+
[ -n "$matched" ] || allow_pretool
|
|
723
|
+
boundary_valid "$repo" >/dev/null && allow_pretool
|
|
724
|
+
# RECORD-ONLY: the Bash write-verb heuristic over-matches read-only commands
|
|
725
|
+
# (`python -c`, heredocs, `>/dev/null`) and can never hard-deny anyway, so raising
|
|
726
|
+
# a user `ask` popup here is pure false-positive noise that trains users to disable
|
|
727
|
+
# the gate. Drop the activity marker for the Stop backstop and ALLOW SILENTLY (emit
|
|
728
|
+
# nothing). Real gated work is caught by the evidence-based Stop hook + `ci`; a real
|
|
729
|
+
# Edit/Write to a gated file still prompts the agent via the Edit branch above.
|
|
730
|
+
note_bashwrite "$repo"
|
|
731
|
+
allow_pretool
|
|
732
|
+
;;
|
|
733
|
+
*) allow_pretool ;;
|
|
734
|
+
esac
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# =============================================================================
|
|
738
|
+
cmd_stop() {
|
|
739
|
+
have jq || allow_stop # silent fail-open
|
|
740
|
+
# Handles BOTH the main Stop and SubagentStop (same wrapper; agent_id present only for
|
|
741
|
+
# the latter). NOTE: only inspects the repo under the current PWD; a gated edit made in
|
|
742
|
+
# a DIFFERENT repo during the same session is not re-checked here (documented limit).
|
|
743
|
+
local input repo sd sidf aidf mpfx capkey actor
|
|
744
|
+
input=$(cat)
|
|
745
|
+
SID=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
|
|
746
|
+
# No reliable session id => cannot scope per-session => fail open (never risk a trap).
|
|
747
|
+
[ -n "$SID" ] && [ "$SID" != "nosession" ] || allow_stop
|
|
748
|
+
# agent_id is populated only on SubagentStop. When present, scope the activity marker,
|
|
749
|
+
# block-once cap, and waiver to THIS subagent so (a) a read-only sibling/main thread's
|
|
750
|
+
# dirty gated work never blocks an unrelated subagent, and (b) the subagent's once-cap
|
|
751
|
+
# is independent of the main Stop's. The session-level baseline (capture_baseline keys
|
|
752
|
+
# on SID) is shared and correct for everyone — only the per-actor markers differ.
|
|
753
|
+
AID=$(printf '%s' "$input" | jq -r '.agent_id // empty' 2>/dev/null)
|
|
754
|
+
AIDKEY=$(printf '%s' "$input" | aid_key_from) # injective key (jq, raw JSON) — see globals
|
|
755
|
+
local transcript agent_transcript empty_is_missing="false"
|
|
756
|
+
if [ -n "$AID" ]; then
|
|
757
|
+
# Claude's SubagentStop carries BOTH paths: transcript_path is the controller/main
|
|
758
|
+
# session; agent_transcript_path is this worker. Only the latter can prove that the
|
|
759
|
+
# worker loaded its required skills. Older hosts may omit it; their main-transcript
|
|
760
|
+
# fallback retains legacy empty-set fail-open because it is not worker evidence.
|
|
761
|
+
agent_transcript=$(printf '%s' "$input" | jq -r '.agent_transcript_path // empty' 2>/dev/null)
|
|
762
|
+
if [ -n "$agent_transcript" ]; then
|
|
763
|
+
transcript="$agent_transcript"
|
|
764
|
+
empty_is_missing="true"
|
|
765
|
+
else
|
|
766
|
+
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
767
|
+
fi
|
|
768
|
+
else
|
|
769
|
+
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
770
|
+
fi
|
|
771
|
+
repo=$(opt_in_root "$PWD") || allow_stop
|
|
772
|
+
[ "$(cfg_enabled "$repo")" = "enabled" ] || allow_stop
|
|
773
|
+
sd=$(state_dir "$repo") || allow_stop
|
|
774
|
+
sidf=$(sid_safe "$SID")
|
|
775
|
+
if [ -n "$AID" ]; then mpfx="s.$sidf.a.$AIDKEY"; else mpfx="s.$sidf"; fi
|
|
776
|
+
capkey="$mpfx.blocked"
|
|
777
|
+
[ -r "$sd/$mpfx.touched" ] || [ -r "$sd/$mpfx.bashwrite" ] || allow_stop # no activity by this actor
|
|
778
|
+
# Invocation-evidence check (name != invoke): a valid boundary normally satisfies the
|
|
779
|
+
# gate — BUT if it recorded owners that were never actually invoked (loaded) this
|
|
780
|
+
# session (per the transcript), the "record" was a bare name, not a real dispatch.
|
|
781
|
+
# Block ONCE (same cap/waiver safety) to surface it. Fail-open: no transcript / no
|
|
782
|
+
# un-invoked owner / parse gap => the boundary satisfies as before.
|
|
783
|
+
if boundary_valid "$repo" >/dev/null; then
|
|
784
|
+
local bdir bf missing
|
|
785
|
+
bdir=$(boundary_dir "$repo" 2>/dev/null); bf="$bdir/boundary.json"
|
|
786
|
+
missing=$(owners_not_invoked "$bf" "$transcript" "$empty_is_missing" 2>/dev/null)
|
|
787
|
+
if [ -z "$missing" ]; then allow_stop; fi # all recorded owners were invoked (or unverifiable)
|
|
788
|
+
# Same no-trap safety as the boundary-missing block: only block a session/actor that
|
|
789
|
+
# ACTUALLY changed gated code this session (not a reverted edit or a Bash-heuristic
|
|
790
|
+
# over-match). This preserves the prior invariant that a no-change session is never
|
|
791
|
+
# blocked, even though a valid boundary exists.
|
|
792
|
+
if [ -n "$AID" ]; then
|
|
793
|
+
agent_gated_changed "$repo" "$sd" "$sidf" "$sd/$mpfx.touched" || allow_stop
|
|
794
|
+
else
|
|
795
|
+
session_gated_changed "$repo" "$sd" "$sidf" || allow_stop
|
|
796
|
+
fi
|
|
797
|
+
[ -e "$sd/s.$sidf.waiver" ] && allow_stop
|
|
798
|
+
[ -n "$AID" ] && [ -e "$sd/$mpfx.waiver" ] && allow_stop
|
|
799
|
+
[ -e "$sd/$capkey" ] && allow_stop
|
|
800
|
+
safe_put "$sd/$capkey" "1" || allow_stop
|
|
801
|
+
[ -r "$sd/$capkey" ] || allow_stop
|
|
802
|
+
emit_stop_block "owner-dispatch: recorded owner(s) [$missing] in [$repo] but this actor's transcript shows they were never invoked (loaded) — naming an owner is not invoking it. Continue the same actor now: invoke each named owner skill, re-check the work against its rules, repair any miss, and re-record with: bash $SELF record --owners \"<owners-you-actually-invoked>\". If [$missing] is a non-skill reference (not an invocable skill), correct the record and proceed. (Fires at most once per actor per session.) This recovery is the agent's job; do not ask the user to perform it."
|
|
803
|
+
fi
|
|
804
|
+
[ -e "$sd/s.$sidf.waiver" ] && allow_stop # session-level waiver suppresses both
|
|
805
|
+
[ -n "$AID" ] && [ -e "$sd/$mpfx.waiver" ] && allow_stop # per-subagent waiver
|
|
806
|
+
[ -e "$sd/$capkey" ] && allow_stop # already blocked once for this actor
|
|
807
|
+
# Evidence gate: block only if this session actually changed a gated path, judged by
|
|
808
|
+
# diffing the current WORKING TREE against the baseline snapshot taken at the first
|
|
809
|
+
# touch. This blocks real new gated work whether it ended up committed or dirty (the
|
|
810
|
+
# worktree reflects committed content too), while a read-only, rejected, reverted/
|
|
811
|
+
# stashed-to-baseline, or untouched-pre-existing-dirty session produces no NEW gated
|
|
812
|
+
# path and is allowed (no false trap). Design boundary: Stop inspects the WORKTREE, so
|
|
813
|
+
# gated work that lives ONLY in the index/HEAD with a worktree deliberately restored to
|
|
814
|
+
# baseline (`git add` + `git restore --worktree`, or commit + `git checkout <base> --`),
|
|
815
|
+
# or a write the Bash heuristic never recorded (`node -e …`), is out of this nudge's
|
|
816
|
+
# reach — the reliable `ci` subcommand (diffs base..HEAD at commit/merge) is the gate
|
|
817
|
+
# for those. Stop is a fast in-session nudge, not the enforcement boundary.
|
|
818
|
+
# Evidence gate — actor-precise: a subagent blocks only for a gated path IT touched
|
|
819
|
+
# (not a sibling's); the main session uses the session-wide check.
|
|
820
|
+
if [ -n "$AID" ]; then
|
|
821
|
+
agent_gated_changed "$repo" "$sd" "$sidf" "$sd/$mpfx.touched" || allow_stop
|
|
822
|
+
else
|
|
823
|
+
session_gated_changed "$repo" "$sd" "$sidf" || allow_stop
|
|
824
|
+
fi
|
|
825
|
+
# Persist the cap FIRST; if we can't, fail open (never block forever on a RO FS).
|
|
826
|
+
safe_put "$sd/$capkey" "1" || allow_stop
|
|
827
|
+
[ -r "$sd/$capkey" ] || allow_stop
|
|
828
|
+
local hint; hint=$(cfg_get "$repo" '.owner_hint' 'the owning product-rd / stack skill')
|
|
829
|
+
[ -n "$AID" ] && actor="this subagent" || actor="this session"
|
|
830
|
+
# Distinguish "never recorded" from "recorded, then expired": reporting an expired record
|
|
831
|
+
# as absent sends diagnosis after a record that was in fact written.
|
|
832
|
+
local why lead; why=$(boundary_state "$repo")
|
|
833
|
+
case "$why" in
|
|
834
|
+
expired)
|
|
835
|
+
lead="edited gated product code in [$repo], where an owner-dispatch boundary exists but expired before this actor finished (it may have been recorded by an earlier actor; boundaries are time-bounded — re-record to extend)" ;;
|
|
836
|
+
discontinuous)
|
|
837
|
+
lead="edited gated product code in [$repo], where an owner-dispatch boundary exists but was recorded on a different line of history (a branch switch, reset, amend, or rebase moved HEAD off it) — that record covers different work, so re-record for this one" ;;
|
|
838
|
+
*)
|
|
839
|
+
lead="edited gated product code in [$repo] but never recorded an owner-dispatch boundary" ;;
|
|
840
|
+
esac
|
|
841
|
+
emit_stop_block "owner-dispatch: $actor $lead. Before finishing: confirm you invoked ${hint} for each touched concern (not just named it). If you did the work, record it — bash $SELF record --owners \"<owners>\"; if this delivery is genuinely exempt (narrow bug/test/doc), waive — touch \"$sd/$mpfx.waiver\". (Fires at most once per actor per session.) Clearing this is yours, agent — invoke + record; don't punt it to the user as an approval (it clears only this gate, not merge/push/destructive actions)."
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
# =============================================================================
|
|
845
|
+
# Standalone invocation-evidence check (also the Stop hook's mechanism, exposed for
|
|
846
|
+
# testing/CI). Given a comma owner list and a transcript, report which skill-shaped
|
|
847
|
+
# owners were never invoked. Exit 0 = all invoked / unverifiable (fail-open); exit 3
|
|
848
|
+
# = at least one skill-shaped owner was recorded but never invoked.
|
|
849
|
+
cmd_verify_invocations() {
|
|
850
|
+
have jq || { echo "verify-invocations: jq required" >&2; return 0; } # fail-open
|
|
851
|
+
local owners="" transcript="" empty_is_missing="false"
|
|
852
|
+
while [ $# -gt 0 ]; do
|
|
853
|
+
case "$1" in
|
|
854
|
+
--owners) owners="${2:-}"; shift 2 ;;
|
|
855
|
+
--transcript) transcript="${2:-}"; shift 2 ;;
|
|
856
|
+
--empty-is-missing) empty_is_missing="true"; shift ;;
|
|
857
|
+
*) shift ;;
|
|
858
|
+
esac
|
|
859
|
+
done
|
|
860
|
+
[ -n "$owners" ] || { echo "verify-invocations: --owners required" >&2; return 2; }
|
|
861
|
+
[ -n "$transcript" ] && [ -r "$transcript" ] || { echo "verify-invocations: transcript unreadable => unverifiable (fail-open)"; return 0; }
|
|
862
|
+
# Reuse owners_not_invoked by synthesizing a minimal boundary json.
|
|
863
|
+
local tmp; tmp=$(mktemp "${TMPDIR:-/tmp}/od-vi.XXXXXX") || return 0
|
|
864
|
+
jq -nc --arg o "$owners" '{touched_owners:($o|split(",")|map(gsub("^\\s+|\\s+$";"")))}' > "$tmp" 2>/dev/null
|
|
865
|
+
local missing; missing=$(owners_not_invoked "$tmp" "$transcript" "$empty_is_missing" 2>/dev/null)
|
|
866
|
+
rm -f "$tmp"
|
|
867
|
+
if [ -n "$missing" ]; then
|
|
868
|
+
echo "verify-invocations: recorded but NEVER invoked: $missing"
|
|
869
|
+
return 3
|
|
870
|
+
fi
|
|
871
|
+
echo "verify-invocations: all skill-shaped owners were invoked (or unverifiable)"
|
|
872
|
+
return 0
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
cmd_record() {
|
|
876
|
+
have jq && have git || { echo "owner-dispatch record: jq+git required" >&2; return 1; }
|
|
877
|
+
local owners="" artifact="" ttl=28800
|
|
878
|
+
while [ $# -gt 0 ]; do
|
|
879
|
+
case "$1" in
|
|
880
|
+
--owners) owners="${2:-}"; shift 2 ;;
|
|
881
|
+
--artifact) artifact="${2:-}"; shift 2 ;;
|
|
882
|
+
--ttl) ttl="${2:-28800}"; shift 2 ;;
|
|
883
|
+
*) shift ;;
|
|
884
|
+
esac
|
|
885
|
+
done
|
|
886
|
+
[ -n "$owners" ] || { echo "owner-dispatch record: --owners \"a,b\" required (the owner skills you actually invoked)" >&2; return 2; }
|
|
887
|
+
local repo; repo=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "owner-dispatch record: not in a git repo" >&2; return 2; }
|
|
888
|
+
local sd; sd=$(state_dir "$repo") || { echo "owner-dispatch record: state dir unavailable/unsafe" >&2; return 1; }
|
|
889
|
+
local now exp head json gdir
|
|
890
|
+
now=$(date +%s); exp=$((now + ttl)); head=$(current_head "$repo")
|
|
891
|
+
# Repository identity for validation. `repo_root` stays for diagnostics (it says WHICH
|
|
892
|
+
# worktree recorded), but it must not be the key: it differs per worktree and vanishes
|
|
893
|
+
# when that worktree is cleaned up after merge.
|
|
894
|
+
gdir=$(boundary_dir "$repo") || { echo "owner-dispatch record: cannot resolve git dir" >&2; return 1; }
|
|
895
|
+
json=$(jq -nc --arg repo "$repo" --arg head "$head" --arg owners "$owners" \
|
|
896
|
+
--arg artifact "$artifact" --argjson created "$now" --argjson expires "$exp" \
|
|
897
|
+
--arg gdir "$gdir" \
|
|
898
|
+
--arg sid "${CLAUDE_SESSION_ID:-${OWNER_DISPATCH_SESSION:-unknown}}" \
|
|
899
|
+
'{repo_root:$repo,repo_git_dir:$gdir,head:$head,touched_owners:($owners|split(",")|map(gsub("^\\s+|\\s+$";""))),artifact:$artifact,created_at:$created,expires_at:$expires,session_id:$sid}' 2>/dev/null) \
|
|
900
|
+
|| { echo "owner-dispatch record: encode failed" >&2; return 1; }
|
|
901
|
+
safe_put "$sd/boundary.json" "$json" || { echo "owner-dispatch record: write failed" >&2; return 1; }
|
|
902
|
+
# No cleanup of s.*.touched/blocked: a valid boundary supersedes them (boundary_valid
|
|
903
|
+
# short-circuits before those markers are consulted), so stale per-session markers are
|
|
904
|
+
# inert — and NOT globbing-deleting avoids any raced-symlink delete risk.
|
|
905
|
+
echo "owner-dispatch: boundary recorded for [$repo] @ ${head:0:8} (owners: $owners; expires in $((ttl/3600))h)"
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
# Load product/exclude globs from a config CONTENT string (used for the BASE config).
|
|
909
|
+
load_globs_from() { # $1 json-string -> sets ARG_PRODUCT_GLOBS / ARG_EXCLUDE_GLOBS
|
|
910
|
+
ARG_PRODUCT_GLOBS=(); ARG_EXCLUDE_GLOBS=()
|
|
911
|
+
local line
|
|
912
|
+
while IFS= read -r line; do [ -n "$line" ] && ARG_PRODUCT_GLOBS+=("$line"); done < <(printf '%s' "$1" | jq -r '(.product_globs // [])[]' 2>/dev/null)
|
|
913
|
+
while IFS= read -r line; do [ -n "$line" ] && ARG_EXCLUDE_GLOBS+=("$line"); done < <(printf '%s' "$1" | jq -r '(.exclude_globs // [])[]' 2>/dev/null)
|
|
914
|
+
ARG_EXCLUDE_GLOBS+=("**/*.md" ".owner-dispatch.json" ".git/**")
|
|
915
|
+
}
|
|
916
|
+
# Fail-closed schema check for an ENABLED config: an enabled-but-unusable config
|
|
917
|
+
# (product_globs missing / empty / not a string array) would silently gate nothing.
|
|
918
|
+
cfg_schema_ok() { # $1 config-json -> 0 if a valid enabled config
|
|
919
|
+
printf '%s' "$1" | jq -e '
|
|
920
|
+
(.enabled==true)
|
|
921
|
+
and ((.product_globs|type)=="array") and ((.product_globs|length)>0)
|
|
922
|
+
and (all(.product_globs[]; (type=="string") and (length>0)))
|
|
923
|
+
and (((.exclude_globs)//[])|type=="array")
|
|
924
|
+
and (((.ci_artifact)//"x")|type=="string")
|
|
925
|
+
and (((.strict)//false)|type=="boolean")
|
|
926
|
+
and (((.gate_bash)//true)|type=="boolean")
|
|
927
|
+
' >/dev/null 2>&1
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
cmd_ci_python() {
|
|
931
|
+
python3 - "$@" <<'PY'
|
|
932
|
+
import fnmatch
|
|
933
|
+
import json
|
|
934
|
+
import os
|
|
935
|
+
import subprocess
|
|
936
|
+
import sys
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def fail(message, code=2):
|
|
940
|
+
print(message, file=sys.stderr)
|
|
941
|
+
return code
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def run_git(args, cwd=None, text=True, check=False):
|
|
945
|
+
result = subprocess.run(
|
|
946
|
+
["git", *args],
|
|
947
|
+
cwd=cwd,
|
|
948
|
+
check=False,
|
|
949
|
+
stdout=subprocess.PIPE,
|
|
950
|
+
stderr=subprocess.DEVNULL,
|
|
951
|
+
text=text,
|
|
952
|
+
)
|
|
953
|
+
if check and result.returncode != 0:
|
|
954
|
+
raise RuntimeError(args)
|
|
955
|
+
return result
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def load_json_text(text):
|
|
959
|
+
if not text:
|
|
960
|
+
return None
|
|
961
|
+
return json.loads(text)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def load_json_file(path):
|
|
965
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
966
|
+
return json.load(f)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def enabled(cfg):
|
|
970
|
+
return bool(isinstance(cfg, dict) and cfg.get("enabled") is True)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def glob_list(cfg, key):
|
|
974
|
+
if not isinstance(cfg, dict):
|
|
975
|
+
return []
|
|
976
|
+
value = cfg.get(key) or []
|
|
977
|
+
return value if isinstance(value, list) else []
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def path_matches_any(rel, globs):
|
|
981
|
+
for glob in globs:
|
|
982
|
+
if not glob:
|
|
983
|
+
continue
|
|
984
|
+
# fnmatch's '*' crosses '/', matching bash `case` semantics used above.
|
|
985
|
+
if fnmatch.fnmatchcase(rel, glob):
|
|
986
|
+
return True
|
|
987
|
+
if glob.startswith("**/") and fnmatch.fnmatchcase(rel, glob[3:]):
|
|
988
|
+
return True
|
|
989
|
+
return False
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def load_globs(cfg):
|
|
993
|
+
product = [g for g in glob_list(cfg, "product_globs") if isinstance(g, str) and g]
|
|
994
|
+
exclude = [g for g in glob_list(cfg, "exclude_globs") if isinstance(g, str) and g]
|
|
995
|
+
exclude.extend(["**/*.md", ".owner-dispatch.json", ".git/**"])
|
|
996
|
+
return product, exclude
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def is_gated_path(rel, cfg):
|
|
1000
|
+
product, exclude = load_globs(cfg)
|
|
1001
|
+
if not product:
|
|
1002
|
+
return False
|
|
1003
|
+
if path_matches_any(rel, exclude):
|
|
1004
|
+
return False
|
|
1005
|
+
return path_matches_any(rel, product)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def schema_ok(cfg):
|
|
1009
|
+
if not isinstance(cfg, dict) or cfg.get("enabled") is not True:
|
|
1010
|
+
return False
|
|
1011
|
+
product = cfg.get("product_globs")
|
|
1012
|
+
exclude = cfg.get("exclude_globs", [])
|
|
1013
|
+
ci_artifact = cfg.get("ci_artifact", "x")
|
|
1014
|
+
return (
|
|
1015
|
+
isinstance(product, list)
|
|
1016
|
+
and len(product) > 0
|
|
1017
|
+
and all(isinstance(x, str) and len(x) > 0 for x in product)
|
|
1018
|
+
and isinstance(exclude, list)
|
|
1019
|
+
and (ci_artifact is None or isinstance(ci_artifact, str))
|
|
1020
|
+
and isinstance(cfg.get("strict", False), bool)
|
|
1021
|
+
and isinstance(cfg.get("gate_bash", True), bool)
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def main(argv):
|
|
1026
|
+
base = ""
|
|
1027
|
+
if len(argv) >= 2 and argv[0] == "--base":
|
|
1028
|
+
base = argv[1]
|
|
1029
|
+
|
|
1030
|
+
top = run_git(["rev-parse", "--show-toplevel"], check=False)
|
|
1031
|
+
if top.returncode != 0:
|
|
1032
|
+
return fail("owner-dispatch ci: not a git repo — FAIL-CLOSED (2)")
|
|
1033
|
+
repo = top.stdout.strip()
|
|
1034
|
+
|
|
1035
|
+
if not base:
|
|
1036
|
+
default_branch = os.environ.get("CI_DEFAULT_BRANCH") or "origin/main"
|
|
1037
|
+
mb = run_git(["merge-base", "HEAD", default_branch], cwd=repo, check=False)
|
|
1038
|
+
base = mb.stdout.strip()
|
|
1039
|
+
if not base:
|
|
1040
|
+
return fail("owner-dispatch ci: could not determine a diff base — pass --base <sha>. FAIL-CLOSED (2).")
|
|
1041
|
+
if run_git(["rev-parse", "--verify", f"{base}^{{commit}}"], cwd=repo).returncode != 0:
|
|
1042
|
+
return fail(f"owner-dispatch ci: invalid base '{base}' — FAIL-CLOSED (2)")
|
|
1043
|
+
|
|
1044
|
+
head_cfg = None
|
|
1045
|
+
head_cfg_path = os.path.join(repo, ".owner-dispatch.json")
|
|
1046
|
+
if os.path.exists(head_cfg_path):
|
|
1047
|
+
try:
|
|
1048
|
+
head_cfg = load_json_file(head_cfg_path)
|
|
1049
|
+
except (OSError, json.JSONDecodeError):
|
|
1050
|
+
head_cfg = None
|
|
1051
|
+
|
|
1052
|
+
base_show = run_git(["show", f"{base}:.owner-dispatch.json"], cwd=repo)
|
|
1053
|
+
base_cfg_text = base_show.stdout if base_show.returncode == 0 else ""
|
|
1054
|
+
try:
|
|
1055
|
+
base_cfg = load_json_text(base_cfg_text)
|
|
1056
|
+
except json.JSONDecodeError:
|
|
1057
|
+
base_cfg = None
|
|
1058
|
+
|
|
1059
|
+
head_enabled = enabled(head_cfg)
|
|
1060
|
+
base_enabled = enabled(base_cfg)
|
|
1061
|
+
|
|
1062
|
+
diff = run_git(["diff", "-z", "--name-only", base, "HEAD"], cwd=repo, text=False)
|
|
1063
|
+
if diff.returncode != 0:
|
|
1064
|
+
return fail("owner-dispatch ci: diff failed — FAIL-CLOSED (2)")
|
|
1065
|
+
changed = [p.decode("utf-8", "surrogateescape") for p in diff.stdout.split(b"\0") if p]
|
|
1066
|
+
|
|
1067
|
+
base_product, base_exclude = load_globs(base_cfg)
|
|
1068
|
+
gated = []
|
|
1069
|
+
for rel in changed:
|
|
1070
|
+
if is_gated_path(rel, head_cfg):
|
|
1071
|
+
gated.append(rel)
|
|
1072
|
+
continue
|
|
1073
|
+
if base_product and not path_matches_any(rel, base_exclude) and path_matches_any(rel, base_product):
|
|
1074
|
+
gated.append(rel)
|
|
1075
|
+
|
|
1076
|
+
gated_disp = "".join(f" {p}" for p in gated)
|
|
1077
|
+
|
|
1078
|
+
tracked = run_git(["ls-files", "--error-unmatch", ".owner-dispatch.json"], cwd=repo).returncode == 0
|
|
1079
|
+
if tracked and not isinstance(head_cfg, dict):
|
|
1080
|
+
return fail("owner-dispatch ci: FAIL-CLOSED (2) — tracked .owner-dispatch.json is malformed or non-object JSON.")
|
|
1081
|
+
|
|
1082
|
+
if base_enabled and not head_enabled:
|
|
1083
|
+
if gated:
|
|
1084
|
+
print(
|
|
1085
|
+
f"owner-dispatch ci: FAIL (1) — this change disables/removes the gate AND touches gated product code:{gated_disp}",
|
|
1086
|
+
file=sys.stderr,
|
|
1087
|
+
)
|
|
1088
|
+
print(" Decommission the gate in a separate config-only change, or keep it enabled.", file=sys.stderr)
|
|
1089
|
+
return 1
|
|
1090
|
+
print("owner-dispatch ci: gate disabled/removed in a config-only change — allowed (clean decommit)")
|
|
1091
|
+
return 0
|
|
1092
|
+
|
|
1093
|
+
if not head_enabled:
|
|
1094
|
+
print("owner-dispatch ci: not opted in — skip")
|
|
1095
|
+
return 0
|
|
1096
|
+
|
|
1097
|
+
if not schema_ok(head_cfg):
|
|
1098
|
+
return fail(
|
|
1099
|
+
"owner-dispatch ci: FAIL-CLOSED (2) — enabled .owner-dispatch.json is schema-invalid "
|
|
1100
|
+
"(product_globs must be a non-empty array of non-empty strings; strict/gate_bash boolean; ci_artifact a string)."
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
if not gated:
|
|
1104
|
+
print("owner-dispatch ci: no gated product files changed — ok")
|
|
1105
|
+
return 0
|
|
1106
|
+
|
|
1107
|
+
artifact_value = head_cfg.get("ci_artifact", "design/owner-dispatch-map.md")
|
|
1108
|
+
if artifact_value is None:
|
|
1109
|
+
artifact_value = "design/owner-dispatch-map.md"
|
|
1110
|
+
artifact = str(artifact_value)
|
|
1111
|
+
if artifact.startswith("./"):
|
|
1112
|
+
artifact = artifact[2:]
|
|
1113
|
+
if artifact.startswith("/") or ".." in artifact.split("/") or artifact == "":
|
|
1114
|
+
print(
|
|
1115
|
+
f"owner-dispatch ci: FAIL (1) — unsafe ci_artifact path '{artifact}' (must be repo-relative, no '..' path component).",
|
|
1116
|
+
file=sys.stderr,
|
|
1117
|
+
)
|
|
1118
|
+
return 1
|
|
1119
|
+
if artifact == ".owner-dispatch.json":
|
|
1120
|
+
print("owner-dispatch ci: FAIL (1) — ci_artifact must not be the config file.", file=sys.stderr)
|
|
1121
|
+
return 1
|
|
1122
|
+
|
|
1123
|
+
head_product, _ = load_globs(head_cfg)
|
|
1124
|
+
if head_product and path_matches_any(artifact, head_product):
|
|
1125
|
+
print(
|
|
1126
|
+
f"owner-dispatch ci: FAIL (1) — ci_artifact '{artifact}' matches product_globs; the map must be a dedicated non-product file.",
|
|
1127
|
+
file=sys.stderr,
|
|
1128
|
+
)
|
|
1129
|
+
return 1
|
|
1130
|
+
if artifact in gated:
|
|
1131
|
+
print(
|
|
1132
|
+
f"owner-dispatch ci: FAIL (1) — ci_artifact '{artifact}' is itself a gated changed file; use a dedicated map.",
|
|
1133
|
+
file=sys.stderr,
|
|
1134
|
+
)
|
|
1135
|
+
return 1
|
|
1136
|
+
|
|
1137
|
+
artifact_path = os.path.join(repo, artifact)
|
|
1138
|
+
if not os.path.exists(artifact_path) or os.path.getsize(artifact_path) <= 0:
|
|
1139
|
+
print(
|
|
1140
|
+
f"owner-dispatch ci: FAIL (1) — gated product files changed:{gated_disp} but no owner-dispatch map at '{artifact}'.",
|
|
1141
|
+
file=sys.stderr,
|
|
1142
|
+
)
|
|
1143
|
+
return 1
|
|
1144
|
+
if artifact not in changed:
|
|
1145
|
+
print(
|
|
1146
|
+
f"owner-dispatch ci: FAIL (1) — '{artifact}' exists but was not updated in this change (stale) for gated files:{gated_disp}",
|
|
1147
|
+
file=sys.stderr,
|
|
1148
|
+
)
|
|
1149
|
+
return 1
|
|
1150
|
+
|
|
1151
|
+
print(f"owner-dispatch ci: gated files changed and '{artifact}' updated in this change — ok")
|
|
1152
|
+
return 0
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
if __name__ == "__main__":
|
|
1156
|
+
sys.exit(main(sys.argv[1:]))
|
|
1157
|
+
PY
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
# =============================================================================
|
|
1161
|
+
# Host-agnostic BACKSTOP for product repos. Unlike the in-session hooks (which fail
|
|
1162
|
+
# OPEN so a bug never bricks editing), CI fails CLOSED: any inability to verify is a
|
|
1163
|
+
# failure, because this is the gate of last resort. Exit: 0 ok / not-opted / clean
|
|
1164
|
+
# decommit; 1 violation; 2 could-not-verify (a gating job must treat 2 as failure).
|
|
1165
|
+
cmd_ci() {
|
|
1166
|
+
have git || { echo "owner-dispatch ci: git required to verify — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1167
|
+
if ! have jq; then
|
|
1168
|
+
have python3 || { echo "owner-dispatch ci: jq missing and python3 fallback unavailable — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1169
|
+
cmd_ci_python "$@"; return $?
|
|
1170
|
+
fi
|
|
1171
|
+
local base=""; [ "${1:-}" = "--base" ] && base="${2:-}"
|
|
1172
|
+
local repo; repo=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "owner-dispatch ci: not a git repo — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1173
|
+
[ -n "$base" ] || base=$(git merge-base HEAD "${CI_DEFAULT_BRANCH:-origin/main}" 2>/dev/null)
|
|
1174
|
+
[ -n "$base" ] || { echo "owner-dispatch ci: could not determine a diff base — pass --base <sha>. FAIL-CLOSED (2)." >&2; return 2; }
|
|
1175
|
+
git rev-parse --verify "$base^{commit}" >/dev/null 2>&1 || { echo "owner-dispatch ci: invalid base '$base' — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1176
|
+
|
|
1177
|
+
local head_enabled base_cfg base_enabled
|
|
1178
|
+
head_enabled=$([ "$(cfg_enabled "$repo")" = "enabled" ] && echo 1 || echo 0)
|
|
1179
|
+
base_cfg=$(git show "$base:.owner-dispatch.json" 2>/dev/null)
|
|
1180
|
+
base_enabled=$(printf '%s' "$base_cfg" | jq -er 'if (.enabled // false) then 1 else 0 end' 2>/dev/null || echo 0)
|
|
1181
|
+
# Direct two-tree diff against the EXPLICIT base (NOT three-dot, which recomputes a
|
|
1182
|
+
# merge-base and breaks on shallow/disconnected CI clones). NUL-delimited via a temp
|
|
1183
|
+
# file (NUL can't survive a shell var) so paths with spaces/newlines classify exactly.
|
|
1184
|
+
local CHANGED=() difftmp rel
|
|
1185
|
+
difftmp=$(mktemp 2>/dev/null) || { echo "owner-dispatch ci: mktemp failed — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1186
|
+
git diff -z --name-only "$base" HEAD > "$difftmp" 2>/dev/null || { rm -f "$difftmp"; echo "owner-dispatch ci: diff failed — FAIL-CLOSED (2)" >&2; return 2; }
|
|
1187
|
+
while IFS= read -r -d '' rel; do CHANGED+=("$rel"); done < "$difftmp"
|
|
1188
|
+
rm -f "$difftmp"
|
|
1189
|
+
|
|
1190
|
+
# Classify changed files against the UNION of base-config and head-config product
|
|
1191
|
+
# globs (base = the contract in force before this change), so an MR cannot dodge the
|
|
1192
|
+
# gate by shrinking product_globs / widening excludes in the same change.
|
|
1193
|
+
load_globs_from "$base_cfg"
|
|
1194
|
+
# bash-3.2-safe array copies (expanding an empty array under `set -u` errors on 3.2).
|
|
1195
|
+
local base_product=() base_exclude=() GATED=()
|
|
1196
|
+
[ "${#ARG_PRODUCT_GLOBS[@]}" -gt 0 ] && base_product=("${ARG_PRODUCT_GLOBS[@]}")
|
|
1197
|
+
[ "${#ARG_EXCLUDE_GLOBS[@]}" -gt 0 ] && base_exclude=("${ARG_EXCLUDE_GLOBS[@]}")
|
|
1198
|
+
if [ "${#CHANGED[@]}" -gt 0 ]; then
|
|
1199
|
+
for rel in "${CHANGED[@]}"; do
|
|
1200
|
+
if is_gated_path "$repo" "$rel"; then GATED+=("$rel"); continue; fi
|
|
1201
|
+
if [ "${#base_product[@]}" -gt 0 ] && ! path_matches_any "$rel" "${base_exclude[@]}" && path_matches_any "$rel" "${base_product[@]}"; then
|
|
1202
|
+
GATED+=("$rel")
|
|
1203
|
+
fi
|
|
1204
|
+
done
|
|
1205
|
+
fi
|
|
1206
|
+
local n_gated=${#GATED[@]} gated_disp=""
|
|
1207
|
+
[ "$n_gated" -gt 0 ] && gated_disp=$(printf ' %s' "${GATED[@]}")
|
|
1208
|
+
|
|
1209
|
+
# A TRACKED-but-unparseable config is "cannot verify" (fail-closed), NOT "not opted
|
|
1210
|
+
# in" and NOT a clean decommit: cfg_enabled returns empty for malformed JSON. This MUST
|
|
1211
|
+
# run BEFORE the decommit branch, else malforming an enabled config in a config-only
|
|
1212
|
+
# change (n_gated=0) would be mistaken for a clean removal and pass.
|
|
1213
|
+
if git -C "$repo" ls-files --error-unmatch .owner-dispatch.json >/dev/null 2>&1; then
|
|
1214
|
+
jq -e 'type=="object"' "$repo/.owner-dispatch.json" >/dev/null 2>&1 || {
|
|
1215
|
+
echo "owner-dispatch ci: FAIL-CLOSED (2) — tracked .owner-dispatch.json is malformed or non-object JSON." >&2; return 2; }
|
|
1216
|
+
fi
|
|
1217
|
+
# Anti-bypass: turning the gate OFF (disable/REMOVE) is allowed ONLY in a change that
|
|
1218
|
+
# touches NO gated product code — a clean, separately-reviewed decommit. (A malformed
|
|
1219
|
+
# config was already rejected above, so only a genuine disable/removal reaches here.)
|
|
1220
|
+
if [ "$base_enabled" = "1" ] && [ "$head_enabled" != "1" ]; then
|
|
1221
|
+
if [ "$n_gated" -gt 0 ]; then
|
|
1222
|
+
echo "owner-dispatch ci: FAIL (1) — this change disables/removes the gate AND touches gated product code:$gated_disp" >&2
|
|
1223
|
+
echo " Decommission the gate in a separate config-only change, or keep it enabled." >&2
|
|
1224
|
+
return 1
|
|
1225
|
+
fi
|
|
1226
|
+
echo "owner-dispatch ci: gate disabled/removed in a config-only change — allowed (clean decommit)"
|
|
1227
|
+
return 0
|
|
1228
|
+
fi
|
|
1229
|
+
[ "$head_enabled" = "1" ] || { echo "owner-dispatch ci: not opted in — skip"; return 0; }
|
|
1230
|
+
# Fail-closed: an enabled config must be schema-valid, else it gates nothing silently.
|
|
1231
|
+
cfg_schema_ok "$(cat "$repo/.owner-dispatch.json" 2>/dev/null)" || {
|
|
1232
|
+
echo "owner-dispatch ci: FAIL-CLOSED (2) — enabled .owner-dispatch.json is schema-invalid (product_globs must be a non-empty array of non-empty strings; strict/gate_bash boolean; ci_artifact a string)." >&2; return 2; }
|
|
1233
|
+
[ "$n_gated" -gt 0 ] || { echo "owner-dispatch ci: no gated product files changed — ok"; return 0; }
|
|
1234
|
+
|
|
1235
|
+
local artifact; artifact=$(cfg_get "$repo" '.ci_artifact' 'design/owner-dispatch-map.md')
|
|
1236
|
+
artifact=${artifact#./} # normalize to git's path form (git reports 'design/x', not './design/x')
|
|
1237
|
+
case "$artifact" in
|
|
1238
|
+
/*|"") echo "owner-dispatch ci: FAIL (1) — unsafe ci_artifact path '$artifact' (must be repo-relative, no '..' path component)." >&2; return 1 ;;
|
|
1239
|
+
esac
|
|
1240
|
+
case "/$artifact/" in
|
|
1241
|
+
*/../*) echo "owner-dispatch ci: FAIL (1) — unsafe ci_artifact path '$artifact' (must be repo-relative, no '..' path component)." >&2; return 1 ;;
|
|
1242
|
+
esac
|
|
1243
|
+
# The map must be a DEDICATED artifact — not the config and not product code, or a
|
|
1244
|
+
# change to that very file would falsely satisfy the gate.
|
|
1245
|
+
[ "$artifact" = ".owner-dispatch.json" ] && { echo "owner-dispatch ci: FAIL (1) — ci_artifact must not be the config file." >&2; return 1; }
|
|
1246
|
+
load_globs "$repo"
|
|
1247
|
+
if [ "${#PRODUCT_GLOBS[@]}" -gt 0 ] && path_matches_any "$artifact" "${PRODUCT_GLOBS[@]}"; then
|
|
1248
|
+
echo "owner-dispatch ci: FAIL (1) — ci_artifact '$artifact' matches product_globs; the map must be a dedicated non-product file." >&2; return 1
|
|
1249
|
+
fi
|
|
1250
|
+
for rel in "${GATED[@]}"; do
|
|
1251
|
+
[ "$rel" = "$artifact" ] && { echo "owner-dispatch ci: FAIL (1) — ci_artifact '$artifact' is itself a gated changed file; use a dedicated map." >&2; return 1; }
|
|
1252
|
+
done
|
|
1253
|
+
if [ ! -s "$repo/$artifact" ]; then
|
|
1254
|
+
echo "owner-dispatch ci: FAIL (1) — gated product files changed:$gated_disp but no owner-dispatch map at '$artifact'." >&2
|
|
1255
|
+
return 1
|
|
1256
|
+
fi
|
|
1257
|
+
# Anti-stale: the artifact must be one of the files actually changed in this diff.
|
|
1258
|
+
local art_updated=0
|
|
1259
|
+
for rel in "${CHANGED[@]}"; do [ "$rel" = "$artifact" ] && { art_updated=1; break; }; done
|
|
1260
|
+
if [ "$art_updated" != "1" ]; then
|
|
1261
|
+
echo "owner-dispatch ci: FAIL (1) — '$artifact' exists but was not updated in this change (stale) for gated files:$gated_disp" >&2
|
|
1262
|
+
return 1
|
|
1263
|
+
fi
|
|
1264
|
+
# RESIDUAL (documented): CI checks the map EXISTS, is updated in-change, and sits on
|
|
1265
|
+
# a safe repo-relative path. It does NOT validate the map's semantic content (that the
|
|
1266
|
+
# listed owners actually cover the gated files) — that stays reviewer/owner judgment.
|
|
1267
|
+
echo "owner-dispatch ci: gated files changed and '$artifact' updated in this change — ok"
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
cmd_status() {
|
|
1271
|
+
# Resolve exactly like the enforcement paths do (opt_in_root realpath's to the physical
|
|
1272
|
+
# path, "to match git's toplevel"). Using the raw logical path here made `status` compare
|
|
1273
|
+
# a logical path against the physical repo_root stored by `record`. Whenever the checkout
|
|
1274
|
+
# is reached through a symlink so `$PWD` stays logical (macOS /tmp among them; entering by
|
|
1275
|
+
# the physical path is unaffected), it reported absent/stale for a perfectly valid
|
|
1276
|
+
# boundary while pretool allowed the edit — the diagnostic surface contradicting the gate
|
|
1277
|
+
# it exists to explain. Fall back to the older resolution only when not opted in, so the
|
|
1278
|
+
# "opted-in: no" output still works outside a gated repo.
|
|
1279
|
+
local repo; repo=$(opt_in_root "$PWD") || repo=$(find_config_dir "$PWD") || repo=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
1280
|
+
[ -n "${repo:-}" ] || { echo "not in a git repo / no opt-in found"; return 0; }
|
|
1281
|
+
echo "repo: $repo"
|
|
1282
|
+
echo "opted-in: $([ "$(cfg_enabled "$repo")" = enabled ] && echo yes || echo 'no (no/disabled/malformed .owner-dispatch.json)')"
|
|
1283
|
+
echo "strict: $(cfg_get "$repo" '.strict' 'false')"
|
|
1284
|
+
echo "HEAD: $(current_head "$repo")"
|
|
1285
|
+
# Report the actual state, not a valid/not-valid collapse: `status` is the command an
|
|
1286
|
+
# agent runs to diagnose a Stop block, so it must distinguish "never recorded" from
|
|
1287
|
+
# "recorded, then expired" for the same reason the Stop message does.
|
|
1288
|
+
echo "boundary: $(boundary_state "$repo")"
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
main() {
|
|
1292
|
+
local sub="${1:-}"; shift || true
|
|
1293
|
+
case "$sub" in
|
|
1294
|
+
pretool) cmd_pretool "$@" ;;
|
|
1295
|
+
stop) cmd_stop "$@" ;;
|
|
1296
|
+
record) cmd_record "$@" ;;
|
|
1297
|
+
verify-invocations) cmd_verify_invocations "$@" ;;
|
|
1298
|
+
ci) cmd_ci "$@" ;;
|
|
1299
|
+
status) cmd_status "$@" ;;
|
|
1300
|
+
# Introspection: where boundary state for a root actually lands. Exists so the
|
|
1301
|
+
# read/write path can be ASSERTED across the worktree boundary rather than
|
|
1302
|
+
# inferred, and so a human debugging a Stop block can compare two roots directly.
|
|
1303
|
+
boundary-path) boundary_dir "${1:-$PWD}" ;;
|
|
1304
|
+
*) echo "usage: owner-dispatch.sh {pretool|stop|record --owners \"a,b\"|ci [--base REF]|status|boundary-path [root]}" >&2; return 2 ;;
|
|
1305
|
+
esac
|
|
1306
|
+
}
|
|
1307
|
+
main "$@"
|