tcw-cli 2.6.4__tar.gz → 2.6.5__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.
Files changed (214) hide show
  1. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/PKG-INFO +1 -1
  2. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/pyproject.toml +1 -1
  3. tcw_cli-2.6.5/tcw/__init__.py +1 -0
  4. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/cli.py +2 -0
  5. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/__init__.py +18 -4
  6. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/base.py +91 -6
  7. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/fs.py +462 -107
  8. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/project.py +101 -30
  9. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/create.py +3 -2
  10. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/intake.py +29 -11
  11. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/jira.py +27 -8
  12. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/sync.py +34 -9
  13. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/cli.py +93 -46
  14. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/recursion.py +61 -18
  15. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/PKG-INFO +1 -1
  16. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/SOURCES.txt +11 -0
  17. tcw_cli-2.6.5/tests/test_boardless_delegate_and_slices.py +80 -0
  18. tcw_cli-2.6.5/tests/test_init_configured_tree_path.py +119 -0
  19. tcw_cli-2.6.5/tests/test_initiative_node_qualified.py +151 -0
  20. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_interrupted_claim.py +194 -0
  21. tcw_cli-2.6.5/tests/test_local_qualified_blockers.py +133 -0
  22. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_non_git_writes.py +13 -0
  23. tcw_cli-2.6.5/tests/test_override_in_linked_worktree.py +45 -0
  24. tcw_cli-2.6.5/tests/test_override_references.py +134 -0
  25. tcw_cli-2.6.5/tests/test_project_graph_paths.py +77 -0
  26. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_recursion.py +2 -1
  27. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_cli.py +5 -5
  28. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_hardening.py +9 -1
  29. tcw_cli-2.6.5/tests/test_tracker_message_tidy.py +158 -0
  30. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_strict_gate.py +77 -10
  31. tcw_cli-2.6.5/tests/test_unreadable_item_files.py +261 -0
  32. tcw_cli-2.6.5/tests/test_unreadable_state_gates.py +148 -0
  33. tcw_cli-2.6.5/tests/test_untag_invalid_tags.py +82 -0
  34. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_worktree_sibling_nodes.py +15 -6
  35. tcw_cli-2.6.4/tcw/__init__.py +0 -1
  36. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/LICENSE +0 -0
  37. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/README.md +0 -0
  38. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/setup.cfg +0 -0
  39. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/capabilities/__init__.py +0 -0
  40. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/capabilities/cli.py +0 -0
  41. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/harness.py +0 -0
  42. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/refs.py +0 -0
  43. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/dist/client/assets/index-C5_pngMt.css +0 -0
  44. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/dist/client/assets/index-Dss8_ZG_.js +0 -0
  45. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/dist/client/index.html +0 -0
  46. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/dist/client/theme-init.js +0 -0
  47. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/dist/server.cjs +0 -0
  48. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/serve/runtime.py +0 -0
  49. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/stdin.py +0 -0
  50. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/__init__.py +0 -0
  51. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/checkouts.py +0 -0
  52. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/store/config_edit.py +0 -0
  53. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/taxonomy/__init__.py +0 -0
  54. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/taxonomy/cli.py +0 -0
  55. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/__init__.py +0 -0
  56. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/claim.py +0 -0
  57. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/ownership.py +0 -0
  58. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/tracker/progress.py +0 -0
  59. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/validate.py +0 -0
  60. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/__init__.py +0 -0
  61. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/generate.py +0 -0
  62. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/hooks.py +0 -0
  63. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/audit-backlog.md +0 -0
  64. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/consolidate-plans.md +0 -0
  65. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/create-work.md +0 -0
  66. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/decompose.md +0 -0
  67. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/delegation.md +0 -0
  68. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/documentation-sync.md +0 -0
  69. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/post-mortem.md +0 -0
  70. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/search.md +0 -0
  71. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/triage-issues.md +0 -0
  72. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/procedures/unattended-work.md +0 -0
  73. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/projection.py +0 -0
  74. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/implement.md +0 -0
  75. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/inbox.md +0 -0
  76. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/plan.md +0 -0
  77. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/postmortem.md +0 -0
  78. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/request.md +0 -0
  79. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/spec.md +0 -0
  80. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/prompts/verify.md +0 -0
  81. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/resolve.py +0 -0
  82. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw/work/templates.py +0 -0
  83. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/dependency_links.txt +0 -0
  84. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/entry_points.txt +0 -0
  85. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/requires.txt +0 -0
  86. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tcw_cli.egg-info/top_level.txt +0 -0
  87. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_body_prompt.py +0 -0
  88. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capabilities.py +0 -0
  89. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capabilities_federation.py +0 -0
  90. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capabilities_reset.py +0 -0
  91. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capabilities_rm.py +0 -0
  92. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capabilities_sidecar.py +0 -0
  93. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capability_gate_children.py +0 -0
  94. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_capability_ref_wording.py +0 -0
  95. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_check_versions.py +0 -0
  96. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_child_status.py +0 -0
  97. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_cli_help_coverage.py +0 -0
  98. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_committed_paths.py +0 -0
  99. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_config_edit.py +0 -0
  100. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_config_edit_writers.py +0 -0
  101. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_configuration_text_home.py +0 -0
  102. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_cross_node_blockers.py +0 -0
  103. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_cut_version.py +0 -0
  104. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_documentation_config.py +0 -0
  105. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_documentation_prompt.py +0 -0
  106. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_documentation_sync_wiring.py +0 -0
  107. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_documented_cli_surface.py +0 -0
  108. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_dynamic_skill_marker.py +0 -0
  109. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_edit_refusal.py +0 -0
  110. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_edit_type.py +0 -0
  111. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_environment_hardness.py +0 -0
  112. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_epic_completability_across_checkouts.py +0 -0
  113. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_epic_completable.py +0 -0
  114. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_eval_coverage.py +0 -0
  115. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_eval_files_changed.py +0 -0
  116. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_eval_fixture.py +0 -0
  117. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_eval_grading.py +0 -0
  118. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_eval_runner.py +0 -0
  119. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_external_work_store.py +0 -0
  120. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_falsification_rule.py +0 -0
  121. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_generate_hook.py +0 -0
  122. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_harness.py +0 -0
  123. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_inbox_title.py +0 -0
  124. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_item_tags_read.py +0 -0
  125. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_legacy_store_config.py +0 -0
  126. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_baseline.py +0 -0
  127. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_config_tags.py +0 -0
  128. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_hooks.py +0 -0
  129. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_inert.py +0 -0
  130. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_policy.py +0 -0
  131. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_lifecycle_validation.py +0 -0
  132. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_literal_store_names.py +0 -0
  133. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_multiproject.py +0 -0
  134. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_plugin_manifests.py +0 -0
  135. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_procedure_config.py +0 -0
  136. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_procedure_verb.py +0 -0
  137. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_project_overrides.py +0 -0
  138. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_project_registry.py +0 -0
  139. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_projection.py +0 -0
  140. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_prompt_fallback.py +0 -0
  141. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_qualified_ref.py +0 -0
  142. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_refs.py +0 -0
  143. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_remote_session_setup.py +0 -0
  144. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_repo_lifecycle.py +0 -0
  145. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_resolve.py +0 -0
  146. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_resolve_procedure.py +0 -0
  147. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_retention.py +0 -0
  148. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_routing_nodes.py +0 -0
  149. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_scaffold.py +0 -0
  150. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve.py +0 -0
  151. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve_descendants.py +0 -0
  152. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve_projection.py +0 -0
  153. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve_resolve.py +0 -0
  154. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve_runtime.py +0 -0
  155. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_serve_write.py +0 -0
  156. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_session_bootstrap.py +0 -0
  157. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_shipped_procedures.py +0 -0
  158. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_shipped_prompts.py +0 -0
  159. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_show_json.py +0 -0
  160. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_sidecar_paths_in_validate.py +0 -0
  161. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_skill_flow.py +0 -0
  162. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_skill_lifecycle_parity.py +0 -0
  163. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_skill_path_pointers.py +0 -0
  164. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_smoke.py +0 -0
  165. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_stage_validate.py +0 -0
  166. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_stage_verb.py +0 -0
  167. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_status_parity.py +0 -0
  168. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_stdin.py +0 -0
  169. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_stdin_cli.py +0 -0
  170. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_store_bounds.py +0 -0
  171. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_store_editor.py +0 -0
  172. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_store_nodes.py +0 -0
  173. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_store_provisioning.py +0 -0
  174. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_store_publication.py +0 -0
  175. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_subprocess_stdin.py +0 -0
  176. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_taxonomy.py +0 -0
  177. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_taxonomy_rm_gaps.py +0 -0
  178. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tombstone.py +0 -0
  179. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_absent.py +0 -0
  180. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_binding.py +0 -0
  181. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_claim.py +0 -0
  182. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_claimability.py +0 -0
  183. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_client.py +0 -0
  184. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_comment.py +0 -0
  185. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_config.py +0 -0
  186. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_create.py +0 -0
  187. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_gate.py +0 -0
  188. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_help.py +0 -0
  189. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_hold.py +0 -0
  190. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_import.py +0 -0
  191. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_import_returns.py +0 -0
  192. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_inheritance.py +0 -0
  193. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_link.py +0 -0
  194. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_ownership.py +0 -0
  195. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_pre_backlog.py +0 -0
  196. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_replay.py +0 -0
  197. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_strict.py +0 -0
  198. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_surface.py +0 -0
  199. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_sync.py +0 -0
  200. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_sync_config.py +0 -0
  201. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_tracker_validate.py +0 -0
  202. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_unattended_work_skill.py +0 -0
  203. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_unplanned_start.py +0 -0
  204. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_unpushed_version_script.py +0 -0
  205. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_unreadable_capabilities_sidecar.py +0 -0
  206. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_validate.py +0 -0
  207. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_validate_moved_stores.py +0 -0
  208. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_validate_target.py +0 -0
  209. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_work.py +0 -0
  210. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_work_autocommit.py +0 -0
  211. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_work_review.py +0 -0
  212. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_work_start.py +0 -0
  213. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_work_tags.py +0 -0
  214. {tcw_cli-2.6.4 → tcw_cli-2.6.5}/tests/test_worktree_completion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tcw-cli
