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.
- project_init-0.3.0/.claude/hooks/dag_workflow.py +610 -0
- project_init-0.3.0/.claude/hooks/github_command_guard.sh +11 -0
- project_init-0.3.0/.claude/hooks/workflow_state_reminder.sh +72 -0
- project_init-0.3.0/.claude/scripts/create_nojira_pr.sh +3 -0
- project_init-0.3.0/.claude/scripts/finish_pr.sh +3 -0
- project_init-0.3.0/.claude/scripts/monitor_pr.sh +249 -0
- project_init-0.3.0/.claude/scripts/promote_review.sh +3 -0
- project_init-0.3.0/.claude/scripts/push_branch.sh +5 -0
- project_init-0.3.0/.claude/scripts/push_wiki.sh +54 -0
- project_init-0.3.0/.claude/settings.json +28 -0
- project_init-0.3.0/.claude/skills/INDEX.md +29 -0
- project_init-0.3.0/.claude/skills/README.md +14 -0
- project_init-0.3.0/.claude/skills/add_command/SKILL.md +59 -0
- project_init-0.3.0/.claude/skills/add_hook/SKILL.md +108 -0
- project_init-0.3.0/.claude/skills/github_workflow/SKILL.md +90 -0
- project_init-0.3.0/.claude/skills/session_summary/SKILL.md +35 -0
- project_init-0.3.0/.claude/skills/start_task/SKILL.md +58 -0
- project_init-0.3.0/.claude/skills/wiki/SKILL.md +84 -0
- project_init-0.3.0/.claude/skills/wiki/templates/architecture.md +27 -0
- project_init-0.3.0/.claude/skills/wiki/templates/implementation-guide.md +57 -0
- project_init-0.3.0/.claude/skills/wiki/templates/preset-guide.md +50 -0
- project_init-0.3.0/.claude/skills/wiki/templates/scaffolder-logic.md +50 -0
- project_init-0.3.0/.claude-plugin/marketplace.json +14 -0
- project_init-0.3.0/.gitattributes +1 -0
- project_init-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +68 -0
- project_init-0.3.0/.github/ISSUE_TEMPLATE/chore.yml +58 -0
- project_init-0.3.0/.github/ISSUE_TEMPLATE/docs.yml +53 -0
- project_init-0.3.0/.github/ISSUE_TEMPLATE/feature.yml +77 -0
- project_init-0.3.0/.github/ISSUE_TEMPLATE/test.yml +54 -0
- project_init-0.3.0/.github/copilot-instructions.md +25 -0
- project_init-0.3.0/.github/workflows/board-automation.yml +204 -0
- project_init-0.3.0/.github/workflows/ci.yml +99 -0
- project_init-0.3.0/.github/workflows/docs.yml +35 -0
- project_init-0.3.0/.github/workflows/release.yml +81 -0
- project_init-0.3.0/.github/workflows/review-status.yml +48 -0
- project_init-0.3.0/.github/workflows/validate-pr.yml +64 -0
- project_init-0.3.0/.gitignore +40 -0
- project_init-0.3.0/AGENTS.md +9 -0
- project_init-0.3.0/CLAUDE.md +93 -0
- project_init-0.3.0/GEMINI.md +9 -0
- project_init-0.3.0/LICENSE +201 -0
- project_init-0.3.0/PKG-INFO +342 -0
- project_init-0.3.0/README.md +311 -0
- project_init-0.3.0/cliff.toml +32 -0
- project_init-0.3.0/docs/README.md +22 -0
- project_init-0.3.0/docs/adr/adr-001-scaffolder-design.md +22 -0
- project_init-0.3.0/docs/adr/adr-002-dotglob-template-convention.md +22 -0
- project_init-0.3.0/docs/adr/adr-003-github-native-workflow.md +28 -0
- project_init-0.3.0/docs/adr/adr-004-obsidian-docs-integration.md +34 -0
- project_init-0.3.0/docs/adr/adr-005-github-pr-board-workflow.md +95 -0
- project_init-0.3.0/docs/adr/adr-006-conventional-commit-titles.md +51 -0
- project_init-0.3.0/docs/adr/adr-007-security-enforcement-layers.md +71 -0
- project_init-0.3.0/docs/adr/adr-008-distribution-channel.md +45 -0
- project_init-0.3.0/docs/adr/adr-009-graphify-memory-preset.md +61 -0
- project_init-0.3.0/docs/adr/adr-010-plugin-marketplace-dual-ship.md +73 -0
- project_init-0.3.0/docs/adr/adr-011-pypi-trusted-publishing.md +56 -0
- project_init-0.3.0/docs/adr/adr-012-prod-safety-guard.md +57 -0
- project_init-0.3.0/docs/development/build-reproducibility.md +24 -0
- project_init-0.3.0/docs/development/contributing.md +35 -0
- project_init-0.3.0/docs/development/template-system.md +82 -0
- project_init-0.3.0/docs/development/testing.md +59 -0
- project_init-0.3.0/docs/guides/using-project-init.md +215 -0
- project_init-0.3.0/install.sh +125 -0
- project_init-0.3.0/justfile +30 -0
- project_init-0.3.0/mkdocs.yml +32 -0
- project_init-0.3.0/plugins/project-init-workflow/.claude-plugin/plugin.json +19 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/dag_workflow.py +610 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/github_command_guard.sh +11 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/hooks.json +60 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/post_edit_lint.sh +58 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/pre_commit_gate.sh +81 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/prod_guard.py +140 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/session_setup.sh +62 -0
- project_init-0.3.0/plugins/project-init-workflow/hooks/workflow_state_reminder.sh +72 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/add_adr/SKILL.md +33 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/add_command/SKILL.md +63 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/add_hook/SKILL.md +112 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/audit/SKILL.md +146 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/create_issue/SKILL.md +59 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/github_workflow/SKILL.md +80 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/request_review/SKILL.md +19 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/review/SKILL.md +17 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/save_memory/SKILL.md +17 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/session_summary/SKILL.md +35 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/start_task/SKILL.md +48 -0
- project_init-0.3.0/plugins/project-init-workflow/skills/status/SKILL.md +15 -0
- project_init-0.3.0/pyproject.toml +88 -0
- project_init-0.3.0/renovate.json +24 -0
- project_init-0.3.0/src/project_init/__init__.py +4 -0
- project_init-0.3.0/src/project_init/__main__.py +662 -0
- project_init-0.3.0/src/project_init/mcps.py +57 -0
- project_init-0.3.0/src/project_init/scaffold.py +374 -0
- project_init-0.3.0/src/project_init/upgrade.py +569 -0
- project_init-0.3.0/templates/base/AGENTS.md.tmpl +50 -0
- project_init-0.3.0/templates/base/CLAUDE.md.tmpl +16 -0
- project_init-0.3.0/templates/base/CONTRIBUTING.md.tmpl +55 -0
- project_init-0.3.0/templates/base/GEMINI.md.tmpl +16 -0
- project_init-0.3.0/templates/base/LICENSE.tmpl +231 -0
- project_init-0.3.0/templates/base/SECURITY.md.tmpl +26 -0
- project_init-0.3.0/templates/base/docs/explanation/index.md +9 -0
- project_init-0.3.0/templates/base/docs/how-to/index.md +7 -0
- project_init-0.3.0/templates/base/docs/index.md.tmpl +20 -0
- project_init-0.3.0/templates/base/docs/reference/index.md +13 -0
- project_init-0.3.0/templates/base/docs/tutorials/index.md +7 -0
- project_init-0.3.0/templates/base/dot_claude/agents/README.md +30 -0
- project_init-0.3.0/templates/base/dot_claude/config.yaml.tmpl +31 -0
- project_init-0.3.0/templates/base/dot_claude/docs/README.md +26 -0
- project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-001-memory-stack.md.tmpl +22 -0
- project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-002-mcp-choices.md.tmpl +32 -0
- project_init-0.3.0/templates/base/dot_claude/docs/adr/adr-template.md +29 -0
- project_init-0.3.0/templates/base/dot_claude/docs/development/conventions.md.tmpl +31 -0
- project_init-0.3.0/templates/base/dot_claude/docs/development/testing.md +25 -0
- project_init-0.3.0/templates/base/dot_claude/docs/guides/developer-onboarding.md +110 -0
- project_init-0.3.0/templates/base/dot_claude/docs/guides/issue-metadata.md +27 -0
- project_init-0.3.0/templates/base/dot_claude/docs/guides/secrets.md +50 -0
- project_init-0.3.0/templates/base/dot_claude/docs/guides/using-memory.md +36 -0
- project_init-0.3.0/templates/base/dot_claude/hooks/README.md +15 -0
- project_init-0.3.0/templates/base/dot_claude/hooks/agent_guard_adapter.py.tmpl +64 -0
- project_init-0.3.0/templates/base/dot_claude/hooks/dag_workflow.py +610 -0
- project_init-0.3.0/templates/base/dot_claude/memory/MEMORY.md.tmpl +11 -0
- project_init-0.3.0/templates/base/dot_claude/memory/README.md +51 -0
- project_init-0.3.0/templates/base/dot_claude/memory/SCHEMA.md +52 -0
- project_init-0.3.0/templates/base/dot_claude/memory/feedback_conventions.md +11 -0
- project_init-0.3.0/templates/base/dot_claude/memory/project_context.md.tmpl +11 -0
- project_init-0.3.0/templates/base/dot_claude/memory/user_role.md +7 -0
- project_init-0.3.0/templates/base/dot_claude/project-init.md.tmpl +174 -0
- project_init-0.3.0/templates/base/dot_claude/rules/go.md +14 -0
- project_init-0.3.0/templates/base/dot_claude/rules/hooks.md +30 -0
- project_init-0.3.0/templates/base/dot_claude/rules/node.md +17 -0
- project_init-0.3.0/templates/base/dot_claude/rules/python.md +25 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/README.md +15 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/create_issue.sh +577 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/create_nojira_pr.sh +3 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/finish_pr.sh +3 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/install_hooks.sh +55 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/monitor_pr.sh +270 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/promote_review.sh +3 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/push_branch.sh +5 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/push_wiki.sh +34 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/setup_github.sh +219 -0
- project_init-0.3.0/templates/base/dot_claude/scripts/start_issue.sh +134 -0
- project_init-0.3.0/templates/base/dot_claude/settings.json.tmpl +83 -0
- project_init-0.3.0/templates/base/dot_claude/skills/README.md +12 -0
- project_init-0.3.0/templates/base/dot_claude/skills/plan/SKILL.md.tmpl +40 -0
- project_init-0.3.0/templates/base/dot_claude/vault/README.md +21 -0
- project_init-0.3.0/templates/base/dot_claude/vault/decisions/README.md +22 -0
- project_init-0.3.0/templates/base/dot_claude/vault/design/README.md +3 -0
- project_init-0.3.0/templates/base/dot_claude/vault/knowledge/README.md +5 -0
- project_init-0.3.0/templates/base/dot_claude/vault/sessions/README.md +5 -0
- project_init-0.3.0/templates/base/dot_devcontainer/devcontainer.json.tmpl +17 -0
- project_init-0.3.0/templates/base/dot_devcontainer/post-create.sh.tmpl +31 -0
- project_init-0.3.0/templates/base/dot_env.example.tmpl +13 -0
- project_init-0.3.0/templates/base/dot_github/CODEOWNERS.tmpl +12 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/bug.yml +98 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/chore.yml +82 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/config.yml +5 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/docs.yml +84 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/feature.yml +87 -0
- project_init-0.3.0/templates/base/dot_github/ISSUE_TEMPLATE/test.yml +90 -0
- project_init-0.3.0/templates/base/dot_github/copilot-instructions.md.tmpl +25 -0
- project_init-0.3.0/templates/base/dot_github/hooks/commit-msg +52 -0
- project_init-0.3.0/templates/base/dot_github/hooks/pre-commit +16 -0
- project_init-0.3.0/templates/base/dot_github/hooks/pre-push +51 -0
- project_init-0.3.0/templates/base/dot_github/pull_request_template.md +22 -0
- project_init-0.3.0/templates/base/dot_github/workflows/board-automation.yml +232 -0
- project_init-0.3.0/templates/base/dot_github/workflows/ci.yml.tmpl +204 -0
- project_init-0.3.0/templates/base/dot_github/workflows/docs.yml.tmpl +98 -0
- project_init-0.3.0/templates/base/dot_github/workflows/issue-validation.yml +72 -0
- project_init-0.3.0/templates/base/dot_github/workflows/review-status.yml +48 -0
- project_init-0.3.0/templates/base/dot_github/workflows/validate-pr.yml +103 -0
- project_init-0.3.0/templates/base/dot_gitignore.tmpl +41 -0
- project_init-0.3.0/templates/base/dot_golangci.yml.tmpl +20 -0
- project_init-0.3.0/templates/base/dot_vscode/extensions.json.tmpl +10 -0
- project_init-0.3.0/templates/base/dot_vscode/settings.json.tmpl +8 -0
- project_init-0.3.0/templates/base/eslint.config.mjs.tmpl +29 -0
- project_init-0.3.0/templates/base/justfile.tmpl +95 -0
- project_init-0.3.0/templates/base/mise.toml.tmpl +20 -0
- project_init-0.3.0/templates/base/mkdocs.yml.tmpl +32 -0
- project_init-0.3.0/templates/base/renovate.json +14 -0
- project_init-0.3.0/templates/base/ruff.toml.tmpl +31 -0
- project_init-0.3.0/templates/base/typedoc.json.tmpl +14 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/audit/SKILL.md +146 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/review/SKILL.md +17 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init-0.3.0/templates/codex/dot_agents/skills/status/SKILL.md +15 -0
- project_init-0.3.0/templates/codex/dot_codex/hooks.json.tmpl +17 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/github_command_guard.sh +11 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/post_edit_lint.sh +58 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/pre_commit_gate.sh +81 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/prod_guard.py +140 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/session_setup.sh +62 -0
- project_init-0.3.0/templates/fallback/dot_claude/hooks/workflow_state_reminder.sh +72 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/INDEX.md +28 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/add_adr/SKILL.md +33 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/add_command/SKILL.md +63 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/add_hook/SKILL.md +112 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/audit/SKILL.md +146 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/create_issue/SKILL.md +59 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/github_workflow/SKILL.md +80 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/request_review/SKILL.md +19 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/review/SKILL.md +17 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/save_memory/SKILL.md +17 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/session_summary/SKILL.md +35 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/start_task/SKILL.md +48 -0
- project_init-0.3.0/templates/fallback/dot_claude/skills/status/SKILL.md +15 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/audit/SKILL.md +146 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/review/SKILL.md +17 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init-0.3.0/templates/gemini/dot_agents/skills/status/SKILL.md +15 -0
- project_init-0.3.0/templates/gemini/dot_claude/scripts/setup_gemini.sh.tmpl +16 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_adr.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_command.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/add_hook.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/audit.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/create_issue.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/github_workflow.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/request_review.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/review.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/save_memory.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/session_summary.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/start_task.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/commands/status.toml +5 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/gemini-extension.json.tmpl +6 -0
- project_init-0.3.0/templates/gemini/dot_gemini-extension/hooks/hooks.json.tmpl +18 -0
- project_init-0.3.0/templates/graphify/dot_claude/docs/guides/using-graphify.md +37 -0
- project_init-0.3.0/templates/graphify/dot_claude/rules/graphify.md +18 -0
- project_init-0.3.0/templates/graphify/dot_claude/scripts/setup_graphify.sh +40 -0
- project_init-0.3.0/templates/obsidian/dot_claude/scripts/lint_memory.sh +115 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/decisions/adr-000-project-setup.md.tmpl +22 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/README.md +31 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/app.json +6 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/community-plugins.json +1 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/dot_obsidian/core-plugins.json +1 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/log.md +6 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/decision.md +16 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/design-note.md +14 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/knowledge-note.md +12 -0
- project_init-0.3.0/templates/obsidian/dot_claude/vault/templates/session-note.md +16 -0
- project_init-0.3.0/templates/presets/obsidian-graphify.toml +16 -0
- project_init-0.3.0/templates/presets/obsidian-only.toml +14 -0
- project_init-0.3.0/tests/__init__.py +1 -0
- project_init-0.3.0/tests/conftest.py +32 -0
- project_init-0.3.0/tests/contracts/test_agent_overlays.py +177 -0
- project_init-0.3.0/tests/contracts/test_agents_canonical.py +94 -0
- project_init-0.3.0/tests/contracts/test_env_tooling.py +126 -0
- project_init-0.3.0/tests/contracts/test_governance.py +127 -0
- project_init-0.3.0/tests/contracts/test_integrity.py +184 -0
- project_init-0.3.0/tests/contracts/test_justfile.py +156 -0
- project_init-0.3.0/tests/contracts/test_plugin_marketplace.py +179 -0
- project_init-0.3.0/tests/contracts/test_prod_guard.py +177 -0
- project_init-0.3.0/tests/contracts/test_quality_toolchain.py +204 -0
- project_init-0.3.0/tests/contracts/test_release_engineering.py +125 -0
- project_init-0.3.0/tests/contracts/test_renovate.py +82 -0
- project_init-0.3.0/tests/contracts/test_scaffold_graphify.py +64 -0
- project_init-0.3.0/tests/contracts/test_scaffold_obsidian.py +304 -0
- project_init-0.3.0/tests/contracts/test_session_bootstrap.py +87 -0
- project_init-0.3.0/tests/contracts/test_skill_index.py +136 -0
- project_init-0.3.0/tests/contracts/test_templates.py +284 -0
- project_init-0.3.0/tests/contracts/test_wiki_skill.py +142 -0
- project_init-0.3.0/tests/helpers.py +84 -0
- project_init-0.3.0/tests/integration/test_cli.py +244 -0
- project_init-0.3.0/tests/integration/test_dag_workflow.py +443 -0
- project_init-0.3.0/tests/integration/test_hooks_and_safety.py +272 -0
- project_init-0.3.0/tests/integration/test_issue_metadata_workflow.py +433 -0
- project_init-0.3.0/tests/integration/test_memory_starters.py +223 -0
- project_init-0.3.0/tests/integration/test_overwrite_protection.py +158 -0
- project_init-0.3.0/tests/integration/test_quality_gate_lint.py +52 -0
- project_init-0.3.0/tests/integration/test_readme_examples.py +66 -0
- project_init-0.3.0/tests/integration/test_session_cold_start.py +140 -0
- project_init-0.3.0/tests/integration/test_upgrade.py +457 -0
- project_init-0.3.0/tests/smoke/test_packaging.py +113 -0
- project_init-0.3.0/tests/unit/test_command_variables.py +71 -0
- project_init-0.3.0/tests/unit/test_mcps.py +197 -0
- project_init-0.3.0/tests/unit/test_presets.py +30 -0
- project_init-0.3.0/tests/unit/test_render_nesting.py +35 -0
- project_init-0.3.0/tools/sync_plugin.py +144 -0
- 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
|
+
'
|