dctracker 1.0.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.
Files changed (87) hide show
  1. dct/__init__.py +6 -0
  2. dct/cli.py +1659 -0
  3. dct/config.py +150 -0
  4. dct/core/__init__.py +0 -0
  5. dct/core/analytics.py +382 -0
  6. dct/core/changelog.py +112 -0
  7. dct/core/checkpoints.py +205 -0
  8. dct/core/decisions.py +238 -0
  9. dct/core/engine.py +32 -0
  10. dct/core/handoff.py +151 -0
  11. dct/core/ids.py +25 -0
  12. dct/core/items.py +382 -0
  13. dct/core/plans/__init__.py +6 -0
  14. dct/core/plans/classify.py +94 -0
  15. dct/core/plans/crud.py +680 -0
  16. dct/core/plans/ingest.py +408 -0
  17. dct/core/plans/parser.py +312 -0
  18. dct/core/projects.py +298 -0
  19. dct/core/sprint.py +315 -0
  20. dct/core/sprints.py +601 -0
  21. dct/core/version.py +116 -0
  22. dct/db.py +558 -0
  23. dct/export.py +107 -0
  24. dct/hooks/check-changelog-on-stop.sh +49 -0
  25. dct/hooks/check-changelog.sh +143 -0
  26. dct/hooks/dct-plan-autoscan.sh +54 -0
  27. dct/hooks/dct-task-mirror.sh +62 -0
  28. dct/server.py +1812 -0
  29. dct/setup.py +258 -0
  30. dct/skills/clog/SKILL.md +73 -0
  31. dct/skills/commit/SKILL.md +153 -0
  32. dct/skills/dct-import/SKILL.md +329 -0
  33. dct/skills/dct-init/SKILL.md +289 -0
  34. dct/skills/handoff/SKILL.md +136 -0
  35. dct/skills/pickup/SKILL.md +115 -0
  36. dct/skills/plan-resolve-uncertain/SKILL.md +82 -0
  37. dct/skills/plan-status/SKILL.md +79 -0
  38. dct/skills/release/SKILL.md +281 -0
  39. dct/skills/track/SKILL.md +121 -0
  40. dct/skills/track-list/SKILL.md +134 -0
  41. dct/skills/track-resolve/SKILL.md +53 -0
  42. dct/skills/track-update/SKILL.md +109 -0
  43. dct/web/__init__.py +1 -0
  44. dct/web/app.py +83 -0
  45. dct/web/blueprints/__init__.py +5 -0
  46. dct/web/blueprints/analytics.py +34 -0
  47. dct/web/blueprints/changelog.py +40 -0
  48. dct/web/blueprints/decisions.py +23 -0
  49. dct/web/blueprints/events.py +69 -0
  50. dct/web/blueprints/handoffs.py +26 -0
  51. dct/web/blueprints/home.py +15 -0
  52. dct/web/blueprints/items.py +128 -0
  53. dct/web/blueprints/plans.py +53 -0
  54. dct/web/blueprints/projects.py +23 -0
  55. dct/web/blueprints/sprints.py +71 -0
  56. dct/web/changes.py +77 -0
  57. dct/web/serializers.py +109 -0
  58. dct/web/static/app.css +609 -0
  59. dct/web/static/app.js +62 -0
  60. dct/web/static/vendor/SOURCES.txt +10 -0
  61. dct/web/static/vendor/htmx.min.js +1 -0
  62. dct/web/static/vendor/uPlot.iife.min.js +2 -0
  63. dct/web/static/vendor/uPlot.min.css +1 -0
  64. dct/web/templates/analytics.html +63 -0
  65. dct/web/templates/base.html +106 -0
  66. dct/web/templates/changelog/list.html +23 -0
  67. dct/web/templates/decisions/list.html +22 -0
  68. dct/web/templates/handoffs/list.html +45 -0
  69. dct/web/templates/home.html +20 -0
  70. dct/web/templates/item_detail.html +91 -0
  71. dct/web/templates/items_list.html +44 -0
  72. dct/web/templates/partials/_bars.html +15 -0
  73. dct/web/templates/partials/_checkpoint_steps.html +22 -0
  74. dct/web/templates/partials/_items_table.html +23 -0
  75. dct/web/templates/partials/_plan_section.html +24 -0
  76. dct/web/templates/partials/_project_card.html +24 -0
  77. dct/web/templates/plans/detail.html +16 -0
  78. dct/web/templates/plans/list.html +15 -0
  79. dct/web/templates/project_home.html +51 -0
  80. dct/web/templates/sprints/detail.html +37 -0
  81. dct/web/templates/sprints/list.html +35 -0
  82. dctracker-1.0.0.dist-info/METADATA +357 -0
  83. dctracker-1.0.0.dist-info/RECORD +87 -0
  84. dctracker-1.0.0.dist-info/WHEEL +5 -0
  85. dctracker-1.0.0.dist-info/entry_points.txt +2 -0
  86. dctracker-1.0.0.dist-info/licenses/LICENSE +21 -0
  87. dctracker-1.0.0.dist-info/top_level.txt +1 -0
