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.
- dct/__init__.py +6 -0
- dct/cli.py +1659 -0
- dct/config.py +150 -0
- dct/core/__init__.py +0 -0
- dct/core/analytics.py +382 -0
- dct/core/changelog.py +112 -0
- dct/core/checkpoints.py +205 -0
- dct/core/decisions.py +238 -0
- dct/core/engine.py +32 -0
- dct/core/handoff.py +151 -0
- dct/core/ids.py +25 -0
- dct/core/items.py +382 -0
- dct/core/plans/__init__.py +6 -0
- dct/core/plans/classify.py +94 -0
- dct/core/plans/crud.py +680 -0
- dct/core/plans/ingest.py +408 -0
- dct/core/plans/parser.py +312 -0
- dct/core/projects.py +298 -0
- dct/core/sprint.py +315 -0
- dct/core/sprints.py +601 -0
- dct/core/version.py +116 -0
- dct/db.py +558 -0
- dct/export.py +107 -0
- dct/hooks/check-changelog-on-stop.sh +49 -0
- dct/hooks/check-changelog.sh +143 -0
- dct/hooks/dct-plan-autoscan.sh +54 -0
- dct/hooks/dct-task-mirror.sh +62 -0
- dct/server.py +1812 -0
- dct/setup.py +258 -0
- dct/skills/clog/SKILL.md +73 -0
- dct/skills/commit/SKILL.md +153 -0
- dct/skills/dct-import/SKILL.md +329 -0
- dct/skills/dct-init/SKILL.md +289 -0
- dct/skills/handoff/SKILL.md +136 -0
- dct/skills/pickup/SKILL.md +115 -0
- dct/skills/plan-resolve-uncertain/SKILL.md +82 -0
- dct/skills/plan-status/SKILL.md +79 -0
- dct/skills/release/SKILL.md +281 -0
- dct/skills/track/SKILL.md +121 -0
- dct/skills/track-list/SKILL.md +134 -0
- dct/skills/track-resolve/SKILL.md +53 -0
- dct/skills/track-update/SKILL.md +109 -0
- dct/web/__init__.py +1 -0
- dct/web/app.py +83 -0
- dct/web/blueprints/__init__.py +5 -0
- dct/web/blueprints/analytics.py +34 -0
- dct/web/blueprints/changelog.py +40 -0
- dct/web/blueprints/decisions.py +23 -0
- dct/web/blueprints/events.py +69 -0
- dct/web/blueprints/handoffs.py +26 -0
- dct/web/blueprints/home.py +15 -0
- dct/web/blueprints/items.py +128 -0
- dct/web/blueprints/plans.py +53 -0
- dct/web/blueprints/projects.py +23 -0
- dct/web/blueprints/sprints.py +71 -0
- dct/web/changes.py +77 -0
- dct/web/serializers.py +109 -0
- dct/web/static/app.css +609 -0
- dct/web/static/app.js +62 -0
- dct/web/static/vendor/SOURCES.txt +10 -0
- dct/web/static/vendor/htmx.min.js +1 -0
- dct/web/static/vendor/uPlot.iife.min.js +2 -0
- dct/web/static/vendor/uPlot.min.css +1 -0
- dct/web/templates/analytics.html +63 -0
- dct/web/templates/base.html +106 -0
- dct/web/templates/changelog/list.html +23 -0
- dct/web/templates/decisions/list.html +22 -0
- dct/web/templates/handoffs/list.html +45 -0
- dct/web/templates/home.html +20 -0
- dct/web/templates/item_detail.html +91 -0
- dct/web/templates/items_list.html +44 -0
- dct/web/templates/partials/_bars.html +15 -0
- dct/web/templates/partials/_checkpoint_steps.html +22 -0
- dct/web/templates/partials/_items_table.html +23 -0
- dct/web/templates/partials/_plan_section.html +24 -0
- dct/web/templates/partials/_project_card.html +24 -0
- dct/web/templates/plans/detail.html +16 -0
- dct/web/templates/plans/list.html +15 -0
- dct/web/templates/project_home.html +51 -0
- dct/web/templates/sprints/detail.html +37 -0
- dct/web/templates/sprints/list.html +35 -0
- dctracker-1.0.0.dist-info/METADATA +357 -0
- dctracker-1.0.0.dist-info/RECORD +87 -0
- dctracker-1.0.0.dist-info/WHEEL +5 -0
- dctracker-1.0.0.dist-info/entry_points.txt +2 -0
- dctracker-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
dct/skills/clog/SKILL.md
ADDED
|
@@ -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.
|