tcw-cli 2.6.4__tar.gz → 2.7.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.
Files changed (220) hide show
  1. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/PKG-INFO +6 -3
  2. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/README.md +5 -2
  3. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/pyproject.toml +1 -1
  4. tcw_cli-2.7.0/tcw/__init__.py +1 -0
  5. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/cli.py +23 -11
  6. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/__init__.py +51 -5
  7. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/base.py +330 -32
  8. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/fs.py +617 -130
  9. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/project.py +287 -41
  10. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/create.py +3 -2
  11. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/intake.py +29 -11
  12. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/jira.py +27 -8
  13. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/sync.py +75 -12
  14. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/cli.py +204 -94
  15. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/recursion.py +64 -18
  16. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/PKG-INFO +6 -3
  17. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/SOURCES.txt +17 -0
  18. tcw_cli-2.7.0/tests/test_blocker_cycles_through_unreadable_items.py +194 -0
  19. tcw_cli-2.7.0/tests/test_boardless_delegate_and_slices.py +80 -0
  20. tcw_cli-2.7.0/tests/test_cross_node_blocker_cycles.py +239 -0
  21. tcw_cli-2.7.0/tests/test_init_configured_tree_path.py +119 -0
  22. tcw_cli-2.7.0/tests/test_initiative_node_qualified.py +151 -0
  23. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_interrupted_claim.py +194 -0
  24. tcw_cli-2.7.0/tests/test_local_qualified_blockers.py +133 -0
  25. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_non_git_writes.py +13 -0
  26. tcw_cli-2.7.0/tests/test_override_in_linked_worktree.py +45 -0
  27. tcw_cli-2.7.0/tests/test_override_letter_case.py +125 -0
  28. tcw_cli-2.7.0/tests/test_override_references.py +134 -0
  29. tcw_cli-2.7.0/tests/test_project_graph_paths.py +77 -0
  30. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_recursion.py +2 -1
  31. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stage_verb.py +102 -22
  32. tcw_cli-2.7.0/tests/test_tracker_catch_up_moves.py +183 -0
  33. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_cli.py +5 -5
  34. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_hardening.py +9 -1
  35. tcw_cli-2.7.0/tests/test_tracker_message_tidy.py +158 -0
  36. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_strict_gate.py +77 -10
  37. tcw_cli-2.7.0/tests/test_transition_hints.py +210 -0
  38. tcw_cli-2.7.0/tests/test_unreadable_item_files.py +261 -0
  39. tcw_cli-2.7.0/tests/test_unreadable_state_gates.py +148 -0
  40. tcw_cli-2.7.0/tests/test_untag_invalid_tags.py +82 -0
  41. tcw_cli-2.7.0/tests/test_upstream_projects.py +739 -0
  42. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work.py +9 -6
  43. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_review.py +2 -2
  44. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_worktree_sibling_nodes.py +15 -6
  45. tcw_cli-2.6.4/tcw/__init__.py +0 -1
  46. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/LICENSE +0 -0
  47. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/setup.cfg +0 -0
  48. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/capabilities/__init__.py +0 -0
  49. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/capabilities/cli.py +0 -0
  50. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/harness.py +0 -0
  51. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/refs.py +0 -0
  52. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/assets/index-C5_pngMt.css +0 -0
  53. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/assets/index-Dss8_ZG_.js +0 -0
  54. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/index.html +0 -0
  55. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/theme-init.js +0 -0
  56. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/server.cjs +0 -0
  57. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/runtime.py +0 -0
  58. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/stdin.py +0 -0
  59. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/__init__.py +0 -0
  60. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/checkouts.py +0 -0
  61. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/config_edit.py +0 -0
  62. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/taxonomy/__init__.py +0 -0
  63. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/taxonomy/cli.py +0 -0
  64. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/__init__.py +0 -0
  65. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/claim.py +0 -0
  66. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/ownership.py +0 -0
  67. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/progress.py +0 -0
  68. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/validate.py +0 -0
  69. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/__init__.py +0 -0
  70. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/generate.py +0 -0
  71. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/hooks.py +0 -0
  72. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/audit-backlog.md +0 -0
  73. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/consolidate-plans.md +0 -0
  74. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/create-work.md +0 -0
  75. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/decompose.md +0 -0
  76. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/delegation.md +0 -0
  77. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/documentation-sync.md +0 -0
  78. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/post-mortem.md +0 -0
  79. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/search.md +0 -0
  80. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/triage-issues.md +0 -0
  81. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/unattended-work.md +0 -0
  82. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/projection.py +0 -0
  83. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/implement.md +0 -0
  84. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/inbox.md +0 -0
  85. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/plan.md +0 -0
  86. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/postmortem.md +0 -0
  87. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/request.md +0 -0
  88. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/spec.md +0 -0
  89. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/verify.md +0 -0
  90. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/resolve.py +0 -0
  91. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/templates.py +0 -0
  92. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/dependency_links.txt +0 -0
  93. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/entry_points.txt +0 -0
  94. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/requires.txt +0 -0
  95. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/top_level.txt +0 -0
  96. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_body_prompt.py +0 -0
  97. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities.py +0 -0
  98. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_federation.py +0 -0
  99. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_reset.py +0 -0
  100. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_rm.py +0 -0
  101. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_sidecar.py +0 -0
  102. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capability_gate_children.py +0 -0
  103. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capability_ref_wording.py +0 -0
  104. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_check_versions.py +0 -0
  105. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_child_status.py +0 -0
  106. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cli_help_coverage.py +0 -0
  107. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_committed_paths.py +0 -0
  108. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_config_edit.py +0 -0
  109. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_config_edit_writers.py +0 -0
  110. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_configuration_text_home.py +0 -0
  111. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cross_node_blockers.py +0 -0
  112. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cut_version.py +0 -0
  113. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_config.py +0 -0
  114. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_prompt.py +0 -0
  115. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_sync_wiring.py +0 -0
  116. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documented_cli_surface.py +0 -0
  117. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_dynamic_skill_marker.py +0 -0
  118. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_edit_refusal.py +0 -0
  119. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_edit_type.py +0 -0
  120. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_environment_hardness.py +0 -0
  121. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_epic_completability_across_checkouts.py +0 -0
  122. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_epic_completable.py +0 -0
  123. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_coverage.py +0 -0
  124. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_files_changed.py +0 -0
  125. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_fixture.py +0 -0
  126. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_grading.py +0 -0
  127. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_runner.py +0 -0
  128. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_external_work_store.py +0 -0
  129. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_falsification_rule.py +0 -0
  130. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_generate_hook.py +0 -0
  131. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_harness.py +0 -0
  132. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_inbox_title.py +0 -0
  133. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_item_tags_read.py +0 -0
  134. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_legacy_store_config.py +0 -0
  135. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_baseline.py +0 -0
  136. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_config_tags.py +0 -0
  137. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_hooks.py +0 -0
  138. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_inert.py +0 -0
  139. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_policy.py +0 -0
  140. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_validation.py +0 -0
  141. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_literal_store_names.py +0 -0
  142. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_multiproject.py +0 -0
  143. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_plugin_manifests.py +0 -0
  144. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_procedure_config.py +0 -0
  145. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_procedure_verb.py +0 -0
  146. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_project_overrides.py +0 -0
  147. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_project_registry.py +0 -0
  148. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_projection.py +0 -0
  149. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_prompt_fallback.py +0 -0
  150. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_qualified_ref.py +0 -0
  151. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_refs.py +0 -0
  152. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_remote_session_setup.py +0 -0
  153. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_repo_lifecycle.py +0 -0
  154. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_resolve.py +0 -0
  155. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_resolve_procedure.py +0 -0
  156. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_retention.py +0 -0
  157. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_routing_nodes.py +0 -0
  158. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_scaffold.py +0 -0
  159. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve.py +0 -0
  160. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_descendants.py +0 -0
  161. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_projection.py +0 -0
  162. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_resolve.py +0 -0
  163. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_runtime.py +0 -0
  164. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_write.py +0 -0
  165. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_session_bootstrap.py +0 -0
  166. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_shipped_procedures.py +0 -0
  167. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_shipped_prompts.py +0 -0
  168. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_show_json.py +0 -0
  169. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_sidecar_paths_in_validate.py +0 -0
  170. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_flow.py +0 -0
  171. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_lifecycle_parity.py +0 -0
  172. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_path_pointers.py +0 -0
  173. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_smoke.py +0 -0
  174. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stage_validate.py +0 -0
  175. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_status_parity.py +0 -0
  176. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stdin.py +0 -0
  177. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stdin_cli.py +0 -0
  178. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_bounds.py +0 -0
  179. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_editor.py +0 -0
  180. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_nodes.py +0 -0
  181. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_provisioning.py +0 -0
  182. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_publication.py +0 -0
  183. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_subprocess_stdin.py +0 -0
  184. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_taxonomy.py +0 -0
  185. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_taxonomy_rm_gaps.py +0 -0
  186. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tombstone.py +0 -0
  187. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_absent.py +0 -0
  188. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_binding.py +0 -0
  189. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_claim.py +0 -0
  190. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_claimability.py +0 -0
  191. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_client.py +0 -0
  192. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_comment.py +0 -0
  193. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_config.py +0 -0
  194. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_create.py +0 -0
  195. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_gate.py +0 -0
  196. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_help.py +0 -0
  197. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_hold.py +0 -0
  198. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_import.py +0 -0
  199. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_import_returns.py +0 -0
  200. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_inheritance.py +0 -0
  201. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_link.py +0 -0
  202. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_ownership.py +0 -0
  203. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_pre_backlog.py +0 -0
  204. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_replay.py +0 -0
  205. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_strict.py +0 -0
  206. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_surface.py +0 -0
  207. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_sync.py +0 -0
  208. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_sync_config.py +0 -0
  209. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_validate.py +0 -0
  210. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unattended_work_skill.py +0 -0
  211. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unplanned_start.py +0 -0
  212. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unpushed_version_script.py +0 -0
  213. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unreadable_capabilities_sidecar.py +0 -0
  214. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate.py +0 -0
  215. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate_moved_stores.py +0 -0
  216. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate_target.py +0 -0
  217. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_autocommit.py +0 -0
  218. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_start.py +0 -0
  219. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_tags.py +0 -0
  220. {tcw_cli-2.6.4 → tcw_cli-2.7.0}/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.7.0
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
@@ -476,7 +476,10 @@ agent through. See [Skills and Agents](#skills-and-agents).
476
476
 
477
477
  **Many repositories, one model.** Projects are identified by ID, never by path.
478
478
  A project can connect to others, inherit their taxonomy and capabilities, and
479
- address their work items as `<project-id>/<slug>`. A checkout holding only some of
479
+ address their work items as `<project-id>/<slug>`. A connection is normally
480
+ declared by both projects; an `upstream` one is declared only by the reader and
481
+ is read-only from its side, so a private project can read a public one that never
482
+ names it. A checkout holding only some of
480
483
  the connected repositories still works: the absent ones drop out rather than
481
484
  breaking your commands. See [Working across repositories](docs/guide/multi-repo.md).
482
485
 
@@ -907,7 +910,7 @@ driving it to completion, verifying it, processing the inbox) described in
907
910
  | Command | What it does |