3
- Version: 2.6.4
3
+ Version: 2.6.5
4
4
  Summary: Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tcw-cli"
7
- version = "2.6.4"
7
+ version = "2.6.5"
8
8
  description = "Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1 @@
1
+ __version__ = "2.6.5"
@@ -406,6 +406,8 @@ def _cmd_validate(args: argparse.Namespace) -> int:
406
406
  for override in registry.overrides():
407
407
  print(f"connected project '{override.id}' is overridden by "
408
408
  f"{override.source} to {override.locator}", file=sys.stderr)
409
+ if override.warning:
410
+ print(f" warning: {override.warning}", file=sys.stderr)
409
411
  registry_problems = registry.check()
410
412
  if registry_problems:
411
413
  for problem in registry_problems:
@@ -236,7 +236,7 @@ def _strict_refuses(work, action: str, slug: str = "", body: dict | None = None)
236
236
  or work.get(slug)) if slug else None
237
237
  epic = (body.get("type") == "epic") if action == "create" else (
238
238
  item is not None and item.type == "epic")
239
- from tcw.tracker.intake import ever_bound
239
+ from tcw.tracker.intake import drop_refusal
240
240
  lead = "refused under strict tracker mode, which the web app cannot check; "
241
241
  if action == "create" and not epic:
242
242
  return lead + "create work from a ticket with `tcw work tracker import <ticket>`."
