@deftai/directive-content 0.55.2 → 0.56.1

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 (217) hide show
  1. package/.githooks/pre-commit +143 -0
  2. package/.githooks/pre-push +121 -0
  3. package/QUICK-START.md +2 -2
  4. package/Taskfile.yml +934 -0
  5. package/UPGRADING.md +47 -1
  6. package/events/README.md +3 -3
  7. package/package.json +5 -4
  8. package/scripts/_agents_md.py +494 -0
  9. package/scripts/_cache_fetch.py +635 -0
  10. package/scripts/_cache_quota.py +529 -0
  11. package/scripts/_cache_refresh.py +163 -0
  12. package/scripts/_cache_validate.py +209 -0
  13. package/scripts/_content_root.py +42 -0
  14. package/scripts/_doctor_state.py +277 -0
  15. package/scripts/_event_detect.py +305 -0
  16. package/scripts/_events.py +514 -0
  17. package/scripts/_lifecycle_hygiene.py +568 -0
  18. package/scripts/_pathspec.py +91 -0
  19. package/scripts/_policy_show_cli.py +266 -0
  20. package/scripts/_precutover.py +92 -0
  21. package/scripts/_project_context.py +224 -0
  22. package/scripts/_project_definition_io.py +164 -0
  23. package/scripts/_relocate_snapshot.py +209 -0
  24. package/scripts/_relocate_states.py +343 -0
  25. package/scripts/_resolve_preflight_path.py +152 -0
  26. package/scripts/_safe_subprocess.py +167 -0
  27. package/scripts/_session_start_hook.py +205 -0
  28. package/scripts/_sor_gate_diff.py +365 -0
  29. package/scripts/_stdio_utf8.py +59 -0
  30. package/scripts/_triage_bootstrap_gitignore.py +904 -0
  31. package/scripts/_triage_classify_cli.py +122 -0
  32. package/scripts/_triage_queue_cli.py +625 -0
  33. package/scripts/_triage_scope_cli.py +343 -0
  34. package/scripts/_triage_scope_drift_cli.py +121 -0
  35. package/scripts/_triage_scope_ignores.py +286 -0
  36. package/scripts/_triage_scope_milestone.py +432 -0
  37. package/scripts/_triage_scope_mutations.py +337 -0
  38. package/scripts/_triage_scope_renderers.py +207 -0
  39. package/scripts/_triage_smoketest_stages.py +674 -0
  40. package/scripts/_triage_subscribe_cli.py +140 -0
  41. package/scripts/_triage_welcome_cli.py +421 -0
  42. package/scripts/_vbrief_build.py +239 -0
  43. package/scripts/_vbrief_fidelity.py +479 -0
  44. package/scripts/_vbrief_legacy.py +589 -0
  45. package/scripts/_vbrief_reconciliation.py +883 -0
  46. package/scripts/_vbrief_routing.py +277 -0
  47. package/scripts/_vbrief_safety.py +778 -0
  48. package/scripts/_vbrief_sources.py +312 -0
  49. package/scripts/_vbrief_speckit.py +262 -0
  50. package/scripts/_vbrief_story_quality.py +353 -0
  51. package/scripts/_vbrief_validation.py +299 -0
  52. package/scripts/build_dist.py +412 -0
  53. package/scripts/cache.py +1078 -0
  54. package/scripts/cache_scanner.py +745 -0
  55. package/scripts/candidates_log.py +432 -0
  56. package/scripts/capacity_backfill.py +680 -0
  57. package/scripts/capacity_show.py +653 -0
  58. package/scripts/ci_local.py +689 -0
  59. package/scripts/code_structure_validate.py +765 -0
  60. package/scripts/codebase_default_extractor.py +495 -0
  61. package/scripts/codebase_map.py +304 -0
  62. package/scripts/codebase_map_fresh.py +104 -0
  63. package/scripts/codebase_projection_registry.py +94 -0
  64. package/scripts/codebase_provider.py +582 -0
  65. package/scripts/doctor.py +2257 -0
  66. package/scripts/framework_commands.py +505 -0
  67. package/scripts/gh_rest.py +882 -0
  68. package/scripts/github_auth_modes.py +437 -0
  69. package/scripts/github_body.py +292 -0
  70. package/scripts/ip_risk.py +531 -0
  71. package/scripts/issue_emit.py +670 -0
  72. package/scripts/issue_ingest.py +1064 -0
  73. package/scripts/migrate_preflight.py +418 -0
  74. package/scripts/migrate_vbrief.py +2677 -0
  75. package/scripts/monitor_pr.py +401 -0
  76. package/scripts/pack_migrate_lessons.py +336 -0
  77. package/scripts/pack_migrate_patterns.py +254 -0
  78. package/scripts/pack_migrate_rules.py +350 -0
  79. package/scripts/pack_migrate_skills.py +423 -0
  80. package/scripts/pack_migrate_strategies.py +311 -0
  81. package/scripts/pack_migrate_swarm_spec.py +250 -0
  82. package/scripts/pack_render.py +434 -0
  83. package/scripts/packs_slice.py +712 -0
  84. package/scripts/platform_capabilities.py +336 -0
  85. package/scripts/policy.py +2826 -0
  86. package/scripts/policy_set.py +324 -0
  87. package/scripts/pr_check_closing_keywords.py +524 -0
  88. package/scripts/pr_check_protected_issues.py +267 -0
  89. package/scripts/pr_merge_readiness.py +1004 -0
  90. package/scripts/pr_wait_mergeable.py +669 -0
  91. package/scripts/prd_render.py +159 -0
  92. package/scripts/preflight_architecture_sor.py +974 -0
  93. package/scripts/preflight_branch.py +289 -0
  94. package/scripts/preflight_cache.py +974 -0
  95. package/scripts/preflight_gh.py +721 -0
  96. package/scripts/preflight_implementation.py +272 -0
  97. package/scripts/preflight_story_start.py +838 -0
  98. package/scripts/preflight_wip_cap.py +149 -0
  99. package/scripts/probe_session.py +545 -0
  100. package/scripts/project_render.py +293 -0
  101. package/scripts/quarantine_ext.py +237 -0
  102. package/scripts/reconcile_issues.py +1442 -0
  103. package/scripts/refresh-path.ps1 +107 -0
  104. package/scripts/release.py +2030 -0
  105. package/scripts/release_e2e.py +1011 -0
  106. package/scripts/release_publish.py +486 -0
  107. package/scripts/release_rollback.py +980 -0
  108. package/scripts/relocate.py +1034 -0
  109. package/scripts/resolve_changelog_unreleased.py +667 -0
  110. package/scripts/resolve_version.py +490 -0
  111. package/scripts/resume_conditions.py +706 -0
  112. package/scripts/ritual_sentinel.py +609 -0
  113. package/scripts/roadmap_render.py +635 -0
  114. package/scripts/rule_ownership_lint.py +325 -0
  115. package/scripts/scm.py +591 -0
  116. package/scripts/scope_audit_log.py +387 -0
  117. package/scripts/scope_decompose.py +654 -0
  118. package/scripts/scope_demote.py +509 -0
  119. package/scripts/scope_lifecycle.py +1126 -0
  120. package/scripts/scope_undo.py +772 -0
  121. package/scripts/session_start.py +406 -0
  122. package/scripts/setup_ghx.py +339 -0
  123. package/scripts/setup_windows.ps1 +220 -0
  124. package/scripts/slice_audit.py +585 -0
  125. package/scripts/slice_record.py +530 -0
  126. package/scripts/slice_record_existing.py +692 -0
  127. package/scripts/slug_normalize.py +178 -0
  128. package/scripts/spec_render.py +477 -0
  129. package/scripts/spec_validate.py +238 -0
  130. package/scripts/subagent_monitor.py +658 -0
  131. package/scripts/swarm_complete_cohort.py +644 -0
  132. package/scripts/swarm_launch.py +1206 -0
  133. package/scripts/swarm_readiness.py +554 -0
  134. package/scripts/swarm_verify_review_clean.py +438 -0
  135. package/scripts/swarm_worktrees.py +497 -0
  136. package/scripts/toolchain-check.py +52 -0
  137. package/scripts/triage_actions.py +871 -0
  138. package/scripts/triage_bootstrap.py +1153 -0
  139. package/scripts/triage_bulk.py +630 -0
  140. package/scripts/triage_classify.py +932 -0
  141. package/scripts/triage_help.py +1685 -0
  142. package/scripts/triage_queue.py +1944 -0
  143. package/scripts/triage_reconcile.py +581 -0
  144. package/scripts/triage_refresh.py +643 -0
  145. package/scripts/triage_scope.py +999 -0
  146. package/scripts/triage_scope_drift.py +575 -0
  147. package/scripts/triage_smoketest.py +396 -0
  148. package/scripts/triage_subscribe.py +399 -0
  149. package/scripts/triage_summary.py +1011 -0
  150. package/scripts/triage_welcome.py +1178 -0
  151. package/scripts/ts_check_lane.py +86 -0
  152. package/scripts/validate-links.py +64 -0
  153. package/scripts/validate_strategy_output.py +212 -0
  154. package/scripts/vbrief_activate.py +228 -0
  155. package/scripts/vbrief_migrate_conformance.py +368 -0
  156. package/scripts/vbrief_reconcile_graph.py +306 -0
  157. package/scripts/vbrief_reconcile_labels.py +460 -0
  158. package/scripts/vbrief_reconcile_umbrellas.py +741 -0
  159. package/scripts/vbrief_validate.py +1195 -0
  160. package/scripts/verify-stubs.py +61 -0
  161. package/scripts/verify_capacity.py +160 -0
  162. package/scripts/verify_encoding.py +699 -0
  163. package/scripts/verify_hooks_installed.py +206 -0
  164. package/scripts/verify_investigation.py +360 -0
  165. package/scripts/verify_judgment_gates.py +827 -0
  166. package/scripts/verify_no_task_runtime.py +171 -0
  167. package/scripts/verify_scm_boundary.py +509 -0
  168. package/scripts/verify_session_ritual.py +389 -0
  169. package/scripts/verify_tools.py +426 -0
  170. package/scripts/verify_vbrief_conformance.py +478 -0
  171. package/tasks/architecture.yml +13 -0
  172. package/tasks/cache.yml +69 -0
  173. package/tasks/capacity.yml +38 -0
  174. package/tasks/change.yml +46 -0
  175. package/tasks/changelog.yml +24 -0
  176. package/tasks/ci.yml +49 -0
  177. package/tasks/codebase.yml +47 -0
  178. package/tasks/commit.yml +30 -0
  179. package/tasks/core.yml +126 -0
  180. package/tasks/deployments.yml +54 -0
  181. package/tasks/framework.yml +74 -0
  182. package/tasks/install.yml +60 -0
  183. package/tasks/issue.yml +50 -0
  184. package/tasks/migrate.yml +73 -0
  185. package/tasks/packs.yml +92 -0
  186. package/tasks/policy.yml +75 -0
  187. package/tasks/pr.yml +89 -0
  188. package/tasks/prd.yml +39 -0
  189. package/tasks/project.yml +27 -0
  190. package/tasks/reconcile.yml +32 -0
  191. package/tasks/relocate.yml +56 -0
  192. package/tasks/roadmap.yml +28 -0
  193. package/tasks/scm.yml +126 -0
  194. package/tasks/scope-undo.yml +36 -0
  195. package/tasks/scope.yml +141 -0
  196. package/tasks/session.yml +19 -0
  197. package/tasks/setup.yml +37 -0
  198. package/tasks/slice.yml +69 -0
  199. package/tasks/spec.yml +41 -0
  200. package/tasks/swarm.yml +85 -0
  201. package/tasks/toolchain.yml +13 -0
  202. package/tasks/triage-actions.yml +94 -0
  203. package/tasks/triage-bootstrap.yml +43 -0
  204. package/tasks/triage-bulk.yml +75 -0
  205. package/tasks/triage-classify.yml +30 -0
  206. package/tasks/triage-queue.yml +50 -0
  207. package/tasks/triage-reconcile.yml +29 -0
  208. package/tasks/triage-scope-drift.yml +29 -0
  209. package/tasks/triage-scope.yml +31 -0
  210. package/tasks/triage-smoketest.yml +33 -0
  211. package/tasks/triage-subscribe.yml +36 -0
  212. package/tasks/triage-summary.yml +29 -0
  213. package/tasks/triage-welcome.yml +32 -0
  214. package/tasks/ts.yml +328 -0
  215. package/tasks/vbrief.yml +206 -0
  216. package/tasks/verify.yml +292 -0
  217. package/templates/agents-entry.md +1 -1
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env python3
2
+ """Shared no-task dispatcher for Deft framework verbs (#1659).
3
+
4
+ This module is the compatibility rail between the old Taskfile verb names
5
+ (``triage:bootstrap``, ``verify:cache-fresh``, etc.) and the Python entrypoints
6
+ that actually implement them. Package-manager installs can route ``deft
7
+ <verb>`` through this module without requiring go-task on PATH, while Taskfile
8
+ consumers can keep using ``task deft:<verb>`` as a thin wrapper.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import contextlib
14
+ import importlib
15
+ import importlib.util
16
+ import inspect
17
+ import io
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ from collections.abc import Callable, Sequence
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from types import ModuleType
25
+ from typing import Literal
26
+
27
+ SCRIPT_DIR = Path(__file__).resolve().parent
28
+ FRAMEWORK_ROOT = SCRIPT_DIR.parent
29
+ if str(SCRIPT_DIR) not in sys.path:
30
+ sys.path.insert(0, str(SCRIPT_DIR))
31
+
32
+ RootMode = Literal["project", "framework"]
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class CommandResult:
37
+ code: int
38
+ stdout: str = ""
39
+ stderr: str = ""
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class CommandSpec:
44
+ name: str
45
+ entrypoint: str | None = None
46
+ default_args: tuple[str, ...] = ()
47
+ project_root_arg: str | None = None
48
+ framework_root_arg: str | None = None
49
+ vbrief_dir_arg: str | None = None
50
+ root_arg: str | None = None
51
+ cwd: RootMode = "project"
52
+ no_argv: bool = False
53
+ aggregate: tuple[str, ...] = ()
54
+ description: str = ""
55
+
56
+
57
+ def _spec(
58
+ name: str,
59
+ entrypoint: str,
60
+ *,
61
+ default_args: Sequence[str] = (),
62
+ project_root_arg: str | None = None,
63
+ framework_root_arg: str | None = None,
64
+ vbrief_dir_arg: str | None = None,
65
+ root_arg: str | None = None,
66
+ cwd: RootMode = "project",
67
+ no_argv: bool = False,
68
+ description: str = "",
69
+ ) -> CommandSpec:
70
+ return CommandSpec(
71
+ name=name,
72
+ entrypoint=entrypoint,
73
+ default_args=tuple(default_args),
74
+ project_root_arg=project_root_arg,
75
+ framework_root_arg=framework_root_arg,
76
+ vbrief_dir_arg=vbrief_dir_arg,
77
+ root_arg=root_arg,
78
+ cwd=cwd,
79
+ no_argv=no_argv,
80
+ description=description,
81
+ )
82
+
83
+
84
+ def _aggregate(name: str, commands: Sequence[str], *, description: str = "") -> CommandSpec:
85
+ return CommandSpec(name=name, aggregate=tuple(commands), description=description)
86
+
87
+
88
+ COMMANDS: dict[str, CommandSpec] = {
89
+ "core:validate": _spec(
90
+ "core:validate", "framework_commands:_cmd_core_validate", cwd="framework"
91
+ ),
92
+ "core:lint": _spec("core:lint", "framework_commands:_cmd_core_lint", cwd="framework"),
93
+ "core:test": _spec("core:test", "framework_commands:_cmd_core_test", cwd="framework"),
94
+ "doctor": _spec("doctor", "doctor:cmd_doctor"),
95
+ "session:start": _spec(
96
+ "session:start", "session_start:main", project_root_arg="--project-root"
97
+ ),
98
+ "triage:welcome": _spec(
99
+ "triage:welcome", "triage_welcome:main", project_root_arg="--project-root"
100
+ ),
101
+ "triage:bootstrap": _spec(
102
+ "triage:bootstrap", "triage_bootstrap:main", project_root_arg="--project-root"
103
+ ),
104
+ "triage:summary": _spec(
105
+ "triage:summary", "triage_summary:main", project_root_arg="--project-root"
106
+ ),
107
+ "triage:queue": _spec(
108
+ "triage:queue",
109
+ "triage_queue:main",
110
+ default_args=("queue",),
111
+ project_root_arg="--project-root",
112
+ ),
113
+ "triage:show": _spec(
114
+ "triage:show",
115
+ "triage_queue:main",
116
+ default_args=("show",),
117
+ project_root_arg="--project-root",
118
+ ),
119
+ "triage:audit": _spec(
120
+ "triage:audit",
121
+ "triage_queue:main",
122
+ default_args=("audit",),
123
+ project_root_arg="--project-root",
124
+ ),
125
+ "triage:accept": _spec("triage:accept", "triage_actions:main", default_args=("accept",)),
126
+ "triage:status": _spec("triage:status", "triage_actions:main", default_args=("status",)),
127
+ "triage:scope": _spec("triage:scope", "triage_scope:main"),
128
+ "migrate:vbrief": _spec("migrate:vbrief", "framework_commands:_cmd_migrate_vbrief"),
129
+ "cache:fetch-all": _spec("cache:fetch-all", "cache:main", default_args=("fetch-all",)),
130
+ "capacity:show": _spec(
131
+ "capacity:show", "capacity_show:main", project_root_arg="--project-root"
132
+ ),
133
+ "scope:demote": _spec("scope:demote", "scope_demote:main", project_root_arg="--project-root"),
134
+ "toolchain:check": _spec("toolchain:check", "toolchain-check.py:main", no_argv=True),
135
+ "verify:stubs": _spec("verify:stubs", "verify-stubs.py:main", no_argv=True),
136
+ "verify:links": _spec("verify:links", "validate-links.py:main", no_argv=True),
137
+ "verify:rule-ownership": _spec(
138
+ "verify:rule-ownership",
139
+ "rule_ownership_lint:main",
140
+ root_arg="--root",
141
+ cwd="framework",
142
+ ),
143
+ "verify:branch": _spec(
144
+ "verify:branch",
145
+ "preflight_branch:main",
146
+ default_args=("--allow-missing-project-definition",),
147
+ project_root_arg="--project-root",
148
+ ),
149
+ "verify:encoding": _spec(
150
+ "verify:encoding", "verify_encoding:main", project_root_arg="--project-root"
151
+ ),
152
+ "verify:vbrief-conformance": _spec(
153
+ "verify:vbrief-conformance",
154
+ "verify_vbrief_conformance:main",
155
+ project_root_arg="--project-root",
156
+ ),
157
+ "verify:destructive-gh-verbs": _spec(
158
+ "verify:destructive-gh-verbs",
159
+ "preflight_gh:main",
160
+ default_args=("--self-test",),
161
+ project_root_arg="--project-root",
162
+ ),
163
+ "verify:scm-boundary": _spec(
164
+ "verify:scm-boundary",
165
+ "verify_scm_boundary:main",
166
+ project_root_arg="--project-root",
167
+ ),
168
+ "verify:no-task-runtime": _spec(
169
+ "verify:no-task-runtime",
170
+ "verify_no_task_runtime:main",
171
+ no_argv=True,
172
+ cwd="framework",
173
+ ),
174
+ "verify:cache-fresh": _spec(
175
+ "verify:cache-fresh",
176
+ "preflight_cache:main",
177
+ default_args=("--allow-missing-bootstrap",),
178
+ project_root_arg="--project-root",
179
+ ),
180
+ "verify:wip-cap": _spec(
181
+ "verify:wip-cap", "preflight_wip_cap:main", project_root_arg="--project-root"
182
+ ),
183
+ "verify:pack-drift": _spec(
184
+ "verify:pack-drift", "pack_render:main", default_args=("--check",), cwd="framework"
185
+ ),
186
+ "verify-strategy-output": _spec(
187
+ "verify-strategy-output",
188
+ "validate_strategy_output:main",
189
+ project_root_arg="--project-root",
190
+ ),
191
+ "vbrief:validate": _spec(
192
+ "vbrief:validate", "vbrief_validate:main", vbrief_dir_arg="--vbrief-dir"
193
+ ),
194
+ "build": _spec(
195
+ "build",
196
+ "build_dist:main",
197
+ default_args=("--version", "__DEFT_VERSION__"),
198
+ cwd="framework",
199
+ ),
200
+ "check:consumer": _aggregate(
201
+ "check:consumer",
202
+ (
203
+ "doctor",
204
+ "toolchain:check",
205
+ "verify:branch",
206
+ "verify:cache-fresh",
207
+ "verify:wip-cap",
208
+ "vbrief:validate",
209
+ "verify-strategy-output",
210
+ ),
211
+ ),
212
+ "check:framework-source": _aggregate(
213
+ "check:framework-source",
214
+ (
215
+ "core:validate",
216
+ "core:lint",
217
+ "core:test",
218
+ "toolchain:check",
219
+ "verify:stubs",
220
+ "verify:links",
221
+ "verify:rule-ownership",
222
+ "verify:branch",
223
+ "verify:encoding",
224
+ "verify:vbrief-conformance",
225
+ "verify:destructive-gh-verbs",
226
+ "verify:scm-boundary",
227
+ "verify:no-task-runtime",
228
+ "verify:cache-fresh",
229
+ "verify:pack-drift",
230
+ "verify:wip-cap",
231
+ "vbrief:validate",
232
+ "verify-strategy-output",
233
+ ),
234
+ ),
235
+ }
236
+
237
+
238
+ def available_commands() -> tuple[str, ...]:
239
+ return tuple(sorted(COMMANDS))
240
+
241
+
242
+ def has_command(name: str) -> bool:
243
+ return name in COMMANDS
244
+
245
+
246
+ def normalize_task_separator(argv: Sequence[str]) -> list[str]:
247
+ """Tolerate Taskfile-style pass-through: ``deft verb -- --flag``."""
248
+ args = list(argv)
249
+ if args[:1] == ["--"]:
250
+ return args[1:]
251
+ return args
252
+
253
+
254
+ def format_framework_command(
255
+ args: Sequence[str],
256
+ *,
257
+ surface: str = "deft",
258
+ task_prefix: str | None = None,
259
+ ) -> str:
260
+ """Render an operator-facing command for the selected invocation surface."""
261
+ parts = list(args)
262
+ if surface == "task":
263
+ prefix = (task_prefix or "").strip()
264
+ if prefix and not prefix.endswith(":"):
265
+ prefix = f"{prefix}:"
266
+ if parts:
267
+ parts[0] = f"{prefix}{parts[0]}"
268
+ return " ".join(["task", *parts])
269
+ return " ".join([surface, *parts])
270
+
271
+
272
+ def _version() -> str:
273
+ try:
274
+ import resolve_version # noqa: PLC0415
275
+
276
+ return resolve_version.resolve_version()
277
+ except Exception: # noqa: BLE001 -- build should still produce a dev artifact
278
+ return "0.0.0-dev"
279
+
280
+
281
+ def _load_module(module_ref: str) -> ModuleType:
282
+ if module_ref.endswith(".py"):
283
+ path = SCRIPT_DIR / module_ref
284
+ module_name = f"_deft_command_{path.stem.replace('-', '_')}"
285
+ spec = importlib.util.spec_from_file_location(module_name, path)
286
+ if spec is None or spec.loader is None:
287
+ raise ImportError(f"cannot load {path}")
288
+ module = importlib.util.module_from_spec(spec)
289
+ sys.modules[module_name] = module
290
+ spec.loader.exec_module(module)
291
+ return module
292
+ return importlib.import_module(module_ref)
293
+
294
+
295
+ def _reject_args(argv: Sequence[str], command: str) -> int:
296
+ if argv:
297
+ print(
298
+ f"error: {command} does not accept arguments: {' '.join(argv)}",
299
+ file=sys.stderr,
300
+ )
301
+ return 2
302
+ return 0
303
+
304
+
305
+ def _cmd_core_validate(argv: list[str] | None = None) -> int:
306
+ if _reject_args(argv or [], "core:validate") != 0:
307
+ return 2
308
+ files = [
309
+ path
310
+ for path in sorted(Path(".").rglob("*.md"))
311
+ if ".git" not in path.parts and "backup" not in path.parts
312
+ ]
313
+ for path in files:
314
+ print(f"✓ {path}")
315
+ print(f"✓ All {len(files)} markdown files validated")
316
+ return 0
317
+
318
+
319
+ def _run_uv(args: Sequence[str]) -> int:
320
+ return subprocess.run(["uv", "--project", str(FRAMEWORK_ROOT), "run", *args]).returncode
321
+
322
+
323
+ def _cmd_core_lint(argv: list[str] | None = None) -> int:
324
+ if _reject_args(argv or [], "core:lint") != 0:
325
+ return 2
326
+ ruff_code = _run_uv(["ruff", "check", "."])
327
+ if ruff_code != 0:
328
+ return ruff_code
329
+ targets = ["run.py"]
330
+ if Path("tests").exists():
331
+ targets.append("tests")
332
+ return _run_uv(["python", "-m", "mypy", *targets])
333
+
334
+
335
+ def _cmd_core_test(argv: list[str] | None = None) -> int:
336
+ if _reject_args(argv or [], "core:test") != 0:
337
+ return 2
338
+ if not Path("tests").exists():
339
+ print("no tests/ (vendored consumer) -- skipping")
340
+ return 0
341
+ return subprocess.run([sys.executable, "-m", "pytest", "tests"]).returncode
342
+
343
+
344
+ def _cmd_migrate_vbrief(argv: list[str] | None = None) -> int:
345
+ import migrate_preflight # noqa: PLC0415
346
+ import migrate_vbrief # noqa: PLC0415
347
+
348
+ project_root = Path.cwd().resolve()
349
+ preflight_code = migrate_preflight.main(
350
+ [
351
+ "--project-root",
352
+ str(project_root),
353
+ "--deft-root",
354
+ str(FRAMEWORK_ROOT),
355
+ ]
356
+ )
357
+ if preflight_code != 0:
358
+ return preflight_code
359
+ return migrate_vbrief.main([str(project_root), *(argv or [])])
360
+
361
+
362
+ def _load_callable(entrypoint: str) -> Callable[..., int | None]:
363
+ module_ref, _, func_name = entrypoint.partition(":")
364
+ if not module_ref or not func_name:
365
+ raise ValueError(f"invalid framework entrypoint: {entrypoint!r}")
366
+ module = _load_module(module_ref)
367
+ func = getattr(module, func_name)
368
+ if not callable(func):
369
+ raise TypeError(f"framework entrypoint is not callable: {entrypoint}")
370
+ return func
371
+
372
+
373
+ def _argv_for_spec(
374
+ spec: CommandSpec,
375
+ argv: Sequence[str],
376
+ *,
377
+ project_root: Path,
378
+ framework_root: Path,
379
+ ) -> list[str]:
380
+ resolved: list[str] = []
381
+ for item in spec.default_args:
382
+ resolved.append(_version() if item == "__DEFT_VERSION__" else item)
383
+ if spec.project_root_arg:
384
+ resolved.extend((spec.project_root_arg, str(project_root)))
385
+ if spec.framework_root_arg:
386
+ resolved.extend((spec.framework_root_arg, str(framework_root)))
387
+ if spec.vbrief_dir_arg:
388
+ resolved.extend((spec.vbrief_dir_arg, str(project_root / "vbrief")))
389
+ if spec.root_arg:
390
+ resolved.extend((spec.root_arg, str(framework_root)))
391
+ resolved.extend(normalize_task_separator(argv))
392
+ return resolved
393
+
394
+
395
+ def _invoke(func: Callable[..., int | None], argv: list[str], *, no_argv: bool) -> int:
396
+ try:
397
+ if no_argv:
398
+ if argv:
399
+ print(
400
+ f"error: this framework command does not accept arguments: {' '.join(argv)}",
401
+ file=sys.stderr,
402
+ )
403
+ return 2
404
+ code = func()
405
+ else:
406
+ signature = inspect.signature(func)
407
+ code = func() if len(signature.parameters) == 0 else func(argv)
408
+ except SystemExit as exc:
409
+ raw = exc.code
410
+ return raw if isinstance(raw, int) else (0 if raw is None else 1)
411
+ return int(code or 0)
412
+
413
+
414
+ def run_framework_command(
415
+ name: str,
416
+ argv: Sequence[str] = (),
417
+ *,
418
+ project_root: Path | None = None,
419
+ framework_root: Path | None = None,
420
+ capture: bool = False,
421
+ output_fn: Callable[[str], None] | None = None,
422
+ ) -> CommandResult:
423
+ root = (project_root or Path.cwd()).resolve()
424
+ framework = (framework_root or FRAMEWORK_ROOT).resolve()
425
+ spec = COMMANDS.get(name)
426
+ if spec is None:
427
+ return CommandResult(2, "", f"unknown framework command: {name}")
428
+
429
+ if spec.aggregate:
430
+ stdout_parts: list[str] = []
431
+ stderr_parts: list[str] = []
432
+ for child in spec.aggregate:
433
+ if output_fn is not None:
434
+ output_fn(f"[deft] {child}")
435
+ result = run_framework_command(
436
+ child,
437
+ (),
438
+ project_root=root,
439
+ framework_root=framework,
440
+ capture=capture,
441
+ output_fn=output_fn,
442
+ )
443
+ stdout_parts.append(result.stdout)
444
+ stderr_parts.append(result.stderr)
445
+ if result.code != 0:
446
+ return CommandResult(
447
+ result.code,
448
+ "".join(stdout_parts),
449
+ "".join(stderr_parts),
450
+ )
451
+ return CommandResult(0, "".join(stdout_parts), "".join(stderr_parts))
452
+
453
+ if spec.entrypoint is None:
454
+ return CommandResult(2, "", f"framework command has no entrypoint: {name}")
455
+
456
+ try:
457
+ func = _load_callable(spec.entrypoint)
458
+ command_argv = _argv_for_spec(
459
+ spec,
460
+ argv,
461
+ project_root=root,
462
+ framework_root=framework,
463
+ )
464
+ except Exception as exc: # noqa: BLE001 -- produce CLI-shaped failure
465
+ return CommandResult(2, "", f"{type(exc).__name__}: {exc}")
466
+
467
+ previous_cwd = Path.cwd()
468
+ cwd = root if spec.cwd == "project" else framework
469
+ stdout = io.StringIO()
470
+ stderr = io.StringIO()
471
+ try:
472
+ os.chdir(cwd)
473
+ if capture:
474
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
475
+ code = _invoke(func, command_argv, no_argv=spec.no_argv)
476
+ else:
477
+ code = _invoke(func, command_argv, no_argv=spec.no_argv)
478
+ except Exception as exc: # noqa: BLE001 -- dispatcher is a command boundary
479
+ print(f"{type(exc).__name__}: {exc}", file=stderr)
480
+ code = 2
481
+ finally:
482
+ os.chdir(previous_cwd)
483
+ return CommandResult(code, stdout.getvalue(), stderr.getvalue())
484
+
485
+
486
+ def main(argv: list[str] | None = None) -> int:
487
+ args = list(sys.argv[1:] if argv is None else argv)
488
+ if not args or args[0] in {"-h", "--help", "help"}:
489
+ print("Usage: framework_commands.py <verb> [args...]")
490
+ print()
491
+ print("Available framework verbs:")
492
+ for name in available_commands():
493
+ print(f" {name}")
494
+ return 0
495
+ command, *rest = args
496
+ result = run_framework_command(command, rest)
497
+ if result.stdout:
498
+ print(result.stdout, end="")
499
+ if result.stderr:
500
+ print(result.stderr, end="", file=sys.stderr)
501
+ return result.code
502
+
503
+
504
+ if __name__ == "__main__":
505
+ raise SystemExit(main())