dct/setup.py ADDED
@@ -0,0 +1,258 @@
1
+ """Init workflow helpers: register MCP user-scope, install hook, install skills."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from dct.config import Config
11
+
12
+ SKILLS_DIR = Path(__file__).resolve().parent / "skills"
13
+ HOOKS_DIR = Path(__file__).resolve().parent / "hooks"
14
+ CLAUDE_SKILLS_DIR = Path.home() / ".claude" / "skills"
15
+ CLAUDE_HOOKS_DIR = Path.home() / ".claude" / "hooks"
16
+ CLAUDE_JSON = Path.home() / ".claude.json"
17
+ CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
18
+
19
+ # Canonical wiring of the four dct hooks into ~/.claude/settings.json.
20
+ # event/matcher come from each hook script's own header (the authoritative
21
+ # self-description) — e.g. dct-task-mirror.sh declares PreToolUse on
22
+ # TodoWrite|TaskCreate. matcher=None means the event takes no matcher (Stop).
23
+ HOOK_WIRING = [
24
+ {"script": "dct-check-changelog.sh", "event": "PreToolUse",
25
+ "matcher": "Bash", "timeout": 10},
26
+ {"script": "dct-check-changelog-on-stop.sh", "event": "Stop",
27
+ "matcher": None, "timeout": 5},
28
+ {"script": "dct-plan-autoscan.sh", "event": "PostToolUse",
29
+ "matcher": "Edit|Write|MultiEdit", "timeout": None},
30
+ {"script": "dct-task-mirror.sh", "event": "PreToolUse",
31
+ "matcher": "TodoWrite|TaskCreate", "timeout": None},
32
+ ]
33
+
34
+ def discover_skills() -> list[str]:
35
+ """Skill directories shipped inside the package (each holds a SKILL.md).
36
+
37
+ Derived from the filesystem on every call — dct/skills/ is the single
38
+ source of truth. Never reintroduce a hardcoded name list: a stale one
39
+ silently dropped plan-status and plan-resolve-uncertain from every
40
+ install for two releases.
41
+ """
42
+ if not SKILLS_DIR.is_dir():
43
+ return []
44
+ return sorted(p.name for p in SKILLS_DIR.iterdir() if (p / "SKILL.md").is_file())
45
+
46
+
47
+ def register_mcp(config: Config) -> tuple[bool, str]:
48
+ """Register dct MCP server at user scope (~/.claude.json).
49
+
50
+ Uses the `dct server` subcommand as the stdio entry point — requires that
51
+ `dct` itself is installed in PATH (via `uv tool install`, `pipx install`,
52
+ or `pip install`). Zero hardcoded filesystem paths in the resulting config.
53
+
54
+ Tries `claude mcp add --scope user` CLI first; falls back to direct
55
+ ~/.claude.json edit if `claude` CLI is unavailable.
56
+ """
57
+ server_cmd = ["dct", "server"]
58
+
59
+ if shutil.which("claude"):
60
+ cmd = ["claude", "mcp", "add", "dct", "--scope", "user", "--"] + server_cmd
61
+ result = subprocess.run(cmd, capture_output=True, text=True)
62
+ if result.returncode == 0:
63
+ return True, "registered via `claude mcp add --scope user`"
64
+ if "already exists" in (result.stderr + result.stdout).lower():
65
+ return True, "already registered"
66
+ # fall through to direct edit
67
+
68
+ # Fallback: direct ~/.claude.json edit
69
+ data = {}
70
+ if CLAUDE_JSON.exists():
71
+ with open(CLAUDE_JSON) as f:
72
+ data = json.load(f)
73
+ data.setdefault("mcpServers", {})["dct"] = {
74
+ "type": "stdio",
75
+ "command": server_cmd[0],
76
+ "args": server_cmd[1:],
77
+ }
78
+ with open(CLAUDE_JSON, "w") as f:
79
+ json.dump(data, f, indent=2, ensure_ascii=False)
80
+ f.write("\n")
81
+ return True, "registered via direct ~/.claude.json edit"
82
+
83
+
84
+ def install_hook(config: Config | None = None) -> bool:
85
+ """Symlink dct hooks into ~/.claude/hooks/ (single source of truth, #224).
86
+
87
+ Installs four hooks as SYMLINKS to the packaged dct/hooks/ directory (like
88
+ install_skills) — site-packages for a wheel install, the repo working tree
89
+ for an editable one, where a repo fix is live immediately without reinstall.
90
+ Copies drift from source — a stale installed copy is exactly what masked the
91
+ #223 clog-count bug timeline. The installed name is `dct-`-prefixed while the
92
+ packaged source is not (e.g. dct/hooks/check-changelog.sh → dct-check-changelog.sh):
93
+ - dct-check-changelog.sh (PreToolUse) — blocks commits without changelog
94
+ - dct-check-changelog-on-stop.sh (Stop) — reminds user at session end
95
+ - dct-plan-autoscan.sh (PostToolUse, v0.2.0) — re-scans plan-like MD files
96
+ in the background after edit/write, injecting ULIDs and updating DB state
97
+ - dct-task-mirror.sh (PostToolUse) — mirrors Claude Code tasks into dct
98
+
99
+ Each hook is independently optional: a source missing from the dct repo (old
100
+ checkout) is skipped silently. An existing pre-#224 regular-file copy is
101
+ replaced in place by the symlink. Wiring into `~/.claude/settings.json` is
102
+ handled separately by `wire_hooks_into_settings()` — called from `dct init`
103
+ (consent-gated) or via `dct hooks wire`; manual reference in docs/hooks.md.
104
+
105
+ Caveat: a symlinked hook tracks the repo WORKING TREE, including uncommitted
106
+ edits — intentional for a dev checkout (instant fixes) but means a dirty repo
107
+ affects live hook behaviour. `config` is accepted for call-site symmetry with
108
+ the other install_* helpers but is unused.
109
+ """
110
+ CLAUDE_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
111
+ installed = 0
112
+
113
+ for src_name, dst_name in [
114
+ ("check-changelog.sh", "dct-check-changelog.sh"),
115
+ ("check-changelog-on-stop.sh", "dct-check-changelog-on-stop.sh"),
116
+ ("dct-plan-autoscan.sh", "dct-plan-autoscan.sh"),
117
+ ("dct-task-mirror.sh", "dct-task-mirror.sh"),
118
+ ]:
119
+ src = HOOKS_DIR / src_name
120
+ dst = CLAUDE_HOOKS_DIR / dst_name
121
+ if not src.exists():
122
+ continue
123
+ # Replace any existing symlink OR a pre-#224 regular-file copy in place.
124
+ if dst.is_symlink() or dst.exists():
125
+ dst.unlink()
126
+ dst.symlink_to(src)
127
+ installed += 1
128
+
129
+ return installed > 0
130
+
131
+
132
+ def wire_hooks_into_settings(settings_path: Path | None = None) -> dict:
133
+ """Merge the four dct hook entries into ~/.claude/settings.json (idempotent).
134
+
135
+ Merge semantics:
136
+ - Missing or empty settings file → treated as ``{}`` and created.
137
+ - Idempotency probe is GLOBAL by script basename: a hook already mentioned
138
+ anywhere in ``hooks`` (any event, any matcher group, any path form —
139
+ absolute, ``~``-relative, with or without ``bash `` prefix) is skipped.
140
+ This deliberately respects a user's custom placement of a dct hook.
141
+ - A missing event list or matcher group is created; an existing group with
142
+ the same matcher gets the entry appended. User-defined hooks and all
143
+ unrelated top-level keys are never modified.
144
+ - Before the first actual write of a pre-existing file, the original is
145
+ copied to ``settings.json.bak-dct-<timestamp>`` next to it.
146
+ - The write is atomic (temp file + ``os.replace``).
147
+ - Malformed JSON or a non-object ``hooks`` key raises ``ValueError`` and
148
+ NOTHING is written — never destructive.
149
+
150
+ Returns a summary dict: ``{"added": [script names], "skipped": [...],
151
+ "backup": str | None, "settings_path": str}``.
152
+ """
153
+ if settings_path is None:
154
+ settings_path = CLAUDE_SETTINGS
155
+ settings_path = Path(settings_path)
156
+
157
+ raw = settings_path.read_text() if settings_path.exists() else None
158
+ if raw is None or not raw.strip():
159
+ settings = {}
160
+ else:
161
+ try:
162
+ settings = json.loads(raw)
163
+ except json.JSONDecodeError as exc:
164
+ raise ValueError(
165
+ f"{settings_path} is not valid JSON ({exc}) — fix it manually; "
166
+ "nothing was written"
167
+ ) from exc
168
+ if not isinstance(settings, dict):
169
+ raise ValueError(
170
+ f"{settings_path}: top-level JSON must be an object; nothing was written"
171
+ )
172
+ hooks = settings.setdefault("hooks", {})
173
+ if not isinstance(hooks, dict):
174
+ raise ValueError(
175
+ f"{settings_path}: 'hooks' must be an object, "
176
+ f"found {type(hooks).__name__}; nothing was written"
177
+ )
178
+
179
+ existing_commands = []
180
+ for groups in hooks.values():
181
+ if not isinstance(groups, list):
182
+ continue
183
+ for group in groups:
184
+ if not isinstance(group, dict):
185
+ continue
186
+ for hook in group.get("hooks") or []:
187
+ if isinstance(hook, dict):
188
+ existing_commands.append(str(hook.get("command", "")))
189
+
190
+ added, skipped = [], []
191
+ for spec in HOOK_WIRING:
192
+ name = spec["script"]
193
+ if any(name in cmd for cmd in existing_commands):
194
+ skipped.append(name)
195
+ continue
196
+ entry = {"type": "command", "command": f"bash {CLAUDE_HOOKS_DIR / name}"}
197
+ if spec["timeout"] is not None:
198
+ entry["timeout"] = spec["timeout"]
199
+ groups = hooks.setdefault(spec["event"], [])
200
+ target = next(
201
+ (g for g in groups
202
+ if isinstance(g, dict) and g.get("matcher") == spec["matcher"]),
203
+ None,
204
+ )
205
+ if target is None:
206
+ target = {"hooks": []}
207
+ if spec["matcher"] is not None:
208
+ target["matcher"] = spec["matcher"]
209
+ groups.append(target)
210
+ target.setdefault("hooks", []).append(entry)
211
+ added.append(name)
212
+
213
+ backup = None
214
+ if added:
215
+ if raw is not None and raw.strip():
216
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
217
+ backup_path = settings_path.with_name(
218
+ f"{settings_path.name}.bak-dct-{stamp}"
219
+ )
220
+ backup_path.write_text(raw)
221
+ backup = str(backup_path)
222
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
223
+ tmp_path = settings_path.with_name(settings_path.name + ".dct-tmp")
224
+ tmp_path.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n")
225
+ os.replace(tmp_path, settings_path)
226
+
227
+ return {
228
+ "added": added,
229
+ "skipped": skipped,
230
+ "backup": backup,
231
+ "settings_path": str(settings_path),
232
+ }
233
+
234
+
235
+ def install_skills(config: Config) -> bool:
236
+ """Symlink every packaged skill (dct/skills/*) into ~/.claude/skills/.
237
+
238
+ Sources come from the installed package — site-packages for a wheel
239
+ install, the repo working tree for an editable one (where a repo fix is
240
+ live without reinstall).
241
+ """
242
+ CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
243
+ installed = 0
244
+
245
+ for name in discover_skills():
246
+ src = SKILLS_DIR / name
247
+ dst = CLAUDE_SKILLS_DIR / name
248
+
249
+ # Remove existing symlink or directory
250
+ if dst.is_symlink():
251
+ dst.unlink()
252
+ elif dst.exists():
253
+ continue # don't overwrite non-symlink dirs
254
+
255
+ dst.symlink_to(src)
256
+ installed += 1
257
+
258
+ return installed > 0
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: clog
3
+ description: |
4
+ Add a changelog entry to dct database.
5
+ Use when: "clog", "changelog", "dodaj do changelogu", "add changelog entry",
6
+ "log this change", "record change", or when committing and changelog needs an entry.
7
+ Natural past-tense triggers (use after finishing a change):
8
+ "zmieniłem", "poprawiłem", "naprawiłem", "dodałem", "usunąłem", "zrobiłem",
9
+ "zrefaktorowałem", "zaktualizowałem",
10
+ "I added", "I fixed", "I changed", "I removed", "I refactored",
11
+ "just fixed", "just added", "logged a change", "note this change".
12
+ ---
13
+
14
+ # /clog — Add Changelog Entry
15
+
16
+ ## Workflow
17
+
18
+ ### Step 1: Gather from context
19
+ From the current work, determine:
20
+ - **project**: Auto-detect from CWD, or ask
21
+ - **category**: added, changed, fixed, removed (see mapping below)
22
+ - **entry**: Concise description of the change (English, ~1 sentence)
23
+ - **component**: Module/subsystem (optional)
24
+ - **source_file**: Main file changed (optional)
25
+ - **related_item_id**: If this change resolves a tracked item (optional)
26
+
27
+ ### Step 2: Confirm
28
+ ```
29
+ Changelog entry:
30
+ [added] New Python parser support (parser)
31
+ File: cmd/parse.go
32
+ ```
33
+ Wait for confirmation.
34
+
35
+ ### Step 3: Create
36
+ Call `mcp__dct__add_changelog(project, category, entry, ...)`.
37
+
38
+ ### Step 4: Report
39
+ ```
40
+ ✓ Changelog #15 [added] — New Python parser support
41
+ ```
42
+
43
+ ## Category mapping
44
+ | Change type | category |
45
+ |---|---|
46
+ | New feature, new file, new capability | added |
47
+ | Refactor, rename, restructure, update behavior | changed |
48
+ | Bug fix, crash fix, correction | fixed |
49
+ | Deleted code, removed feature, deprecated | removed |
50
+
51
+ ## Conventional commit → category
52
+ | Prefix | category |
53
+ |---|---|
54
+ | feat: | added |
55
+ | fix: | fixed |
56
+ | refactor:, chore:, perf: | changed |
57
+ | revert: | removed |
58
+
59
+ ## Multiple entries
60
+
61
+ If a commit touches multiple areas, create multiple changelog entries —
62
+ one per logical change. Example: refactor that also fixes a bug and adds
63
+ a helper:
64
+
65
+ ```
66
+ ✓ Changelog #20 [changed] — Extracted shared auth validation (core/auth)
67
+ ✓ Changelog #21 [fixed] — Token refresh race condition on concurrent login (core/auth)
68
+ ✓ Changelog #22 [added] — is_expired() helper on Session model (core/auth)
69
+ ```
70
+
71
+ Each entry is a separate `add_changelog` call. Don't concatenate changes
72
+ into one entry — it makes the generated changelog.md harder to skim at
73
+ release time.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: commit
3
+ description: |
4
+ Create a git commit following dct changelog-first workflow and conventional commit format.
5
+ Use when: "commit", "commit this", "zapisz", "save this", "commitnij", or when the
6
+ user has finished implementing something and the natural next step is to commit.
7
+ ---
8
+
9
+ # /commit — Git Commit (dct-aware)
10
+
11
+ Universal commit workflow. Works in any dct-registered project — auto-detects from CWD.
12
+ Mirrors the global hook's exemption rules so the first attempt passes.
13
+
14
+ ## Workflow
15
+
16
+ ### Step 1: Pre-flight parallel probe
17
+
18
+ Run these 4 Bash commands **in parallel** (one message, four tool calls):
19
+
20
+ ```
21
+ git status
22
+ git diff --cached --stat
23
+ git diff --stat
24
+ git log --oneline -5
25
+ ```
26
+
27
+ If nothing changed AND nothing staged → inform user "Clean tree, nothing to commit" and stop.
28
+ If only untracked files with no staged or modified files → ask user whether to track them.
29
+
30
+ ### Step 2: Detect project and fetch current_version
31
+
32
+ Call `mcp__dct__list_projects()` and match longest project path prefix against CWD.
33
+
34
+ - If CWD is inside a registered project → remember `slug`, `current_version`.
35
+ - If not a dct project → `current_version` is null, continue without version suffix.
36
+
37
+ ### Step 2b: Version-bump detection — redirect to /release
38
+
39
+ If staged changes look like a version bump (matching any of the conditions
40
+ below), STOP and suggest `/release` to the user instead of committing here:
41
+
42
+ - `changelog.md` / `CHANGELOG.md` is staged AND contains a new `## [X.Y.Z]` heading
43
+ - Any of `pyproject.toml`, `Cargo.toml`, `package.json`, `tauri.conf.json`, `plugin.json` has a staged `version = "..."` change
44
+ - `mcp__dct__count_unreleased_changelog(project)` returned a large batch (≥10) that looks release-ready
45
+
46
+ Present via `AskUserQuestion`:
47
+ - question: "Staged changes look like a version bump. Prefer /release?"
48
+ - options: `"Run /release instead"` / `"No — regular commit"` / `"Cancel"`.
49
+
50
+ Release commits use the literal `release: vX.Y.Z` format (no scope, no version
51
+ suffix on subject) — different from regular /commit messages.
52
+
53
+ ### Step 3: Changelog guard
54
+
55
+ If project detected:
56
+ - Call `mcp__dct__count_unreleased_changelog(project)`.
57
+ - If count == 0 AND staged files contain non-exempt code (see Exempt patterns below):
58
+ - Prompt user via AskUserQuestion: "No unreleased changelog entry. Add one now? [Yes, via /clog] [Skip, changelog not needed for this commit] [Cancel]"
59
+ - On "Yes" → invoke `/clog` or call `mcp__dct__add_changelog` inline.
60
+ - On "Skip" → proceed but warn this may hit the commit hook.
61
+ - On "Cancel" → stop.
62
+
63
+ **Exempt patterns** (no changelog required if ALL staged files match):
64
+ - `.claude/`, `.claude-plugin/`, `.github/`, `.cursor/`, `.vscode/`, `.gstack/`
65
+ - `docs/`, `scripts/`, `tests/`, `dct/skills/`, `test_*.py`, `conftest.py`
66
+ - `CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, `README.md`, `LICENSE*`
67
+ - `changelog.md`, `CHANGELOG*`, `.gitignore`
68
+ - Config files: `pyproject.toml`, `Cargo.toml`, `package.json`, `biome.json`, `tsconfig*.json`
69
+
70
+ ### Step 4: Stage files explicitly
71
+
72
+ Show user the proposed staging plan:
73
+ ```
74
+ Files to stage:
75
+ dct/core/items.py
76
+ tests/test_items.py
77
+ ```
78
+
79
+ Stage each path **by name** — one per `git add <path>` call, or multiple paths in one call
80
+ listed explicitly. NEVER:
81
+ - `git add -A`
82
+ - `git add .`
83
+ - `git add *`
84
+
85
+ NEVER stage (blacklist):
86
+ - `.env`, `.env.*` (any env files)
87
+ - `credentials*`, `secrets*`, `*.pem`, `*.key`
88
+ - Binaries, generated artifacts (`dist/`, `build/`, `target/`, `*.pyc`)
89
+
90
+ ### Step 5: Draft and commit
91
+
92
+ Build the commit message with conventional format:
93
+
94
+ ```
95
+ <type>(<scope>): <description> (v<current_version>)
96
+ ```
97
+
98
+ - `type` ∈ {feat, fix, refactor, chore, docs, test, perf, style, revert}
99
+ - `scope` = component name (match project's component taxonomy: `core`, `cli`, `server`, etc.)
100
+ - `description` = imperative, lowercase, no trailing period, ≤ 72 chars
101
+ - `(v<version>)` suffix — **OMIT if `current_version` is null** or if this IS the release commit.
102
+
103
+ **Release commits** have a different format (set by `/release`):
104
+ ```
105
+ release: vX.Y.Z
106
+ ```
107
+
108
+ Execute via HEREDOC to preserve formatting:
109
+
110
+ ```bash
111
+ git commit -m "$(cat <<'EOF'
112
+ <type>(<scope>): <description> (v<version>)
113
+
114
+ Optional body paragraph explaining WHY (not what).
115
+
116
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117
+ EOF
118
+ )"
119
+ ```
120
+
121
+ ### Step 6: Hook-reject recovery
122
+
123
+ If the dct hook (or any project hook) rejects the commit:
124
+
125
+ 1. Read the hook's error message carefully — it tells you exactly what's missing.
126
+ 2. Fix the underlying issue (usually: add changelog entry via `/clog`).
127
+ 3. Re-stage any new files from the fix.
128
+ 4. Create a **NEW** commit — do NOT `git commit --amend`. Amend modifies the PREVIOUS commit, which may not exist (rejected commits don't happen).
129
+ 5. Never bypass with `--no-verify`, `--no-gpg-sign`, or similar escape hatches.
130
+
131
+ ### Step 7: Verify
132
+
133
+ After successful commit:
134
+ ```bash
135
+ git log --oneline -3
136
+ git status
137
+ ```
138
+
139
+ Report to user:
140
+ ```
141
+ ✓ Committed abc1234 — feat(core): add checkpoint CRUD (v0.1.0)
142
+ ```
143
+
144
+ ## Rules (hard)
145
+
146
+ - NEVER commit without user approval of the staging plan AND message draft.
147
+ - NEVER stage `.env`, credentials, binaries, or anything matching the blacklist.
148
+ - NEVER use `git add -A`, `git add .`, or broad globs.
149
+ - NEVER use `--amend` unless the user explicitly asks (to edit a successful commit).
150
+ - NEVER use `--no-verify`. Hooks exist for reasons — if blocked, fix and re-commit.
151
+ - NEVER force-push to main/master branches.
152
+ - ALWAYS include `Co-Authored-By` trailer with Claude's model identifier.
153
+ - Release commits use literal `release: vX.Y.Z` format — no scope, no description.