@@ -245,9 +245,8 @@ def _strict_refuses(work, action: str, slug: str = "", body: dict | None = None)
245
245
  return lead + f"use `tcw work start {slug}{take_over}`, which claims the ticket."
246
246
  if action == "complete" and not epic and body.get("resolution") == "done":
247
247
  return lead + f"use `tcw work complete {slug}`, which checks the ticket."
248
- if action == "drop" and ever_bound(work, slug):
249
- return lead + (f"{slug} is, or was, bound to a ticket. Discard it instead: "
250
- f"`tcw work complete {slug} --resolution wontfix --confirm`.")
248
+ if action == "drop" and (refusal := drop_refusal(work, slug)):
249
+ return lead + refusal
251
250
  return None
252
251
 
253
252
 
@@ -635,6 +634,12 @@ class TcwHandler(BaseHTTPRequestHandler):
635
634
  return
636
635
  try:
637
636
  resource = work.read_artifact(slug, name)
637
+ except UnicodeDecodeError:
638
+ # Before ValueError, which it is: the decoder's own message names
639
+ # a byte offset, not the file.
640
+ self._send(HTTPStatus.BAD_REQUEST,
641
+ f"{name} is not valid UTF-8; fix or replace the file".encode("utf-8"))
642
+ return
638
643
  except ValueError as e:
