project-init 0.3.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.
Files changed (293) hide show
  1. project_init-0.3.0/.claude/hooks/dag_workflow.py +610 -0
  2. project_init-0.3.0/.claude/hooks/github_command_guard.sh +11 -0
  3. project_init-0.3.0/.claude/hooks/workflow_state_reminder.sh +72 -0
  4. project_init-0.3.0/.claude/scripts/create_nojira_pr.sh +3 -0
  5. project_init-0.3.0/.claude/scripts/finish_pr.sh +3 -0
  6. project_init-0.3.0/.claude/scripts/monitor_pr.sh +249 -0
  7. project_init-0.3.0/.claude/scripts/promote_review.sh +3 -0
  8. project_init-0.3.0/.claude/scripts/push_branch.sh +5 -0
  9. project_init-0.3.0/.claude/scripts/push_wiki.sh +54 -0
  10. project_init-0.3.0/.claude/settings.json +28 -0
  11. project_init-0.3.0/.claude/skills/INDEX.md +29 -0
  12. project_init-0.3.0/.claude/skills/README.md +14 -0
  13. project_init-0.3.0/.claude/skills/add_command/SKILL.md +59 -0
  14. project_init-0.3.0/.claude/skills/add_hook/SKILL.md +108 -0
  15. project_init-0.3.0/.claude/skills/github_workflow/SKILL.md +90 -0
  16. project_init-0.3.0/.claude/skills/session_summary/SKILL.md +35 -0
  17. project_init-0.3.0/.claude/skills/start_task/SKILL.md +58 -0
  18. project_init-0.3.0/.claude/skills/wiki/SKILL.md +84 -0
  19. project_init-0.3.0/.claude/skills/wiki/templates/architecture.md +27 -0
  20. project_init-0.3.0/.claude/skills/wiki/templates/implementation-guide.md +57 -0
  21. project_init-0.3.0/.claude/skills/wiki/templates/preset-guide.md +50 -0
  22. project_init-0.3.0/.claude/skills/wiki/templates/scaffolder-logic.md +50 -0
  23. project_init-0.3.0/.claude-plugin/marketplace.json +14 -0
  24. project_init-0.3.0/.gitattributes +1 -0
  25. project_init-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +68 -0
  26. project_init-0.3.0/.github/ISSUE_TEMPLATE/chore.yml +58 -0
  27. project_init-0.3.0/.github/ISSUE_TEMPLATE/docs.yml +53 -0
  28. project_init-0.3.0/.github/ISSUE_TEMPLATE/feature.yml +77 -0
  29. project_init-0.3.0/.github/ISSUE_TEMPLATE/test.yml +54 -0
  30. project_init-0.3.0/.github/copilot-instructions.md +25 -0
  31. project_init-0.3.0/.github/workflows/board-automation.yml +204 -0
  32. project_init-0.3.0/.github/workflows/ci.yml +99 -0
  33. project_init-0.3.0/.github/workflows/docs.yml +35 -0
  34. project_init-0.3.0/.github/workflows/release.yml +81 -0
  35. project_init-0.3.0/.github/workflows/review-status.yml +48 -0
  36. project_init-0.3.0/.github/workflows/validate-pr.yml +64 -0
  37. project_init-0.3.0/.gitignore +40 -0
  38. project_init-0.3.0/AGENTS.md +9 -0
  39. project_init-0.3.0/CLAUDE.md +93 -0
  40. project_init-0.3.0/GEMINI.md +9 -0
  41. project_init-0.3.0/LICENSE +201 -0
  42. project_init-0.3.0/PKG-INFO +342 -0
  43. project_init-0.3.0/README.md +311 -0
  44. project_init-0.3.0/cliff.toml +32 -0
  45. project_init-0.3.0/docs/README.md +22 -0
  46. project_init-0.3.0/docs/adr/adr-001-scaffolder-design.md +22 -0
  47. project_init-0.3.0/docs/adr/adr-002-dotglob-template-convention.md +22 -0
  48. project_init-0.3.0/docs/adr/adr-003-github-native-workflow.md +28 -0
  49. project_init-0.3.0/docs/adr/adr-004-obsidian-docs-integration.md +34 -0
  50. project_init-0.3.0/docs/adr/adr-005-github-pr-board-workflow.md +95 -0
  51. project_init-0.3.0/docs/adr/adr-006-conventional-commit-titles.md +51 -0
  52. project_init-0.3.0/docs/adr/adr-007-security-enforcement-layers.md +71 -0
  53. project_init-0.3.0/docs/adr/adr-008-distribution-channel.md +45 -0
  54. project_init-0.3.0/docs/adr/adr-009-graphify-memory-preset.md +61 -0
  55. project_init-0.3.0/docs/adr/adr-010-plugin-marketplace-dual-ship.md +73 -0
  56. project_init-0.3.0/docs/adr/adr-011-pypi-trusted-publishing.md +56 -0
  57. project_init-0.3.0/docs/adr/adr-012-prod-safety-guard.md +57 -0
  58. project_init-0.3.0/docs/development/build-reproducibility.md +24 -0
  59. project_init-0.3.0/docs/development/contributing.md +35 -0
  60. project_init-0.3.0/docs/development/template-system.md +82 -0
  61. project_init-0.3.0/docs/development/testing.md +59 -0
  62. project_init-0.3.0/docs/guides/using-project-init.md +215 -0
  63. project_init-0.3.0/install.sh +125 -0
  64. project_init-0.3.0/justfile +30 -0
  65. project_init-0.3.0/mkdocs.yml +32 -0
  66. project_init-0.3.0/plugins/project-init-workflow/.claude-plugin/plugin.json +19 -0
  67. project_init-0.3.0/plugins/project-init-workflow/hooks/dag_workflow.py +610 -0
  68. project_init-0.3.0/plugins/project-init-workflow/hooks/github_command_guard.sh +11 -0
  69. project_init-0.3.0/plugins/project-init-workflow/hooks/hooks.json +60 -0
  70. project_init-0.3.0/plugins/project-init-workflow/hooks/post_edit_lint.sh +58 -0
  71. project_init-0.3.0/plugins/project-init-workflow/hooks/pre_commit_gate.sh +81 -0
  72. project_init-0.3.0/plugins/project-init-workflow/hooks/prod_guard.py +140 -0
  73. project_init-0.3.0/plugins/project-init-workflow/hooks/session_setup.sh +62 -0
  74. project_init-0.3.0/plugins/project-init-workflow/hooks/workflow_state_reminder.sh +72 -0
  75. project_init-0.3.0/plugins/project-init-workflow/skills/add_adr/SKILL.md +33 -0
  76. project_init-0.3.0/plugins/project-init-workflow/skills/add_command/SKILL.md +63 -0
  77. project_init-0.3.0/plugins/project-init-workflow/skills/add_hook/SKILL.md +112 -0
  78. project_init-0.3.0/plugins/project-init-workflow/skills/audit/SKILL.md +146 -0
  79. project_init-0.3.0/plugins/project-init-workflow/skills/create_issue/SKILL.md +59 -0
  80. project_init-0.3.0/plugins/project-init-workflow/skills/github_workflow/SKILL.md +80 -0
  81. project_init-0.3.0/plugins/project-init-workflow/skills/request_review/SKILL.md +19 -0
  82. project_init-0.3.0/plugins/project-init-workflow/skills/review/SKILL.md +17 -0
  83. project_init-0.3.0/plugins/project-init-workflow/skills/save_memory/SKILL.md +17 -0
  84. project_init-0.3.0/plugins/project-init-workflow/skills/session_summary/SKILL.md +35 -0
  85. project_init-0.3.0/plugins/project-init-workflow/skills/start_task/SKILL.md +48 -0
  86. project_init-0.3.0/plugins/project-init-workflow/skills/status/SKILL.md +15 -0
  87. project_init-0.3.0/pyproject.toml +88 -0
  88. project_init-0.3.0/renovate.json +24 -0
  89. project_init-0.3.0/src/project_init/__init__.py +4 -0
  90. project_init-0.3.0/src/project_init/__main__.py +662 -0
  91. project_init-0.3.0/src/project_init/mcps.py +57 -0
  92. project_init-0.3.0/src/project_init/scaffold.py +374 -0
  93. project_init-0.3.0/src/project_init/upgrade.py +569 -0
  94. project_init-0.3.0/templates/base/AGENTS.md.tmpl +50 -0
  95. project_init-0.3.0/templates/base/CLAUDE.md.tmpl +16 -0
  96. project_init-0.3.0/templates/base/CONTRIBUTING.md.tmpl +55 -0
  97. project_init-0.3.0/templates/base/GEMINI.md.tmpl +16 -0
  98. project_init-0.3.0/templates/base/LICENSE.tmpl +231 -0
  99. project_init-0.3.0/templates/base/SECURITY.md.tmpl +26 -0
  100. project_init-0.3.0/templates/base/docs/explanation/index.md +9 -0
  101. project_init-0.3.0/templates/base/docs/how-to/index.md +7 -0
  102. project_init-0.3.0/templates/base/docs/index.md.tmpl +20 -0
  103. project_init-0.3.0/templates/base/docs/reference/index.md +13 -0
  104. project_init-0.3.0/templates/base/docs/tutorials/index.md +7 -0
  105. project_init-0.3.0/templates/base/dot_claude/agents/README.md +30 -0
  106. project_init-0.3.0/templates/base/dot_claude/config.yaml.tmpl +31 -0
  107. project_init-0.3.0/templates/base/dot_claude/docs/README.md +26 -0
  108. project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-001-memory-stack.md.tmpl +22 -0
  109. project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-002-mcp-choices.md.tmpl +32 -0
  110. project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-template.md +29 -0
  111. project_init-0.3.0/templates/base/dot_claude/docs/development/conventions.md.tmpl +31 -0
  112. project_init-0.3.0/templates/base/dot_claude/docs/development/testing.md +25 -0
  113. project_init-0.3.0/templates/base/dot_claude/docs/guides/developer-onboarding.md +110 -0
  114. project_init-0.3.0/templates/base/dot_claude/docs/guides/issue-metadata.md +27 -0
  115. project_init-0.3.0/templates/base/dot_claude/docs/guides/secrets.md +50 -0
  116. project_init-0.3.0/templates/base/dot_claude/docs/guides/using-memory.md +36 -0
  117. project_init-0.3.0/templates/base/dot_claude/hooks/README.md +15 -0
  118. project_init-0.3.0/templates/base/dot_claude/hooks/agent_guard_adapter.py.tmpl +64 -0
  119. project_init-0.3.0/templates/base/dot_claude/hooks/dag_workflow.py +610 -0
  120. project_init-0.3.0/templates/base/dot_claude/memory/MEMORY.md.tmpl +11 -0
  121. project_init-0.3.0/templates/base/dot_claude/memory/README.md +51 -0
  122. project_init-0.3.0/templates/base/dot_claude/memory/SCHEMA.md +52 -0
  123. project_init-0.3.0/templates/base/dot_claude/memory/feedback_conventions.md +11 -0
  124. project_init-0.3.0/templates/base/dot_claude/memory/project_context.md.tmpl +11 -0
  125. project_init-0.3.0/templates/base/dot_claude/memory/user_role.md +7 -0
  126. project_init-0.3.0/templates/base/dot_claude/project-init.md.tmpl +174 -0
  127. project_init-0.3.0/templates/base/dot_claude/rules/go.md +14 -0
  128. project_init-0.3.0/templates/base/dot_claude/rules/hooks.md +30 -0
  129. project_init-0.3.0/templates/base/dot_claude/rules/node.md +17 -0
  130. project_init-0.3.0/templates/base/dot_claude/rules/python.md +25 -0
  131. project_init-0.3.0/templates/base/dot_claude/scripts/README.md +15 -0
  132. project_init-0.3.0/templates/base/dot_claude/scripts/create_issue.sh +577 -0
  133. project_init-0.3.0/templates/base/dot_claude/scripts/create_nojira_pr.sh +3 -0
  134. project_init-0.3.0/templates/base/dot_claude/scripts/finish_pr.sh +3 -0
  135. project_init-0.3.0/templates/base/dot_claude/scripts/install_hooks.sh +55 -0
  136. project_init-0.3.0/templates/base/dot_claude/scripts/monitor_pr.sh +270 -0
  137. project_init-0.3.0/templates/base/dot_claude/scripts/promote_review.sh +3 -0
  138. project_init-0.3.0/templates/base/dot_claude/scripts/push_branch.sh +5 -0
  139. project_init-0.3.0/templates/base/dot_claude/scripts/push_wiki.sh +34 -0
  140. project_init-0.3.0/templates/base/dot_claude/scripts/setup_github.sh +219 -0
  141. project_init-0.3.0/templates/base/dot_claude/scripts/start_issue.sh +134 -0
  142. project_init-0.3.0/templates/base/dot_claude/settings.json.tmpl +83 -0
  143. project_init-0.3.0/templates/base/dot_claude/skills/README.md +12 -0
  144. project_init-0.3.0/templates/base/dot_claude/skills/plan/SKILL.md.tmpl +40 -0
  145. project_init-0.3.0/templates/base/dot_claude/vault/README.md +21 -0
  146. project_init-0.3.0/templates/base/dot_claude/vault/decisions/README.md +22 -0
  147. project_init-0.3.0/templates/base/dot_claude/vault/design/README.md +3 -0
  148. project_init-0.3.0/templates/base/dot_claude/vault/knowledge/README.md +5 -0
  149. project_init-0.3.0/templates/base/dot_claude/vault/sessions/README.md +5 -0
  150. project_init-0.3.0/templates/base/dot_devcontainer/devcontainer.json.tmpl +17 -0
  151. project_init-0.3.0/templates/base/dot_devcontainer/post-create.sh.tmpl +31 -0
  152. project_init-0.3.0/templates/base/dot_env.example.tmpl +13 -0
  153. project_init-0.3.0/templates/base/dot_github/CODEOWNERS.tmpl +12 -0
  154. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/bug.yml +98 -0
  155. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/chore.yml +82 -0
  156. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/config.yml +5 -0
  157. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/docs.yml +84 -0
  158. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/feature.yml +87 -0
  159. project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/test.yml +90 -0
  160. project_init-0.3.0/templates/base/dot_github/copilot-instructions.md.tmpl +25 -0
  161. project_init-0.3.0/templates/base/dot_github/hooks/commit-msg +52 -0
  162. project_init-0.3.0/templates/base/dot_github/hooks/pre-commit +16 -0
  163. project_init-0.3.0/templates/base/dot_github/hooks/pre-push +51 -0
  164. project_init-0.3.0/templates/base/dot_github/pull_request_template.md +22 -0
  165. project_init-0.3.0/templates/base/dot_github/workflows/board-automation.yml +232 -0
  166. project_init-0.3.0/templates/base/dot_github/workflows/ci.yml.tmpl +204 -0
  167. project_init-0.3.0/templates/base/dot_github/workflows/docs.yml.tmpl +98 -0
  168. project_init-0.3.0/templates/base/dot_github/workflows/issue-validation.yml +72 -0
  169. project_init-0.3.0/templates/base/dot_github/workflows/review-status.yml +48 -0
  170. project_init-0.3.0/templates/base/dot_github/workflows/validate-pr.yml +103 -0
  171. project_init-0.3.0/templates/base/dot_gitignore.tmpl +41 -0
  172. project_init-0.3.0/templates/base/dot_golangci.yml.tmpl +20 -0
  173. project_init-0.3.0/templates/base/dot_vscode/extensions.json.tmpl +10 -0
  174. project_init-0.3.0/templates/base/dot_vscode/settings.json.tmpl +8 -0
  175. project_init-0.3.0/templates/base/eslint.config.mjs.tmpl +29 -0
  176. project_init-0.3.0/templates/base/justfile.tmpl +95 -0
  177. project_init-0.3.0/templates/base/mise.toml.tmpl +20 -0
  178. project_init-0.3.0/templates/base/mkdocs.yml.tmpl +32 -0
  179. project_init-0.3.0/templates/base/renovate.json +14 -0
  180. project_init-0.3.0/templates/base/ruff.toml.tmpl +31 -0
  181. project_init-0.3.0/templates/base/typedoc.json.tmpl +14 -0
  182. project_init-0.3.0/templates/codex/dot_agents/skills/add_adr/SKILL.md +33 -0
  183. project_init-0.3.0/templates/codex/dot_agents/skills/add_command/SKILL.md +63 -0
  184. project_init-0.3.0/templates/codex/dot_agents/skills/add_hook/SKILL.md +112 -0
  185. project_init-0.3.0/templates/codex/dot_agents/skills/audit/SKILL.md +146 -0
  186. project_init-0.3.0/templates/codex/dot_agents/skills/create_issue/SKILL.md +59 -0
  187. project_init-0.3.0/templates/codex/dot_agents/skills/github_workflow/SKILL.md +80 -0
  188. project_init-0.3.0/templates/codex/dot_agents/skills/request_review/SKILL.md +19 -0
  189. project_init-0.3.0/templates/codex/dot_agents/skills/review/SKILL.md +17 -0
  190. project_init-0.3.0/templates/codex/dot_agents/skills/save_memory/SKILL.md +17 -0
  191. project_init-0.3.0/templates/codex/dot_agents/skills/session_summary/SKILL.md +35 -0
  192. project_init-0.3.0/templates/codex/dot_agents/skills/start_task/SKILL.md +48 -0
  193. project_init-0.3.0/templates/codex/dot_agents/skills/status/SKILL.md +15 -0
  194. project_init-0.3.0/templates/codex/dot_codex/hooks.json.tmpl +17 -0
  195. project_init-0.3.0/templates/fallback/dot_claude/hooks/github_command_guard.sh +11 -0
  196. project_init-0.3.0/templates/fallback/dot_claude/hooks/post_edit_lint.sh +58 -0
  197. project_init-0.3.0/templates/fallback/dot_claude/hooks/pre_commit_gate.sh +81 -0
  198. project_init-0.3.0/templates/fallback/dot_claude/hooks/prod_guard.py +140 -0
  199. project_init-0.3.0/templates/fallback/dot_claude/hooks/session_setup.sh +62 -0
  200. project_init-0.3.0/templates/fallback/dot_claude/hooks/workflow_state_reminder.sh +72 -0
  201. project_init-0.3.0/templates/fallback/dot_claude/skills/INDEX.md +28 -0
  202. project_init-0.3.0/templates/fallback/dot_claude/skills/add_adr/SKILL.md +33 -0
  203. project_init-0.3.0/templates/fallback/dot_claude/skills/add_command/SKILL.md +63 -0
  204. project_init-0.3.0/templates/fallback/dot_claude/skills/add_hook/SKILL.md +112 -0
  205. project_init-0.3.0/templates/fallback/dot_claude/skills/audit/SKILL.md +146 -0
  206. project_init-0.3.0/templates/fallback/dot_claude/skills/create_issue/SKILL.md +59 -0
  207. project_init-0.3.0/templates/fallback/dot_claude/skills/github_workflow/SKILL.md +80 -0
  208. project_init-0.3.0/templates/fallback/dot_claude/skills/request_review/SKILL.md +19 -0
  209. project_init-0.3.0/templates/fallback/dot_claude/skills/review/SKILL.md +17 -0
  210. project_init-0.3.0/templates/fallback/dot_claude/skills/save_memory/SKILL.md +17 -0
  211. project_init-0.3.0/templates/fallback/dot_claude/skills/session_summary/SKILL.md +35 -0
  212. project_init-0.3.0/templates/fallback/dot_claude/skills/start_task/SKILL.md +48 -0
  213. project_init-0.3.0/templates/fallback/dot_claude/skills/status/SKILL.md +15 -0
  214. project_init-0.3.0/templates/gemini/dot_agents/skills/add_adr/SKILL.md +33 -0
  215. project_init-0.3.0/templates/gemini/dot_agents/skills/add_command/SKILL.md +63 -0
  216. project_init-0.3.0/templates/gemini/dot_agents/skills/add_hook/SKILL.md +112 -0
  217. project_init-0.3.0/templates/gemini/dot_agents/skills/audit/SKILL.md +146 -0
  218. project_init-0.3.0/templates/gemini/dot_agents/skills/create_issue/SKILL.md +59 -0
  219. project_init-0.3.0/templates/gemini/dot_agents/skills/github_workflow/SKILL.md +80 -0
  220. project_init-0.3.0/templates/gemini/dot_agents/skills/request_review/SKILL.md +19 -0
  221. project_init-0.3.0/templates/gemini/dot_agents/skills/review/SKILL.md +17 -0
  222. project_init-0.3.0/templates/gemini/dot_agents/skills/save_memory/SKILL.md +17 -0
  223. project_init-0.3.0/templates/gemini/dot_agents/skills/session_summary/SKILL.md +35 -0
  224. project_init-0.3.0/templates/gemini/dot_agents/skills/start_task/SKILL.md +48 -0
  225. project_init-0.3.0/templates/gemini/dot_agents/skills/status/SKILL.md +15 -0
  226. project_init-0.3.0/templates/gemini/dot_claude/scripts/setup_gemini.sh.tmpl +16 -0
  227. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_adr.toml +5 -0
  228. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_command.toml +5 -0
  229. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_hook.toml +5 -0
  230. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/audit.toml +5 -0
  231. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/create_issue.toml +5 -0
  232. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/github_workflow.toml +5 -0
  233. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/request_review.toml +5 -0
  234. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/review.toml +5 -0
  235. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/save_memory.toml +5 -0
  236. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/session_summary.toml +5 -0
  237. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/start_task.toml +5 -0
  238. project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/status.toml +5 -0
  239. project_init-0.3.0/templates/gemini/dot_gemini-extension/gemini-extension.json.tmpl +6 -0
  240. project_init-0.3.0/templates/gemini/dot_gemini-extension/hooks/hooks.json.tmpl +18 -0
  241. project_init-0.3.0/templates/graphify/dot_claude/docs/guides/using-graphify.md +37 -0
  242. project_init-0.3.0/templates/graphify/dot_claude/rules/graphify.md +18 -0
  243. project_init-0.3.0/templates/graphify/dot_claude/scripts/setup_graphify.sh +40 -0
  244. project_init-0.3.0/templates/obsidian/dot_claude/scripts/lint_memory.sh +115 -0
  245. project_init-0.3.0/templates/obsidian/dot_claude/vault/decisions/adr-000-project-setup.md.tmpl +22 -0
  246. project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/README.md +31 -0
  247. project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/app.json +6 -0
  248. project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/community-plugins.json +1 -0
  249. project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/core-plugins.json +1 -0
  250. project_init-0.3.0/templates/obsidian/dot_claude/vault/log.md +6 -0
  251. project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/decision.md +16 -0
  252. project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/design-note.md +14 -0
  253. project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/knowledge-note.md +12 -0
  254. project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/session-note.md +16 -0
  255. project_init-0.3.0/templates/presets/obsidian-graphify.toml +16 -0
  256. project_init-0.3.0/templates/presets/obsidian-only.toml +14 -0
  257. project_init-0.3.0/tests/__init__.py +1 -0
  258. project_init-0.3.0/tests/conftest.py +32 -0
  259. project_init-0.3.0/tests/contracts/test_agent_overlays.py +177 -0
  260. project_init-0.3.0/tests/contracts/test_agents_canonical.py +94 -0
  261. project_init-0.3.0/tests/contracts/test_env_tooling.py +126 -0
  262. project_init-0.3.0/tests/contracts/test_governance.py +127 -0
  263. project_init-0.3.0/tests/contracts/test_integrity.py +184 -0
  264. project_init-0.3.0/tests/contracts/test_justfile.py +156 -0
  265. project_init-0.3.0/tests/contracts/test_plugin_marketplace.py +179 -0
  266. project_init-0.3.0/tests/contracts/test_prod_guard.py +177 -0
  267. project_init-0.3.0/tests/contracts/test_quality_toolchain.py +204 -0
  268. project_init-0.3.0/tests/contracts/test_release_engineering.py +125 -0
  269. project_init-0.3.0/tests/contracts/test_renovate.py +82 -0
  270. project_init-0.3.0/tests/contracts/test_scaffold_graphify.py +64 -0
  271. project_init-0.3.0/tests/contracts/test_scaffold_obsidian.py +304 -0
  272. project_init-0.3.0/tests/contracts/test_session_bootstrap.py +87 -0
  273. project_init-0.3.0/tests/contracts/test_skill_index.py +136 -0
  274. project_init-0.3.0/tests/contracts/test_templates.py +284 -0
  275. project_init-0.3.0/tests/contracts/test_wiki_skill.py +142 -0
  276. project_init-0.3.0/tests/helpers.py +84 -0
  277. project_init-0.3.0/tests/integration/test_cli.py +244 -0
  278. project_init-0.3.0/tests/integration/test_dag_workflow.py +443 -0
  279. project_init-0.3.0/tests/integration/test_hooks_and_safety.py +272 -0
  280. project_init-0.3.0/tests/integration/test_issue_metadata_workflow.py +433 -0
  281. project_init-0.3.0/tests/integration/test_memory_starters.py +223 -0
  282. project_init-0.3.0/tests/integration/test_overwrite_protection.py +158 -0
  283. project_init-0.3.0/tests/integration/test_quality_gate_lint.py +52 -0
  284. project_init-0.3.0/tests/integration/test_readme_examples.py +66 -0
  285. project_init-0.3.0/tests/integration/test_session_cold_start.py +140 -0
  286. project_init-0.3.0/tests/integration/test_upgrade.py +457 -0
  287. project_init-0.3.0/tests/smoke/test_packaging.py +113 -0
  288. project_init-0.3.0/tests/unit/test_command_variables.py +71 -0
  289. project_init-0.3.0/tests/unit/test_mcps.py +197 -0
  290. project_init-0.3.0/tests/unit/test_presets.py +30 -0
  291. project_init-0.3.0/tests/unit/test_render_nesting.py +35 -0
  292. project_init-0.3.0/tools/sync_plugin.py +144 -0
  293. project_init-0.3.0/uv.lock +679 -0
