dctracker 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dctracker-1.2.0/dctracker.egg-info → dctracker-1.3.0}/PKG-INFO +14 -9
- {dctracker-1.2.0 → dctracker-1.3.0}/README.md +11 -6
- dctracker-1.3.0/dct/__init__.py +6 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/cli.py +321 -21
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/analytics.py +13 -8
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/changelog.py +4 -4
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/checkpoints.py +101 -7
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/decisions.py +41 -4
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/engine.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/handoff.py +89 -33
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/items.py +92 -44
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/plans/crud.py +11 -11
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/plans/parser.py +0 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/projects.py +7 -7
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/provenance.py +4 -1
- dctracker-1.3.0/dct/core/sprint.py +508 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/sprints.py +371 -154
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/db.py +51 -7
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/export.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/integrations/claude.py +7 -5
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/mcp/server.py +332 -49
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/migrate.py +9 -7
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/paths.py +6 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/server.py +1 -1
- dctracker-1.3.0/dct/services/__init__.py +7 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/setup.py +1 -1
- dctracker-1.3.0/dct/skills/handoff/SKILL.md +188 -0
- dctracker-1.3.0/dct/skills/pickup/SKILL.md +147 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/track-update/SKILL.md +6 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/update.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/app.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/changelog.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/decisions.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/events.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/handoffs.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/items.py +5 -6
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/plans.py +3 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/sprints.py +16 -6
- dctracker-1.3.0/dct/workflows/__init__.py +7 -0
- dctracker-1.3.0/dct/workflows/session.py +596 -0
- {dctracker-1.2.0 → dctracker-1.3.0/dctracker.egg-info}/PKG-INFO +14 -9
- {dctracker-1.2.0 → dctracker-1.3.0}/dctracker.egg-info/SOURCES.txt +6 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/dctracker.egg-info/requires.txt +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/pyproject.toml +14 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_adversarial.py +16 -5
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_analytics.py +41 -34
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_changelog.py +5 -1
- dctracker-1.3.0/tests/test_check_numbers.py +219 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_checkpoints.py +9 -4
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_cli_sprints.py +1 -16
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_cli_web.py +0 -1
- dctracker-1.3.0/tests/test_cli_workflows.py +616 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_config.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_handoff.py +7 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_ids.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_items.py +8 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_migrate.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_migration.py +165 -13
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_plan_ingest.py +5 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_plan_parser.py +0 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_projects.py +9 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_server_acks.py +4 -14
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_server_alwaysload.py +9 -6
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_server_handoff_view.py +29 -18
- dctracker-1.3.0/tests/test_server_workflow_ops.py +527 -0
- dctracker-1.3.0/tests/test_sprint.py +310 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_sprints_crud.py +139 -35
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_update.py +2 -4
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_version.py +2 -3
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_web_analytics.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_web_app.py +3 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_web_items.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_web_structural.py +5 -5
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_windows_support.py +1 -3
- dctracker-1.3.0/tests/test_workflows.py +1287 -0
- dctracker-1.2.0/dct/__init__.py +0 -6
- dctracker-1.2.0/dct/core/sprint.py +0 -315
- dctracker-1.2.0/dct/services/__init__.py +0 -5
- dctracker-1.2.0/dct/skills/handoff/SKILL.md +0 -129
- dctracker-1.2.0/dct/skills/pickup/SKILL.md +0 -110
- dctracker-1.2.0/dct/workflows/__init__.py +0 -3
- dctracker-1.2.0/tests/test_sprint.py +0 -154
- {dctracker-1.2.0 → dctracker-1.3.0}/LICENSE +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/MANIFEST.in +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/assets/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/config.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/ids.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/plans/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/plans/classify.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/plans/ingest.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/core/version.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/hooks/check-changelog-on-stop.sh +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/hooks/check-changelog.sh +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/hooks/dct-plan-autoscan.sh +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/hooks/dct-task-mirror.sh +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/integrations/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/mcp/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/clog/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/commit/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/dct-import/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/dct-init/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/plan-resolve-uncertain/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/plan-status/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/release/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/track/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/track-list/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/skills/track-resolve/SKILL.md +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/__init__.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/analytics.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/home.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/blueprints/projects.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/changes.py +9 -9
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/serializers.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/app.css +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/app.js +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/vendor/SOURCES.txt +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/vendor/htmx.min.js +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/vendor/uPlot.iife.min.js +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/static/vendor/uPlot.min.css +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/analytics.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/base.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/changelog/list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/decisions/list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/handoffs/list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/home.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/item_detail.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/items_list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/partials/_bars.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/partials/_checkpoint_steps.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/partials/_items_table.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/partials/_plan_section.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/partials/_project_card.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/plans/detail.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/plans/list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/project_home.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/sprints/detail.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dct/web/templates/sprints/list.html +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dctracker.egg-info/dependency_links.txt +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dctracker.egg-info/entry_points.txt +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/dctracker.egg-info/top_level.txt +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/setup.cfg +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_changes.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_cli_prompts.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_decisions_crud.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_engine.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_export.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_hook_bypass.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_hook_changelog_stop.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_hook_plan_autoscan.py +2 -2
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_hook_task_mirror.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_ingest_dogfood.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_packaging.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_paths.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_plan_classify.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_plans_crud.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_provenance.py +1 -1
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_restructure.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_setup.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_setup_wiring.py +0 -0
- {dctracker-1.2.0 → dctracker-1.3.0}/tests/test_web_views.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dctracker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Deterministic Context Tracker (dct) — cross-project tracking, changelog, and release tooling for Claude Code.
|
|
5
5
|
Author-email: fotodeveloper <dev@fotodeveloper.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: platformdirs>=4.9.6; sys_platform == "win32"
|
|
|
25
25
|
Provides-Extra: pg
|
|
26
26
|
Requires-Dist: psycopg[binary]; extra == "pg"
|
|
27
27
|
Provides-Extra: mcp
|
|
28
|
-
Requires-Dist: mcp[cli]; extra == "mcp"
|
|
28
|
+
Requires-Dist: mcp[cli]<2,>=1.2; extra == "mcp"
|
|
29
29
|
Provides-Extra: web
|
|
30
30
|
Requires-Dist: flask>=3.0; extra == "web"
|
|
31
31
|
Requires-Dist: markdown>=3.5; extra == "web"
|
|
@@ -34,7 +34,7 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
34
34
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
35
35
|
Provides-Extra: all
|
|
36
36
|
Requires-Dist: psycopg[binary]; extra == "all"
|
|
37
|
-
Requires-Dist: mcp[cli]; extra == "all"
|
|
37
|
+
Requires-Dist: mcp[cli]<2,>=1.2; extra == "all"
|
|
38
38
|
Requires-Dist: flask>=3.0; extra == "all"
|
|
39
39
|
Requires-Dist: markdown>=3.5; extra == "all"
|
|
40
40
|
Requires-Dist: pytest; extra == "all"
|
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
|
48
48
|
[](https://github.com/fotodeveloper/dct/actions/workflows/ci.yml)
|
|
49
49
|
[](LICENSE)
|
|
50
50
|
|
|
51
|
-
Persistent, queryable memory for AI coding assistants — your assistant reads facts from a database instead of guessing. One user-scoped database tracks issues, todos, changelog entries, plans, sprints, and decisions across all your projects. Built for [Claude Code](https://claude.com/claude-code) (skills, hooks,
|
|
51
|
+
Persistent, queryable memory for AI coding assistants — your assistant reads facts from a database instead of guessing. One user-scoped database tracks issues, todos, changelog entries, plans, sprints, and decisions across all your projects. Built for [Claude Code](https://claude.com/claude-code) (skills, hooks, 52 MCP tools); the MCP server and CLI work with any MCP-capable assistant.
|
|
52
52
|
|
|
53
53
|
Part of the **dc tools** family ([dctools.dev](https://dctools.dev)) — *deterministic context* for AI-assisted development.
|
|
54
54
|
|
|
@@ -122,7 +122,7 @@ markdown, and makes the release commit.
|
|
|
122
122
|
- **Decision records** — ADRs, sprint amendments, inline plan decisions with multi-anchor linkage
|
|
123
123
|
- **LLM-assisted classification** — uncertain plan sections auto-resolved via `claude --print` (haiku by default)
|
|
124
124
|
- **Passive autoscan hook** — PostToolUse hook re-scans plan-like files in background on every edit
|
|
125
|
-
- **MCP server** — native Claude Code integration via FastMCP
|
|
125
|
+
- **MCP server** — native Claude Code integration via FastMCP
|
|
126
126
|
- **CLI** — fast `dct` command for hooks and scripts
|
|
127
127
|
- **Skills** — Claude Code slash commands (`/track`, `/clog`, `/dct-import`, `/plan-status`, …)
|
|
128
128
|
- **Soft deletes** — ledger pattern, nothing is ever lost
|
|
@@ -257,7 +257,12 @@ dct sprint create "Payments hardening" --code pay-1 --goal "Close the P1 backlog
|
|
|
257
257
|
dct sprint show 3 # Details + linked items
|
|
258
258
|
dct sprint activate 3 # Shortcuts: activate / complete / defer
|
|
259
259
|
|
|
260
|
-
#
|
|
260
|
+
# Session edges (same composed ops the /pickup and /handoff skills use)
|
|
261
|
+
dct pickup # Snapshot: sprint, handoffs, gates, decisions
|
|
262
|
+
# (read-only — --consume defaults to never)
|
|
263
|
+
dct handoff create --prompt-file - --add-items 22,6 # Sprint change + handoff, one transaction
|
|
264
|
+
# --remove-items cuts scope, --items replaces;
|
|
265
|
+
# omit all three to leave the sprint untouched
|
|
261
266
|
dct handoff update 12 --prompt "Wire the toggle into settings next"
|
|
262
267
|
|
|
263
268
|
# Config
|
|
@@ -267,7 +272,7 @@ dct config path # Show resolved config/data locations
|
|
|
267
272
|
|
|
268
273
|
## MCP Tools
|
|
269
274
|
|
|
270
|
-
dct ships **
|
|
275
|
+
dct ships **52 MCP tools**, registered at user scope so they're callable from any Claude Code session as `mcp__dct__<name>`. Zero-arg calls auto-detect the project from the session's working directory; pass `project=<slug>` to override. Write ops return a minimal ack (id + a few post-state scalars); fetch full context with `get_item(id)` / `get_handoff(id)`. Thirteen tools are always resident (`alwaysLoad`) — ten mid-session daily drivers plus the three composed workflow ops (`pickup_work`, `handoff_work`, `get_context_snapshot`); the rest of the catalogue loads on demand via ToolSearch:
|
|
271
276
|
|
|
272
277
|
| Tool | Purpose |
|
|
273
278
|
|---|---|
|
|
@@ -282,7 +287,7 @@ dct ships **49 MCP tools**, registered at user scope so they're callable from an
|
|
|
282
287
|
| `reject_checkpoint` | Mark a checkpoint rejected (scope change) — ledger-preserving |
|
|
283
288
|
| `add_changelog` | Add an unreleased changelog entry |
|
|
284
289
|
|
|
285
|
-
Session-edge tools stay deferred: `get_handoff` (full handoff dict — untruncated prompt + context_summary), `create_handoff`, `set_sprint`, `release_changelog`, and more. Full categorized catalogue
|
|
290
|
+
Session-edge tools stay deferred: `get_handoff` (full handoff dict — untruncated prompt + context_summary), `create_handoff`, `set_sprint`, `release_changelog`, and more. Full categorized catalogue: **[docs/mcp-tools.md](docs/mcp-tools.md)**.
|
|
286
291
|
|
|
287
292
|
## Plans, Roadmaps & Decisions
|
|
288
293
|
|
|
@@ -360,7 +365,7 @@ The `DCT_DATABASE_URL` environment variable overrides `[database].url`. For the
|
|
|
360
365
|
## Testing
|
|
361
366
|
|
|
362
367
|
```bash
|
|
363
|
-
.venv/bin/pytest -q # All tests
|
|
368
|
+
.venv/bin/pytest -q # All tests
|
|
364
369
|
.venv/bin/pytest tests/test_plan_parser.py -v # Parser only
|
|
365
370
|
.venv/bin/pytest tests/test_plan_ingest.py -v # Ingest + sticky-resolved semantics
|
|
366
371
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/fotodeveloper/dct/actions/workflows/ci.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Persistent, queryable memory for AI coding assistants — your assistant reads facts from a database instead of guessing. One user-scoped database tracks issues, todos, changelog entries, plans, sprints, and decisions across all your projects. Built for [Claude Code](https://claude.com/claude-code) (skills, hooks,
|
|
8
|
+
Persistent, queryable memory for AI coding assistants — your assistant reads facts from a database instead of guessing. One user-scoped database tracks issues, todos, changelog entries, plans, sprints, and decisions across all your projects. Built for [Claude Code](https://claude.com/claude-code) (skills, hooks, 52 MCP tools); the MCP server and CLI work with any MCP-capable assistant.
|
|
9
9
|
|
|
10
10
|
Part of the **dc tools** family ([dctools.dev](https://dctools.dev)) — *deterministic context* for AI-assisted development.
|
|
11
11
|
|
|
@@ -79,7 +79,7 @@ markdown, and makes the release commit.
|
|
|
79
79
|
- **Decision records** — ADRs, sprint amendments, inline plan decisions with multi-anchor linkage
|
|
80
80
|
- **LLM-assisted classification** — uncertain plan sections auto-resolved via `claude --print` (haiku by default)
|
|
81
81
|
- **Passive autoscan hook** — PostToolUse hook re-scans plan-like files in background on every edit
|
|
82
|
-
- **MCP server** — native Claude Code integration via FastMCP
|
|
82
|
+
- **MCP server** — native Claude Code integration via FastMCP
|
|
83
83
|
- **CLI** — fast `dct` command for hooks and scripts
|
|
84
84
|
- **Skills** — Claude Code slash commands (`/track`, `/clog`, `/dct-import`, `/plan-status`, …)
|
|
85
85
|
- **Soft deletes** — ledger pattern, nothing is ever lost
|
|
@@ -214,7 +214,12 @@ dct sprint create "Payments hardening" --code pay-1 --goal "Close the P1 backlog
|
|
|
214
214
|
dct sprint show 3 # Details + linked items
|
|
215
215
|
dct sprint activate 3 # Shortcuts: activate / complete / defer
|
|
216
216
|
|
|
217
|
-
#
|
|
217
|
+
# Session edges (same composed ops the /pickup and /handoff skills use)
|
|
218
|
+
dct pickup # Snapshot: sprint, handoffs, gates, decisions
|
|
219
|
+
# (read-only — --consume defaults to never)
|
|
220
|
+
dct handoff create --prompt-file - --add-items 22,6 # Sprint change + handoff, one transaction
|
|
221
|
+
# --remove-items cuts scope, --items replaces;
|
|
222
|
+
# omit all three to leave the sprint untouched
|
|
218
223
|
dct handoff update 12 --prompt "Wire the toggle into settings next"
|
|
219
224
|
|
|
220
225
|
# Config
|
|
@@ -224,7 +229,7 @@ dct config path # Show resolved config/data locations
|
|
|
224
229
|
|
|
225
230
|
## MCP Tools
|
|
226
231
|
|
|
227
|
-
dct ships **
|
|
232
|
+
dct ships **52 MCP tools**, registered at user scope so they're callable from any Claude Code session as `mcp__dct__<name>`. Zero-arg calls auto-detect the project from the session's working directory; pass `project=<slug>` to override. Write ops return a minimal ack (id + a few post-state scalars); fetch full context with `get_item(id)` / `get_handoff(id)`. Thirteen tools are always resident (`alwaysLoad`) — ten mid-session daily drivers plus the three composed workflow ops (`pickup_work`, `handoff_work`, `get_context_snapshot`); the rest of the catalogue loads on demand via ToolSearch:
|
|
228
233
|
|
|
229
234
|
| Tool | Purpose |
|
|
230
235
|
|---|---|
|
|
@@ -239,7 +244,7 @@ dct ships **49 MCP tools**, registered at user scope so they're callable from an
|
|
|
239
244
|
| `reject_checkpoint` | Mark a checkpoint rejected (scope change) — ledger-preserving |
|
|
240
245
|
| `add_changelog` | Add an unreleased changelog entry |
|
|
241
246
|
|
|
242
|
-
Session-edge tools stay deferred: `get_handoff` (full handoff dict — untruncated prompt + context_summary), `create_handoff`, `set_sprint`, `release_changelog`, and more. Full categorized catalogue
|
|
247
|
+
Session-edge tools stay deferred: `get_handoff` (full handoff dict — untruncated prompt + context_summary), `create_handoff`, `set_sprint`, `release_changelog`, and more. Full categorized catalogue: **[docs/mcp-tools.md](docs/mcp-tools.md)**.
|
|
243
248
|
|
|
244
249
|
## Plans, Roadmaps & Decisions
|
|
245
250
|
|
|
@@ -317,7 +322,7 @@ The `DCT_DATABASE_URL` environment variable overrides `[database].url`. For the
|
|
|
317
322
|
## Testing
|
|
318
323
|
|
|
319
324
|
```bash
|
|
320
|
-
.venv/bin/pytest -q # All tests
|
|
325
|
+
.venv/bin/pytest -q # All tests
|
|
321
326
|
.venv/bin/pytest tests/test_plan_parser.py -v # Parser only
|
|
322
327
|
.venv/bin/pytest tests/test_plan_ingest.py -v # Ingest + sticky-resolved semantics
|
|
323
328
|
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import atexit
|
|
5
|
+
import contextlib
|
|
5
6
|
import os
|
|
6
7
|
import signal
|
|
7
8
|
import socket
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
11
|
+
from datetime import UTC
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
|
|
12
14
|
from dct import __version__, migrate, paths
|
|
13
|
-
from dct.config import load_config, save_config
|
|
14
|
-
from dct.db import get_engine, create_tables
|
|
15
|
+
from dct.config import Config, load_config, save_config
|
|
15
16
|
from dct.core.engine import bootstrap_engine
|
|
17
|
+
from dct.core.items import TERMINAL_STATUSES
|
|
18
|
+
from dct.db import create_tables, get_engine
|
|
16
19
|
|
|
17
20
|
# Windows-policy seam (tests flip it): dispatches the daemonization,
|
|
18
21
|
# liveness-probe and signal paths that differ on win32 (W8-W10, ADR #52).
|
|
@@ -35,6 +38,7 @@ def _get_engine():
|
|
|
35
38
|
def _detect_project(engine) -> str:
|
|
36
39
|
"""Auto-detect project slug from CWD. Exits if not found."""
|
|
37
40
|
import os
|
|
41
|
+
|
|
38
42
|
from dct.core.projects import get_project_by_path
|
|
39
43
|
|
|
40
44
|
cwd = os.getcwd()
|
|
@@ -72,6 +76,59 @@ def _prompt_version(default: str = "0.1.0") -> str | None:
|
|
|
72
76
|
return shortcut.get(choice, choice)
|
|
73
77
|
|
|
74
78
|
|
|
79
|
+
def _read_text_arg(text: str | None, path: str | None) -> str | None:
|
|
80
|
+
"""Resolve a `--foo` / `--foo-file` flag pair to text ('-' = stdin).
|
|
81
|
+
|
|
82
|
+
The file wins when both are given. Returns None when neither is supplied,
|
|
83
|
+
so callers can tell "not passed" from an empty string. Raises (OSError) on
|
|
84
|
+
an unreadable path — call it INSIDE the command's try/except so the user
|
|
85
|
+
gets `✗ Error: ...` instead of a traceback.
|
|
86
|
+
|
|
87
|
+
Encoding is pinned to UTF-8 on BOTH paths: handoff prompts routinely carry
|
|
88
|
+
'✓', '—' and non-ASCII prose, and the platform default (cp1252 on Windows,
|
|
89
|
+
or a C-locale ASCII stdin) would mangle or reject them.
|
|
90
|
+
"""
|
|
91
|
+
if path:
|
|
92
|
+
if path != "-":
|
|
93
|
+
return Path(path).read_text(encoding="utf-8")
|
|
94
|
+
# Decode the raw byte stream ourselves so a C-locale stdin still takes
|
|
95
|
+
# UTF-8. `buffer` is absent when stdin is a StringIO (tests, embedders)
|
|
96
|
+
# — then the text stream is already decoded.
|
|
97
|
+
buffer = getattr(sys.stdin, "buffer", None)
|
|
98
|
+
if buffer is None:
|
|
99
|
+
return sys.stdin.read()
|
|
100
|
+
return buffer.read().decode("utf-8")
|
|
101
|
+
return text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _json_default(obj):
|
|
105
|
+
"""json.dumps fallback for core payloads — datetime → ISO 8601, rest → str.
|
|
106
|
+
|
|
107
|
+
Core dicts carry real datetime objects (created_at, consumed_at, ...);
|
|
108
|
+
without this every `--json` surface would raise TypeError.
|
|
109
|
+
"""
|
|
110
|
+
from datetime import date, datetime
|
|
111
|
+
|
|
112
|
+
if isinstance(obj, (datetime, date)):
|
|
113
|
+
return obj.isoformat()
|
|
114
|
+
return str(obj)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _default_identity() -> str:
|
|
118
|
+
"""Actor identity used when pickup/handoff callers omit theirs.
|
|
119
|
+
|
|
120
|
+
Shape: `<client> @ <UTC minute>` — the provenance client when the host
|
|
121
|
+
self-identifies (Claude Code, a G2 adapter), else "cli". Commands ECHO the
|
|
122
|
+
resolved value so attribution is never silent.
|
|
123
|
+
"""
|
|
124
|
+
from datetime import datetime
|
|
125
|
+
|
|
126
|
+
from dct.core.provenance import get_provenance
|
|
127
|
+
|
|
128
|
+
client = get_provenance().actor_client or "cli"
|
|
129
|
+
return f"{client} @ {datetime.now(UTC).strftime('%Y-%m-%dT%H:%MZ')}"
|
|
130
|
+
|
|
131
|
+
|
|
75
132
|
def _print_table(headers: list[str], rows: list[list[str]], max_widths: dict[int, int] | None = None) -> None:
|
|
76
133
|
"""Print a simple aligned table."""
|
|
77
134
|
if not rows:
|
|
@@ -239,6 +296,7 @@ def cmd_hooks_wire(args):
|
|
|
239
296
|
|
|
240
297
|
def cmd_project_add(args):
|
|
241
298
|
import os
|
|
299
|
+
|
|
242
300
|
from dct.core.projects import create_project, set_version
|
|
243
301
|
|
|
244
302
|
# #227: realpath (not abspath) so a symlinked path is stored canonically and
|
|
@@ -360,7 +418,11 @@ def cmd_project_changelog_path(args):
|
|
|
360
418
|
With a relpath → set it (validated to stay within the project root). Without
|
|
361
419
|
→ print the current relpath and its resolved absolute path.
|
|
362
420
|
"""
|
|
363
|
-
from dct.core.projects import
|
|
421
|
+
from dct.core.projects import (
|
|
422
|
+
get_project,
|
|
423
|
+
resolve_changelog_path,
|
|
424
|
+
set_changelog_path,
|
|
425
|
+
)
|
|
364
426
|
|
|
365
427
|
engine = _get_engine()
|
|
366
428
|
|
|
@@ -400,6 +462,7 @@ def cmd_plan_rescan(args):
|
|
|
400
462
|
injection + [x] state reading (§9.2).
|
|
401
463
|
"""
|
|
402
464
|
import os
|
|
465
|
+
|
|
403
466
|
from dct.core.plans.ingest import ingest_file
|
|
404
467
|
from dct.core.projects import get_project_by_path
|
|
405
468
|
|
|
@@ -614,19 +677,69 @@ def cmd_delete(args):
|
|
|
614
677
|
|
|
615
678
|
# ── Handoff commands ──────────────────────────────────────────────────────────
|
|
616
679
|
|
|
617
|
-
def
|
|
618
|
-
|
|
680
|
+
def cmd_handoff_create(args):
|
|
681
|
+
"""`dct handoff create` — session-end op: set sprint membership + leave the
|
|
682
|
+
handoff prompt in ONE transaction (workflows.session.handoff_work)."""
|
|
683
|
+
import json
|
|
684
|
+
|
|
685
|
+
from dct.workflows.session import handoff_work
|
|
619
686
|
|
|
620
687
|
engine = _get_engine()
|
|
688
|
+
try:
|
|
689
|
+
project = args.project or _detect_project(engine)
|
|
690
|
+
prompt = _read_text_arg(args.prompt, args.prompt_file)
|
|
691
|
+
identity = args.created_by or _default_identity()
|
|
692
|
+
|
|
693
|
+
def _ids(raw: str | None, flag: str) -> list[int] | None:
|
|
694
|
+
# None → flag unused; empty/garbage → error (a blank shell var
|
|
695
|
+
# must never read as "no items" or wipe anything — K-27).
|
|
696
|
+
if raw is None:
|
|
697
|
+
return None
|
|
698
|
+
try:
|
|
699
|
+
ids = [int(x.strip()) for x in raw.split(",") if x.strip()]
|
|
700
|
+
except ValueError:
|
|
701
|
+
raise ValueError(
|
|
702
|
+
f"{flag} expects comma-separated item ids, got: {raw!r}"
|
|
703
|
+
) from None
|
|
704
|
+
if not ids:
|
|
705
|
+
raise ValueError(f"{flag} got no ids ({raw!r}) — omit the flag instead")
|
|
706
|
+
return ids
|
|
707
|
+
|
|
708
|
+
result = handoff_work(
|
|
709
|
+
engine, project,
|
|
710
|
+
prompt=prompt or "",
|
|
711
|
+
created_by=identity,
|
|
712
|
+
item_ids=_ids(args.items, "--items"),
|
|
713
|
+
add_item_ids=_ids(args.add_items, "--add-items"),
|
|
714
|
+
remove_item_ids=_ids(args.remove_items, "--remove-items"),
|
|
715
|
+
sprint=args.sprint,
|
|
716
|
+
scope=args.scope,
|
|
717
|
+
context_summary=args.context_summary,
|
|
718
|
+
)
|
|
719
|
+
except Exception as e:
|
|
720
|
+
print(f"✗ Error: {e}", file=sys.stderr)
|
|
721
|
+
sys.exit(1)
|
|
621
722
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
723
|
+
if args.json:
|
|
724
|
+
print(json.dumps(result, indent=2, default=_json_default))
|
|
725
|
+
return
|
|
726
|
+
|
|
727
|
+
h = result["handoff"]
|
|
728
|
+
print(f"✓ Handoff #{h['id']} created (scope: {h.get('scope') or 'none'}, by {identity})")
|
|
729
|
+
sprint = result.get("sprint")
|
|
730
|
+
if sprint:
|
|
731
|
+
print(f" sprint #{sprint['sprint_id']}: {sprint['open_size']} open items")
|
|
732
|
+
else:
|
|
733
|
+
print(" sprint untouched")
|
|
626
734
|
|
|
627
|
-
|
|
735
|
+
|
|
736
|
+
def cmd_handoff_update(args):
|
|
737
|
+
from dct.core.handoff import update_handoff
|
|
738
|
+
|
|
739
|
+
engine = _get_engine()
|
|
628
740
|
|
|
629
741
|
try:
|
|
742
|
+
prompt = _read_text_arg(args.prompt, args.prompt_file)
|
|
630
743
|
result = update_handoff(
|
|
631
744
|
engine, args.id,
|
|
632
745
|
prompt=prompt or None,
|
|
@@ -642,6 +755,129 @@ def cmd_handoff_update(args):
|
|
|
642
755
|
sys.exit(1)
|
|
643
756
|
|
|
644
757
|
|
|
758
|
+
# ── Session pickup (#413) ─────────────────────────────────────────────────────
|
|
759
|
+
|
|
760
|
+
def _cols_rows_dicts(table: dict | None) -> list[dict]:
|
|
761
|
+
"""{cols, rows} → list of dicts — the compact wire shape read by name."""
|
|
762
|
+
if not table:
|
|
763
|
+
return []
|
|
764
|
+
return [dict(zip(table["cols"], row)) for row in table["rows"]]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
_CONTROL_CHARS = {c: None for c in range(0x20) if c not in (0x09, 0x0A)} | {0x7F: None}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _strip_controls(text: str | None) -> str:
|
|
771
|
+
"""Drop C0 control characters from DB text before printing it.
|
|
772
|
+
|
|
773
|
+
Handoff prompts and item titles are free text written by another session;
|
|
774
|
+
an ESC sequence in there would repaint or clear the reader's terminal. Tabs
|
|
775
|
+
and newlines are legitimate formatting and survive — everything else below
|
|
776
|
+
0x20, plus DEL, is removed. None → "".
|
|
777
|
+
"""
|
|
778
|
+
if not text:
|
|
779
|
+
return ""
|
|
780
|
+
return text.translate(_CONTROL_CHARS)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _render_pickup(snap: dict, identity: str | None) -> None:
|
|
784
|
+
"""Human view of `dct pickup`: what was consumed, then the state that
|
|
785
|
+
matters at session start. Sections dropped via --include are simply
|
|
786
|
+
absent from the snapshot, so every lookup is guarded. ALL free text coming
|
|
787
|
+
from the DB is passed through _strip_controls — a stored escape sequence
|
|
788
|
+
must not drive the reader's terminal."""
|
|
789
|
+
picked = snap.get("picked_up")
|
|
790
|
+
summary = snap.get("summary", {})
|
|
791
|
+
|
|
792
|
+
if picked:
|
|
793
|
+
print(f"✓ Picked up handoff #{picked['id']} (consumed as {identity})")
|
|
794
|
+
print()
|
|
795
|
+
for line in _strip_controls(picked.get("prompt")).splitlines():
|
|
796
|
+
print(f" {line}")
|
|
797
|
+
else:
|
|
798
|
+
print(f"— no handoff consumed ({summary.get('handoffs_pending', 0)} pending)")
|
|
799
|
+
|
|
800
|
+
sprint = snap.get("sprint")
|
|
801
|
+
if sprint:
|
|
802
|
+
items = _cols_rows_dicts(sprint.get("items"))
|
|
803
|
+
pending_cp = sum(
|
|
804
|
+
1 for c in _cols_rows_dicts(sprint.get("checkpoints")) if c.get("status") == "pending"
|
|
805
|
+
)
|
|
806
|
+
print(f"\nSprint items ({pending_cp} pending checkpoints):")
|
|
807
|
+
_print_table(
|
|
808
|
+
["#", "sprint", "prio", "status", "cp", "title"],
|
|
809
|
+
[
|
|
810
|
+
[
|
|
811
|
+
str(i["id"]), str(i.get("sprint_id") or ""), i.get("priority") or "",
|
|
812
|
+
i.get("status") or "", "{}/{}".format(*(i.get("cp") or [0, 0])),
|
|
813
|
+
_strip_controls(i.get("title")),
|
|
814
|
+
]
|
|
815
|
+
for i in items
|
|
816
|
+
],
|
|
817
|
+
max_widths={5: 60},
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
gates = _cols_rows_dicts(snap.get("gates"))
|
|
821
|
+
if gates:
|
|
822
|
+
print("\nPending gates:")
|
|
823
|
+
_print_table(
|
|
824
|
+
["#", "cp", "kind", "prio", "title", "text"],
|
|
825
|
+
[
|
|
826
|
+
[
|
|
827
|
+
str(g["item_id"]), str(g["checkpoint_id"]), g.get("kind") or "",
|
|
828
|
+
g.get("priority") or "", _strip_controls(g.get("item_title")),
|
|
829
|
+
_strip_controls(g.get("text")),
|
|
830
|
+
]
|
|
831
|
+
for g in gates
|
|
832
|
+
],
|
|
833
|
+
max_widths={4: 30, 5: 50},
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
if summary:
|
|
837
|
+
print(
|
|
838
|
+
f"\nSprint: size {summary.get('sprint_size', 0)}, "
|
|
839
|
+
f"done {summary.get('sprint_completion', '0/0')} · "
|
|
840
|
+
f"open in project: {summary.get('total_open_in_project', 0)} · "
|
|
841
|
+
f"handoffs pending: {summary.get('handoffs_pending', 0)} · "
|
|
842
|
+
f"gates: {summary.get('gates_pending', 0)}"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def cmd_pickup(args):
|
|
847
|
+
"""`dct pickup` — session-start op: one snapshot read + (possibly) consume
|
|
848
|
+
ONE handoff (workflows.session.pickup_work)."""
|
|
849
|
+
import json
|
|
850
|
+
|
|
851
|
+
from dct.workflows.session import parse_sections, pickup_work
|
|
852
|
+
|
|
853
|
+
engine = _get_engine()
|
|
854
|
+
consume = {"auto": None, "always": True, "never": False}[args.consume]
|
|
855
|
+
# consumed_by is REQUIRED by pickup_work whenever consumption is possible;
|
|
856
|
+
# default it (and echo it) rather than let the op fail on a missing identity.
|
|
857
|
+
identity = args.consumed_by or (None if consume is False else _default_identity())
|
|
858
|
+
|
|
859
|
+
try:
|
|
860
|
+
project = args.project or _detect_project(engine)
|
|
861
|
+
include = parse_sections(args.include)
|
|
862
|
+
snap = pickup_work(
|
|
863
|
+
engine, project,
|
|
864
|
+
consume=consume,
|
|
865
|
+
handoff_id=args.handoff_id,
|
|
866
|
+
scope=args.scope,
|
|
867
|
+
consumed_by=identity,
|
|
868
|
+
since=args.since,
|
|
869
|
+
include=include,
|
|
870
|
+
)
|
|
871
|
+
except Exception as e:
|
|
872
|
+
print(f"✗ Error: {e}", file=sys.stderr)
|
|
873
|
+
sys.exit(1)
|
|
874
|
+
|
|
875
|
+
if args.json:
|
|
876
|
+
print(json.dumps(snap, indent=2, default=_json_default))
|
|
877
|
+
return
|
|
878
|
+
_render_pickup(snap, identity)
|
|
879
|
+
|
|
880
|
+
|
|
645
881
|
# ── Changelog commands ────────────────────────────────────────────────────────
|
|
646
882
|
|
|
647
883
|
def cmd_clog_add(args):
|
|
@@ -695,7 +931,8 @@ def cmd_clog_count(args):
|
|
|
695
931
|
|
|
696
932
|
def cmd_clog_release(args):
|
|
697
933
|
import subprocess
|
|
698
|
-
|
|
934
|
+
|
|
935
|
+
from dct.core.changelog import count_unreleased, release_entries
|
|
699
936
|
from dct.core.projects import get_project
|
|
700
937
|
from dct.core.version import bump
|
|
701
938
|
|
|
@@ -777,6 +1014,7 @@ def cmd_internal_task_mirror(args):
|
|
|
777
1014
|
project: dct project slug. Empty → autodetect from CWD.
|
|
778
1015
|
"""
|
|
779
1016
|
import json
|
|
1017
|
+
|
|
780
1018
|
from dct.config import load_config
|
|
781
1019
|
|
|
782
1020
|
try:
|
|
@@ -812,8 +1050,8 @@ def cmd_internal_task_mirror(args):
|
|
|
812
1050
|
if not todos:
|
|
813
1051
|
return
|
|
814
1052
|
|
|
815
|
-
from dct.core.items import list_items, create_item
|
|
816
1053
|
from dct.core.checkpoints import add_checkpoint
|
|
1054
|
+
from dct.core.items import create_item, list_items
|
|
817
1055
|
from dct.core.projects import get_project_by_path
|
|
818
1056
|
|
|
819
1057
|
engine = _get_engine()
|
|
@@ -941,7 +1179,7 @@ def cmd_checkpoint_add(args):
|
|
|
941
1179
|
|
|
942
1180
|
|
|
943
1181
|
def cmd_checkpoint_list(args):
|
|
944
|
-
from dct.core.checkpoints import
|
|
1182
|
+
from dct.core.checkpoints import get_progress, list_checkpoints
|
|
945
1183
|
|
|
946
1184
|
engine = _get_engine()
|
|
947
1185
|
cps = list_checkpoints(engine, args.item_id)
|
|
@@ -1125,7 +1363,8 @@ def cmd_upgrade(args):
|
|
|
1125
1363
|
matrix so the user knows what takes effect when.
|
|
1126
1364
|
"""
|
|
1127
1365
|
import subprocess
|
|
1128
|
-
|
|
1366
|
+
|
|
1367
|
+
from dct.update import detect_install_method, dist_pkg, upgrade_command
|
|
1129
1368
|
|
|
1130
1369
|
method = detect_install_method()
|
|
1131
1370
|
|
|
@@ -1138,14 +1377,18 @@ def cmd_upgrade(args):
|
|
|
1138
1377
|
cmd = upgrade_command(method)
|
|
1139
1378
|
print(f"Detected install method: {method}")
|
|
1140
1379
|
print(f"Upgrading {dist_pkg()} … ({' '.join(cmd)})")
|
|
1141
|
-
result = subprocess.run(cmd)
|
|
1380
|
+
result = subprocess.run(cmd, check=False)
|
|
1142
1381
|
if result.returncode != 0:
|
|
1143
1382
|
print(f"✗ Upgrade command failed (exit {result.returncode})", file=sys.stderr)
|
|
1144
1383
|
sys.exit(result.returncode)
|
|
1145
1384
|
|
|
1146
1385
|
# Refresh Claude Code integration: re-link current skills/hooks (idempotent),
|
|
1147
1386
|
# then remove links to skills/hooks that the new package no longer ships.
|
|
1148
|
-
from dct.integrations.claude import
|
|
1387
|
+
from dct.integrations.claude import (
|
|
1388
|
+
install_hook,
|
|
1389
|
+
install_skills,
|
|
1390
|
+
prune_dangling_symlinks,
|
|
1391
|
+
)
|
|
1149
1392
|
|
|
1150
1393
|
linked_ok = True
|
|
1151
1394
|
removed: list[str] = []
|
|
@@ -1190,6 +1433,7 @@ def _maybe_print_legacy_nudge() -> None:
|
|
|
1190
1433
|
|
|
1191
1434
|
def cmd_project_detect(args):
|
|
1192
1435
|
import os
|
|
1436
|
+
|
|
1193
1437
|
from dct.core.projects import get_project_by_path
|
|
1194
1438
|
|
|
1195
1439
|
engine = _get_engine()
|
|
@@ -1608,7 +1852,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1608
1852
|
# resolve
|
|
1609
1853
|
p_resolve = sub.add_parser("resolve", help="Resolve/close an item")
|
|
1610
1854
|
p_resolve.add_argument("id", type=int)
|
|
1611
|
-
p_resolve.add_argument(
|
|
1855
|
+
p_resolve.add_argument(
|
|
1856
|
+
"--status", "-s", default="resolved", choices=list(TERMINAL_STATUSES)
|
|
1857
|
+
)
|
|
1612
1858
|
p_resolve.add_argument("--resolution", "-r")
|
|
1613
1859
|
|
|
1614
1860
|
# note
|
|
@@ -1741,10 +1987,62 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1741
1987
|
p_verb.add_argument("id", type=int)
|
|
1742
1988
|
p_verb.set_defaults(_forced_status=_st)
|
|
1743
1989
|
|
|
1744
|
-
#
|
|
1990
|
+
# pickup (session-start op — snapshot + handoff consume, #413)
|
|
1991
|
+
p_pickup = sub.add_parser(
|
|
1992
|
+
"pickup",
|
|
1993
|
+
help="Session-start op: project snapshot + consume the pending handoff",
|
|
1994
|
+
)
|
|
1995
|
+
p_pickup.add_argument("--project", help="Project slug (default: auto-detect from CWD)")
|
|
1996
|
+
p_pickup.add_argument("--since", default="last_release",
|
|
1997
|
+
help="Activity window anchor for recent_changes: "
|
|
1998
|
+
"last_release (default) | all | <N>h (e.g. 24h) "
|
|
1999
|
+
"| ISO date")
|
|
2000
|
+
p_pickup.add_argument("--include", default=None,
|
|
2001
|
+
help="Comma-separated snapshot sections (sprint,handoff,gates,"
|
|
2002
|
+
"decisions,recent_changes). 'none' = summary only.")
|
|
2003
|
+
p_pickup.add_argument("--consume", choices=["auto", "always", "never"], default="never",
|
|
2004
|
+
help="never (default): pure read — pickup does not write; "
|
|
2005
|
+
"auto: consume an unambiguous single candidate; "
|
|
2006
|
+
"always: consume or fail (autonomous flows)")
|
|
2007
|
+
p_pickup.add_argument("--handoff-id", type=int, default=None,
|
|
2008
|
+
help="Consume this specific handoff (wins over --scope)")
|
|
2009
|
+
p_pickup.add_argument("--scope", default=None,
|
|
2010
|
+
help="Only consider handoffs carrying this scope tag")
|
|
2011
|
+
p_pickup.add_argument("--consumed-by", default=None,
|
|
2012
|
+
help="Session identity stamped on consume "
|
|
2013
|
+
"(default: '<client> @ <UTC minute>', echoed)")
|
|
2014
|
+
p_pickup.add_argument("--json", action="store_true", help="Print the raw snapshot as JSON")
|
|
2015
|
+
|
|
2016
|
+
# handoff (session-end ledger)
|
|
1745
2017
|
p_handoff = sub.add_parser("handoff", help="Manage session handoffs")
|
|
1746
2018
|
p_ho_sub = p_handoff.add_subparsers(dest="handoff_command")
|
|
1747
2019
|
|
|
2020
|
+
p_ho_create = p_ho_sub.add_parser(
|
|
2021
|
+
"create",
|
|
2022
|
+
help="Session-end op: set sprint membership + leave a handoff prompt (one transaction)",
|
|
2023
|
+
)
|
|
2024
|
+
p_ho_create.add_argument("--prompt", default=None, help="Handoff prompt text")
|
|
2025
|
+
p_ho_create.add_argument("--prompt-file", default=None,
|
|
2026
|
+
help="Read the prompt from a file ('-' = stdin)")
|
|
2027
|
+
p_ho_create.add_argument("--created-by", default=None,
|
|
2028
|
+
help="Session identity (default: '<client> @ <UTC minute>', echoed)")
|
|
2029
|
+
p_ho_create.add_argument("--items", default=None,
|
|
2030
|
+
help="REPLACE the sprint's OPEN membership with these ids "
|
|
2031
|
+
"(e.g. 22,21,6) — a conscious replan; resolved members "
|
|
2032
|
+
"stay as history. Mutually exclusive with --add-items/"
|
|
2033
|
+
"--remove-items. Omit all item flags to keep the sprint.")
|
|
2034
|
+
p_ho_create.add_argument("--add-items", default=None,
|
|
2035
|
+
help="ADD these item ids to the target sprint (additive edge)")
|
|
2036
|
+
p_ho_create.add_argument("--remove-items", default=None,
|
|
2037
|
+
help="CUT these OPEN items from the sprint's scope "
|
|
2038
|
+
"(terminal members refuse — they are history)")
|
|
2039
|
+
p_ho_create.add_argument("--sprint", default=None,
|
|
2040
|
+
help="Target sprint id or code (required when several are active)")
|
|
2041
|
+
p_ho_create.add_argument("--scope", default=None, help="Scope tag")
|
|
2042
|
+
p_ho_create.add_argument("--context-summary", default=None, help="Context summary snapshot")
|
|
2043
|
+
p_ho_create.add_argument("--project", help="Project slug (default: auto-detect from CWD)")
|
|
2044
|
+
p_ho_create.add_argument("--json", action="store_true", help="Print the raw result as JSON")
|
|
2045
|
+
|
|
1748
2046
|
p_ho_update = p_ho_sub.add_parser("update", help="Amend an unconsumed handoff in place")
|
|
1749
2047
|
p_ho_update.add_argument("id", type=int, help="Handoff id")
|
|
1750
2048
|
p_ho_update.add_argument("--prompt", default=None, help="New prompt text")
|
|
@@ -1752,6 +2050,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1752
2050
|
p_ho_update.add_argument("--scope", default=None, help="New scope tag")
|
|
1753
2051
|
p_ho_update.add_argument("--context-summary", default=None, help="New context summary snapshot")
|
|
1754
2052
|
|
|
2053
|
+
# web (read-only browser dashboard daemon — v0.4.1)
|
|
1755
2054
|
p_web = sub.add_parser("web", help="Read-only browser dashboard daemon")
|
|
1756
2055
|
p_web_sub = p_web.add_subparsers(dest="web_command")
|
|
1757
2056
|
|
|
@@ -1799,6 +2098,7 @@ COMMAND_MAP = {
|
|
|
1799
2098
|
"resolve": cmd_resolve,
|
|
1800
2099
|
"note": cmd_note,
|
|
1801
2100
|
"delete": cmd_delete,
|
|
2101
|
+
"pickup": cmd_pickup,
|
|
1802
2102
|
"_task-mirror": cmd_internal_task_mirror,
|
|
1803
2103
|
"_web-serve": cmd_internal_web_serve,
|
|
1804
2104
|
}
|
|
@@ -1852,6 +2152,7 @@ CHECKPOINT_COMMAND_MAP = {
|
|
|
1852
2152
|
}
|
|
1853
2153
|
|
|
1854
2154
|
HANDOFF_COMMAND_MAP = {
|
|
2155
|
+
"create": cmd_handoff_create,
|
|
1855
2156
|
"update": cmd_handoff_update,
|
|
1856
2157
|
}
|
|
1857
2158
|
|
|
@@ -1928,7 +2229,8 @@ def _maybe_nudge(args) -> None:
|
|
|
1928
2229
|
suppressed, or opted-out command) is silent and never affects the command's
|
|
1929
2230
|
own output or exit code.
|
|
1930
2231
|
"""
|
|
1931
|
-
|
|
2232
|
+
# A passive nicety must never break a command.
|
|
2233
|
+
with contextlib.suppress(Exception):
|
|
1932
2234
|
if not sys.stderr.isatty() or _nudge_suppressed(args):
|
|
1933
2235
|
return
|
|
1934
2236
|
from dct.config import load_config
|
|
@@ -1940,8 +2242,6 @@ def _maybe_nudge(args) -> None:
|
|
|
1940
2242
|
notice = get_cached_notice(config=cfg)
|
|
1941
2243
|
if notice:
|
|
1942
2244
|
print(notice, file=sys.stderr)
|
|
1943
|
-
except Exception:
|
|
1944
|
-
pass # a passive nicety must never break a command
|
|
1945
2245
|
|
|
1946
2246
|
|
|
1947
2247
|
def main():
|