639
644
  self._send(HTTPStatus.BAD_REQUEST, str(e).encode("utf-8"))
640
645
  return
@@ -806,6 +811,15 @@ class TcwHandler(BaseHTTPRequestHandler):
806
811
  })
807
812
  else:
808
813
  artifacts_list.append({"name": name, "present": False})
814
+ except UnicodeDecodeError:
815
+ # Not valid UTF-8: listed with the snapshot's revision, so a
816
+ # guarded save can replace it; opening it names the file.
817
+ artifacts_list.append({
818
+ "name": name,
819
+ "present": name in lifecycle_present,
820
+ "revision": detail.artifact_revisions.get(name, ""),
821
+ "mediaType": "text/markdown",
822
+ })
809
823
  except ValueError:
810
824
  pass
811
825
  # Sidecar discovery in detail
@@ -182,6 +182,10 @@ class ProjectOverride:
182
182
  source: str
183
183
  locator: Any
184
184
  problem: str | None = None
185
+ # It worked, and still may not be what was meant: set when the project it
186
+ # delivered disagrees with where the rest of the graph was read from. Never
187
+ # a problem, and never fatal — the override is followed as stated.
188
+ warning: str | None = None
185
189
 
186
190
 
187
191
  class ProjectRegistry(ABC):
@@ -3774,6 +3778,13 @@ class WorkStore(ABC):
3774
3778
  Returns the written ``SidecarResource`` with a fresh revision.
3775
3779
  """
3776
3780
 
3781
+ def qualify_initiative(self, value: str) -> str:
3782
+ """`value` as it should be stored: `<project-id>/<slug>` when the epic
3783
+ lives in another project, so the reference cannot be taken by a
3784
+ same-named epic nearer the item. A store that holds one project's items
3785
+ only has nothing to qualify."""
3786
+ return value
3787
+
3777
3788
  def initiative_epic(self, item: WorkItem) -> WorkItem | None:
3778
3789
  """Resolve `item`'s initiative epic, if any.
3779
3790
 
@@ -3861,6 +3872,41 @@ class WorkStore(ABC):
3861
3872
  filesystem store does, and overrides this."""
3862
3873
  return [(item, False) for item in self.query()]
3863
3874
 
3875
+ def unreadable_open_items(self) -> list[tuple[str, str]]:
3876
+ """`(slug, reason)` for every open item whose recorded fields this store
3877
+ cannot read. Such an item still lists, but without its `parent` or
3878
+ `initiative`, so the gates that stop an item resolving over open work
3879
+ cannot tell whether it belongs beneath one. A store that never holds a
3880
+ record it cannot read has none."""
3881
+ return []
3882
+
3883
+ def unreadable_slice_candidates(self) -> list[tuple[str, str]]:
3884
+ """`(label, reason)` for every open item, wherever this store's epics
3885
+ draw slices from, whose fields cannot be read. Local only by default;
3886
+ a store that finds slices in other nodes also looks there."""
3887
+ return self.unreadable_open_items()
3888
+
3889
+ def require_readable_slices(self, slug: str) -> None:
3890
+ """Refuse to close epic `slug` while an open item where its slices live
3891
+ cannot be read — it may be one of them. The epic's own damage is left
3892
+ to the move, whose refusal names it plainly."""
3893
+ found = [f for f in self.unreadable_slice_candidates() if f[0] != slug]
3894
+ if found:
3895
+ raise self._unreadable_refusal("complete epic", slug,
3896
+ "its initiative children", found)
3897
+
3898
+ def _unreadable_others(self, slug: str) -> list[tuple[str, str]]:
3899
+ return [f for f in self.unreadable_open_items() if f[0] != slug]
3900
+
3901
+ @staticmethod
3902
+ def _unreadable_refusal(verb: str, slug: str, what: str,
3903
+ found: list[tuple[str, str]]) -> ValueError:
3904
+ named = ", ".join(f"{label} ({reason})" for label, reason in found)
3905
+ return ValueError(f"Cannot {verb} {slug}: the state of these open items "
3906
+ f"cannot be read, so whether they are {what} is "
3907
+ f"unknown: {named}. Fix or replace each state.yaml "
3908
+ f"(`tcw validate` lists them) and retry.")
3909
+
3864
3910
  def independent_descendants(self, slug: str) -> list[WorkItem]:
3865
3911
  """Every item beneath `slug` — the whole subtree, not only direct
