claude-code-conductor 2.12.0__tar.gz → 2.14.0__tar.gz
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.
- claude_code_conductor-2.14.0/.claude/agents/planner.md +69 -0
- claude_code_conductor-2.14.0/.claude/agents/project-setup.md +77 -0
- claude_code_conductor-2.14.0/.claude/hooks/_hook_utils.py +55 -0
- claude_code_conductor-2.14.0/.claude/hooks/check_agent_invocation.py +113 -0
- claude_code_conductor-2.14.0/.claude/hooks/planner_check.py +307 -0
- claude_code_conductor-2.12.0/.claude/agents/planner.md → claude_code_conductor-2.14.0/.claude/rules/plan-design-guidelines.md +53 -89
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/settings.json +20 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/parallel-agents/SKILL.md +11 -6
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/setup/SKILL.md +7 -1
- claude_code_conductor-2.14.0/.claude/skills/setup/reference.md +57 -0
- claude_code_conductor-2.14.0/.claude/skills/setup/templates/coding-standards-template.md +29 -0
- claude_code_conductor-2.14.0/.claude/skills/setup/templates/project-conventions-template.md +23 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.gitignore +4 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/CHANGELOG.md +110 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/PKG-INFO +1 -1
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/__init__.py +1 -1
- claude_code_conductor-2.14.0/tests/hooks/test_check_agent_invocation.py +231 -0
- claude_code_conductor-2.14.0/tests/hooks/test_hook_utils.py +226 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_planner_check.py +449 -87
- claude_code_conductor-2.14.0/tests/hooks/test_planner_check_dev.py +186 -0
- claude_code_conductor-2.14.0/tests/skills/test_planner_lightweight.py +97 -0
- claude_code_conductor-2.14.0/tests/skills/test_setup_templates.py +97 -0
- claude_code_conductor-2.12.0/.claude/agents/project-setup.md +0 -125
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/consolidate_memory.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/permission_handler.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/permission_handler_toast.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/post_tool.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/recall_inject.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/restore_session.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/schema.sql +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/select_tier.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/session_stop.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/session_utils.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/stop.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/rules/code-review-checklist.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/rules/security-review-checklist.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/code-review/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/recall/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/skills/start/SKILL.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/fastembed-LICENSE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/fastembed-NOTICE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/onnxruntime-LICENSE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/README.md +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/hatch_build.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/pyproject.toml +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/_terminal.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/adapters.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_recall.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_tier.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/db.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/embedding.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/mcp_server.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/question.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/recall_chunker.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/src/c3/recall_index.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/__init__.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/conftest.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_consolidate_memory.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_permission_handler_toast.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_recall_inject.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_record_review_decision.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_record_tier_outcome.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_review_hint_inject.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_select_tier.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_session_start.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_session_stop.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/_skill_helpers.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_init_session_no_task_type.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_recall_skill.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_start_skill_new_flow.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_ask.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_entry.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_recall.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_cli_tier.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_embedding.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_mcp_server_elicit.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_recall_chunker.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_recall_index.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.12.0 → claude_code_conductor-2.14.0}/tests/test_worktree_guard.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
model: opus
|
|
4
|
+
description: 計画立案担当。全レポートを統合しタスク分解した plan-report を出力する。ソース編集不可。
|
|
5
|
+
tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Glob
|
|
9
|
+
- Grep
|
|
10
|
+
- Skill
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Planner
|
|
14
|
+
<!-- ペルソナ定義: /start コマンドで親 Claude がこのペルソナを採用して対話を行う。サブエージェントとして起動しない。 -->
|
|
15
|
+
|
|
16
|
+
## Core Mandate
|
|
17
|
+
requirements-report・architecture-report・各種レビューレポートを統合し、実装可能なタスクに分解した plan-report を出力する。
|
|
18
|
+
|
|
19
|
+
## Key Scope
|
|
20
|
+
|
|
21
|
+
✅ 担当すること:
|
|
22
|
+
- タスク分解と優先度決定
|
|
23
|
+
- マイルストーン設定
|
|
24
|
+
- 並列実行可能なタスクグループの識別
|
|
25
|
+
- 各エージェントへの作業指示の明文化
|
|
26
|
+
- plan-report の出力・更新
|
|
27
|
+
|
|
28
|
+
❌ 担当しないこと:
|
|
29
|
+
- 設計判断(architect の担当)
|
|
30
|
+
- ソースコードの編集
|
|
31
|
+
- テスト・レビューの実施
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
**Before:**
|
|
36
|
+
- **必読: `.claude/rules/plan-design-guidelines.md`** を Read する(depends_on 設計・TDD 3-wave 分解・writes 衝突回避・自動検査ルール R2〜R6・出力直前の自己チェックリスト)
|
|
37
|
+
- 利用可能な全レポートを Read する(requirements / architecture / test / review)
|
|
38
|
+
- レポートが存在しないフェーズはスキップして正常とする
|
|
39
|
+
|
|
40
|
+
**During:**
|
|
41
|
+
- レビュー指摘がある場合は優先度を付けて反映する
|
|
42
|
+
- タスクは「1タスク = 1コミット」の粒度を意識して分解する
|
|
43
|
+
- `plan-design-guidelines.md` のルール 1〜13 と R2/R3/R4/R5/R6 を全て遵守する
|
|
44
|
+
|
|
45
|
+
**After:**
|
|
46
|
+
- Skill ツールで `report-timestamp` を呼び出してタイムスタンプを取得し、Write ツールで `.claude/reports/plan-report-{timestamp}.md` に出力する
|
|
47
|
+
- plan-report の**先頭に YAML フロントマターを必ず付与する**。最低限以下を出力すること:
|
|
48
|
+
- `po_plan_version: "0.1"`
|
|
49
|
+
- `name`(プランの表示名・文字列)
|
|
50
|
+
- `cwd: "../.."`(plan-report からプロジェクトルートへの相対パス)
|
|
51
|
+
- `tasks: [...]`(各タスクは `id` / `agent` / `read_only` / `prompt` を必須とする。書き込みあり = `read_only: false`、読み取り専用レビューのみ = `read_only: true`)
|
|
52
|
+
- `tasks[].id` は英数字・ハイフン・アンダースコアのみで一意にする。Markdown 本文の依存関係セクションと `tasks[].depends_on` を一致させる
|
|
53
|
+
- フロントマターは YAML パーサで再パース可能でなければならない(インデントずれ・タブ混入禁止)
|
|
54
|
+
- 出力前に `plan-design-guidelines.md` の「出力直前の自己チェックリスト」を必ず満たすこと
|
|
55
|
+
|
|
56
|
+
## Tools & Constraints
|
|
57
|
+
制限:
|
|
58
|
+
- ソースファイルの編集・書き込みは行わない
|
|
59
|
+
- plan-report の YAML フロントマター内で `tasks[].id` の重複・未定義の `depends_on` 参照・エージェント名の typo を出力しない(`c3 plan validate` で検証可能)
|
|
60
|
+
- `.claude/rules/plan-design-guidelines.md` のルール 1〜13 と自己チェックリストに違反した plan-report を出力しない
|
|
61
|
+
- 自動検査対象に違反する plan-report を出力しない:
|
|
62
|
+
- R2/R4/R6(配布対象): `.claude/hooks/planner_check.py` が PostToolUse で WARN を出す
|
|
63
|
+
- R3(C3 固有): `.dev/hooks/_planner_check.py` が PostToolUse で exit 2 ブロック
|
|
64
|
+
- R5(worktree 違反): `.claude/hooks/check_agent_invocation.py` が Agent ツール呼び出し時に exit 2 ブロック
|
|
65
|
+
|
|
66
|
+
## Related Agents
|
|
67
|
+
- 上流: architect(architecture-report を受け取る)
|
|
68
|
+
- 下流: developer・tester(plan-report を受け渡す)
|
|
69
|
+
- 再起動元: code-reviewer・security-reviewer(指摘反映後に再計画)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-setup
|
|
3
|
+
model: opus
|
|
4
|
+
description: プロジェクト初期設定担当。収集済みのスタック情報と規約情報を受け取り rules/ に配置する。
|
|
5
|
+
tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Glob
|
|
9
|
+
- WebSearch
|
|
10
|
+
- WebFetch
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Project Setup
|
|
14
|
+
|
|
15
|
+
## Core Mandate
|
|
16
|
+
親 Claude から渡されたスタック情報・規約情報をもとに、テンプレートを埋めて
|
|
17
|
+
`.claude/rules/coding-standards.md` と `.claude/rules/project-conventions.md` を生成する。
|
|
18
|
+
ユーザーとの対話は行わない。
|
|
19
|
+
|
|
20
|
+
## Key Scope
|
|
21
|
+
|
|
22
|
+
✅ 担当すること:
|
|
23
|
+
- 渡されたスタック情報をもとに標準規約を WebSearch / WebFetch で調査・収集
|
|
24
|
+
- `.claude/rules/coding-standards.md` の生成
|
|
25
|
+
- `.claude/rules/project-conventions.md` の生成
|
|
26
|
+
|
|
27
|
+
❌ 担当しないこと:
|
|
28
|
+
- ユーザーへの質問・ヒアリング(親 Claude が実施済み)
|
|
29
|
+
- 規約ファイル以外のソースファイルの編集
|
|
30
|
+
- プロジェクトの設計・アーキテクチャ判断
|
|
31
|
+
|
|
32
|
+
## Workflow
|
|
33
|
+
|
|
34
|
+
**Step 1: テンプレートと参照を Read する**
|
|
35
|
+
|
|
36
|
+
以下を Read してプレースホルダ構造と置換ルールを把握する:
|
|
37
|
+
- `.claude/skills/setup/templates/coding-standards-template.md`
|
|
38
|
+
- `.claude/skills/setup/templates/project-conventions-template.md`
|
|
39
|
+
- `.claude/skills/setup/reference.md`(言語→拡張子マッピング・公式スタイルガイド参照先)
|
|
40
|
+
|
|
41
|
+
**Step 2: 既存ファイルの確認**
|
|
42
|
+
|
|
43
|
+
Glob で `.claude/rules/coding-standards.md` と `.claude/rules/project-conventions.md` の存在を確認する。
|
|
44
|
+
存在する場合は Read して、上書きではなく更新として差分を反映する。
|
|
45
|
+
|
|
46
|
+
**Step 3: 標準規約の Web 検索**
|
|
47
|
+
|
|
48
|
+
プロンプトに含まれるスタック情報と `reference.md` の「公式スタイルガイド参照先」をもとに以下を調査する:
|
|
49
|
+
- 言語の公式スタイルガイド(PEP8、Google Style Guide、StandardJS 等)
|
|
50
|
+
- フレームワークのベストプラクティス(公式ドキュメント優先)
|
|
51
|
+
- セキュリティガイドライン(OWASP、CWE 等)
|
|
52
|
+
- テストフレームワークのベストプラクティス
|
|
53
|
+
|
|
54
|
+
**Step 4: 2 ファイルを生成**
|
|
55
|
+
|
|
56
|
+
テンプレートの `{プレースホルダ}` を以下のルールで置換し、Write ツールで出力する:
|
|
57
|
+
|
|
58
|
+
- `{LANG_PATHS}` ← `reference.md` の言語→glob マッピングを YAML リスト行に展開
|
|
59
|
+
- `{STACK_NAME}` / `{LANGUAGE}` / `{FRAMEWORK}` / `{RUNTIME}` / `{DATABASE}` ← 親プロンプトのスタック情報
|
|
60
|
+
- `{LAST_UPDATED}` ← 今日の日付(YYYY-MM-DD)
|
|
61
|
+
- `{STYLE_GUIDE_NOTES}` / `{NAMING_RULES}` / `{TEST_RULES}` / `{SECURITY_BASELINE}` ← Step 3 の Web 検索結果
|
|
62
|
+
- `{PROJECT_NAMING_RULES}` / `{COMMENT_POLICY}` / `{TEST_COVERAGE_GOAL}` / `{BRANCH_COMMIT_RULES}` / `{OTHER_RULES}` ← 親プロンプトのヒアリング結果
|
|
63
|
+
|
|
64
|
+
出力先:
|
|
65
|
+
- `.claude/rules/coding-standards.md`
|
|
66
|
+
- `.claude/rules/project-conventions.md`
|
|
67
|
+
|
|
68
|
+
**Step 5: 完了報告**
|
|
69
|
+
|
|
70
|
+
生成した 2 ファイルのパスと主要な規約の概要を出力する。
|
|
71
|
+
|
|
72
|
+
## Tools & Constraints
|
|
73
|
+
制限: 規約ファイル以外のソースファイルは編集しない
|
|
74
|
+
|
|
75
|
+
## Related Agents
|
|
76
|
+
- 起動元: 親 Claude(コマンドファイルが全情報を収集してからプロンプトに渡す)
|
|
77
|
+
- 下流参照: architect・developer・tester・code-reviewer・security-reviewer
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared utilities for .claude/hooks/ scripts (配布対象).
|
|
3
|
+
|
|
4
|
+
複数 hook で共有するヘルパー関数を集約する。各 hook はスタンドアロン実行されるため、
|
|
5
|
+
このファイルへのアクセスは `sys.path.insert(0, dirname(__file__))` で hooks/ を
|
|
6
|
+
PYTHONPATH に追加してから `from _hook_utils import ...` する経路を取る。
|
|
7
|
+
|
|
8
|
+
## Exports
|
|
9
|
+
|
|
10
|
+
- ``write_debug_log(log_path, line)`` — ``C3_HOOK_DEBUG=1`` のときのみログを追記する
|
|
11
|
+
fail-safe な書き込みヘルパー。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
DEBUG_ENV = "C3_HOOK_DEBUG"
|
|
21
|
+
|
|
22
|
+
# 制御文字を除去するための正規表現。debug ログは端末には直接表示されないが、
|
|
23
|
+
# ファイルが汚染されると後段で `cat` などで確認した際にエスケープが解釈される
|
|
24
|
+
# 可能性があるため除去する。
|
|
25
|
+
#
|
|
26
|
+
# 除去範囲:
|
|
27
|
+
# - C0 制御文字 (\x00-\x1f) — NUL/BEL/BS/HT/LF/VT/FF/CR/ESC など。ANSI エスケープ
|
|
28
|
+
# シーケンスの ESC (\x1b) もここに含まれる
|
|
29
|
+
# - DEL (\x7f) — 古い端末で破壊的削除制御に使われる
|
|
30
|
+
# - C1 制御文字 (\x80-\x9f) — Latin-1 拡張領域の制御文字。一部の端末・ロケールで
|
|
31
|
+
# エスケープシーケンスのプリフィクス(例: CSI = \x9b)として解釈される
|
|
32
|
+
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_debug_log(log_path: Path, line: str) -> None:
|
|
36
|
+
"""``C3_HOOK_DEBUG=1`` のとき、ログファイルに ``ISO8601 line`` を 1 行追記する。
|
|
37
|
+
|
|
38
|
+
- 環境変数未設定なら即 return(コスト 0)。
|
|
39
|
+
- ファイル作成・書き込みに失敗しても本体動作を止めない(``OSError`` を握りつぶす)。
|
|
40
|
+
- 各 hook 固有のフォーマットはこの関数の呼び出し側で組み立て、``line`` 引数として渡す。
|
|
41
|
+
- ``log_path`` は ``Path`` 前提。各 hook 側で ``__file__`` ベースの絶対パスに統一すること。
|
|
42
|
+
- ``line`` に含まれる C0/C1 制御文字(ANSI ESC を含む)と DEL は除去してから書き込む。
|
|
43
|
+
呼び出し側 hook の入力に制御文字が混入してもログファイルが汚染されないようにする。
|
|
44
|
+
"""
|
|
45
|
+
if os.environ.get(DEBUG_ENV) != "1":
|
|
46
|
+
return
|
|
47
|
+
try:
|
|
48
|
+
import datetime as _dt
|
|
49
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
timestamp = _dt.datetime.now().isoformat(timespec="seconds")
|
|
51
|
+
sanitized = _CONTROL_CHARS_RE.sub("", str(line))
|
|
52
|
+
with log_path.open("a", encoding="utf-8") as fh:
|
|
53
|
+
fh.write(f"{timestamp} {sanitized}\n")
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse Agent hook: read_only タスクの worktree 違反を BLOCK する。
|
|
3
|
+
|
|
4
|
+
## 検査ルール
|
|
5
|
+
|
|
6
|
+
- **R5**: subagent_type が `code-reviewer` / `security-reviewer` のとき
|
|
7
|
+
`isolation: "worktree"` は禁止。worktree 自動クリーンアップで
|
|
8
|
+
`.claude/reports/*.md`(gitignored)が消失するため。
|
|
9
|
+
|
|
10
|
+
## fail-safe 設計
|
|
11
|
+
|
|
12
|
+
- `tool_input` のキー名(`subagent_type` / `isolation`)は Claude Code 公式仕様に
|
|
13
|
+
ドキュメント化されていない(2026-05-21 時点)。キー名が想定と異なる場合は
|
|
14
|
+
検出できず exit 0(許可)にフォールバックする。誤検知で全 Agent 呼び出しを
|
|
15
|
+
ブロックすることはない。
|
|
16
|
+
- デバッグ用に `C3_HOOK_DEBUG=1` を設定すると `tool_input` を
|
|
17
|
+
`.claude/tmp/agent_hook_debug.log` に追記する(キー名検証用)。
|
|
18
|
+
|
|
19
|
+
## 出力経路
|
|
20
|
+
|
|
21
|
+
- BLOCK 時は **stderr** に `[CheckAgentInvocation BLOCK]` を出力し ``exit 2`` する。
|
|
22
|
+
PreToolUse の ``exit 2`` は Claude Code が動作をブロックする公式仕様。
|
|
23
|
+
PreToolUse ``exit 2`` 時に stdout JSON で LLM コンテキストに注入する公式仕様は
|
|
24
|
+
2026-05-21 時点で見当たらないため、本 hook は stderr のみで運用している
|
|
25
|
+
(PostToolUse とは設計が異なる点に注意)。
|
|
26
|
+
|
|
27
|
+
## 過去パターン根拠
|
|
28
|
+
|
|
29
|
+
- 2026-05-21 のフルワークフロー動作確認で発生したルール違反 1
|
|
30
|
+
(read_only: true タスクに isolation:worktree を指定し、reports が消失)への対策。
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# 共通ヘルパー (_hook_utils.write_debug_log) を hooks/ 経由で import するため、
|
|
39
|
+
# このスクリプトのディレクトリを PYTHONPATH に追加する。
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
+
from _hook_utils import write_debug_log # noqa: E402
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
45
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
46
|
+
except AttributeError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
REVIEWER_TYPES = frozenset({"code-reviewer", "security-reviewer"})
|
|
50
|
+
|
|
51
|
+
# プロジェクトルートと debug ログのパスはスクリプトファイルからの絶対パスに固定する。
|
|
52
|
+
# cwd 依存だと worktree コンテキストで `.claude/tmp/` が意図しない場所に作られる。
|
|
53
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
54
|
+
DEBUG_LOG_PATH = PROJECT_ROOT / ".claude" / "tmp" / "agent_hook_debug.log"
|
|
55
|
+
|
|
56
|
+
# DoS 防御の読み取り上限(hook はローカル前提だが多層防御として明示する)。
|
|
57
|
+
_STDIN_READ_LIMIT_BYTES = 1 * 1024 * 1024 # 1 MiB
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _escape_for_log(value: str) -> str:
|
|
61
|
+
"""改行を含む値をログ・stderr に安全に表示するための簡易エスケープ。"""
|
|
62
|
+
return value.replace("\r", "\\r").replace("\n", "\\n")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
try:
|
|
67
|
+
payload = json.loads(sys.stdin.read(_STDIN_READ_LIMIT_BYTES))
|
|
68
|
+
except (json.JSONDecodeError, ValueError):
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
if payload.get("tool_name") != "Agent":
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
|
|
74
|
+
tool_input = payload.get("tool_input")
|
|
75
|
+
if not isinstance(tool_input, dict):
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
|
|
78
|
+
subagent_type = tool_input.get("subagent_type", "")
|
|
79
|
+
isolation = tool_input.get("isolation", "")
|
|
80
|
+
if not isinstance(subagent_type, str):
|
|
81
|
+
subagent_type = ""
|
|
82
|
+
if not isinstance(isolation, str):
|
|
83
|
+
isolation = ""
|
|
84
|
+
|
|
85
|
+
if subagent_type not in REVIEWER_TYPES:
|
|
86
|
+
# 対象外(reviewer 系でない)は素通り。デバッグログも残さない(ノイズ削減)
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
isolation_display = _escape_for_log(isolation) if isolation else "none"
|
|
90
|
+
|
|
91
|
+
if isolation == "worktree":
|
|
92
|
+
write_debug_log(
|
|
93
|
+
DEBUG_LOG_PATH,
|
|
94
|
+
f"{subagent_type or 'unknown'} isolation={isolation_display} BLOCK R5",
|
|
95
|
+
)
|
|
96
|
+
print(
|
|
97
|
+
f"[CheckAgentInvocation BLOCK] R5: subagent_type={subagent_type!r} "
|
|
98
|
+
f'(read_only) タスクには isolation: "worktree" を指定できません。'
|
|
99
|
+
f" worktree 自動クリーンアップで .claude/reports/*.md が消失します。"
|
|
100
|
+
f" isolation を省略して main リポジトリで直接実行してください。",
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
103
|
+
sys.exit(2)
|
|
104
|
+
|
|
105
|
+
write_debug_log(
|
|
106
|
+
DEBUG_LOG_PATH,
|
|
107
|
+
f"{subagent_type or 'unknown'} isolation={isolation_display} PASS",
|
|
108
|
+
)
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: plan-report の YAML frontmatter 機械検査(配布対象)。
|
|
3
|
+
|
|
4
|
+
`.claude/reports/plan-report-*.md` への Write/Edit を検出し、汎用ルールに違反していれば
|
|
5
|
+
WARN を出す。出力は 2 経路:
|
|
6
|
+
|
|
7
|
+
- **stderr**: 人間(ターミナル表示)向け [PlannerCheck WARN] テキスト
|
|
8
|
+
- **stdout JSON**: `hookSpecificOutput.additionalContext` で LLM に system reminder として注入
|
|
9
|
+
|
|
10
|
+
Claude Code 公式仕様(https://code.claude.com/docs/en/hooks)に従い、PostToolUse の
|
|
11
|
+
exit 0 では stdout JSON のみが LLM コンテキストに伝達される。stderr は人間にしか
|
|
12
|
+
届かないため、両方出力して二重防衛とする。
|
|
13
|
+
|
|
14
|
+
## 検査ルール(配布対象)
|
|
15
|
+
|
|
16
|
+
- **R2**: `agent: code-reviewer` / `security-reviewer` の task の writes パスに
|
|
17
|
+
タイムスタンプ(YYYYMMDD 単独、または YYYYMMDD-HHMMSS)が含まれていれば WARN。
|
|
18
|
+
parallel-agents skill の成果物取り込みでタイムスタンプを動的に取得すると
|
|
19
|
+
writes と実ファイル名が乖離するため、task_id ベースの固定名にする。
|
|
20
|
+
- **R4**: 同一 writes パスを複数 task が宣言していて、後発 task の depends_on に
|
|
21
|
+
先発 task が含まれていない場合は WARN(並列実行で破壊的競合)。
|
|
22
|
+
- **R6**: plan-report のタスク総数 >= 3 かつ `agent: code-reviewer` /
|
|
23
|
+
`agent: security-reviewer` が 0 件の場合は WARN(レビュー全削除検出)。
|
|
24
|
+
2026-05-21 のルール違反 2 対策として追加。
|
|
25
|
+
|
|
26
|
+
## 開発元のみのルール
|
|
27
|
+
|
|
28
|
+
- **R3**(`src/c3/_template/` への writes 禁止)は C3 開発リポジトリ固有のため
|
|
29
|
+
`.dev/hooks/_planner_check.py` に分離して開発元でのみ動作。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# 共通ヘルパー (_hook_utils.write_debug_log) を hooks/ 経由で import するため、
|
|
39
|
+
# このスクリプトのディレクトリを PYTHONPATH に追加する。
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
+
from _hook_utils import write_debug_log # noqa: E402
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
45
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
46
|
+
except AttributeError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import yaml
|
|
51
|
+
except ImportError:
|
|
52
|
+
# PyYAML 未インストール環境では silent exit
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# プロジェクトルートと debug ログのパスはスクリプトファイルからの絶対パスに固定する。
|
|
57
|
+
# cwd 依存だと worktree コンテキストで `.claude/tmp/` が意図しない場所に作られる。
|
|
58
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
59
|
+
DEBUG_LOG_PATH = PROJECT_ROOT / ".claude" / "tmp" / "planner_check_debug.log"
|
|
60
|
+
|
|
61
|
+
# DoS 防御の読み取り上限(hook はローカル前提だが多層防御として明示する)。
|
|
62
|
+
_STDIN_READ_LIMIT_BYTES = 1 * 1024 * 1024 # 1 MiB
|
|
63
|
+
_FILE_READ_LIMIT_BYTES = 512 * 1024 # 512 KiB
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*(?:\n|\Z)", re.DOTALL)
|
|
67
|
+
# YYYYMMDD(8 桁)または YYYYMMDD-HHMMSS(8 桁 + ハイフン + 6 桁)。
|
|
68
|
+
# 前後が英数字なら task_id の一部とみなし誤検知を回避する。
|
|
69
|
+
# 注意: ハイフン区切りで孤立した 8 桁数字(例: report-12345678.md)は YYYYMMDD と
|
|
70
|
+
# 区別できないため意図的に WARN を発火させる(test_planner_check.py
|
|
71
|
+
# test_8digit_standalone_in_filename_triggers_warn 参照)。
|
|
72
|
+
# task_id に 8 桁数字のみを使う場合は英数字混在 ID に変更すること。
|
|
73
|
+
_TIMESTAMP_RE = re.compile(r"(?<![A-Za-z0-9])\d{8}(?:-\d{6})?(?![A-Za-z0-9])")
|
|
74
|
+
|
|
75
|
+
# R6 検出の閾値: タスク総数がこの値以上で reviewer 0 件なら WARN を出す
|
|
76
|
+
_R6_TASK_THRESHOLD = 3
|
|
77
|
+
|
|
78
|
+
_REVIEWER_AGENTS = frozenset({"code-reviewer", "security-reviewer"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _sanitize(s: str) -> str:
|
|
82
|
+
"""ターミナルインジェクション対策: 制御文字と JSON 互換性を壊す Unicode 行区切りを除去する。
|
|
83
|
+
|
|
84
|
+
除去対象:
|
|
85
|
+
- C0/C1 制御文字 (\\x00-\\x1f) と DEL (\\x7f) — ANSI エスケープ (ESC 0x1b) を含む
|
|
86
|
+
- U+2028 (Line Separator) / U+2029 (Paragraph Separator) — 一部の JS/JSON
|
|
87
|
+
パーサが行区切りとして扱い、ensure_ascii=False の JSON 解析を破壊する
|
|
88
|
+
"""
|
|
89
|
+
return re.sub("[\x00-\x1f\x7f\u2028\u2029]", "", str(s))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize(path: str) -> str:
|
|
93
|
+
"""Windows のバックスラッシュを `/` に統一する。"""
|
|
94
|
+
return path.replace("\\", "/")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_plan_report(file_path: str) -> bool:
|
|
98
|
+
"""basename が `plan-report-*.md` で、パスに `..` トラバーサルを含まない。
|
|
99
|
+
|
|
100
|
+
`..` セグメントを含むパス(``../../plan-report-foo.md`` 等)は後段の
|
|
101
|
+
``open(file_path)`` で任意ファイル読み取りの経路になりうるため拒否する。
|
|
102
|
+
"""
|
|
103
|
+
normalized = _normalize(file_path)
|
|
104
|
+
if ".." in normalized.split("/"):
|
|
105
|
+
return False
|
|
106
|
+
basename = os.path.basename(normalized)
|
|
107
|
+
return basename.startswith("plan-report-") and basename.endswith(".md")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_frontmatter(text: str) -> dict | None:
|
|
111
|
+
match = _FRONTMATTER_RE.match(text)
|
|
112
|
+
if not match:
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
parsed = yaml.safe_load(match.group(1))
|
|
116
|
+
except yaml.YAMLError:
|
|
117
|
+
return None
|
|
118
|
+
if not isinstance(parsed, dict):
|
|
119
|
+
return None
|
|
120
|
+
return parsed
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _check_r2_reviewer_timestamp(task: dict) -> list[str]:
|
|
124
|
+
"""reviewer の writes にタイムスタンプ入りファイル名があれば違反メッセージ。"""
|
|
125
|
+
if task.get("agent") not in _REVIEWER_AGENTS:
|
|
126
|
+
return []
|
|
127
|
+
writes = task.get("writes")
|
|
128
|
+
if not isinstance(writes, list):
|
|
129
|
+
return []
|
|
130
|
+
violations: list[str] = []
|
|
131
|
+
for w in writes:
|
|
132
|
+
if not isinstance(w, str):
|
|
133
|
+
continue
|
|
134
|
+
basename = os.path.basename(_normalize(w))
|
|
135
|
+
if _TIMESTAMP_RE.search(basename):
|
|
136
|
+
violations.append(
|
|
137
|
+
f"R2: task {task.get('id')!r} ({task.get('agent')}) の writes "
|
|
138
|
+
f"{w!r} にタイムスタンプ風パターンが含まれています。"
|
|
139
|
+
"task_id ベースのファイル名にしてください"
|
|
140
|
+
)
|
|
141
|
+
return violations
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_ancestor_map(by_id: dict[str, dict]) -> dict[str, set[str]]:
|
|
145
|
+
"""各 task の depends_on 推移閉包(祖先集合)を返す。"""
|
|
146
|
+
ancestors: dict[str, set[str]] = {tid: set() for tid in by_id}
|
|
147
|
+
for tid in by_id:
|
|
148
|
+
stack = list(by_id[tid].get("depends_on", []) or [])
|
|
149
|
+
while stack:
|
|
150
|
+
dep = stack.pop()
|
|
151
|
+
if dep in ancestors[tid]:
|
|
152
|
+
continue
|
|
153
|
+
ancestors[tid].add(dep)
|
|
154
|
+
dep_task = by_id.get(dep)
|
|
155
|
+
if dep_task:
|
|
156
|
+
stack.extend(dep_task.get("depends_on", []) or [])
|
|
157
|
+
return ancestors
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _all_pairs_transitively_ordered(
|
|
161
|
+
claim_ids: list[str], ancestors: dict[str, set[str]]
|
|
162
|
+
) -> bool:
|
|
163
|
+
"""claim_ids 内の全ペアが depends_on 推移閉包で順序付けされているか。"""
|
|
164
|
+
for i, a in enumerate(claim_ids):
|
|
165
|
+
for b in claim_ids[i + 1:]:
|
|
166
|
+
if a in ancestors.get(b, set()) or b in ancestors.get(a, set()):
|
|
167
|
+
continue
|
|
168
|
+
return False
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _check_r4_writes_conflicts(tasks: list) -> list[str]:
|
|
173
|
+
"""同一 writes パスを宣言する複数 task が depends_on で順序付けされていなければ警告。"""
|
|
174
|
+
by_id: dict[str, dict] = {}
|
|
175
|
+
for task in tasks:
|
|
176
|
+
if not isinstance(task, dict):
|
|
177
|
+
continue
|
|
178
|
+
tid = task.get("id")
|
|
179
|
+
if isinstance(tid, str) and tid:
|
|
180
|
+
by_id[tid] = task
|
|
181
|
+
|
|
182
|
+
claims: dict[str, list[str]] = {}
|
|
183
|
+
for tid, task in by_id.items():
|
|
184
|
+
writes = task.get("writes")
|
|
185
|
+
if not isinstance(writes, list):
|
|
186
|
+
continue
|
|
187
|
+
for w in writes:
|
|
188
|
+
if not isinstance(w, str):
|
|
189
|
+
continue
|
|
190
|
+
claims.setdefault(_normalize(w), []).append(tid)
|
|
191
|
+
|
|
192
|
+
ancestors = _build_ancestor_map(by_id)
|
|
193
|
+
|
|
194
|
+
violations: list[str] = []
|
|
195
|
+
for path, claim_ids in claims.items():
|
|
196
|
+
if len(claim_ids) < 2:
|
|
197
|
+
continue
|
|
198
|
+
if _all_pairs_transitively_ordered(claim_ids, ancestors):
|
|
199
|
+
continue
|
|
200
|
+
# claim_ids は task id の生 join のため、特殊文字を repr で逃がして安全に出力する
|
|
201
|
+
joined = ", ".join(repr(cid) for cid in sorted(claim_ids))
|
|
202
|
+
violations.append(
|
|
203
|
+
f"R4: writes {path!r} を複数 task ({joined}) "
|
|
204
|
+
"が宣言していますが depends_on で順序付けされていません。"
|
|
205
|
+
"並列実行で破壊的競合が起きる可能性があります"
|
|
206
|
+
)
|
|
207
|
+
return violations
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _check_r6_reviewer_absence(tasks: list) -> list[str]:
|
|
211
|
+
"""plan-report 内のレビュータスクが完全消失している場合の警告。
|
|
212
|
+
|
|
213
|
+
検出条件: タスク総数 >= _R6_TASK_THRESHOLD AND reviewer 系 agent が 0 件。
|
|
214
|
+
閾値が 3 なのは、小規模な単発タスク(ドキュメント修正など)の合理的な省略を巻き込まないため。
|
|
215
|
+
"""
|
|
216
|
+
if not isinstance(tasks, list) or len(tasks) < _R6_TASK_THRESHOLD:
|
|
217
|
+
return []
|
|
218
|
+
reviewer_tasks = [
|
|
219
|
+
t for t in tasks
|
|
220
|
+
if isinstance(t, dict) and t.get("agent") in _REVIEWER_AGENTS
|
|
221
|
+
]
|
|
222
|
+
if reviewer_tasks:
|
|
223
|
+
return []
|
|
224
|
+
return [
|
|
225
|
+
f"R6: plan-report のタスク総数 {len(tasks)} 件に対して "
|
|
226
|
+
"code-reviewer / security-reviewer タスクが 0 件です。"
|
|
227
|
+
"レビュータスクを意図的に省略する場合はユーザー承認を取ってください"
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main() -> None:
|
|
232
|
+
try:
|
|
233
|
+
payload = json.loads(sys.stdin.read(_STDIN_READ_LIMIT_BYTES))
|
|
234
|
+
except (json.JSONDecodeError, ValueError):
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
|
|
237
|
+
if payload.get("tool_name") not in ("Write", "Edit"):
|
|
238
|
+
sys.exit(0)
|
|
239
|
+
|
|
240
|
+
file_path = payload.get("tool_input", {}).get("file_path", "")
|
|
241
|
+
if not isinstance(file_path, str) or not file_path:
|
|
242
|
+
sys.exit(0)
|
|
243
|
+
|
|
244
|
+
if not _is_plan_report(file_path):
|
|
245
|
+
sys.exit(0)
|
|
246
|
+
|
|
247
|
+
basename = os.path.basename(_normalize(file_path))
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
with open(file_path, encoding="utf-8") as fh:
|
|
251
|
+
text = fh.read(_FILE_READ_LIMIT_BYTES)
|
|
252
|
+
except (OSError, UnicodeDecodeError):
|
|
253
|
+
sys.exit(0)
|
|
254
|
+
|
|
255
|
+
frontmatter = _extract_frontmatter(text)
|
|
256
|
+
if frontmatter is None:
|
|
257
|
+
sys.exit(0)
|
|
258
|
+
|
|
259
|
+
tasks = frontmatter.get("tasks")
|
|
260
|
+
if not isinstance(tasks, list):
|
|
261
|
+
sys.exit(0)
|
|
262
|
+
|
|
263
|
+
warnings: list[str] = []
|
|
264
|
+
for task in tasks:
|
|
265
|
+
if not isinstance(task, dict):
|
|
266
|
+
continue
|
|
267
|
+
warnings.extend(_check_r2_reviewer_timestamp(task))
|
|
268
|
+
|
|
269
|
+
warnings.extend(_check_r4_writes_conflicts(tasks))
|
|
270
|
+
warnings.extend(_check_r6_reviewer_absence(tasks))
|
|
271
|
+
|
|
272
|
+
if warnings:
|
|
273
|
+
# 検出されたルール名(R2/R4/R6)を抽出してデバッグログに記録
|
|
274
|
+
detected_rules = sorted({m.split(":")[0] for m in warnings if ":" in m})
|
|
275
|
+
write_debug_log(DEBUG_LOG_PATH, f"{basename} WARN {','.join(detected_rules)}")
|
|
276
|
+
# 経路1: stderr に人間向けメッセージ(ターミナル表示用)
|
|
277
|
+
print("[PlannerCheck WARN] plan-report の検査で違反を検出しました:",
|
|
278
|
+
file=sys.stderr)
|
|
279
|
+
sanitized_warnings = [_sanitize(msg) for msg in warnings]
|
|
280
|
+
for msg in sanitized_warnings:
|
|
281
|
+
print(f" - {msg}", file=sys.stderr)
|
|
282
|
+
|
|
283
|
+
# 経路2: stdout JSON で LLM コンテキストに system reminder として注入
|
|
284
|
+
# Claude Code が hookSpecificOutput.additionalContext を読み取り、
|
|
285
|
+
# LLM のコンテキストウィンドウに挿入する(公式仕様)
|
|
286
|
+
additional_context = (
|
|
287
|
+
"[PlannerCheck WARN] plan-report の検査で違反を検出しました:\n"
|
|
288
|
+
+ "\n".join(f" - {msg}" for msg in sanitized_warnings)
|
|
289
|
+
+ "\n\n"
|
|
290
|
+
+ "ルールの詳細は .claude/rules/plan-design-guidelines.md を参照。"
|
|
291
|
+
+ "意図的に許容する場合はユーザーに確認を取ること。"
|
|
292
|
+
)
|
|
293
|
+
output = {
|
|
294
|
+
"hookSpecificOutput": {
|
|
295
|
+
"hookEventName": "PostToolUse",
|
|
296
|
+
"additionalContext": additional_context,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
300
|
+
else:
|
|
301
|
+
write_debug_log(DEBUG_LOG_PATH, f"{basename} PASS")
|
|
302
|
+
|
|
303
|
+
sys.exit(0)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
main()
|