project-init 0.3.0__py3-none-any.whl
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/__init__.py +4 -0
- project_init/__main__.py +662 -0
- project_init/mcps.py +57 -0
- project_init/scaffold.py +374 -0
- project_init/templates/base/AGENTS.md.tmpl +50 -0
- project_init/templates/base/CLAUDE.md.tmpl +16 -0
- project_init/templates/base/CONTRIBUTING.md.tmpl +55 -0
- project_init/templates/base/GEMINI.md.tmpl +16 -0
- project_init/templates/base/LICENSE.tmpl +231 -0
- project_init/templates/base/SECURITY.md.tmpl +26 -0
- project_init/templates/base/docs/explanation/index.md +9 -0
- project_init/templates/base/docs/how-to/index.md +7 -0
- project_init/templates/base/docs/index.md.tmpl +20 -0
- project_init/templates/base/docs/reference/index.md +13 -0
- project_init/templates/base/docs/tutorials/index.md +7 -0
- project_init/templates/base/dot_claude/agents/README.md +30 -0
- project_init/templates/base/dot_claude/config.yaml.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/README.md +26 -0
- project_init/templates/base/dot_claude/docs/adr/adr-001-memory-stack.md.tmpl +22 -0
- project_init/templates/base/dot_claude/docs/adr/adr-002-mcp-choices.md.tmpl +32 -0
- project_init/templates/base/dot_claude/docs/adr/adr-template.md +29 -0
- project_init/templates/base/dot_claude/docs/development/conventions.md.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/development/testing.md +25 -0
- project_init/templates/base/dot_claude/docs/guides/developer-onboarding.md +110 -0
- project_init/templates/base/dot_claude/docs/guides/issue-metadata.md +27 -0
- project_init/templates/base/dot_claude/docs/guides/secrets.md +50 -0
- project_init/templates/base/dot_claude/docs/guides/using-memory.md +36 -0
- project_init/templates/base/dot_claude/hooks/README.md +15 -0
- project_init/templates/base/dot_claude/hooks/agent_guard_adapter.py.tmpl +64 -0
- project_init/templates/base/dot_claude/hooks/dag_workflow.py +610 -0
- project_init/templates/base/dot_claude/memory/MEMORY.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/README.md +51 -0
- project_init/templates/base/dot_claude/memory/SCHEMA.md +52 -0
- project_init/templates/base/dot_claude/memory/feedback_conventions.md +11 -0
- project_init/templates/base/dot_claude/memory/project_context.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/user_role.md +7 -0
- project_init/templates/base/dot_claude/project-init.md.tmpl +174 -0
- project_init/templates/base/dot_claude/rules/go.md +14 -0
- project_init/templates/base/dot_claude/rules/hooks.md +30 -0
- project_init/templates/base/dot_claude/rules/node.md +17 -0
- project_init/templates/base/dot_claude/rules/python.md +25 -0
- project_init/templates/base/dot_claude/scripts/README.md +15 -0
- project_init/templates/base/dot_claude/scripts/create_issue.sh +577 -0
- project_init/templates/base/dot_claude/scripts/create_nojira_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/finish_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/install_hooks.sh +55 -0
- project_init/templates/base/dot_claude/scripts/monitor_pr.sh +270 -0
- project_init/templates/base/dot_claude/scripts/promote_review.sh +3 -0
- project_init/templates/base/dot_claude/scripts/push_branch.sh +5 -0
- project_init/templates/base/dot_claude/scripts/push_wiki.sh +34 -0
- project_init/templates/base/dot_claude/scripts/setup_github.sh +219 -0
- project_init/templates/base/dot_claude/scripts/start_issue.sh +134 -0
- project_init/templates/base/dot_claude/settings.json.tmpl +83 -0
- project_init/templates/base/dot_claude/skills/README.md +12 -0
- project_init/templates/base/dot_claude/skills/plan/SKILL.md.tmpl +40 -0
- project_init/templates/base/dot_claude/vault/README.md +21 -0
- project_init/templates/base/dot_claude/vault/decisions/README.md +22 -0
- project_init/templates/base/dot_claude/vault/design/README.md +3 -0
- project_init/templates/base/dot_claude/vault/knowledge/README.md +5 -0
- project_init/templates/base/dot_claude/vault/sessions/README.md +5 -0
- project_init/templates/base/dot_devcontainer/devcontainer.json.tmpl +17 -0
- project_init/templates/base/dot_devcontainer/post-create.sh.tmpl +31 -0
- project_init/templates/base/dot_env.example.tmpl +13 -0
- project_init/templates/base/dot_github/CODEOWNERS.tmpl +12 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/bug.yml +98 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/chore.yml +82 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/config.yml +5 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/docs.yml +84 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/feature.yml +87 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/test.yml +90 -0
- project_init/templates/base/dot_github/copilot-instructions.md.tmpl +25 -0
- project_init/templates/base/dot_github/hooks/commit-msg +52 -0
- project_init/templates/base/dot_github/hooks/pre-commit +16 -0
- project_init/templates/base/dot_github/hooks/pre-push +51 -0
- project_init/templates/base/dot_github/pull_request_template.md +22 -0
- project_init/templates/base/dot_github/workflows/board-automation.yml +232 -0
- project_init/templates/base/dot_github/workflows/ci.yml.tmpl +204 -0
- project_init/templates/base/dot_github/workflows/docs.yml.tmpl +98 -0
- project_init/templates/base/dot_github/workflows/issue-validation.yml +72 -0
- project_init/templates/base/dot_github/workflows/review-status.yml +48 -0
- project_init/templates/base/dot_github/workflows/validate-pr.yml +103 -0
- project_init/templates/base/dot_gitignore.tmpl +41 -0
- project_init/templates/base/dot_golangci.yml.tmpl +20 -0
- project_init/templates/base/dot_vscode/extensions.json.tmpl +10 -0
- project_init/templates/base/dot_vscode/settings.json.tmpl +8 -0
- project_init/templates/base/eslint.config.mjs.tmpl +29 -0
- project_init/templates/base/justfile.tmpl +95 -0
- project_init/templates/base/mise.toml.tmpl +20 -0
- project_init/templates/base/mkdocs.yml.tmpl +32 -0
- project_init/templates/base/renovate.json +14 -0
- project_init/templates/base/ruff.toml.tmpl +31 -0
- project_init/templates/base/typedoc.json.tmpl +14 -0
- project_init/templates/codex/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/codex/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/codex/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/codex/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/codex/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/codex/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/codex/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/codex/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/codex/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/codex/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/codex/dot_codex/hooks.json.tmpl +17 -0
- project_init/templates/fallback/dot_claude/hooks/github_command_guard.sh +11 -0
- project_init/templates/fallback/dot_claude/hooks/post_edit_lint.sh +58 -0
- project_init/templates/fallback/dot_claude/hooks/pre_commit_gate.sh +81 -0
- project_init/templates/fallback/dot_claude/hooks/prod_guard.py +140 -0
- project_init/templates/fallback/dot_claude/hooks/session_setup.sh +62 -0
- project_init/templates/fallback/dot_claude/hooks/workflow_state_reminder.sh +72 -0
- project_init/templates/fallback/dot_claude/skills/INDEX.md +28 -0
- project_init/templates/fallback/dot_claude/skills/add_adr/SKILL.md +33 -0
- project_init/templates/fallback/dot_claude/skills/add_command/SKILL.md +63 -0
- project_init/templates/fallback/dot_claude/skills/add_hook/SKILL.md +112 -0
- project_init/templates/fallback/dot_claude/skills/audit/SKILL.md +146 -0
- project_init/templates/fallback/dot_claude/skills/create_issue/SKILL.md +59 -0
- project_init/templates/fallback/dot_claude/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/fallback/dot_claude/skills/request_review/SKILL.md +19 -0
- project_init/templates/fallback/dot_claude/skills/review/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/save_memory/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/session_summary/SKILL.md +35 -0
- project_init/templates/fallback/dot_claude/skills/start_task/SKILL.md +48 -0
- project_init/templates/fallback/dot_claude/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/gemini/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/gemini/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/gemini/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/gemini/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/gemini/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/gemini/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/gemini/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/gemini/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/gemini/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_claude/scripts/setup_gemini.sh.tmpl +16 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_adr.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_command.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_hook.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/audit.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/create_issue.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/github_workflow.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/request_review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/save_memory.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/session_summary.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/start_task.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/status.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/gemini-extension.json.tmpl +6 -0
- project_init/templates/gemini/dot_gemini-extension/hooks/hooks.json.tmpl +18 -0
- project_init/templates/graphify/dot_claude/docs/guides/using-graphify.md +37 -0
- project_init/templates/graphify/dot_claude/rules/graphify.md +18 -0
- project_init/templates/graphify/dot_claude/scripts/setup_graphify.sh +40 -0
- project_init/templates/obsidian/dot_claude/scripts/lint_memory.sh +115 -0
- project_init/templates/obsidian/dot_claude/vault/decisions/adr-000-project-setup.md.tmpl +22 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/README.md +31 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/app.json +6 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/community-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/core-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/log.md +6 -0
- project_init/templates/obsidian/dot_claude/vault/templates/decision.md +16 -0
- project_init/templates/obsidian/dot_claude/vault/templates/design-note.md +14 -0
- project_init/templates/obsidian/dot_claude/vault/templates/knowledge-note.md +12 -0
- project_init/templates/obsidian/dot_claude/vault/templates/session-note.md +16 -0
- project_init/templates/presets/obsidian-graphify.toml +16 -0
- project_init/templates/presets/obsidian-only.toml +14 -0
- project_init/upgrade.py +569 -0
- project_init-0.3.0.dist-info/METADATA +342 -0
- project_init-0.3.0.dist-info/RECORD +173 -0
- project_init-0.3.0.dist-info/WHEEL +4 -0
- project_init-0.3.0.dist-info/entry_points.txt +2 -0
- project_init-0.3.0.dist-info/licenses/LICENSE +201 -0
project_init/mcps.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""MCP catalog and emit helpers for the project-init wizard.
|
|
2
|
+
|
|
3
|
+
All commands use bunx (bun's npx equivalent) — no npm/npx anywhere.
|
|
4
|
+
PI-15 (replace npx with bun) is satisfied by construction here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Core MCPs always offered as a multi-select in the wizard.
|
|
10
|
+
# Absent intentionally (PI-25 / PI-26):
|
|
11
|
+
# linear — gh CLI + GitHub Issues covers all needs (~15 tools saved)
|
|
12
|
+
# github — gh CLI covers PR/issue management (~35 tools saved)
|
|
13
|
+
# filesystem — Claude Code built-in Read/Write/Edit/Glob/Grep overlap entirely (~10 tools saved)
|
|
14
|
+
MCP_CATALOG: list[dict] = [
|
|
15
|
+
{
|
|
16
|
+
"id": "context7",
|
|
17
|
+
"name": "Context7",
|
|
18
|
+
"description": "Live library documentation lookup",
|
|
19
|
+
"command": "claude mcp add context7 bunx @upstash/context7-mcp",
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Database MCPs — mutually exclusive, offered as a single-choice follow-up.
|
|
24
|
+
DB_CATALOG: dict[str, dict] = {
|
|
25
|
+
"postgres": {
|
|
26
|
+
"id": "postgres",
|
|
27
|
+
"name": "Postgres",
|
|
28
|
+
"command": "claude mcp add postgres bunx @modelcontextprotocol/server-postgres",
|
|
29
|
+
},
|
|
30
|
+
"sqlite": {
|
|
31
|
+
"id": "sqlite",
|
|
32
|
+
"name": "SQLite",
|
|
33
|
+
"command": "claude mcp add sqlite bunx mcp-server-sqlite",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Browser automation MCP — offered as a yes/no follow-up.
|
|
38
|
+
PLAYWRIGHT_MCP: dict = {
|
|
39
|
+
"id": "playwright",
|
|
40
|
+
"name": "Playwright",
|
|
41
|
+
"command": "claude mcp add playwright bunx @playwright/mcp",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_installed_mcps(selected: list[dict]) -> str:
|
|
46
|
+
"""Human-readable comma-separated list for template substitution."""
|
|
47
|
+
if not selected:
|
|
48
|
+
return "none"
|
|
49
|
+
return ", ".join(m["id"] for m in selected)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_installed_mcps_yaml(selected: list[dict]) -> str:
|
|
53
|
+
"""Inline YAML list string for config.yaml template."""
|
|
54
|
+
if not selected:
|
|
55
|
+
return "[]"
|
|
56
|
+
items = ", ".join(f'"{m["id"]}"' for m in selected)
|
|
57
|
+
return f"[{items}]"
|
project_init/scaffold.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Core scaffolding logic — pure functions, no user interaction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent
|
|
11
|
+
_TEMPLATES_DIR = _PACKAGE_DIR / "templates"
|
|
12
|
+
if not _TEMPLATES_DIR.exists():
|
|
13
|
+
# Dev mode: templates live at repo root, not inside the package.
|
|
14
|
+
_TEMPLATES_DIR = _PACKAGE_DIR.parent.parent / "templates"
|
|
15
|
+
|
|
16
|
+
_DOT_PREFIX = "dot_"
|
|
17
|
+
_VAR_RE = re.compile(r"\{\{(\w+)\}\}")
|
|
18
|
+
# Matches an INNERMOST conditional block — the tempered dot forbids another
|
|
19
|
+
# opener inside the body. _render loops to a fixpoint, so nested
|
|
20
|
+
# {{#if outer}}...{{#if inner}}...{{/if}}...{{/if}} resolves inside-out.
|
|
21
|
+
_BLOCK_RE = re.compile(
|
|
22
|
+
r"\{\{#if\s+(\w+)\}\}((?:(?!\{\{#if\s).)*?)\{\{/if(?:\s+\w+)?\}\}",
|
|
23
|
+
re.DOTALL,
|
|
24
|
+
)
|
|
25
|
+
# Used by strict mode to detect unrendered handlebars-style markers.
|
|
26
|
+
# The (?<!\$) negative lookbehind exempts GitHub Actions expressions (${{ ... }}).
|
|
27
|
+
_ANY_PLACEHOLDER_RE = re.compile(r"(?<!\$)\{\{[^}]+\}\}")
|
|
28
|
+
|
|
29
|
+
# Paths under these dirs are never overwritten on re-run (idempotency).
|
|
30
|
+
_PRESERVE_DIRS = {"memory", "vault"}
|
|
31
|
+
# Except READMEs — those are always refreshed.
|
|
32
|
+
_ALWAYS_OVERWRITE = {"README.md"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TemplateRenderError(Exception):
|
|
36
|
+
"""Raised in strict mode when unrendered placeholders survive scaffolding."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_presets() -> list[dict]:
|
|
40
|
+
"""Return all available presets as parsed dicts, sorted by name."""
|
|
41
|
+
presets_dir = _TEMPLATES_DIR / "presets"
|
|
42
|
+
results = []
|
|
43
|
+
for p in sorted(presets_dir.glob("*.toml")):
|
|
44
|
+
with p.open("rb") as f:
|
|
45
|
+
results.append(tomllib.load(f))
|
|
46
|
+
return results
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_preset(name: str) -> dict:
|
|
50
|
+
"""Load a single preset by name (e.g. 'obsidian-only')."""
|
|
51
|
+
path = _TEMPLATES_DIR / "presets" / f"{name}.toml"
|
|
52
|
+
if not path.exists():
|
|
53
|
+
available = [p.stem for p in (_TEMPLATES_DIR / "presets").glob("*.toml")]
|
|
54
|
+
msg = f"Unknown preset {name!r}. Available: {', '.join(available)}"
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
with path.open("rb") as f:
|
|
57
|
+
return tomllib.load(f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _render(text: str, variables: dict[str, str]) -> str:
|
|
61
|
+
"""Replace {{var}} placeholders and process {{#if var}}...{{/if var}} blocks.
|
|
62
|
+
|
|
63
|
+
Conditional blocks may nest; each pass substitutes the innermost blocks,
|
|
64
|
+
looping until no block remains (unclosed markers survive for strict mode
|
|
65
|
+
to flag).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def _replace_block(m: re.Match) -> str:
|
|
69
|
+
key = m.group(1)
|
|
70
|
+
return m.group(2) if variables.get(key) else ""
|
|
71
|
+
|
|
72
|
+
while True:
|
|
73
|
+
replaced = _BLOCK_RE.sub(_replace_block, text)
|
|
74
|
+
if replaced == text:
|
|
75
|
+
break
|
|
76
|
+
text = replaced
|
|
77
|
+
# Then simple variable substitution.
|
|
78
|
+
return _VAR_RE.sub(lambda m: variables.get(m.group(1), m.group(0)), text)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _dot_rename(name: str) -> str:
|
|
82
|
+
"""Rename 'dot_foo' to '.foo'."""
|
|
83
|
+
if name.startswith(_DOT_PREFIX):
|
|
84
|
+
return "." + name[len(_DOT_PREFIX) :]
|
|
85
|
+
return name
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _should_preserve(rel_path: Path, target: Path) -> bool:
|
|
89
|
+
"""Return True if this file should be skipped on re-run."""
|
|
90
|
+
dest = target / rel_path
|
|
91
|
+
if not dest.exists():
|
|
92
|
+
return False
|
|
93
|
+
# Check if any parent dir is in the preserve set.
|
|
94
|
+
if rel_path.name in _ALWAYS_OVERWRITE:
|
|
95
|
+
return False
|
|
96
|
+
return any(part in _PRESERVE_DIRS for part in rel_path.parts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _rendered_bytes(src: Path, variables: dict[str, str], is_template: bool) -> bytes | None:
|
|
100
|
+
"""Return the bytes scaffolding would write for *src*, or None if skipped.
|
|
101
|
+
|
|
102
|
+
Mirrors :func:`_emit_file`'s content rules without touching the filesystem,
|
|
103
|
+
so callers can compare against an existing file before deciding to write.
|
|
104
|
+
"""
|
|
105
|
+
if is_template:
|
|
106
|
+
rendered = _render(src.read_text(encoding="utf-8"), variables)
|
|
107
|
+
if not rendered.strip():
|
|
108
|
+
return None
|
|
109
|
+
return rendered.encode("utf-8")
|
|
110
|
+
return src.read_bytes()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _write_bytes(dest: Path, content: bytes, src: Path) -> None:
|
|
114
|
+
"""Write *content* to *dest*, preserving *src*'s executable bit."""
|
|
115
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
dest.write_bytes(content)
|
|
117
|
+
if src.stat().st_mode & 0o111:
|
|
118
|
+
dest.chmod(dest.stat().st_mode | 0o111)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _new_sibling(dest: Path, content: bytes) -> Path:
|
|
122
|
+
"""Pick a ``.new`` sibling path for a file we must not overwrite (PI-179).
|
|
123
|
+
|
|
124
|
+
Mirrors the upgrade conflict convention: an existing ``.new`` may hold a
|
|
125
|
+
user's in-progress merge, so reuse it only when its content already equals
|
|
126
|
+
the fresh render; otherwise take ``.new.1``, ``.new.2``, …
|
|
127
|
+
"""
|
|
128
|
+
candidate = dest.parent / (dest.name + ".new")
|
|
129
|
+
counter = 0
|
|
130
|
+
while candidate.exists() and candidate.read_bytes() != content:
|
|
131
|
+
counter += 1
|
|
132
|
+
candidate = dest.parent / (dest.name + f".new.{counter}")
|
|
133
|
+
return candidate
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _has_pending_sibling(dest: Path) -> bool:
|
|
137
|
+
"""True if an unresolved ``.new``/``.new.N`` sibling already exists for *dest*.
|
|
138
|
+
|
|
139
|
+
A pending sibling means a prior scaffold preserved this file and the user
|
|
140
|
+
has not merged it yet, so a re-run must keep protecting it (PI-179).
|
|
141
|
+
"""
|
|
142
|
+
base = dest.name + ".new"
|
|
143
|
+
return (dest.parent / base).exists() or any(
|
|
144
|
+
p.name.startswith(base + ".") for p in dest.parent.glob(dest.name + ".new.*")
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _protected_as_sibling(
|
|
149
|
+
src: Path, dest: Path, variables: dict[str, str], is_template: bool, *, first: bool
|
|
150
|
+
) -> Path | None:
|
|
151
|
+
"""Write the fresh render beside *dest* instead of overwriting it (PI-179).
|
|
152
|
+
|
|
153
|
+
Returns the ``.new`` sibling path written when *dest* is a user-owned file
|
|
154
|
+
that must be protected — i.e. it exists with content differing from the
|
|
155
|
+
render, and either this is the *first* scaffold or an unresolved sibling
|
|
156
|
+
from an earlier run is still pending (so a re-run does not clobber a file
|
|
157
|
+
the user has not merged). Returns None when the normal write should proceed.
|
|
158
|
+
"""
|
|
159
|
+
if not dest.exists() or not (first or _has_pending_sibling(dest)):
|
|
160
|
+
return None
|
|
161
|
+
content = _rendered_bytes(src, variables, is_template)
|
|
162
|
+
if content is None or dest.read_bytes() == content:
|
|
163
|
+
return None
|
|
164
|
+
sibling = _new_sibling(dest, content)
|
|
165
|
+
_write_bytes(sibling, content, src)
|
|
166
|
+
return sibling
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _output_rel_path(src: Path, layer_dir: Path) -> tuple[Path, bool]:
|
|
170
|
+
"""Map a template source file to its output-relative path.
|
|
171
|
+
|
|
172
|
+
Renames ``dot_`` segments to dotfiles and strips the ``.tmpl`` suffix.
|
|
173
|
+
Returns the relative path and whether the file is a render template.
|
|
174
|
+
"""
|
|
175
|
+
rel_parts = [_dot_rename(p) for p in src.relative_to(layer_dir).parts]
|
|
176
|
+
is_template = rel_parts[-1].endswith(".tmpl")
|
|
177
|
+
if is_template:
|
|
178
|
+
rel_parts[-1] = rel_parts[-1][: -len(".tmpl")]
|
|
179
|
+
return Path(*rel_parts), is_template
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _emit_file(
|
|
183
|
+
src: Path,
|
|
184
|
+
dest: Path,
|
|
185
|
+
variables: dict[str, str],
|
|
186
|
+
is_template: bool,
|
|
187
|
+
) -> str | None:
|
|
188
|
+
"""Write one template file to *dest*; return rendered text for .tmpl files.
|
|
189
|
+
|
|
190
|
+
Returns None when the file was skipped: a template whose rendered output
|
|
191
|
+
is empty or whitespace-only is not created at all. Wrapping an entire
|
|
192
|
+
.tmpl file in ``{{#if lang}}...{{/if}}`` therefore makes the file itself
|
|
193
|
+
conditional on that variable.
|
|
194
|
+
"""
|
|
195
|
+
if is_template:
|
|
196
|
+
rendered = _render(src.read_text(encoding="utf-8"), variables)
|
|
197
|
+
if not rendered.strip():
|
|
198
|
+
return None
|
|
199
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
dest.write_text(rendered, encoding="utf-8")
|
|
201
|
+
else:
|
|
202
|
+
rendered = ""
|
|
203
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
shutil.copy2(src, dest)
|
|
205
|
+
|
|
206
|
+
# Preserve executable bit.
|
|
207
|
+
if src.stat().st_mode & 0o111:
|
|
208
|
+
dest.chmod(dest.stat().st_mode | 0o111)
|
|
209
|
+
return rendered
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_no_placeholders(rendered_files: list[tuple[Path, str]]) -> None:
|
|
213
|
+
"""Raise TemplateRenderError if any rendered file kept a ``{{...}}`` marker."""
|
|
214
|
+
offenders: list[str] = []
|
|
215
|
+
for rel_path, content in rendered_files:
|
|
216
|
+
for match in _ANY_PLACEHOLDER_RE.finditer(content):
|
|
217
|
+
offenders.append(f"{rel_path}: {match.group()}")
|
|
218
|
+
if offenders:
|
|
219
|
+
msg = "strict mode: unrendered placeholders survived scaffolding:\n " + "\n ".join(
|
|
220
|
+
offenders
|
|
221
|
+
)
|
|
222
|
+
raise TemplateRenderError(msg)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _iter_layer_files(layers: list[str]):
|
|
226
|
+
"""Yield (src, layer_dir) for every file across the preset's template layers."""
|
|
227
|
+
for layer_name in layers:
|
|
228
|
+
layer_dir = _TEMPLATES_DIR / layer_name
|
|
229
|
+
if not layer_dir.exists():
|
|
230
|
+
msg = f"Template layer {layer_name!r} not found at {layer_dir}"
|
|
231
|
+
raise FileNotFoundError(msg)
|
|
232
|
+
for src in sorted(layer_dir.rglob("*")):
|
|
233
|
+
if src.is_dir():
|
|
234
|
+
continue
|
|
235
|
+
yield src, layer_dir
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _commit_staged(
|
|
239
|
+
work_dir: Path,
|
|
240
|
+
target: Path,
|
|
241
|
+
staged: list[Path],
|
|
242
|
+
*,
|
|
243
|
+
first: bool = False,
|
|
244
|
+
conflicts: list[tuple[Path, Path]] | None = None,
|
|
245
|
+
) -> list[Path]:
|
|
246
|
+
"""Copy validated files from the strict-mode staging dir into *target*.
|
|
247
|
+
|
|
248
|
+
Honors rerun idempotency: user-owned memory/vault files are not overwritten.
|
|
249
|
+
When a *conflicts* list is passed (protection, PI-179), a user-owned file
|
|
250
|
+
with different content is kept and the fresh render lands as a ``.new``
|
|
251
|
+
sibling. A file is user-owned on the *first* scaffold, or on any run while an
|
|
252
|
+
unresolved sibling from an earlier run is still pending. Each protected file
|
|
253
|
+
is recorded as ``(original_rel, sibling_rel)``.
|
|
254
|
+
"""
|
|
255
|
+
created: list[Path] = []
|
|
256
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
for rel_path in staged:
|
|
258
|
+
if _should_preserve(rel_path, target):
|
|
259
|
+
continue
|
|
260
|
+
src = work_dir / rel_path
|
|
261
|
+
dest = target / rel_path
|
|
262
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
protect = conflicts is not None and (first or _has_pending_sibling(dest))
|
|
264
|
+
if protect and dest.exists() and dest.read_bytes() != src.read_bytes():
|
|
265
|
+
sibling = _new_sibling(dest, src.read_bytes())
|
|
266
|
+
shutil.copy2(src, sibling)
|
|
267
|
+
conflicts.append((rel_path, sibling.relative_to(target)))
|
|
268
|
+
continue
|
|
269
|
+
shutil.copy2(src, dest)
|
|
270
|
+
created.append(rel_path)
|
|
271
|
+
return created
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def scaffold(
|
|
275
|
+
target: Path,
|
|
276
|
+
preset: dict,
|
|
277
|
+
variables: dict[str, str],
|
|
278
|
+
*,
|
|
279
|
+
strict: bool = False,
|
|
280
|
+
conflicts: list[tuple[Path, Path]] | None = None,
|
|
281
|
+
) -> list[Path]:
|
|
282
|
+
"""Copy + render template layers into *target*. Return created file paths.
|
|
283
|
+
|
|
284
|
+
A .tmpl file whose rendered output is empty or whitespace-only is skipped
|
|
285
|
+
entirely (see :func:`_emit_file`) — this is how language-specific config
|
|
286
|
+
files are made conditional.
|
|
287
|
+
|
|
288
|
+
When *strict* is True, raise :class:`TemplateRenderError` if any
|
|
289
|
+
``{{...}}`` placeholder or unclosed conditional survives rendering.
|
|
290
|
+
|
|
291
|
+
In strict mode, all output is written to a temporary directory first.
|
|
292
|
+
Only on successful validation are rendered files committed to target.
|
|
293
|
+
|
|
294
|
+
Passing a *conflicts* list turns on overwrite protection (PI-179): a
|
|
295
|
+
user-owned file whose content differs from the fresh render is never
|
|
296
|
+
overwritten — the render is written as a ``<file>.new`` sibling and the pair
|
|
297
|
+
``(original_rel, sibling_rel)`` is appended to *conflicts*. A file counts as
|
|
298
|
+
user-owned on the first scaffold (no recorded ``config.yaml``) or, on any
|
|
299
|
+
later run, while an unresolved sibling from an earlier run is still pending —
|
|
300
|
+
so a re-run never clobbers a file the user has not merged yet. memory/vault
|
|
301
|
+
preservation is unchanged either way.
|
|
302
|
+
"""
|
|
303
|
+
import uuid
|
|
304
|
+
|
|
305
|
+
first_scaffold = not (target / ".claude" / "config.yaml").exists()
|
|
306
|
+
|
|
307
|
+
layers: list[str] = preset["layers"]
|
|
308
|
+
created: list[Path] = []
|
|
309
|
+
staged: list[Path] = []
|
|
310
|
+
written: set[Path] = set() # paths this run has emitted (later layers may overwrite)
|
|
311
|
+
rendered_files: list[tuple[Path, str]] = [] # for strict-mode scan
|
|
312
|
+
|
|
313
|
+
# For strict mode: write to temp, validate, then copy into target.
|
|
314
|
+
# Non-strict: write directly to target (best-effort behavior acceptable per PI-21).
|
|
315
|
+
if strict:
|
|
316
|
+
# Use a temp directory under target.parent so staged files are close to
|
|
317
|
+
# the final target. UUID suffix prevents collisions.
|
|
318
|
+
temp_suffix = f".partial-{uuid.uuid4().hex[:8]}"
|
|
319
|
+
work_dir = target.parent / (target.name + temp_suffix)
|
|
320
|
+
else:
|
|
321
|
+
work_dir = target
|
|
322
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
for src, layer_dir in _iter_layer_files(layers):
|
|
326
|
+
rel_path, is_template = _output_rel_path(src, layer_dir)
|
|
327
|
+
|
|
328
|
+
# For non-strict mode, check preservation against the actual target.
|
|
329
|
+
# For strict mode, we're writing to temp, so skip preservation check.
|
|
330
|
+
if not strict and _should_preserve(rel_path, target):
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
# Overwrite protection (non-strict; strict handles it at commit time
|
|
334
|
+
# in _commit_staged): never clobber a user-owned file — write the
|
|
335
|
+
# render as a `.new` sibling instead (PI-179). Skip when an earlier
|
|
336
|
+
# layer of THIS run already wrote the path: later layers legitimately
|
|
337
|
+
# overwrite earlier ones, that is not a user file. work_dir == target
|
|
338
|
+
# in non-strict mode, so dest is the real file.
|
|
339
|
+
if (
|
|
340
|
+
not strict
|
|
341
|
+
and conflicts is not None
|
|
342
|
+
and rel_path not in written
|
|
343
|
+
and (
|
|
344
|
+
sibling := _protected_as_sibling(
|
|
345
|
+
src, work_dir / rel_path, variables, is_template, first=first_scaffold
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
is not None
|
|
349
|
+
):
|
|
350
|
+
conflicts.append((rel_path, rel_path.parent / sibling.name))
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
rendered = _emit_file(src, work_dir / rel_path, variables, is_template)
|
|
354
|
+
if rendered is None:
|
|
355
|
+
continue
|
|
356
|
+
if is_template and strict:
|
|
357
|
+
rendered_files.append((rel_path, rendered))
|
|
358
|
+
(staged if strict else created).append(rel_path)
|
|
359
|
+
written.add(rel_path)
|
|
360
|
+
|
|
361
|
+
if strict:
|
|
362
|
+
_validate_no_placeholders(rendered_files)
|
|
363
|
+
created = _commit_staged(
|
|
364
|
+
work_dir, target, staged, first=first_scaffold, conflicts=conflicts
|
|
365
|
+
)
|
|
366
|
+
shutil.rmtree(work_dir)
|
|
367
|
+
|
|
368
|
+
except Exception:
|
|
369
|
+
# On any error (validation or I/O), clean up temp directory in strict mode.
|
|
370
|
+
if strict and work_dir.exists():
|
|
371
|
+
shutil.rmtree(work_dir)
|
|
372
|
+
raise
|
|
373
|
+
|
|
374
|
+
return created
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# {{project_name}}
|
|
2
|
+
|
|
3
|
+
{{project_description}}
|
|
4
|
+
|
|
5
|
+
Canonical instructions for **all** coding agents (and humans pairing with
|
|
6
|
+
them). Claude Code reads [CLAUDE.md](CLAUDE.md), which redirects here.
|
|
7
|
+
|
|
8
|
+
## Start here
|
|
9
|
+
|
|
10
|
+
All agentic-development infrastructure lives under [`.claude/`](.claude/) —
|
|
11
|
+
the directory name is historical; the contents are agent-neutral (bash
|
|
12
|
+
scripts, markdown skills, memory, and docs):
|
|
13
|
+
|
|
14
|
+
- [`.claude/project-init.md`](.claude/project-init.md) — workflow, conventions, task tracking
|
|
15
|
+
- [`.claude/memory/MEMORY.md`](.claude/memory/MEMORY.md) — memory index (read first for context)
|
|
16
|
+
- [`.claude/docs/`](.claude/docs/) — system of record: ADRs and development guides
|
|
17
|
+
- [`.claude/vault/`](.claude/vault/) — human workspace (Obsidian vault)
|
|
18
|
+
|
|
19
|
+
## Skills (load on demand)
|
|
20
|
+
|
|
21
|
+
Before any GitHub action (create issue, branch, push, PR, merge), check
|
|
22
|
+
the available skills (plugin-provided in the default scaffold;{{#if no_plugin}} indexed in [`.claude/skills/INDEX.md`](.claude/skills/INDEX.md);{{/if no_plugin}} `/help` lists them) and load the relevant
|
|
23
|
+
skill file. Skills are plain markdown (SKILL.md) — load only the one that
|
|
24
|
+
matches what you are about to do.
|
|
25
|
+
|
|
26
|
+
> Scaffolded with [project-init]({{project_init_url}}) on {{created_date}}.
|
|
27
|
+
|
|
28
|
+
## Key rules for agents
|
|
29
|
+
|
|
30
|
+
- **TDD** — write failing tests before any implementation.
|
|
31
|
+
- **GitHub Projects** — work is tracked on the GitHub Projects board backed by GitHub Issues. Before starting non-trivial work, create or reference an issue.
|
|
32
|
+
- **GitHub workflow** — for any push, PR, review, or merge action, load the `github_workflow` skill{{#if no_plugin}} (`.claude/skills/github_workflow/SKILL.md`){{/if no_plugin}}. Quick ref: branch = `<type>/<KEY>-<n>-<slug>` | PR title = `type(KEY-N): desc` (no scope = no issue) | body includes `Closes #N`.
|
|
33
|
+
{{#if lint_command}}- **Commands** — the `justfile` is the canonical command surface: `just --list` shows every recipe (`setup`, `lint`, `format`, `test`, `docs`, `ci`). Prefer `just <recipe>` over raw tool invocations so every agent and CI run the same commands.
|
|
34
|
+
- **Lint** — `just lint` must pass before closing a task. The linter config enforces docstrings and complexity caps on project code — fix the code, don't loosen the gate.
|
|
35
|
+
{{/if}}- **Docs** — follow the Diátaxis layout in [`docs/`](docs/) (see `docs/index.md`). Record architectural decisions with the `add_adr` skill.
|
|
36
|
+
- **Ownership boundaries** — each tool owns exactly one concern: {{#if mise}}`mise` owns toolchain versions, {{/if mise}}uv/bun own dependencies, `just` owns commands, `.env` owns environment variables. Don't blur them (no mise tasks/env, no version pins in scripts, no commands outside the justfile).
|
|
37
|
+
- **No secrets in code** — never hardcode API keys, tokens, or personal data. Copy `.env.example` to `.env` (gitignored) and load it explicitly; see `.claude/docs/guides/secrets.md` for the escalation path to org secret managers.
|
|
38
|
+
- **No prod credentials in agent sessions** — destructive infra/DB commands are flagged by the `prod_guard` hook (ask interactively, hard-block in fully autonomous mode; escape hatch: `safety.allow` in `.claude/config.yaml`). That guard is a guardrail only — the guarantee is credential separation: production credentials belong to review-gated CI jobs, never to a shell an agent runs in (ADR-012).
|
|
39
|
+
- **Enforcement is agent-agnostic** — secret scanning (gitleaks) and lifecycle gating run as git hooks plus CI checks (`validate-pr`, `secret-scan`), binding every agent and human alike. Run `.claude/scripts/install_hooks.sh` once per clone to activate them.
|
|
40
|
+
{{#if other_agents}}- **Agent support tiers** — only the Claude Code path is functionally CI-tested.{{#if codex}} Codex: skills are discoverable under `.agents/skills/` and the command guard runs via `.codex/hooks.json` (adapter: `.claude/hooks/agent_guard_adapter.py`).{{/if codex}}{{#if gemini}} Gemini CLI: link the project extension once per clone (`.claude/scripts/setup_gemini.sh`) for workflow /commands and the command guard; `GEMINI.md` stays the instruction entry point.{{/if gemini}}{{#if ollama}} Ollama-based agents (Aider, Goose, OpenHands, …): instructions-level only — this file, the portable bash lifecycle scripts, and markdown memory; no agent-specific wiring is maintained.{{/if ollama}} Codex/Gemini overlays are validated by contract tests on the rendered files, not by running those agents; the real security boundary for every agent is the git/CI enforcement above.
|
|
41
|
+
{{/if other_agents}}
|
|
42
|
+
|
|
43
|
+
## Claude Code specifics
|
|
44
|
+
|
|
45
|
+
This section applies only when the agent is Claude Code. Other agents get
|
|
46
|
+
no automatic hooks — rely on the git/CI enforcement above.
|
|
47
|
+
|
|
48
|
+
- Hooks in `.claude/settings.json` fire automatically: `pre_commit_gate` (lint gate on commit), `github_command_guard` (steers toward lifecycle scripts), `post_edit_lint`, `workflow_state_reminder`.
|
|
49
|
+
- Skills are auto-discovered and invocable as `/command`s.
|
|
50
|
+
- Review plugins: `pr-review-toolkit@claude-plugins-official` is pre-enabled in `.claude/settings.json`; also consider `/plugin install code-review@claude-plugins-official` for full PR reviews.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# {{project_name}} — Claude Code entry point
|
|
2
|
+
|
|
3
|
+
Canonical agent instructions live in [AGENTS.md](AGENTS.md) — the
|
|
4
|
+
Linux Foundation standard file most coding agents read natively. Read it
|
|
5
|
+
before working in this codebase; it is the source of truth for workflow,
|
|
6
|
+
conventions, memory, tools, and branch naming. Its "Claude Code specifics"
|
|
7
|
+
section applies to this session.
|
|
8
|
+
|
|
9
|
+
## Compact Instructions
|
|
10
|
+
|
|
11
|
+
When compacting this conversation, preserve:
|
|
12
|
+
- The project ticket key and GitHub issue number being worked on (e.g. `PI-42`, GitHub `#42`)
|
|
13
|
+
- Files modified in this session (list by path)
|
|
14
|
+
- Test results: pass/fail count and any failing test names
|
|
15
|
+
- Unresolved errors or lint failures
|
|
16
|
+
- Any decisions made that aren't yet committed to `docs/adr/`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Contributing to {{project_name}}
|
|
2
|
+
|
|
3
|
+
{{project_description}}
|
|
4
|
+
|
|
5
|
+
This project was scaffolded with [project-init]({{project_init_url}}): agent
|
|
6
|
+
instructions live in [AGENTS.md](AGENTS.md) (canonical; `CLAUDE.md` and
|
|
7
|
+
`GEMINI.md` redirect there), and the conventions below bind humans and
|
|
8
|
+
coding agents alike. Claude Code gets deterministic enforcement (hooks);
|
|
9
|
+
other agents and humans get the same rules via git hooks and CI.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
{{#if justfile}}
|
|
13
|
+
```bash
|
|
14
|
+
just setup
|
|
15
|
+
```
|
|
16
|
+
{{/if justfile}}
|
|
17
|
+
Install the repo git hooks once per clone — they enforce commit-message
|
|
18
|
+
format, secret scanning, and branch naming locally:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
.claude/scripts/install_hooks.sh
|
|
22
|
+
```
|
|
23
|
+
{{#if justfile}}
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
The justfile is the canonical command surface — `just --list` shows every
|
|
27
|
+
recipe. CI runs the same recipes, so if `just lint` and `just test` pass
|
|
28
|
+
locally, CI agrees.
|
|
29
|
+
{{/if justfile}}
|
|
30
|
+
## Branches, commits, and PRs
|
|
31
|
+
|
|
32
|
+
| What | Pattern | Example |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Branch | `<type>/<KEY>-<n>-<slug>` | `feat/KEY-42-add-oauth` |
|
|
35
|
+
| PR title | `type(KEY-N): description` | `feat(KEY-42): add OAuth login` |
|
|
36
|
+
| No-issue PR | `type: description` | `chore: bump dev dependency` |
|
|
37
|
+
| PR body | must include `Closes #N` (skip for no-issue PRs) | |
|
|
38
|
+
|
|
39
|
+
`KEY` is the project issue key set in `.claude/config.yaml`
|
|
40
|
+
(`project_key`). Types: `feat` `fix` `chore` `docs` `test`. Work starts
|
|
41
|
+
from an issue — use `.claude/scripts/create_issue.sh` (or the
|
|
42
|
+
`start_task` skill in Claude Code) so metadata lands on the project board.
|
|
43
|
+
|
|
44
|
+
## Review flow
|
|
45
|
+
|
|
46
|
+
1. Open a draft PR early; push with `.claude/scripts/push_branch.sh`.
|
|
47
|
+
2. Mark ready when CI is green. Respond to every review comment —
|
|
48
|
+
including bot reviews — either by fixing or by explaining why not.
|
|
49
|
+
3. Merges go through `.claude/scripts/monitor_pr.sh <n> --merge` so CI
|
|
50
|
+
and review gates are honored.
|
|
51
|
+
|
|
52
|
+
## Security
|
|
53
|
+
|
|
54
|
+
See [SECURITY.md](SECURITY.md) for how to report vulnerabilities —
|
|
55
|
+
please do not open public issues for security problems.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Gemini Agent Instructions
|
|
2
|
+
|
|
3
|
+
Canonical instructions for this project live in [AGENTS.md](AGENTS.md).
|
|
4
|
+
|
|
5
|
+
Read [AGENTS.md](AGENTS.md) before working in this codebase. It is the
|
|
6
|
+
source of truth for workflow, conventions, memory, tools, and branch
|
|
7
|
+
naming. Ignore its "Claude Code specifics" section — enforcement that
|
|
8
|
+
applies to you (git hooks, CI checks) is listed under "Key rules for
|
|
9
|
+
agents".
|
|
10
|
+
|
|
11
|
+
Before any GitHub action (create issue, branch, push, PR, merge), check
|
|
12
|
+
[`.claude/skills/INDEX.md`](.claude/skills/INDEX.md) and load the relevant
|
|
13
|
+
skill file.
|
|
14
|
+
|
|
15
|
+
{{#if lint_command}}Discover project commands with `just --list` — the justfile is the canonical, agent-agnostic command surface (lint, format, test, docs, ci).
|
|
16
|
+
{{/if}}
|