3866
3912
  children — that has a status of its own, open or resolved.
@@ -3917,10 +3963,24 @@ class WorkStore(ABC):
3917
3963
  """A blocker entry: a ref to an item this store holds or once held →
3918
3964
  {slug}, else {external}. A resolved item already reduced to its
3919
3965
  tombstone is still this store's item; recording it as external text
3920
- would make it block forever."""
3966
+ would make it block forever.
3967
+
3968
+ So is a ref that names one of this store's items another way — as
3969
+ `<status>/<slug>`, or qualified with this node's own project id. Left as
3970
+ text, it escaped the self-block and cycle checks, which compare slugs,
3971
+ and a reference to the item itself blocked it forever."""
3921
3972
  ref = self._normalize_ref(ref)
3922
- known = self.get(ref) is not None or self.tombstone(ref) is not None
3923
- return {"slug": ref} if known else {"external": ref}
3973
+ for candidate in (ref, *self._local_forms(ref)):
3974
+ if self.get(candidate) is not None or self.tombstone(candidate) is not None:
3975
+ return {"slug": candidate}
3976
+ return {"external": ref}
3977
+
3978
+ def _local_forms(self, ref: str) -> list[str]:
3979
+ """Bare slugs `ref` may name in this store besides itself. `<status>/<slug>`
3980
+ is model vocabulary every store shares; a store that knows its own
3981
+ project id adds `<own-id>/<slug>`."""
3982
+ status, sep, rest = ref.partition("/")
3983
+ return [rest] if sep and status in WORK_STATUSES and rest and "/" not in rest else []
3924
3984
 
3925
3985
  @staticmethod
3926
3986
  def _same_entry(a: dict, b: dict) -> bool:
@@ -3931,6 +3991,13 @@ class WorkStore(ABC):
3931
3991
  return a["external"] == b["external"]
3932
3992
  return False
3933
3993
 
3994
+ def _same_item(self, entry: dict, e: dict) -> bool:
3995
+ """`_same_entry`, or `e` is text stored before this store read it as
3996
+ `entry`'s item — `<status>/<slug>`, say. Re-adding that item must not
3997
+ keep both, and the text form may block forever."""
3998
+ return self._same_entry(entry, e) or (
3999
+ "slug" in entry and "external" in e and self._entry_for(e["external"]) == entry)
4000
+
3934
4001
  def _reaches(self, start: str, target: str, *,
3935
4002
  settled: frozenset[str] = frozenset()) -> bool:
3936
4003
  """True if `start` (transitively, via blocked_by slugs) depends on `target`.
@@ -3968,7 +4035,8 @@ class WorkStore(ABC):
3968
4035
  self._check_new_blocker(slug, entry, ref)
3969
4036
  if any(self._same_entry(entry, e) for e in item.blocked_by):
3970
4037
  return # idempotent