908
911
  | -------------------- | ----------------------------------------------------------------- |
909
912
  | `tcw work inbox` | `inbox list`, `inbox show` and `inbox accept` raw requests, and Jira tickets awaiting triage |
910
- | `tcw work nodes` | lists this project's parent and child projects |
913
+ | `tcw work nodes` | lists this project's parent, child and upstream projects |
911
914
  | `tcw work delegate` | writes a request into the nearest child project with a board |
912
915
  | `tcw work escalate` | writes a request into the parent project's inbox |
913
916
  | `tcw work reconcile` | reads every project below with a board; writes the epic's roll-up |
@@ -260,7 +260,10 @@ agent through. See [Skills and Agents](#skills-and-agents).
260
260
 
261
261
  **Many repositories, one model.** Projects are identified by ID, never by path.
262
262
  A project can connect to others, inherit their taxonomy and capabilities, and
263
- address their work items as `<project-id>/<slug>`. A checkout holding only some of
263
+ address their work items as `<project-id>/<slug>`. A connection is normally
264
+ declared by both projects; an `upstream` one is declared only by the reader and
265
+ is read-only from its side, so a private project can read a public one that never
266
+ names it. A checkout holding only some of
264
267
  the connected repositories still works: the absent ones drop out rather than
265
268
  breaking your commands. See [Working across repositories](docs/guide/multi-repo.md).
266
269
 
@@ -691,7 +694,7 @@ driving it to completion, verifying it, processing the inbox) described in
691
694
  | Command | What it does |
