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/export.py ADDED
@@ -0,0 +1,107 @@
1
+ """Export changelog entries to Keep a Changelog markdown format."""
2
+
3
+ from collections import defaultdict
4
+ from datetime import date
5
+
6
+ import sqlalchemy as sa
7
+
8
+ from dct.db import changelog_entries, projects, row_to_dict
9
+
10
+
11
+ def export_changelog_md(
12
+ engine: sa.Engine,
13
+ project_slug: str,
14
+ include_unreleased: bool = True,
15
+ version: str | None = None,
16
+ ) -> str:
17
+ """Generate Keep a Changelog format markdown from DB entries.
18
+
19
+ When `version` is given, render ONLY that version's `## [version] - DATE`
20
+ block — WITHOUT the top-level `# Changelog` header and WITHOUT the
21
+ [Unreleased] section. This single-version unit is what the append-only
22
+ /release flow prepends into an existing changelog.md (see item #214); an
23
+ unknown version renders to an empty string. When `version` is None
24
+ (default) the full file is rendered: header + [Unreleased] + every version.
25
+ """
26
+ with engine.begin() as conn:
27
+ row = conn.execute(
28
+ sa.select(projects.c.id).where(projects.c.slug == project_slug, projects.c.deleted_at.is_(None))
29
+ ).first()
30
+ if not row:
31
+ raise ValueError(f"Project '{project_slug}' not found")
32
+ pid = row._mapping["id"]
33
+ q = (
34
+ sa.select(changelog_entries)
35
+ .where(changelog_entries.c.project_id == pid, changelog_entries.c.deleted_at.is_(None))
36
+ )
37
+ if version is not None:
38
+ q = q.where(changelog_entries.c.version == version)
39
+ q = q.order_by(changelog_entries.c.released_at.desc().nulls_first(), changelog_entries.c.created_at)
40
+ rows = [row_to_dict(r) for r in conn.execute(q)]
41
+
42
+ category_order = ["added", "changed", "fixed", "removed"]
43
+
44
+ # Group by version
45
+ versions: dict[str | None, list[dict]] = defaultdict(list)
46
+ for row in rows:
47
+ versions[row["version"]].append(row)
48
+
49
+ # Single-version render: emit just the one block, no file-level header, so
50
+ # the append-only release flow can prepend it into the existing file.
51
+ if version is not None:
52
+ entries = versions.get(version)
53
+ if not entries:
54
+ return ""
55
+ block: list[str] = []
56
+ _render_version_block(version, entries, category_order, block)
57
+ return "\n".join(block)
58
+
59
+ lines = ["# Changelog\n", ""]
60
+
61
+ # Unreleased first
62
+ if include_unreleased and None in versions:
63
+ lines.append("## [Unreleased]")
64
+ lines.append("")
65
+ _render_version_entries(versions[None], category_order, lines)
66
+
67
+ # Released versions (sorted by released_at of each version's first entry, desc)
68
+ released = {k: v for k, v in versions.items() if k is not None}
69
+ for ver in sorted(released, key=lambda v: released[v][0].get("released_at") or "", reverse=True):
70
+ _render_version_block(ver, released[ver], category_order, lines)
71
+
72
+ return "\n".join(lines)
73
+
74
+
75
+ def _render_version_block(
76
+ version: str, entries: list[dict], category_order: list[str], lines: list[str]
77
+ ) -> None:
78
+ """Append a `## [version] - DATE` header (date from the first entry's
79
+ released_at, or today when unreleased) followed by its category sections.
80
+ Shared by the full-file render and the single-version render so the two
81
+ paths can never drift apart."""
82
+ released_at = entries[0].get("released_at")
83
+ if released_at:
84
+ date_str = released_at.strftime("%Y-%m-%d") if hasattr(released_at, "strftime") else str(released_at)[:10]
85
+ else:
86
+ date_str = date.today().isoformat()
87
+ lines.append(f"## [{version}] - {date_str}")
88
+ lines.append("")
89
+ _render_version_entries(entries, category_order, lines)
90
+
91
+
92
+ def _render_version_entries(entries: list[dict], category_order: list[str], lines: list[str]) -> None:
93
+ by_category: dict[str, list[dict]] = defaultdict(list)
94
+ for e in entries:
95
+ by_category[e["category"]].append(e)
96
+
97
+ for cat in category_order:
98
+ if cat not in by_category:
99
+ continue
100
+ lines.append(f"### {cat.capitalize()}")
101
+ lines.append("")
102
+ for e in by_category[cat]:
103
+ entry_text = e["entry"]
104
+ if e.get("component"):
105
+ entry_text = f"{entry_text} ({e['component']})"
106
+ lines.append(f"- {entry_text}")
107
+ lines.append("")
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # dct global Stop hook — non-blocking reminder after every turn.
3
+ # Checks if app files changed without unreleased changelog entries.
4
+ # Install: ~/.claude/hooks/dct-check-changelog-on-stop.sh
5
+
6
+ # Require dct CLI
7
+ command -v dct >/dev/null || exit 0
8
+
9
+ # Check if we're in a dct-registered project
10
+ project=$(dct project detect 2>/dev/null)
11
+ if [ -z "$project" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ # #222: opt-in — only remind for projects that manage their changelog in dct.
16
+ if ! dct project manages-changelog "$project" 2>/dev/null; then
17
+ exit 0
18
+ fi
19
+
20
+ # Exempt patterns
21
+ exempt_re="^\.(claude|github|cursor|vscode|gstack)/|^docs/|^scripts/|^tests/|^test_|conftest\.py$|CLAUDE\.md$|README\.md$|LICENSE|\.gitignore$|^changelog|^CHANGELOG|^VERSION$|pyproject\.toml$|Cargo\.toml$|package\.json$"
22
+
23
+ # Check for modified/staged/untracked app files
24
+ changed=$(git diff --name-only 2>/dev/null; git diff --cached --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
25
+ [ -z "$changed" ] && exit 0
26
+
27
+ # Deduplicate and filter exempt files
28
+ app_files=$(echo "$changed" | sort -u | grep -vE "$exempt_re")
29
+ [ -z "$app_files" ] && exit 0
30
+
31
+ # Check if changelog was modified
32
+ changelog_changed=$(echo "$changed" | sort -u | grep -iE "^changelog|^CHANGELOG" | head -1)
33
+ if [ -n "$changelog_changed" ]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Check dct DB for unreleased entries
38
+ count=$(dct clog count --project "$project" 2>/dev/null)
39
+ if [ "${count:-0}" -gt 0 ]; then
40
+ exit 0
41
+ fi
42
+
43
+ # Reminder (non-blocking — always exit 0)
44
+ file_count=$(echo "$app_files" | wc -l | tr -d ' ')
45
+ echo ""
46
+ echo "📝 Reminder: ${file_count} app file(s) changed in ${project}/ but no changelog entry found."
47
+ echo " Use /clog to add an entry."
48
+ echo ""
49
+ exit 0
@@ -0,0 +1,143 @@
1
+ #!/bin/bash
2
+ # dct global hook — PreToolUse for Bash(git commit*)
3
+ # Blocks commits without changelog entries (staged changelog file OR dct DB entry).
4
+ # Opt-in (#222): enforces ONLY projects with manages_changelog=TRUE — a project
5
+ # keeping its own changelog opts out via `dct project changelog <slug> off`.
6
+ # Install: ~/.claude/hooks/dct-check-changelog.sh
7
+
8
+ # Consume stdin (required by hook protocol)
9
+ input=$(cat)
10
+
11
+ # Require jq
12
+ command -v jq >/dev/null || exit 0
13
+
14
+ # Require dct CLI
15
+ command -v dct >/dev/null || exit 0
16
+
17
+ # Extract command from hook input
18
+ command=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null)
19
+
20
+ # Only check git commit commands
21
+ case "$command" in
22
+ *"git commit"*) ;;
23
+ *) exit 0 ;;
24
+ esac
25
+
26
+ # Skip amend commits (strip message content to avoid false matches)
27
+ flags=$(echo "$command" | sed 's/ -m [^ ]*//g; s/ -m ".*"//g; s/ --message[= ][^ ]*//g; s/ --message ".*"//g')
28
+ case "$flags" in
29
+ *"--amend"*) exit 0 ;;
30
+ esac
31
+
32
+ # Release-commit bypass: skip hook entirely when commit message starts with "release:"
33
+ # /release skill produces clean release commits with zero unreleased entries after stamping.
34
+ # Two forms to handle: inline -m "release: ..." and HEREDOC heredoc where the first line
35
+ # of the message body is "release: ...".
36
+
37
+ # Form 1 — inline -m "..." / -m '...' / --message="..."
38
+ inline_msg=$(echo "$command" | sed -n \
39
+ -e 's/.*-m "\([^"]*\)".*/\1/p' \
40
+ -e "s/.*-m '\([^']*\)'.*/\1/p" \
41
+ -e 's/.*--message="\([^"]*\)".*/\1/p' \
42
+ | head -1)
43
+ case "$inline_msg" in
44
+ release:*) exit 0 ;;
45
+ esac
46
+
47
+ # Form 2 — HEREDOC: multi-line command containing a line that starts with "release:"
48
+ if echo "$command" | grep -qE '^release:'; then
49
+ exit 0
50
+ fi
51
+
52
+ # Check if we're in a dct-registered project
53
+ project=$(dct project detect 2>/dev/null)
54
+ if [ -z "$project" ]; then
55
+ exit 0 # Not a dct project — pass through
56
+ fi
57
+
58
+ # #222: opt-in — only enforce projects that manage their changelog in dct.
59
+ # A project registered for item-tracking but keeping its own changelog opts out
60
+ # via `dct project changelog <slug> off` and is skipped here.
61
+ if ! dct project manages-changelog "$project" 2>/dev/null; then
62
+ exit 0
63
+ fi
64
+
65
+ # Get staged files
66
+ staged=$(git diff --cached --name-only 2>/dev/null)
67
+ if [ -z "$staged" ]; then
68
+ exit 0 # Nothing staged
69
+ fi
70
+
71
+ # Exempt patterns — files that don't require changelog entries
72
+ exempt_patterns=(
73
+ "^\.claude/"
74
+ "^\.github/"
75
+ "^\.cursor/"
76
+ "^\.vscode/"
77
+ "^\.gstack/"
78
+ "^docs/"
79
+ "^scripts/"
80
+ "^tests/"
81
+ "^test_"
82
+ "conftest\.py$"
83
+ "CLAUDE\.md$"
84
+ "CLAUDE\.local\.md$"
85
+ "AGENTS\.md$"
86
+ "README\.md$"
87
+ "LICENSE"
88
+ "\.gitignore$"
89
+ "^changelog"
90
+ "^CHANGELOG"
91
+ "^VERSION$"
92
+ "pyproject\.toml$"
93
+ "Cargo\.toml$"
94
+ "package\.json$"
95
+ "biome\.json$"
96
+ "Dockerfile"
97
+ "docker-compose"
98
+ )
99
+
100
+ # Check if ALL staged files are exempt
101
+ has_app_code=false
102
+ while IFS= read -r file; do
103
+ is_exempt=false
104
+ for pattern in "${exempt_patterns[@]}"; do
105
+ if echo "$file" | grep -qE "$pattern"; then
106
+ is_exempt=true
107
+ break
108
+ fi
109
+ done
110
+ if [ "$is_exempt" = false ]; then
111
+ has_app_code=true
112
+ break
113
+ fi
114
+ done <<< "$staged"
115
+
116
+ if [ "$has_app_code" = false ]; then
117
+ exit 0 # All files are exempt
118
+ fi
119
+
120
+ # Check for changelog in staged files
121
+ has_changelog=false
122
+ while IFS= read -r file; do
123
+ case "$file" in
124
+ changelog*|CHANGELOG*|changelog_unreleased*) has_changelog=true; break ;;
125
+ esac
126
+ done <<< "$staged"
127
+
128
+ # If no changelog file staged, check dct database for unreleased entries
129
+ if [ "$has_changelog" = false ]; then
130
+ count=$(dct clog count --project "$project" 2>/dev/null)
131
+ if [ "${count:-0}" -gt 0 ]; then
132
+ has_changelog=true
133
+ fi
134
+ fi
135
+
136
+ if [ "$has_changelog" = true ]; then
137
+ exit 0
138
+ fi
139
+
140
+ # Block the commit
141
+ reason="App code staged but no changelog entry found for project '$project'. Add a changelog entry with /clog or stage a changelog file."
142
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$reason\"}}"
143
+ exit 2
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # dct-plan-autoscan.sh — PostToolUse hook for Claude Code.
3
+ #
4
+ # Rescans plan-like markdown files (plans, specs, ADRs, roadmaps, backlogs,
5
+ # retros) after an Edit/Write/MultiEdit. Runs in the background so the user
6
+ # never waits. Handles brand-new plans (ULID injection on first save) and
7
+ # re-sync of already-tracked plans.
8
+ #
9
+ # Follows the five hook rules from dct CLAUDE.md:
10
+ # 1. No interactive prompts (non-interactive only).
11
+ # 2. Consume stdin (Claude Code hooks supply JSON there).
12
+ # 3. Graceful when deps missing (command -v dct / jq).
13
+ # 4. Exit codes meaningful: 0 = pass-through, non-zero avoided.
14
+ # 5. No message-content inspection needed here (not a commit hook).
15
+ set -euo pipefail
16
+
17
+ # Rule 2: consume stdin even if we partially inspect it.
18
+ input=$(cat)
19
+
20
+ # Rule 3: graceful on missing deps — dct or jq absent means no-op.
21
+ command -v dct >/dev/null || exit 0
22
+ command -v jq >/dev/null || exit 0
23
+
24
+ # Extract tool name + file path from the JSON payload CC hooks pass.
25
+ tool=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)
26
+ file=$(echo "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
27
+
28
+ # Only trigger on edit/write-style tools.
29
+ case "$tool" in
30
+ Edit|Write|MultiEdit) ;;
31
+ *) exit 0 ;;
32
+ esac
33
+
34
+ # Only act on .md files matching our discovery patterns.
35
+ case "$file" in
36
+ */docs/plans/*.md|*/docs/specs/*.md|*/docs/adr/*.md|*/docs/retros/*.md) ;;
37
+ */ROADMAP.md|*ROADMAP.md|*/TODOS.md|*TODOS.md|*/FUTURE.md|*FUTURE.md) ;;
38
+ */BACKLOG.md|*BACKLOG.md) ;;
39
+ *) exit 0 ;;
40
+ esac
41
+
42
+ # Must exist (edit may have been rejected / the file was deleted).
43
+ [ -f "$file" ] || exit 0
44
+
45
+ # Fire-and-forget: detach a background rescan, log to a unique temp file so
46
+ # parallel hook invocations don't clobber each other. The `disown` is key —
47
+ # without it, the hook would block until the subprocess exits, defeating the
48
+ # whole point of the background pattern.
49
+ log_dir="${TMPDIR:-/tmp}"
50
+ log="${log_dir%/}/dct-rescan-$(date +%s)-$$.log"
51
+ nohup dct plan rescan "$file" --quiet --resolve-uncertain >"$log" 2>&1 &
52
+ disown
53
+
54
+ exit 0
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # dct-task-mirror.sh — PreToolUse hook for Claude Code.
3
+ #
4
+ # Mirrors TaskCreate / TodoWrite calls to dct checkpoints. Thin shell
5
+ # dispatcher: validates payload, checks config gate, detects project,
6
+ # then delegates actual work to `dct _task-mirror` CLI subcommand (which
7
+ # uses the venv-installed Python with dct importable — shell/system
8
+ # python cannot reliably find dct on PYTHONPATH).
9
+ #
10
+ # Each todo becomes a `kind='cc-task'` checkpoint on a session-scoped item
11
+ # titled "CC task list — session <id>". cc-task is non-blocking for
12
+ # resolve_item gate semantics.
13
+ #
14
+ # Respects [mirror].task_to_dct config: auto (default) / ask / off.
15
+ #
16
+ # Follows the five hook rules from dct CLAUDE.md:
17
+ # 1. No interactive prompts.
18
+ # 2. Consume stdin.
19
+ # 3. Graceful on missing deps.
20
+ # 4. exit 0 on every path.
21
+ # 5. No commit-message inspection.
22
+ set -euo pipefail
23
+
24
+ # Rule 2: drain stdin once; reuse for both tool detection and forwarding.
25
+ input=$(cat)
26
+
27
+ # Rule 3: graceful on missing deps.
28
+ command -v jq >/dev/null || exit 0
29
+ command -v dct >/dev/null || exit 0
30
+
31
+ tool=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)
32
+ session_id=$(echo "$input" | jq -r '.session_id // "unknown"' 2>/dev/null || true)
33
+
34
+ case "$tool" in
35
+ TaskCreate|TodoWrite) ;;
36
+ *) exit 0 ;;
37
+ esac
38
+
39
+ # Config gate.
40
+ mode=$(dct config get mirror.task_to_dct 2>/dev/null || echo "auto")
41
+ case "$mode" in
42
+ off|ask) exit 0 ;;
43
+ auto|"") ;;
44
+ *) exit 0 ;;
45
+ esac
46
+
47
+ # Project autodetect (hook CWD = current project).
48
+ project=$(dct project detect 2>/dev/null || true)
49
+ [ -z "$project" ] && exit 0
50
+
51
+ log_dir="${TMPDIR:-/tmp}"
52
+ log="${log_dir%/}/dct-task-mirror-$(date +%s)-$$.log"
53
+
54
+ # Fire-and-forget. The background subshell pipes the payload to the
55
+ # internal CLI worker and logs any output for diagnosis.
56
+ {
57
+ nohup bash -c "printf '%s' \"\$1\" | dct _task-mirror --session-id \"\$2\" --project \"\$3\" >>\"\$4\" 2>&1" \
58
+ _ "$input" "$session_id" "$project" "$log" &
59
+ disown
60
+ } </dev/null >/dev/null 2>&1
61
+
62
+ exit 0