3971
- self.set_field(slug, "blocked_by", item.blocked_by + [entry])
4038
+ kept = [e for e in item.blocked_by if not self._same_item(entry, e)]
4039
+ self.set_field(slug, "blocked_by", kept + [entry])
3972
4040
 
3973
4041
  def check_blocker_edits(self, slug: str, *, add: list[str] = (),
3974
4042
  remove: list[str] = (), blocks: list[str] = ()) -> None:
@@ -3990,7 +4058,7 @@ class WorkStore(ABC):
3990
4058
  entry = self._entry_for(ref)
3991
4059
  self._check_new_blocker(slug, entry, ref)
3992
4060
  if not any(self._same_entry(entry, e) for e in proposed):
3993
- proposed.append(entry)
4061
+ proposed = [e for e in proposed if not self._same_item(entry, e)] + [entry]
3994
4062
  for ref in blocks:
3995
4063
  target = self._require(ref).slug
3996
4064
  if target == slug:
@@ -4012,8 +4080,11 @@ class WorkStore(ABC):
4012
4080
  def _without(self, entries: list[dict], slug: str, ref: str) -> list[dict]:
4013
4081
  """`entries` less the blocker `ref` names; refuses a ref that names none."""
4014
4082
  norm = self._normalize_ref(ref)
4083
+ # The forms `_entry_for` records as a slug remove it too: removing with
4084
+ # the text that added it has to work.
4085
+ slugs = {norm, *self._local_forms(norm)}
4015
4086
  kept = [e for e in entries
4016
- if e.get("slug") != norm and e.get("external") != norm]
4087
+ if e.get("slug") not in slugs and e.get("external") != norm]
4017
4088
  if len(kept) == len(entries) and norm.endswith(")"):
4018
4089
  # A label copied from `list` may carry the reason it still blocks,
4019
4090
  # "external: <text> (<why>)"; the stored text has no reason, and the
@@ -4230,6 +4301,13 @@ class WorkStore(ABC):
4230
4301
  force: bool = False) -> WorkItem:
4231
4302
  dest = resolution_status(resolution) # raises on a bad resolution
4232
4303
  item = self._require(slug)
4304
+ # Before the backlog shortcut is decided, since it reads the same slices:
4305
+ # a slice whose state cannot be read has lost its `initiative`, so both
4306
+ # would pass over it. Forceable, like the partial-graph refusal below —
4307
+ # both say "cannot verify", not "something is open" — though a damaged
4308
+ # item on this board still stops it at the unforceable parent gate.
4309
+ if item.type == "epic" and not force:
4310
+ self.require_readable_slices(slug)
4233
4311
  # A completable epic (all children resolved) may close straight from
4234
4312
  # `backlog` — coordinator epics never needed their own start/active. This
4235
4313
  # is a scoped exception, not a global `(backlog, completed)` transition,
@@ -4311,6 +4389,11 @@ class WorkStore(ABC):
4311
4389
  beneath = self.independent_descendants(slug)
4312
4390
  if beneath:
4313
4391
  raise ValueError(drop_refused_over_children(slug, beneath))
4392
+ # A damaged item may name this one as its parent without it showing —
4393
+ # and a drop leaves no record for it to name. Dropping the damaged item
4394
+ # itself stays open, as a way out.
4395
+ if (unreadable := self._unreadable_others(slug)):
4396
+ raise self._unreadable_refusal("drop", slug, "beneath it", unreadable)
4314
4397
  self._delete(slug)
4315
4398
 
4316
4399
  def _require_live_parent(self, parent: str, *, moving: str | None = None,
@@ -4341,6 +4424,8 @@ class WorkStore(ABC):
4341
4424
  """Refuse when any item beneath `slug` is still open. Shared by `complete`
4342
4425
  and by callers that must refuse before doing anything irreversible of
4343
4426
  their own, such as merging a worktree branch."""
4427
+ if (unreadable := self._unreadable_others(slug)):
4428
+ raise self._unreadable_refusal(verb, slug, "beneath it", unreadable)
4344
4429
  still_open = self.open_descendants(slug)
4345
4430
  if still_open:
4346
4431
  raise ValueError(f"Cannot {verb} {slug}; these items beneath it are "