692
695
  | -------------------- | ----------------------------------------------------------------- |
693
696
  | `tcw work inbox` | `inbox list`, `inbox show` and `inbox accept` raw requests, and Jira tickets awaiting triage |
694
- | `tcw work nodes` | lists this project's parent and child projects |
697
+ | `tcw work nodes` | lists this project's parent, child and upstream projects |
695
698
  | `tcw work delegate` | writes a request into the nearest child project with a board |
696
699
  | `tcw work escalate` | writes a request into the parent project's inbox |
697
700
  | `tcw work reconcile` | reads every project below with a board; writes the epic's roll-up |
@@ -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.7.0"
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.7.0"
@@ -210,12 +210,14 @@ def _declared_nodes_in_graph(
210
210
  # Every project the registry holds, not `ancestors + descendants`. That
211
211
  # triple omits a sibling and a sibling of an ancestor — the shapes a
212
212
  # workspace is actually made of — so a declaration living on one was
213
- # never read, and the repository it named never obtained.
214
- roots += [Path(project.locator) for project in
215
- FsProjectRegistry.open(root).projects()]
213
+ # never read, and the repository it named never obtained. Except an
214
+ # upstream: what it connects to is its own to obtain, not its readers'.
215
+ registry = FsProjectRegistry.open(root)
216
+ roots += [Path(project.locator) for project in registry.projects()
217
+ if registry.read_only_reason(project.id) is None]
216
218
  except Exception:
217
219
  pass
218
- found: list[tuple[Path, str, object]] = []
220
+ found: list[tuple[Path, str, object, str]] = []
219
221
  problems: list[str] = []
220
222
  for candidate in roots:
221
223
  candidate = candidate.resolve()
@@ -224,7 +226,7 @@ def _declared_nodes_in_graph(
224
226
  seen.add(candidate)
225
227
  declared, candidate_problems = declared_connected_projects(candidate)
226
228
  problems += [f"{candidate / SENTINEL}: {p}" for p in candidate_problems]
227
- found += [(candidate, pid, d) for pid, d in declared]
229
+ found += [(candidate, pid, d, relation) for pid, d, relation in declared]
228
230
  return found, problems
229
231
 
230
232
 
@@ -250,7 +252,7 @@ def _provision_nodes(node_root: Path, *, refresh: bool, dry_run: bool) -> bool:
250
252
 
251
253
  failed = False
252
254
  seen: set[Path] = set()
253
- queue: list[tuple[Path, str, object]] = []
255
+ queue: list[tuple[Path, str, object, str]] = []
254
256
  # "A project that is already here always wins" is the rule the whole feature
255
257
  # rests on, and the walk is the one place it was not applied: a declaration
256
258
  # is written by whichever node knows about that edge, so an ancestor
@@ -298,8 +300,14 @@ def _provision_nodes(node_root: Path, *, refresh: bool, dry_run: bool) -> bool:
298
300
  return None
299
301
  return located if located != target.resolve() else None
300
302
 
301
- def enqueue(root: Path) -> None:
303
+ def enqueue(root: Path, relation: str = "children") -> None:
302
304
  nonlocal failed
305
+ if relation == "upstream":
306
+ # Obtained so it can be read; its own connections are not followed,
307
+ # as the registry does not follow them. A reader must never fetch a
308
+ # project named only by something it reads — a public upstream can
309
+ # still name the private repository it is migrating away from.
310
+ return
303
311
  found, problems = _declared_nodes_in_graph(root, enqueued)
304
312
  for problem in problems:
305
313
  print(f"tcw provision: {problem}", file=sys.stderr)
@@ -314,7 +322,7 @@ def _provision_nodes(node_root: Path, *, refresh: bool, dry_run: bool) -> bool:
314
322
  enqueued: set[Path] = set()
315
323
  enqueue(node_root)
316
324
  while queue:
317
- source, project_id, declaration = queue.pop(0)
325
+ source, project_id, declaration, relation = queue.pop(0)
318
326
  target = provisioned_store_root(source, declaration)
319
327
  if target in seen:
320
328
  continue
@@ -335,13 +343,13 @@ def _provision_nodes(node_root: Path, *, refresh: bool, dry_run: bool) -> bool:
335
343
  # provisioning precisely when an earlier one was already in place.
336
344
  print(f" {project_id}: already available")
337
345
  have.add(project_id)
338
- enqueue(present)
346
+ enqueue(present, relation)
339
347
  continue
340
348
  provisioner = FsStoreProvisioner(source, NODE_TARGET, declaration)
341
349
  if provisioner.is_available() and not refresh:
342
350
  print(f" {project_id}: already available at {target}")
343
351
  have.add(project_id)
344
- enqueue(target)
352
+ enqueue(target, relation)
345
353
  continue
346
354
  print(f"→ {project_id}: {provisioner.describe().split(': ', 1)[1]}")
347
355
  try:
@@ -363,7 +371,7 @@ def _provision_nodes(node_root: Path, *, refresh: bool, dry_run: bool) -> bool:
363
371
  # routine when a parent and a grandparent both name it — report
364
372
  # "already available" in a run that fetched nothing.
365
373
  have.add(project_id)
366
- enqueue(target)
374
+ enqueue(target, relation)
367
375
  return failed
368
376
 
369
377
 
@@ -406,6 +414,8 @@ def _cmd_validate(args: argparse.Namespace) -> int:
406
414
  for override in registry.overrides():
407
415
  print(f"connected project '{override.id}' is overridden by "
408
416
  f"{override.source} to {override.locator}", file=sys.stderr)
417
+ if override.warning:
418
+ print(f" warning: {override.warning}", file=sys.stderr)
409
419
  registry_problems = registry.check()
410
420
  if registry_problems:
411
421
  for problem in registry_problems:
@@ -435,6 +445,8 @@ def _cmd_validate(args: argparse.Namespace) -> int:
435
445
  print(f"{entry.declared_in}: connected project '{entry.id}' is declared "
436
446
  f"at {entry.locator}, which is not in this checkout; it was found "
437
447
  f"at {registry.get(entry.id).locator}", file=sys.stderr)
448
+ for warning in registry.warnings():
449
+ print(f"warning: {warning}", file=sys.stderr)
438
450
  from tcw.validate import validate
439
451
  recurse = args.path is None and not args.no_recurse
440
452
  projects = [registry.current, *registry.descendants()] if recurse else [registry.current]
@@ -24,7 +24,8 @@ from tcw.store.base import (
24
24
  )
25
25
  from tcw.store.fs import (
26
26
  FsCapabilitiesStore, FsTaxonomyStore, FsWorkStore, descendant_nodes,
27
- find_node_root, heading_slug, registered_project_id, resolve_qualified_work_ref,
27
+ find_node_root, heading_slug, registered_project_id, qualified_work_ref_read_only,
28
+ resolve_qualified_work_ref,
28
29
  )
29
30
  from tcw.refs import resolve_tcw_ref
30
31
  from tcw.validate import ValidationTarget, validate
@@ -236,7 +237,7 @@ def _strict_refuses(work, action: str, slug: str = "", body: dict | None = None)
236
237
  or work.get(slug)) if slug else None
237
238
  epic = (body.get("type") == "epic") if action == "create" else (
238
239
  item is not None and item.type == "epic")
239
- from tcw.tracker.intake import ever_bound
240
+ from tcw.tracker.intake import drop_refusal
240
241
  lead = "refused under strict tracker mode, which the web app cannot check; "
241
242
  if action == "create" and not epic:
242
243
  return lead + "create work from a ticket with `tcw work tracker import <ticket>`."
@@ -245,9 +246,8 @@ def _strict_refuses(work, action: str, slug: str = "", body: dict | None = None)
245
246
  return lead + f"use `tcw work start {slug}{take_over}`, which claims the ticket."
246
247
  if action == "complete" and not epic and body.get("resolution") == "done":
247
248
  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`.")
249
+ if action == "drop" and (refusal := drop_refusal(work, slug)):
250
+ return lead + refusal
251
251
  return None
252
252
 
253
253
 
@@ -508,6 +508,21 @@ class TcwHandler(BaseHTTPRequestHandler):
508
508
  return resolve_qualified_work_ref(self.server.node_root, slug)
509
509
  return FsWorkStore.open(self.server.node_root), slug
510
510
 
511
+ def _refused_read_only(self, slug: str) -> bool:
512
+ """Answer 403 and return True when `slug` names an item in a project this
513
+ node reads but may not write — an upstream project, or one reached only
514
+ through one. Every route that changes an item or runs its project's
515
+ scripts asks this first; reads do not, so the board can still show it."""
516
+ if not self.server.include_descendants:
517
+ return False
518
+ reason = qualified_work_ref_read_only(self.server.node_root, slug)
519
+ if reason is None:
520
+ return False
521
+ self._send_err(HTTPStatus.FORBIDDEN,
522
+ f"{reason}; change it from that project itself",
523
+ code="read-only-project")
524
+ return True
525
+
511
526
  def _hosted_projects(self) -> set[str]:
512
527
  """Project IDs whose items this server actually serves — the descendants it
513
528
  aggregates, or nothing when not aggregating.
@@ -635,6 +650,12 @@ class TcwHandler(BaseHTTPRequestHandler):
635
650
  return
636
651
  try:
637
652
  resource = work.read_artifact(slug, name)
653
+ except UnicodeDecodeError:
654
+ # Before ValueError, which it is: the decoder's own message names
655
+ # a byte offset, not the file.
656
+ self._send(HTTPStatus.BAD_REQUEST,
657
+ f"{name} is not valid UTF-8; fix or replace the file".encode("utf-8"))
658
+ return
638
659
  except ValueError as e:
639
660
  self._send(HTTPStatus.BAD_REQUEST, str(e).encode("utf-8"))
640
661
  return
@@ -806,6 +827,15 @@ class TcwHandler(BaseHTTPRequestHandler):
806
827
  })
807
828
  else:
808
829
  artifacts_list.append({"name": name, "present": False})
830
+ except UnicodeDecodeError:
831
+ # Not valid UTF-8: listed with the snapshot's revision, so a
832
+ # guarded save can replace it; opening it names the file.
833
+ artifacts_list.append({
834
+ "name": name,
835
+ "present": name in lifecycle_present,
836
+ "revision": detail.artifact_revisions.get(name, ""),
837
+ "mediaType": "text/markdown",
838
+ })
809
839
  except ValueError:
810
840
  pass
811
841
  # Sidecar discovery in detail
@@ -979,6 +1009,8 @@ class TcwHandler(BaseHTTPRequestHandler):
979
1009
  slug = _decode_path_param(m.group(1))
980
1010
  action = _decode_path_param(m.group(2))
981
1011
  qslug = slug # preserve the addressed (qualified) slug
1012
+ if self._refused_read_only(slug):
1013
+ return
982
1014
  resolved = self._resolve_work(slug)
983
1015
  if resolved is None:
984
1016
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1202,6 +1234,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1202
1234
  if m:
1203
1235
  slug = _decode_path_param(m.group(1))
1204
1236
  qslug = slug # preserve the addressed (qualified) slug
1237
+ if self._refused_read_only(slug):
1238
+ return
1205
1239
  resolved = self._resolve_work(slug)
1206
1240
  if resolved is None:
1207
1241
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1356,6 +1390,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1356
1390
  if name not in WORK_ARTIFACTS:
1357
1391
  self._send(HTTPStatus.BAD_REQUEST, b"unknown artifact")
1358
1392
  return
1393
+ if self._refused_read_only(slug):
1394
+ return
1359
1395
  resolved = self._resolve_work(slug)
1360
1396
  if resolved is None:
1361
1397
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1394,6 +1430,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1394
1430
  if m:
1395
1431
  slug = _decode_path_param(m.group(1))
1396
1432
  stage_id = _decode_path_param(m.group(2))
1433
+ if self._refused_read_only(slug):
1434
+ return
1397
1435
  resolved = self._resolve_work(slug)
1398
1436
  if resolved is None:
1399
1437
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1429,6 +1467,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1429
1467
  f"{name} is written by `{owner}`, not edited; "
1430
1468
  f"run that command instead.")
1431
1469
  return
1470
+ if self._refused_read_only(slug):
1471
+ return
1432
1472
  resolved = self._resolve_work(slug)
1433
1473
  if resolved is None:
1434
1474
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1484,6 +1524,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1484
1524
  if m:
1485
1525
  slug = _decode_path_param(m.group(1))
1486
1526
  stage_id = _decode_path_param(m.group(2))
1527
+ if self._refused_read_only(slug):
1528
+ return
1487
1529
  resolved = self._resolve_work(slug)
1488
1530
  if resolved is None:
1489
1531
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1501,6 +1543,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1501
1543
  m = re.match(r"^/api/work/([^/]+)$", path)
1502
1544
  if m:
1503
1545
  slug = _decode_path_param(m.group(1))
1546
+ if self._refused_read_only(slug):
1547
+ return
1504
1548
  resolved = self._resolve_work(slug)
1505
1549
  if resolved is None:
1506
1550
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")
@@ -1570,6 +1614,8 @@ class TcwHandler(BaseHTTPRequestHandler):
1570
1614
  slug_q, _, stage_q = middle.partition("/plan-stages/")
1571
1615
  slug = _decode_path_param(slug_q)
1572
1616
  stage_id = _decode_path_param(stage_q)
1617
+ if self._refused_read_only(slug):
1618
+ return
1573
1619
  resolved = self._resolve_work(slug)
1574
1620
  if resolved is None:
1575
1621
  self._send(HTTPStatus.NOT_FOUND, b"no such work item")