claude-dev-env 2.4.0 → 2.7.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.
Files changed (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -245,28 +245,23 @@ class TestResolveAgentSlug:
245
245
 
246
246
 
247
247
  class TestResolveAgentWorktreeRoot:
248
- def should_use_userprofile_scratch_on_windows(
248
+ def should_nest_under_the_repository_claude_worktrees_directory(
249
249
  self,
250
- monkeypatch: pytest.MonkeyPatch,
250
+ tmp_path: Path,
251
251
  ) -> None:
252
252
  module = load_create_fresh_branch_module()
253
- monkeypatch.setattr(module.sys, "platform", "win32")
254
- monkeypatch.setenv("USERPROFILE", r"C:\Users\example")
255
- worktree_root = module.resolve_agent_worktree_root("grok")
256
- assert worktree_root == (
257
- Path(r"C:\Users\example") / "AppData" / "Local" / "Temp" / "grok"
258
- )
253
+ worktree_root = module.resolve_agent_worktree_root(tmp_path, "grok")
254
+ assert worktree_root == tmp_path / ".claude" / "worktrees" / "grok"
259
255
 
260
- def should_fall_back_to_gettempdir_off_windows(
256
+ def should_keep_each_agent_in_its_own_subdirectory(
261
257
  self,
262
- monkeypatch: pytest.MonkeyPatch,
263
258
  tmp_path: Path,
264
259
  ) -> None:
265
260
  module = load_create_fresh_branch_module()
266
- monkeypatch.setattr(module.sys, "platform", "linux")
267
- monkeypatch.setattr(module.tempfile, "gettempdir", lambda: str(tmp_path))
268
- worktree_root = module.resolve_agent_worktree_root("claude")
269
- assert worktree_root == tmp_path / "claude"
261
+ grok_root = module.resolve_agent_worktree_root(tmp_path, "grok")
262
+ claude_root = module.resolve_agent_worktree_root(tmp_path, "claude")
263
+ assert grok_root != claude_root
264
+ assert grok_root.parent == claude_root.parent
270
265
 
271
266
 
272
267
  class TestResolveUniqueWorktreePath:
@@ -301,18 +296,9 @@ class TestAgentSlugPathSafety:
301
296
  def should_reject_parent_segment_agent_slug_without_mkdir_outside(
302
297
  self,
303
298
  tmp_path: Path,
304
- monkeypatch: pytest.MonkeyPatch,
305
299
  ) -> None:
306
300
  module = load_create_fresh_branch_module()
307
- agent_scratch_parent = tmp_path / "agent-scratch"
308
- agent_scratch_parent.mkdir()
309
- escaped_agent_root = (agent_scratch_parent / ".." / "escape-agent").resolve()
310
- monkeypatch.setattr(module.sys, "platform", "linux")
311
- monkeypatch.setattr(
312
- module.tempfile,
313
- "gettempdir",
314
- lambda: str(agent_scratch_parent),
315
- )
301
+ escaped_agent_root = (tmp_path / "escape-agent").resolve()
316
302
  with pytest.raises(ValueError, match="agent slug"):
317
303
  module.create_fresh_branch(
318
304
  branch_name="fix/safe-branch",
@@ -321,22 +307,13 @@ class TestAgentSlugPathSafety:
321
307
  base_ref=DEFAULT_BASE_REF,
322
308
  )
323
309
  assert not escaped_agent_root.exists()
324
- assert not (agent_scratch_parent / "claude").exists()
310
+ assert not (tmp_path / "unused-repo").exists()
325
311
 
326
312
  def should_reject_empty_agent_slug(
327
313
  self,
328
314
  tmp_path: Path,
329
- monkeypatch: pytest.MonkeyPatch,
330
315
  ) -> None:
331
316
  module = load_create_fresh_branch_module()
332
- agent_scratch_parent = tmp_path / "agent-scratch"
333
- agent_scratch_parent.mkdir()
334
- monkeypatch.setattr(module.sys, "platform", "linux")
335
- monkeypatch.setattr(
336
- module.tempfile,
337
- "gettempdir",
338
- lambda: str(agent_scratch_parent),
339
- )
340
317
  with pytest.raises(ValueError, match="agent slug"):
341
318
  module.create_fresh_branch(
342
319
  branch_name="fix/safe-branch",
@@ -344,25 +321,16 @@ class TestAgentSlugPathSafety:
344
321
  agent_slug="",
345
322
  base_ref=DEFAULT_BASE_REF,
346
323
  )
347
- assert not any(agent_scratch_parent.iterdir())
324
+ assert not (tmp_path / "unused-repo").exists()
348
325
 
349
326
 
350
327
  class TestBranchNamePathSafety:
351
328
  def should_reject_parent_segments_without_mkdir_outside(
352
329
  self,
353
330
  tmp_path: Path,
354
- monkeypatch: pytest.MonkeyPatch,
355
331
  ) -> None:
356
332
  module = load_create_fresh_branch_module()
357
- agent_scratch_parent = tmp_path / "agent-scratch"
358
- agent_scratch_parent.mkdir()
359
- monkeypatch.setattr(module.sys, "platform", "linux")
360
- monkeypatch.setattr(
361
- module.tempfile,
362
- "gettempdir",
363
- lambda: str(agent_scratch_parent),
364
- )
365
- escaped_path = agent_scratch_parent / "escape"
333
+ escaped_path = tmp_path / "escape"
366
334
  with pytest.raises(ValueError, match="relative path"):
367
335
  module.create_fresh_branch(
368
336
  branch_name="fix/../../escape",
@@ -371,23 +339,14 @@ class TestBranchNamePathSafety:
371
339
  base_ref=DEFAULT_BASE_REF,
372
340
  )
373
341
  assert not escaped_path.exists()
374
- assert not (agent_scratch_parent / "claude").exists()
342
+ assert not (tmp_path / "unused-repo").exists()
375
343
 
376
344
  def should_reject_absolute_branch_without_mkdir_outside(
377
345
  self,
378
346
  tmp_path: Path,
379
- monkeypatch: pytest.MonkeyPatch,
380
347
  ) -> None:
381
348
  module = load_create_fresh_branch_module()
382
- agent_scratch_parent = tmp_path / "agent-scratch"
383
- agent_scratch_parent.mkdir()
384
349
  outside_target = tmp_path / "outside-evil"
385
- monkeypatch.setattr(module.sys, "platform", "linux")
386
- monkeypatch.setattr(
387
- module.tempfile,
388
- "gettempdir",
389
- lambda: str(agent_scratch_parent),
390
- )
391
350
  with pytest.raises(ValueError, match="relative path"):
392
351
  module.create_fresh_branch(
393
352
  branch_name=str(outside_target),
@@ -396,7 +355,7 @@ class TestBranchNamePathSafety:
396
355
  base_ref=DEFAULT_BASE_REF,
397
356
  )
398
357
  assert not outside_target.exists()
399
- assert not (agent_scratch_parent / "claude").exists()
358
+ assert not (tmp_path / "unused-repo").exists()
400
359
 
401
360
 
402
361
  class TestCreateFreshBranchIntegration:
@@ -409,13 +368,6 @@ class TestCreateFreshBranchIntegration:
409
368
  repository_path = build_repo_with_origin(tmp_path / "repo")
410
369
  caller_head_before = read_head_branch(repository_path)
411
370
  caller_commit_before = read_head_commit(repository_path)
412
- agent_scratch_parent = tmp_path / "agent-scratch"
413
- monkeypatch.setattr(module.sys, "platform", "linux")
414
- monkeypatch.setattr(
415
- module.tempfile,
416
- "gettempdir",
417
- lambda: str(agent_scratch_parent),
418
- )
419
371
  success_payload = module.create_fresh_branch(
420
372
  branch_name="fix/example-one",
421
373
  repo_path=repository_path,
@@ -424,7 +376,14 @@ class TestCreateFreshBranchIntegration:
424
376
  )
425
377
  worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
426
378
  assert worktree_path.is_dir()
427
- assert worktree_path == agent_scratch_parent / "grok" / "fix" / "example-one"
379
+ assert worktree_path == (
380
+ repository_path.resolve()
381
+ / ".claude"
382
+ / "worktrees"
383
+ / "grok"
384
+ / "fix"
385
+ / "example-one"
386
+ )
428
387
  assert read_head_branch(worktree_path) == "fix/example-one"
429
388
  assert read_head_commit(worktree_path) == caller_commit_before
430
389
  assert success_payload[PAYLOAD_KEY_BRANCH] == "fix/example-one"
@@ -445,14 +404,10 @@ class TestCreateFreshBranchIntegration:
445
404
  ) -> None:
446
405
  module = load_create_fresh_branch_module()
447
406
  repository_path = build_repo_with_origin(tmp_path / "repo")
448
- agent_scratch_parent = tmp_path / "agent-scratch"
449
- monkeypatch.setattr(module.sys, "platform", "linux")
450
- monkeypatch.setattr(
451
- module.tempfile,
452
- "gettempdir",
453
- lambda: str(agent_scratch_parent),
407
+ repository_worktree_root = (
408
+ repository_path.resolve() / ".claude" / "worktrees" / "claude"
454
409
  )
455
- occupied_path = agent_scratch_parent / "claude" / "fix" / "collision"
410
+ occupied_path = repository_worktree_root / "fix" / "collision"
456
411
  occupied_path.mkdir(parents=True)
457
412
  success_payload = module.create_fresh_branch(
458
413
  branch_name="fix/collision",
@@ -461,7 +416,7 @@ class TestCreateFreshBranchIntegration:
461
416
  base_ref=DEFAULT_BASE_REF,
462
417
  )
463
418
  worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
464
- assert worktree_path == agent_scratch_parent / "claude" / "fix" / "collision-2"
419
+ assert worktree_path == repository_worktree_root / "fix" / "collision-2"
465
420
  assert worktree_path.is_dir()
466
421
  assert read_head_branch(worktree_path) == "fix/collision"
467
422
 
@@ -472,13 +427,6 @@ class TestCreateFreshBranchIntegration:
472
427
  ) -> None:
473
428
  module = load_create_fresh_branch_module()
474
429
  repository_path = build_repo_with_origin(tmp_path / "repo")
475
- agent_scratch_parent = tmp_path / "agent-scratch"
476
- monkeypatch.setattr(module.sys, "platform", "linux")
477
- monkeypatch.setattr(
478
- module.tempfile,
479
- "gettempdir",
480
- lambda: str(agent_scratch_parent),
481
- )
482
430
  module.create_fresh_branch(
483
431
  branch_name="fix/dup",
484
432
  repo_path=repository_path,
@@ -515,13 +463,6 @@ class TestCreateFreshBranchIntegration:
515
463
  )
516
464
  local_main_commit = read_head_commit(repository_path)
517
465
  assert local_main_commit != origin_tip_before
518
- agent_scratch_parent = tmp_path / "agent-scratch"
519
- monkeypatch.setattr(module.sys, "platform", "linux")
520
- monkeypatch.setattr(
521
- module.tempfile,
522
- "gettempdir",
523
- lambda: str(agent_scratch_parent),
524
- )
525
466
  success_payload = module.create_fresh_branch(
526
467
  branch_name="fix/bare-base",
527
468
  repo_path=repository_path,
@@ -543,13 +484,6 @@ class TestWorktreeBranchTracking:
543
484
  ) -> None:
544
485
  module = load_create_fresh_branch_module()
545
486
  repository_path = build_repo_with_origin(tmp_path / "repo")
546
- agent_scratch_parent = tmp_path / "agent-scratch"
547
- monkeypatch.setattr(module.sys, "platform", "linux")
548
- monkeypatch.setattr(
549
- module.tempfile,
550
- "gettempdir",
551
- lambda: str(agent_scratch_parent),
552
- )
553
487
  success_payload = module.create_fresh_branch(
554
488
  branch_name="fix/no-upstream",
555
489
  repo_path=repository_path,
@@ -574,13 +508,6 @@ class TestWorktreeBranchTracking:
574
508
  repository_path,
575
509
  empty_hooks_path=empty_hooks_path,
576
510
  )
577
- agent_scratch_parent = tmp_path / "agent-scratch"
578
- monkeypatch.setattr(module.sys, "platform", "linux")
579
- monkeypatch.setattr(
580
- module.tempfile,
581
- "gettempdir",
582
- lambda: str(agent_scratch_parent),
583
- )
584
511
  success_payload = module.create_fresh_branch(
585
512
  branch_name="feature/silent-push",
586
513
  repo_path=repository_path,
@@ -619,13 +546,6 @@ class TestMainCli:
619
546
  ) -> None:
620
547
  module = load_create_fresh_branch_module()
621
548
  repository_path = build_repo_with_origin(tmp_path / "repo")
622
- agent_scratch_parent = tmp_path / "agent-scratch"
623
- monkeypatch.setattr(module.sys, "platform", "linux")
624
- monkeypatch.setattr(
625
- module.tempfile,
626
- "gettempdir",
627
- lambda: str(agent_scratch_parent),
628
- )
629
549
  monkeypatch.setattr(
630
550
  sys,
631
551
  "argv",
@@ -697,15 +617,7 @@ class TestMainCli:
697
617
  ) -> None:
698
618
  module = load_create_fresh_branch_module()
699
619
  repository_path = build_repo_with_origin(tmp_path / "repo")
700
- agent_scratch_parent = tmp_path / "agent-scratch"
701
- agent_scratch_parent.mkdir()
702
- escaped_path = agent_scratch_parent / "escape"
703
- monkeypatch.setattr(module.sys, "platform", "linux")
704
- monkeypatch.setattr(
705
- module.tempfile,
706
- "gettempdir",
707
- lambda: str(agent_scratch_parent),
708
- )
620
+ escaped_path = tmp_path / "escape"
709
621
  monkeypatch.setattr(
710
622
  sys,
711
623
  "argv",
@@ -726,7 +638,7 @@ class TestMainCli:
726
638
  assert PAYLOAD_KEY_ERROR in error_payload
727
639
  assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
728
640
  assert not escaped_path.exists()
729
- assert not (agent_scratch_parent / "claude").exists()
641
+ assert not (tmp_path / "unused-repo").exists()
730
642
 
731
643
  def should_print_error_json_for_absolute_branch_name(
732
644
  self,
@@ -736,15 +648,7 @@ class TestMainCli:
736
648
  ) -> None:
737
649
  module = load_create_fresh_branch_module()
738
650
  repository_path = build_repo_with_origin(tmp_path / "repo")
739
- agent_scratch_parent = tmp_path / "agent-scratch"
740
- agent_scratch_parent.mkdir()
741
651
  outside_target = tmp_path / "outside-evil"
742
- monkeypatch.setattr(module.sys, "platform", "linux")
743
- monkeypatch.setattr(
744
- module.tempfile,
745
- "gettempdir",
746
- lambda: str(agent_scratch_parent),
747
- )
748
652
  monkeypatch.setattr(
749
653
  sys,
750
654
  "argv",
@@ -765,4 +669,4 @@ class TestMainCli:
765
669
  assert PAYLOAD_KEY_ERROR in error_payload
766
670
  assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
767
671
  assert not outside_target.exists()
768
- assert not (agent_scratch_parent / "claude").exists()
672
+ assert not (tmp_path / "unused-repo").exists()
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: issue-tracker
3
+ description: >-
4
+ File, update, and close GitHub work as one epic with native sub-issues.
5
+ Dedup open and closed issues first. Edit status only inside marker sections.
6
+ Refresh the epic checklist so it matches its children. Triggers: issue tracker,
7
+ file an issue, track this issue, open an epic, update the epic, close the issue,
8
+ refresh the epic checklist, attach a sub-issue.
9
+ argument-hint: "[issue action - file | update | close | refresh-epic | full handoff]"
10
+ ---
11
+
12
+ # Issue tracker
13
+
14
+ **User wants an issue action done. Here is how those actions work.**
15
+
16
+ **dedup -> do the work -> markers only -> numbers + URLs**
17
+
18
+ Run every step the ask needs in one go (file, label, attach, refresh, and so on). The `issue-tracker` agent is the primary handler for one action per turn; this skill is the session path when the agent is unavailable or the ask spans several steps.
19
+
20
+ Prefer the **same warm** `issue-tracker` agent for follow-ups on the same issue or a related issue on the same epic. Spawn a new agent only for an unrelated work-stream or when the warm agent is gone.
21
+
22
+ ## Voice
23
+
24
+ Use the `plain-brief` output style (`output-styles/plain-brief.md`). End with the issue number(s) and URL(s) the caller must keep.
25
+
26
+ ## Gotchas
27
+
28
+ Append a bullet when an action fails in a new way.
29
+
30
+ - Use REST `.id`, not display `#N`, to attach a sub-issue - [operation-matrix](reference/operation-matrix.md).
31
+ - `gh` attach: `-F sub_issue_id=<n>` (typed int), never `-f`.
32
+ - Status goes in markers, not comments - comments are cross-links only.
33
+ - Dedup open **and** closed; closed twin -> reopen vs file new, never silent refile.
34
+ - Edit only between markers - free-form epic edits break the children checklist.
35
+ - Auto-close is PR-body-first: a related-only UI link does not close the issue on merge.
36
+
37
+ ## The model
38
+
39
+ One **epic** issue owns a work-stream. Each unit of work is a native GitHub **sub-issue** under it. The epic checklist and every status block live inside marker pairs. Skeletons and refresh steps: [reference/epic-and-sub-issue-model.md](reference/epic-and-sub-issue-model.md).
40
+
41
+ ## Labels
42
+
43
+ - Parent: `epic`.
44
+ - Sub-issue: `type: roadblock`, `type: task`, `type: bug`, or `type: enhancement`.
45
+
46
+ Create a missing label, then apply it. Label create is in the operation matrix.
47
+
48
+ ## Markers
49
+
50
+ - Status (any issue): `<!-- issue-tracker:status -->` … `<!-- /issue-tracker:status -->`
51
+ - Children checklist (epic only): `<!-- issue-tracker:children -->` … `<!-- /issue-tracker:children -->`
52
+
53
+ Update path: read body -> replace only the text between the target pair -> write the full body back.
54
+
55
+ ## Dedup
56
+
57
+ Search open and closed issues on the target repo before create. Open twin -> update in place. Closed twin -> ask reopen vs file new.
58
+
59
+ ## Tools
60
+
61
+ Prefer GitHub MCP. Fall back to `gh` on the same REST endpoints. Full map and `.id` rule: [reference/operation-matrix.md](reference/operation-matrix.md).
62
+
63
+ ## Handoff input
64
+
65
+ Optional issue-candidate JSON from orchestrator or closeout. Full schema and consume path: [reference/handoff-schema.md](reference/handoff-schema.md).
66
+
67
+ ## Close the sub-issue through the PR (required when a fix ships)
68
+
69
+ GitHub closes the sub-issue when someone **merges** a pull request into the **default branch** and that PR carries a closing keyword for the issue.
70
+
71
+ **Required on the PR that merges into the default branch:** put `Closes #N` in the PR body for each finished sub-issue. One keyword per issue: write `Closes #12` and `Closes #13`, not `Closes #12, #13` (only the first number closes in the comma form).
72
+
73
+ **Preferred on commits:** also put `Closes #N` in the first commit message when the fix starts. Commit alone is backup; the PR body is the contract.
74
+
75
+ **Stacked / intermediate PRs** that do not merge into the default branch: use a plain `#N` reference in the body so the issue links without a false close promise. Put `Closes #N` only on the PR that actually lands on the default branch.
76
+
77
+ **Not enough:** a Development "related" link with no closing keyword. **Not enough:** closing the PR without merging.
78
+
79
+ **Who owns `#N`:** the agent or session writing the PR must use the correct sub-issue number. Nothing in this package verifies the number matches the fix.
80
+
81
+ ## Return shape
82
+
83
+ Reply with every affected issue number and URL - no narration after that payload.
84
+
85
+ ## File index
86
+
87
+ | File | Purpose |
88
+ |------|---------|
89
+ | `SKILL.md` | Hub: how issue actions work, gotchas, model, markers, dedup, PR auto-close, return shape |
90
+ | `reference/operation-matrix.md` | MCP + `gh` per action; sub-issue `.id` rule |
91
+ | `reference/epic-and-sub-issue-model.md` | Epic model, markers, body skeleton, checklist refresh |
92
+ | `reference/handoff-schema.md` | Issue-candidate JSON and consume flow |
@@ -0,0 +1,55 @@
1
+ # Epic and sub-issue model
2
+
3
+ One **epic** issue holds a whole work-stream. Each unit of work under it is a native GitHub **sub-issue**. The epic carries the `epic` label; each sub-issue carries a `type:` label. The epic body mirrors its children as a checklist, and every issue — epic and sub-issue alike — carries a status section. Both sections live between marker comments so a body edit can replace one section and leave the rest untouched.
4
+
5
+ ## The two marker pairs
6
+
7
+ - `<!-- issue-tracker:status -->` … `<!-- /issue-tracker:status -->` — on every issue. Holds the current status line.
8
+ - `<!-- issue-tracker:children -->` … `<!-- /issue-tracker:children -->` — on the epic only. Holds the checklist of sub-issues.
9
+
10
+ An edit reads the whole body, swaps only the text between one marker pair, and writes the whole body back. Text outside the markers stays byte-for-byte the same.
11
+
12
+ ## Epic body skeleton
13
+
14
+ ```markdown
15
+ ## Epic — <work-stream label>
16
+
17
+ <One sentence naming the work-stream and its goal.>
18
+
19
+ <!-- issue-tracker:status -->
20
+ Status: in progress — 1 of 3 children closed.
21
+ <!-- /issue-tracker:status -->
22
+
23
+ <!-- issue-tracker:children -->
24
+ - [x] owner/repo#12 — parser rejects an empty path
25
+ - [ ] owner/repo#13 — attach reads the display number
26
+ - [ ] owner/repo#14 — label create skips an existing label
27
+ <!-- /issue-tracker:children -->
28
+ ```
29
+
30
+ ## Sub-issue body skeleton
31
+
32
+ ```markdown
33
+ ## <sub-issue title>
34
+
35
+ <One sentence: what the work is.>
36
+
37
+ <!-- issue-tracker:status -->
38
+ Status: open.
39
+ <!-- /issue-tracker:status -->
40
+
41
+ ## Detail
42
+
43
+ <The evidence, where, impact, and proposed fix — self-contained.>
44
+ ```
45
+
46
+ ## Refresh the epic checklist
47
+
48
+ After you create a sub-issue or close one, refresh the epic's `children` section so it matches the children:
49
+
50
+ 1. Read the epic body.
51
+ 2. Add a `- [ ] owner/repo#<N> — <title>` line for a newly created sub-issue, or flip a line to `- [x]` for a closed one.
52
+ 3. Update the epic's status line to name the closed count against the total.
53
+ 4. Write the whole body back with the issue-update op, replacing only the two marker sections you touched.
54
+
55
+ A created or closed sub-issue always triggers this refresh, so the epic checklist stays in step with the sub-issues at every step.
@@ -0,0 +1,64 @@
1
+ # Handoff schema
2
+
3
+ An orchestrator or the closeout skill hands the tracker one **issue-candidate** JSON record per obstacle. The tracker consumes each record through the full path and returns the issue number and URL.
4
+
5
+ ## The record
6
+
7
+ ```json
8
+ {
9
+ "kind": "roadblock | task | bug | enhancement",
10
+ "title": "<short imperative title>",
11
+ "epic": "<work-stream label the sub-issue belongs under>",
12
+ "summary": "<one sentence a reader with zero session context acts on>",
13
+ "evidence": "<verbatim line captured this session — error text, command, or log line>",
14
+ "where": "<file, hook, gate, or tool the evidence names — path relative to the repo root>",
15
+ "impact": "<what the obstacle cost: work blocked, count of times hit, workaround forced>",
16
+ "proposed_fix": "<the specific change — name the failure mode and the condition>",
17
+ "blocking": true
18
+ }
19
+ ```
20
+
21
+ ## Field meanings
22
+
23
+ | Field | Meaning |
24
+ |-------|---------|
25
+ | `kind` | The sub-issue's `type:` label stem. `roadblock`, `task`, `bug`, or `enhancement`. |
26
+ | `title` | The sub-issue title. Short and imperative. |
27
+ | `epic` | The work-stream the sub-issue belongs under. The tracker finds or creates the matching epic. |
28
+ | `summary` | The one-sentence body opener. Self-contained — a reader with no session context acts on it. |
29
+ | `evidence` | A verbatim line captured this session, placed in a fenced block in the body. |
30
+ | `where` | The file, hook, gate, or tool the evidence names. A repo-relative path, no volatile temp path. |
31
+ | `impact` | What the obstacle cost. |
32
+ | `proposed_fix` | The specific change. |
33
+ | `blocking` | `true` routes the sub-issue to `type: roadblock`, overriding `kind`. `false` keeps the `kind` label. |
34
+
35
+ `kind` maps to the `type:` label. `blocking: true` takes precedence and routes the sub-issue to `type: roadblock`.
36
+
37
+ ## Filled example
38
+
39
+ ```json
40
+ {
41
+ "kind": "bug",
42
+ "title": "Inline-collection gate fires in exempt test files",
43
+ "epic": "hook exemptions for test files",
44
+ "summary": "The code_rules_enforcer inline-collection check blocks a valid list literal in a test file, where test files are exempt from the magic-value gate.",
45
+ "evidence": "BLOCKED: [MAGIC_VALUE] Inline list literal [200, 404, 500] in a function body -- extract to a named constant in config/.",
46
+ "where": "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py",
47
+ "impact": "Hit 3 times in one session on three test files; each forced a workaround move to a module constant.",
48
+ "proposed_fix": "Extend the magic-value test-file exemption to cover the inline-collection check, so list and set literals in test bodies pass.",
49
+ "blocking": false
50
+ }
51
+ ```
52
+
53
+ ## Consume flow
54
+
55
+ For each record, in order:
56
+
57
+ 1. **Dedup** — search open and closed issues on the target repository for a twin.
58
+ 2. **Find or create the epic** — match the `epic` label; open a parent with the `epic` label when none matches.
59
+ 3. **Create the sub-issue** — body from `summary`, `evidence`, `where`, `impact`, `proposed_fix`.
60
+ 4. **Labels** — apply the `type:` label from `kind`, or `type: roadblock` when `blocking` is `true`. Create the label when the repository lacks it.
61
+ 5. **Attach the native sub-issue** — read the child's REST database `.id` and attach it under the epic.
62
+ 6. **Refresh the epic checklist** — add the child's `- [ ] owner/repo#<N>` line and update the epic status.
63
+
64
+ The return is the sub-issue number and URL (and the epic number and URL when the epic was created this call).
@@ -0,0 +1,41 @@
1
+ # Operation matrix
2
+
3
+ Each issue op maps to a GitHub MCP tool and a `gh` fallback that reaches the same REST endpoint. Prefer the MCP tool; fall back to `gh` when the MCP server is unreachable. The REST endpoint column names the authoritative contract both surfaces call, so a session with neither surface can still drive the raw API.
4
+
5
+ The MCP tool names below match the connected GitHub MCP server's issue toolset. When a server exposes a tool under a different name, pick the tool by what the row describes, and the REST endpoint tells you what it must call.
6
+
7
+ | Operation | GitHub MCP tool | REST endpoint | `gh` fallback |
8
+ |-----------|-----------------|---------------|---------------|
9
+ | Search open + closed | `mcp__github__search_issues` | `GET /search/issues?q=<terms>+repo:{owner}/{repo}` | `gh issue list --search "<terms>" --state all` |
10
+ | Create epic / sub-issue | `mcp__github__issue_write` (create) | `POST /repos/{owner}/{repo}/issues` | `gh issue create --title "<t>" --body-file <path> --label <label>` |
11
+ | Read issue body | `mcp__github__issue_read` | `GET /repos/{owner}/{repo}/issues/{number}` | `gh issue view <number> --json body` |
12
+ | Update body / labels | `mcp__github__issue_write` (update) | `PATCH /repos/{owner}/{repo}/issues/{number}` | `gh issue edit <number> --body-file <path> --add-label <label>` |
13
+ | Attach native sub-issue | `mcp__github__sub_issue_write` | `POST /repos/{owner}/{repo}/issues/{number}/sub_issues` | `gh api repos/{owner}/{repo}/issues/{number}/sub_issues -F sub_issue_id=<int>` |
14
+ | Add cross-reference comment | `mcp__github__add_issue_comment` | `POST /repos/{owner}/{repo}/issues/{number}/comments` | `gh issue comment <number> --body-file <path>` |
15
+ | Create label | `mcp__github__` label-create tool | `POST /repos/{owner}/{repo}/labels` | `gh label create <name> --color <hex> --description "<text>"` |
16
+
17
+ ## The sub-issue `.id` rule (read this before every attach)
18
+
19
+ The sub-issues endpoint identifies the child by its REST **database `.id`** — a large integer such as `2138472019` — not by its display number (`#42`). The two are different values. Passing the display number attaches nothing.
20
+
21
+ Read the child's `.id` from the create op's response, or fetch it:
22
+
23
+ ```
24
+ gh api repos/{owner}/{repo}/issues/{number} --jq .id
25
+ ```
26
+
27
+ `gh issue view <number> --json id` returns the GraphQL node id, a base64 string — the wrong value for this endpoint. Use `gh api ... --jq .id` for the REST database id.
28
+
29
+ The gh attach uses `-F` (typed field), which sends the id as a JSON integer. `-f` sends a string, and the endpoint rejects a string sub-issue id.
30
+
31
+ ```
32
+ gh api repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=2138472019
33
+ ```
34
+
35
+ ## Body content through `--body-file`
36
+
37
+ Every `gh issue create`, `gh issue edit`, and `gh issue comment` passes body content with `--body-file <path>`, never `--body "<text>"`. A `--body` string mangles backticks on GitHub — they land as literal `\``. Write the body to a temp file and point `--body-file` at it.
38
+
39
+ ## Paginated reads
40
+
41
+ `gh issue list` needs no pagination flags. For a `gh api` read of a paginated list endpoint (an issue's comments, a repository's issues), pass `--paginate --slurp` and pipe to external `jq` — `gh`'s built-in `--jq` runs per page and gives wrong cross-page results.