@@ -0,0 +1,610 @@
1
+ #!/usr/bin/env python3
2
+ """DAG-based workflow enforcement for the GitHub lifecycle.
3
+
4
+ Subcommands:
5
+ check <node> exit 0 if every prerequisite of <node> is satisfied,
6
+ exit 2 otherwise (with reason on stdout).
7
+ guard read PreToolUse hook input JSON from stdin, map the
8
+ Bash command to a target node, emit
9
+ {decision: block, reason: ...} if disallowed.
10
+ nodes list every DAG node and its prerequisites.
11
+ push [<branch>] [N] push current (or named) branch with retry + remote-SHA
12
+ verification (handles transient GitHub 5xx).
13
+ promote [<pr>] mark current (or numbered) draft PR ready for review.
14
+ finish [<pr>] [--review-cycle N]
15
+ push, promote, then exec monitor_pr.sh --merge.
16
+ create-pr-nojira <type> <title> [--branch B] [--base B]
17
+ create a no-issue feature branch + draft PR.
18
+
19
+ The `check` and `guard` paths are pure read-only; they're used by hooks and
20
+ lifecycle scripts. The other subcommands consolidate the bash lifecycle
21
+ scripts (push_branch.sh, promote_review.sh, finish_pr.sh,
22
+ create_nojira_pr.sh) so the tool is the single source of truth for the
23
+ GitHub workflow. The .sh files become thin shims that exec into here.
24
+
25
+ Issue-ref prefix detection:
26
+ By default, branches matching `[A-Z]{2,}-<n>` (e.g. PI-98, ACME-42) are
27
+ treated as issue-backed. Override with the DAG_ISSUE_PREFIX env var to pin
28
+ a specific prefix (e.g. DAG_ISSUE_PREFIX=PROJ matches only `PROJ-<n>`).
29
+ Branches with no recognized prefix fall through to the no-jira flow.
30
+
31
+ stdlib only.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ import json
37
+ import os
38
+ import re
39
+ import subprocess
40
+ import sys
41
+ import time
42
+ from pathlib import Path
43
+
44
+ CACHE_PATH = Path(".claude/.workflow-state.json")
45
+
46
+ GRAPH: dict[str, list[str]] = {
47
+ "issue.created": [],
48
+ "branch.created": [],
49
+ "branch.pushed": ["branch.created"],
50
+ "pr.opened": ["branch.pushed", "issue.created"],
51
+ "ci.green": ["pr.opened"],
52
+ "review.approved": ["pr.opened"],
53
+ "pr.merged": ["ci.green", "review.approved"],
54
+ }
55
+
56
+ _CONFIGURED_PREFIX = os.environ.get("DAG_ISSUE_PREFIX", "").strip()
57
+ if _CONFIGURED_PREFIX:
58
+ ISSUE_RE: re.Pattern[str] = re.compile(
59
+ rf"\b{re.escape(_CONFIGURED_PREFIX)}-(\d+)\b", re.IGNORECASE
60
+ )
61
+ else:
62
+ ISSUE_RE = re.compile(r"\b[A-Z]{2,}-(\d+)\b")
63
+
64
+
65
+ def _run(cmd: list[str]) -> tuple[int, str]:
66
+ try:
67
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
68
+ except (subprocess.TimeoutExpired, FileNotFoundError):
69
+ return 1, ""
70
+ return proc.returncode, proc.stdout
71
+
72
+
73
+ def _gh(args: list[str]) -> tuple[int, str]:
74
+ return _run(["gh", *args])
75
+
76
+
77
+ def _git(args: list[str]) -> tuple[int, str]:
78
+ return _run(["git", *args])
79
+
80
+
81
+ def _current_branch() -> str | None:
82
+ code, out = _git(["branch", "--show-current"])
83
+ branch = out.strip()
84
+ return branch if code == 0 and branch else None
85
+
86
+
87
+ def _issue_from_branch(branch: str) -> int | None:
88
+ m = ISSUE_RE.search(branch)
89
+ return int(m.group(1)) if m else None
90
+
91
+
92
+ def check_issue_created() -> tuple[bool, str]:
93
+ branch = _current_branch()
94
+ if not branch:
95
+ return False, "no current branch"
96
+ n = _issue_from_branch(branch)
97
+ if n is None:
98
+ return True, f"branch '{branch}' has no issue ref (no-jira flow allowed)"
99
+ code, out = _gh(["issue", "view", str(n), "--json", "number,state"])
100
+ if code != 0:
101
+ return False, f"issue #{n} not found via gh"
102
+ try:
103
+ data = json.loads(out or "{}")
104
+ except json.JSONDecodeError:
105
+ return False, f"issue #{n}: malformed gh output"
106
+ if data.get("number") != n:
107
+ return False, f"issue #{n} not present"
108
+ return True, f"issue #{n} exists ({data.get('state', 'UNKNOWN')})"
109
+
110
+
111
+ def check_branch_created() -> tuple[bool, str]:
112
+ branch = _current_branch()
113
+ if not branch:
114
+ return False, "not in a git repo / no current branch"
115
+ if branch in {"main", "master"}:
116
+ return False, "must be on a feature branch, not main/master"
117
+ return True, f"on branch '{branch}'"
118
+
119
+
120
+ def check_branch_pushed() -> tuple[bool, str]:
121
+ branch = _current_branch()
122
+ if not branch:
123
+ return False, "no current branch"
124
+ code, _ = _git(["rev-parse", "--verify", f"origin/{branch}"])
125
+ if code != 0:
126
+ return False, f"origin/{branch} does not exist (push the branch first)"
127
+ code, out = _git(["rev-list", "--count", f"origin/{branch}..HEAD"])
128
+ if code == 0 and out.strip() and out.strip() != "0":
129
+ return False, f"branch has {out.strip()} unpushed commit(s)"
130
+ return True, f"branch '{branch}' is pushed and up to date with origin"
131
+
132
+
133
+ def check_pr_opened() -> tuple[bool, str]:
134
+ code, out = _gh(["pr", "view", "--json", "number,state"])
135
+ if code != 0:
136
+ return False, "no PR exists for the current branch"
137
+ try:
138
+ data = json.loads(out or "{}")
139
+ except json.JSONDecodeError:
140
+ return False, "malformed gh pr view output"
141
+ if data.get("state") != "OPEN":
142
+ return False, f"PR is {data.get('state', 'unknown')}, not OPEN"
143
+ return True, f"PR #{data.get('number')} is open"
144
+
145
+
146
+ def check_ci_green() -> tuple[bool, str]:
147
+ code, out = _gh(["pr", "view", "--json", "number,statusCheckRollup"])
148
+ if code != 0:
149
+ return False, "cannot read PR / CI status"
150
+ try:
151
+ data = json.loads(out or "{}")
152
+ except json.JSONDecodeError:
153
+ return False, "malformed gh statusCheckRollup output"
154
+ n = data.get("number", "?")
155
+ rollup = data.get("statusCheckRollup") or []
156
+ pending = failing = 0
157
+ for entry in rollup:
158
+ name = (entry.get("name") or entry.get("context") or "").lower()
159
+ if "review/decision" in name:
160
+ continue
161
+ status = (entry.get("status") or "").upper()
162
+ conclusion = (entry.get("conclusion") or "").upper()
163
+ if conclusion in {"FAILURE", "TIMED_OUT", "CANCELLED", "ERROR", "ACTION_REQUIRED"}:
164
+ failing += 1
165
+ elif status in {"PENDING", "QUEUED", "IN_PROGRESS", "WAITING"} or (
166
+ not conclusion and not status
167
+ ):
168
+ pending += 1
169
+ if failing:
170
+ return False, f"PR #{n}: {failing} CI check(s) failing"
171
+ if pending:
172
+ return False, f"PR #{n}: {pending} CI check(s) still running"
173
+ return True, f"PR #{n}: CI green"
174
+
175
+
176
+ def check_review_approved() -> tuple[bool, str]:
177
+ code, out = _gh(["pr", "view", "--json", "number,reviewDecision"])
178
+ if code != 0:
179
+ return False, "cannot read PR review decision"
180
+ try:
181
+ data = json.loads(out or "{}")
182
+ except json.JSONDecodeError:
183
+ return False, "malformed gh reviewDecision output"
184
+ n = data.get("number", "?")
185
+ decision = data.get("reviewDecision") or ""
186
+ if decision == "APPROVED":
187
+ return True, f"PR #{n}: review approved"
188
+ if decision == "CHANGES_REQUESTED":
189
+ return False, f"PR #{n}: review requested changes"
190
+ return False, f"PR #{n}: review pending (decision={decision or 'none'})"
191
+
192
+
193
+ def check_pr_merged() -> tuple[bool, str]:
194
+ code, out = _gh(["pr", "view", "--json", "number,state"])
195
+ if code != 0:
196
+ return False, "no PR for current branch"
197
+ try:
198
+ data = json.loads(out or "{}")
199
+ except json.JSONDecodeError:
200
+ return False, "malformed gh output"
201
+ if data.get("state") == "MERGED":
202
+ return True, f"PR #{data.get('number')} is merged"
203
+ return False, f"PR #{data.get('number')} state is {data.get('state')}"
204
+
205
+
206
+ CHECKS = {
207
+ "issue.created": check_issue_created,
208
+ "branch.created": check_branch_created,
209
+ "branch.pushed": check_branch_pushed,
210
+ "pr.opened": check_pr_opened,
211
+ "ci.green": check_ci_green,
212
+ "review.approved": check_review_approved,
213
+ "pr.merged": check_pr_merged,
214
+ }
215
+
216
+
217
+ def prereqs_satisfied(node: str, _seen: set[str] | None = None) -> tuple[bool, str]:
218
+ """Walk all ancestors of `node` and return (True, '') if every prereq passes,
219
+ else (False, '<first failing prereq>: <reason>').
220
+ """
221
+ if node not in GRAPH:
222
+ return False, f"unknown node: {node}"
223
+ seen = _seen if _seen is not None else set()
224
+ if node in seen:
225
+ return True, ""
226
+ seen.add(node)
227
+ for prereq in GRAPH[node]:
228
+ ok, reason = CHECKS[prereq]()
229
+ if not ok:
230
+ return False, f"{prereq}: {reason}"
231
+ ok, reason = prereqs_satisfied(prereq, seen)
232
+ if not ok:
233
+ return False, reason
234
+ return True, "all prerequisites satisfied"
235
+
236
+
237
+ # Steering rules: command pattern -> (target_node | None, redirect_message)
238
+ # The first matching rule wins. target_node=None means a hard block with no
239
+ # DAG validation; otherwise, prereqs of target_node are appended to the reason.
240
+ COMMAND_RULES: list[tuple[re.Pattern[str], str | None, str]] = [
241
+ (
242
+ re.compile(r"git\s+push\s+(?:\S+\s+)?(?:origin\s+)?(?:main|master)\b"),
243
+ None,
244
+ "Direct pushes to main/master are blocked. Open a feature branch and PR.",
245
+ ),
246
+ (
247
+ re.compile(r"\bgh\s+api\s+repos/[^/\s]+/[^/\s]+/pulls/\d+/merge\b"),
248
+ "pr.merged",
249
+ "Use .claude/scripts/monitor_pr.sh <pr> --merge instead of `gh api .../merge` so CI and review gates are honored.",
250
+ ),
251
+ (
252
+ re.compile(r"\bgh\s+pr\s+merge\b"),
253
+ "pr.merged",
254
+ "Use .claude/scripts/monitor_pr.sh <pr> --merge instead of `gh pr merge` so CI, review waits, and review cycles are handled.",
255
+ ),
256
+ (
257
+ re.compile(r"\bgh\s+pr\s+checks\b.*--watch"),
258
+ None,
259
+ "Use .claude/scripts/monitor_pr.sh <pr> --merge instead of `gh pr checks --watch`.",
260
+ ),
261
+ (
262
+ re.compile(r"\bgh\s+pr\s+ready\b"),
263
+ "pr.opened",
264
+ "Use .claude/scripts/promote_review.sh instead of `gh pr ready`.",
265
+ ),
266
+ (
267
+ re.compile(r"\bgh\s+pr\s+create\b"),
268
+ "pr.opened",
269
+ "Use .claude/scripts/start_issue.sh (issue-backed) or .claude/scripts/create_nojira_pr.sh (no issue) instead of `gh pr create`.",
270
+ ),
271
+ (
272
+ re.compile(r"\bgh\s+issue\s+create\b"),
273
+ None,
274
+ "Use .claude/scripts/create_issue.sh (or the start_task skill) so priority, references, and acceptance criteria are captured.",
275
+ ),
276
+ (
277
+ re.compile(r"\bgit\s+push\b"),
278
+ "branch.pushed",
279
+ "Use .claude/scripts/push_branch.sh instead of raw `git push` so transient GitHub failures are retried and the remote SHA is verified.",
280
+ ),
281
+ ]
282
+
283
+
284
+ _HEREDOC_RE = re.compile(
285
+ r"<<-?\s*['\"]?(\w+)['\"]?[ \t]*\n.*?\n\1[ \t]*(?:\n|$)",
286
+ re.DOTALL,
287
+ )
288
+
289
+
290
+ def _strip_heredocs(cmd: str) -> str:
291
+ """Remove heredoc body text so pattern rules don't fire on body content."""
292
+ return _HEREDOC_RE.sub("", cmd)
293
+
294
+
295
+ def _redirect_target_exists(reason: str) -> bool:
296
+ """Best-effort: scan the redirect message for a `.claude/scripts/<name>`
297
+ reference and check whether the file exists. If no reference is found,
298
+ treat the rule as always-applicable (e.g. main/master block).
299
+ """
300
+ m = re.search(r"\.claude/scripts/([\w.-]+)", reason)
301
+ if not m:
302
+ return True
303
+ return (Path(".claude/scripts") / m.group(1)).exists()
304
+
305
+
306
+ def guard(payload: dict) -> dict | None:
307
+ cmd = ((payload.get("tool_input") or {}).get("command") or "").strip()
308
+ if not cmd:
309
+ return None
310
+
311
+ # Strip heredoc bodies so pattern rules don't fire on body content
312
+ # (e.g. `gh issue create --body "$(cat <<'EOF'\n...git push...\nEOF\n)"`)
313
+ cmd_scan = _strip_heredocs(cmd)
314
+
315
+ for pattern, target, message in COMMAND_RULES:
316
+ if not pattern.search(cmd_scan):
317
+ continue
318
+ # If the redirect points at a wrapper script that doesn't exist in
319
+ # this repo, skip (don't block — there's nothing to redirect to).
320
+ if not _redirect_target_exists(message):
321
+ continue
322
+
323
+ reason = message
324
+ if target is not None:
325
+ ok, why = prereqs_satisfied(target)
326
+ if not ok:
327
+ reason = f"{message}\n\nDAG prerequisite for {target} not met: {why}."
328
+ return {"decision": "block", "reason": reason}
329
+ return None
330
+
331
+
332
+ def cmd_check(node: str) -> int:
333
+ if node not in GRAPH:
334
+ sys.stdout.write(f"unknown node: {node}\n")
335
+ return 2
336
+ ok, reason = prereqs_satisfied(node)
337
+ if ok:
338
+ # Also report the node's own check, for human consumption.
339
+ own_ok, own_reason = CHECKS[node]()
340
+ marker = "OK" if own_ok else "REACHABLE (state not yet satisfied)"
341
+ sys.stdout.write(f"{marker}: {node} — {own_reason}\n")
342
+ return 0 if own_ok else 0 # prereqs satisfied = transition allowed
343
+ sys.stdout.write(f"BLOCKED: cannot reach {node}: {reason}\n")
344
+ return 2
345
+
346
+
347
+ def cmd_guard() -> int:
348
+ raw = sys.stdin.read()
349
+ if not raw:
350
+ return 0
351
+ try:
352
+ payload = json.loads(raw)
353
+ except json.JSONDecodeError:
354
+ return 0
355
+ result = guard(payload)
356
+ if result is not None:
357
+ sys.stdout.write(json.dumps(result))
358
+ return 0
359
+
360
+
361
+ def _detect_pr_number() -> int | None:
362
+ code, out = _gh(["pr", "view", "--json", "number", "-q", ".number"])
363
+ if code != 0:
364
+ return None
365
+ try:
366
+ return int(out.strip())
367
+ except ValueError:
368
+ return None
369
+
370
+
371
+ def cmd_push(branch: str | None, max_retries: int, *, force: bool = False) -> int:
372
+ """Push the current (or named) branch with retry + remote-SHA verification.
373
+
374
+ Handles transient GitHub 5xx where `git push` exits non-zero but the
375
+ commit actually landed on the remote. *force* adds --force-with-lease
376
+ for the rebase-after-squash-merge case; main/master are refused.
377
+ """
378
+ if branch is None:
379
+ branch = _current_branch()
380
+ if not branch:
381
+ sys.stderr.write("push: no current branch\n")
382
+ return 1
383
+ if force and branch in ("main", "master"):
384
+ sys.stderr.write("push: refusing to force-push main/master\n")
385
+ return 1
386
+
387
+ code, sha_out = _git(["rev-parse", branch])
388
+ if code != 0:
389
+ sys.stderr.write(f"push: cannot resolve sha for {branch}\n")
390
+ return 1
391
+ expected_sha = sha_out.strip()
392
+
393
+ def remote_has_sha() -> bool:
394
+ code, out = _git(["ls-remote", "origin", f"refs/heads/{branch}"])
395
+ if code != 0:
396
+ return False
397
+ for line in out.splitlines():
398
+ parts = line.split()
399
+ if parts and parts[0] == expected_sha:
400
+ return True
401
+ return False
402
+
403
+ for attempt in range(max_retries + 1):
404
+ push_cmd = ["git", "push", "-u", "origin", branch]
405
+ if force:
406
+ push_cmd.append("--force-with-lease")
407
+ proc = subprocess.run(push_cmd)
408
+ if proc.returncode == 0:
409
+ sys.stdout.write(f"push: pushed {branch} ({expected_sha})\n")
410
+ return 0
411
+ if remote_has_sha():
412
+ sys.stdout.write(
413
+ f"push: remote already has {expected_sha} on {branch} "
414
+ "(transient error, treating as success)\n"
415
+ )
416
+ _git(["branch", f"--set-upstream-to=origin/{branch}", branch])
417
+ return 0
418
+ if attempt < max_retries:
419
+ time.sleep(3)
420
+ sys.stderr.write(f"push: failed after {max_retries} retries\n")
421
+ return 1
422
+
423
+
424
+ def cmd_promote(pr_number: int | None) -> int:
425
+ """Mark a draft PR ready for review."""
426
+ if pr_number is None:
427
+ pr_number = _detect_pr_number()
428
+ if pr_number is None:
429
+ sys.stderr.write(
430
+ "promote: no PR found for current branch. Pass a PR number.\n"
431
+ )
432
+ return 1
433
+ code, out = _gh(
434
+ ["pr", "view", str(pr_number), "--json", "isDraft", "-q", ".isDraft"]
435
+ )
436
+ if code != 0:
437
+ sys.stderr.write(f"promote: cannot read PR #{pr_number}\n")
438
+ return 1
439
+ if out.strip() == "false":
440
+ _, url = _gh(["pr", "view", str(pr_number), "--json", "url", "-q", ".url"])
441
+ sys.stdout.write(
442
+ f"PR #{pr_number} is already ready for review: {url.strip()}\n"
443
+ )
444
+ return 0
445
+ code, _ = _gh(["pr", "ready", str(pr_number)])
446
+ if code != 0:
447
+ sys.stderr.write(f"promote: gh pr ready failed for #{pr_number}\n")
448
+ return code
449
+ _, url = _gh(["pr", "view", str(pr_number), "--json", "url", "-q", ".url"])
450
+ sys.stdout.write(
451
+ f"PR #{pr_number} is now ready for review: {url.strip()}\n"
452
+ )
453
+ return 0
454
+
455
+
456
+ def cmd_finish(pr_number: int | None, review_cycle: int | None) -> int:
457
+ """Push, promote, then hand off to monitor_pr.sh for CI/review/merge."""
458
+ rc = cmd_push(None, 3)
459
+ if rc != 0:
460
+ return rc
461
+ if pr_number is None:
462
+ pr_number = _detect_pr_number()
463
+ if pr_number is None:
464
+ sys.stderr.write("finish: no PR found for current branch.\n")
465
+ return 1
466
+ rc = cmd_promote(pr_number)
467
+ if rc != 0:
468
+ return rc
469
+ monitor_args = [".claude/scripts/monitor_pr.sh", str(pr_number), "--merge"]
470
+ if review_cycle is not None:
471
+ monitor_args += ["--review-cycle", str(review_cycle)]
472
+ return subprocess.run(monitor_args).returncode
473
+
474
+
475
+ _VALID_TYPES = {"feat", "fix", "chore", "docs", "test"}
476
+ _BRANCH_RE = re.compile(r"^(feat|fix|chore|docs|test)/[A-Za-z0-9._/-]+$")
477
+
478
+
479
+ def _slugify(text: str) -> str:
480
+ s = re.sub(r"[^a-z0-9]+", "-", text.lower())
481
+ return s.strip("-")
482
+
483
+
484
+ def cmd_create_pr_nojira(
485
+ type_: str, title: str, branch: str | None, base: str | None
486
+ ) -> int:
487
+ """Create a no-issue feature branch (if needed) and open a draft PR."""
488
+ if type_ not in _VALID_TYPES:
489
+ sys.stderr.write(
490
+ f"ERROR: invalid type '{type_}'. Valid: {' '.join(sorted(_VALID_TYPES))}\n"
491
+ )
492
+ return 1
493
+ if not title.strip():
494
+ sys.stderr.write("ERROR: title must not be empty\n")
495
+ return 1
496
+
497
+ current = _current_branch()
498
+ if not branch:
499
+ if current and current not in {"main", "master"}:
500
+ branch = current
501
+ else:
502
+ slug = _slugify(title)
503
+ if not slug:
504
+ sys.stderr.write(
505
+ "ERROR: title must contain at least one letter or number\n"
506
+ )
507
+ return 1
508
+ prefix = "nojira-"
509
+ max_slug = max(12, 80 - len(type_) - 1 - len(prefix))
510
+ slug = slug[:max_slug].rstrip("-")
511
+ branch = f"{type_}/{prefix}{slug}"
512
+
513
+ if not _BRANCH_RE.match(branch):
514
+ sys.stderr.write(
515
+ f"ERROR: branch '{branch}' must start with feat|fix|chore|docs|test/\n"
516
+ )
517
+ return 1
518
+
519
+ if current == branch:
520
+ sys.stdout.write(f"Already on branch {branch}\n")
521
+ else:
522
+ code, _ = _git(["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"])
523
+ if code == 0:
524
+ sys.stdout.write(f"Branch {branch} already exists - switching\n")
525
+ _git(["checkout", branch])
526
+ else:
527
+ _git(["checkout", "-b", branch])
528
+
529
+ rc = cmd_push(branch, 3)
530
+ if rc != 0:
531
+ return rc
532
+
533
+ code, url = _gh(["pr", "view", "--json", "url", "-q", ".url"])
534
+ if code == 0 and url.strip():
535
+ sys.stdout.write(f"Draft PR already exists: {url.strip()}\n")
536
+ return 0
537
+
538
+ # Conventional Commits, no scope = no linked issue (ADR-006)
539
+ pr_title = f"{type_}: {title}"
540
+ pr_body = "No linked issue (nojira)."
541
+ args = ["pr", "create", "--draft", "--title", pr_title, "--body", pr_body]
542
+ if base:
543
+ args += ["--base", base]
544
+ code, out = _gh(args)
545
+ if code != 0:
546
+ sys.stderr.write("create-pr-nojira: gh pr create failed\n")
547
+ return code
548
+ sys.stdout.write(f"Draft PR: {out.strip()}\n")
549
+ return 0
550
+
551
+
552
+ def main(argv: list[str] | None = None) -> int:
553
+ parser = argparse.ArgumentParser(prog="dag_workflow")
554
+ sub = parser.add_subparsers(dest="cmd", required=True)
555
+
556
+ p_check = sub.add_parser("check", help="check whether a node is reachable")
557
+ p_check.add_argument("node", help="DAG node name (e.g. pr.merged)")
558
+
559
+ sub.add_parser("guard", help="PreToolUse hook entrypoint (reads stdin)")
560
+ sub.add_parser("nodes", help="list all DAG nodes")
561
+
562
+ p_push = sub.add_parser("push", help="push current branch with retry + SHA verify")
563
+ p_push.add_argument("branch", nargs="?", default=None)
564
+ p_push.add_argument("max_retries", nargs="?", type=int, default=3)
565
+ p_push.add_argument(
566
+ "--force-with-lease",
567
+ action="store_true",
568
+ dest="force_with_lease",
569
+ help="force-push safely after a rebase (refused on main/master)",
570
+ )
571
+
572
+ p_promote = sub.add_parser("promote", help="mark a draft PR ready for review")
573
+ p_promote.add_argument("pr_number", nargs="?", type=int, default=None)
574
+
575
+ p_finish = sub.add_parser("finish", help="push, promote, monitor_pr --merge")
576
+ p_finish.add_argument("pr_number", nargs="?", type=int, default=None)
577
+ p_finish.add_argument("--review-cycle", type=int, default=None)
578
+
579
+ p_nojira = sub.add_parser(
580
+ "create-pr-nojira",
581
+ help="create a no-issue branch + draft PR",
582
+ )
583
+ p_nojira.add_argument("type", choices=sorted(_VALID_TYPES))
584
+ p_nojira.add_argument("title")
585
+ p_nojira.add_argument("--branch", default=None)
586
+ p_nojira.add_argument("--base", default=None)
587
+
588
+ args = parser.parse_args(argv)
589
+
590
+ if args.cmd == "check":
591
+ return cmd_check(args.node)
592
+ if args.cmd == "guard":
593
+ return cmd_guard()
594
+ if args.cmd == "nodes":
595
+ for node, prereqs in GRAPH.items():
596
+ sys.stdout.write(f"{node}: requires={prereqs or '[]'}\n")
597
+ return 0
598
+ if args.cmd == "push":
599
+ return cmd_push(args.branch, args.max_retries, force=args.force_with_lease)
600
+ if args.cmd == "promote":
601
+ return cmd_promote(args.pr_number)
602
+ if args.cmd == "finish":
603
+ return cmd_finish(args.pr_number, args.review_cycle)
604
+ if args.cmd == "create-pr-nojira":
605
+ return cmd_create_pr_nojira(args.type, args.title, args.branch, args.base)
606
+ return 1
607
+
608
+
609
+ if __name__ == "__main__":
610
+ sys.exit(main())
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ # github_command_guard.sh — delegate to dag_workflow.py guard.
3
+ # PreToolUse hook on Bash. Receives tool input JSON on stdin.
4
+ #
5
+ # All command-pattern matching, redirect rules, and DAG prerequisite checks
6
+ # live in .claude/hooks/dag_workflow.py. Adding a new banned command means
7
+ # editing COMMAND_RULES there, not this file.
8
+
9
+ set -euo pipefail
10
+
11
+ exec python3 "$(dirname "$0")/dag_workflow.py" guard
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # workflow_state_reminder.sh — inject the full lifecycle rules when a prompt
3
+ # mentions GitHub workflow actions, plus the current DAG state if available.
4
+ # UserPromptSubmit hook. Receives prompt JSON on stdin.
5
+
6
+ set -euo pipefail
7
+
8
+ INPUT=$(cat)
9
+
10
+ # Try to derive a current-state snapshot from dag_workflow.py.
11
+ # Failures are non-fatal — the static rules are always injected.
12
+ DAG_STATE=$(python3 "$(dirname "$0")/dag_workflow.py" nodes 2>/dev/null || true)
13
+
14
+ printf '%s' "$INPUT" | DAG_STATE="$DAG_STATE" python3 -c '
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+
20
+ try:
21
+ data = json.load(sys.stdin)
22
+ except Exception:
23
+ sys.exit(0)
24
+
25
+ prompt = (
26
+ data.get("prompt")
27
+ or data.get("user_prompt")
28
+ or data.get("message")
29
+ or ""
30
+ )
31
+
32
+ trigger = re.search(
33
+ r"\b(start work|implement|push|merge|finish|create issue|new issue|"
34
+ r"create pr|open pr|pull request|review|ticket|branch|ship)\b",
35
+ prompt,
36
+ re.I,
37
+ )
38
+ if not trigger:
39
+ sys.exit(0)
40
+
41
+ dag_state = os.environ.get("DAG_STATE", "").strip()
42
+ state_block = f"\n\nCurrent DAG nodes:\n{dag_state}\n" if dag_state else ""
43
+
44
+ context = (
45
+ "GitHub workflow rules (enforced by .claude/hooks/dag_workflow.py):\n"
46
+ "\n"
47
+ "Lifecycle order (DAG):\n"
48
+ " issue.created -> branch.created -> branch.pushed -> pr.opened\n"
49
+ " \\-> ci.green -+\n"
50
+ " \\-> review.approved -+-> pr.merged\n"
51
+ "\n"
52
+ "Use these scripts. Do NOT call the raw command — the DAG hook will block:\n"
53
+ " - .claude/scripts/create_issue.sh (not: gh issue create)\n"
54
+ " - .claude/scripts/start_issue.sh (not: gh pr create, for issue-backed)\n"
55
+ " - .claude/scripts/create_nojira_pr.sh (not: gh pr create, for no-issue work)\n"
56
+ " - .claude/scripts/push_branch.sh (not: git push)\n"
57
+ " - .claude/scripts/promote_review.sh (not: gh pr ready)\n"
58
+ " - .claude/scripts/monitor_pr.sh <pr> --merge (not: gh pr merge / gh api .../merge / gh pr checks --watch)\n"
59
+ "\n"
60
+ "Naming:\n"
61
+ " branch: <type>/PI-<n>-<kebab-slug> e.g. feat/PI-98-dag-workflow\n"
62
+ " PR title: type(PI-N): description e.g. feat(PI-98): Add DAG enforcement\n"
63
+ " (no scope = no linked issue, e.g. fix: Correct typo) — ADR-006\n"
64
+ " PR body: must include `Closes #N`\n"
65
+ "\n"
66
+ "Iterating before push: edit, test, debug freely. The DAG only fires on\n"
67
+ "guarded commands (push, PR create/ready/merge). Push only when ready.\n"
68
+ f"{state_block}"
69
+ )
70
+
71
+ print(json.dumps({"additionalContext": context}))
72
+ '