tcw-cli 2.6.3__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.
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/PKG-INFO +6 -3
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/README.md +5 -2
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/pyproject.toml +1 -1
- tcw_cli-2.6.5/tcw/__init__.py +1 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/cli.py +2 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/__init__.py +18 -4
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/base.py +91 -6
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/fs.py +462 -107
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/project.py +101 -30
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/create.py +3 -2
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/intake.py +29 -11
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/jira.py +27 -8
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/sync.py +34 -9
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/cli.py +93 -46
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/documentation-sync.md +4 -4
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/unattended-work.md +1 -1
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/recursion.py +61 -18
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/PKG-INFO +6 -3
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/SOURCES.txt +11 -0
- tcw_cli-2.6.5/tests/test_boardless_delegate_and_slices.py +80 -0
- tcw_cli-2.6.5/tests/test_cut_version.py +236 -0
- tcw_cli-2.6.5/tests/test_init_configured_tree_path.py +119 -0
- tcw_cli-2.6.5/tests/test_initiative_node_qualified.py +151 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_interrupted_claim.py +194 -0
- tcw_cli-2.6.5/tests/test_local_qualified_blockers.py +133 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_non_git_writes.py +13 -0
- tcw_cli-2.6.5/tests/test_override_in_linked_worktree.py +45 -0
- tcw_cli-2.6.5/tests/test_override_references.py +134 -0
- tcw_cli-2.6.5/tests/test_project_graph_paths.py +77 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_recursion.py +2 -1
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_repo_lifecycle.py +2 -2
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_cli.py +5 -5
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_hardening.py +9 -1
- tcw_cli-2.6.5/tests/test_tracker_message_tidy.py +158 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_strict_gate.py +77 -10
- tcw_cli-2.6.5/tests/test_unreadable_item_files.py +261 -0
- tcw_cli-2.6.5/tests/test_unreadable_state_gates.py +148 -0
- tcw_cli-2.6.5/tests/test_untag_invalid_tags.py +82 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_worktree_sibling_nodes.py +15 -6
- tcw_cli-2.6.3/tcw/__init__.py +0 -1
- tcw_cli-2.6.3/tests/test_cut_version.py +0 -116
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/LICENSE +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/setup.cfg +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/harness.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/refs.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/dist/client/assets/index-C5_pngMt.css +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/dist/client/assets/index-Dss8_ZG_.js +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/stdin.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/store/config_edit.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/__init__.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/claim.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/ownership.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/tracker/progress.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/validate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/generate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/audit-backlog.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/consolidate-plans.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/create-work.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/decompose.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/delegation.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/post-mortem.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/search.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/procedures/triage-issues.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/projection.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/inbox.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw/work/templates.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capabilities_rm.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capability_gate_children.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_check_versions.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_child_status.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_cli_help_coverage.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_committed_paths.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_config_edit.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_config_edit_writers.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_configuration_text_home.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_cross_node_blockers.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_dynamic_skill_marker.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_edit_refusal.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_edit_type.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_epic_completability_across_checkouts.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_eval_coverage.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_eval_files_changed.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_eval_fixture.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_eval_grading.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_eval_runner.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_external_work_store.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_harness.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_item_tags_read.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_legacy_store_config.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_config_tags.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_literal_store_names.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_procedure_config.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_procedure_verb.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_projection.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_refs.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_resolve.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_resolve_procedure.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_retention.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_routing_nodes.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_shipped_procedures.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_show_json.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_sidecar_paths_in_validate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_skill_path_pointers.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_smoke.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_stage_validate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_stage_verb.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_stdin.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_taxonomy_rm_gaps.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_absent.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_binding.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_claim.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_claimability.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_client.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_comment.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_config.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_create.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_gate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_help.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_hold.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_import.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_import_returns.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_inheritance.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_link.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_ownership.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_pre_backlog.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_replay.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_strict.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_surface.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_sync.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_sync_config.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_tracker_validate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_unattended_work_skill.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_unplanned_start.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_unreadable_capabilities_sidecar.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_validate.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_validate_moved_stores.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_work.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_work_review.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_work_start.py +0 -0
- {tcw_cli-2.6.3 → tcw_cli-2.6.5}/tests/test_work_tags.py +0 -0
- {tcw_cli-2.6.3 → 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.
|
|
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
|
|
@@ -1102,8 +1102,11 @@ that an agent read them. Running it starts real agent sessions and costs money;
|
|
|
1102
1102
|
### Releasing
|
|
1103
1103
|
|
|
1104
1104
|
`python scripts/cut_version.py <patch|minor|major|X.Y.Z>` bumps the version in all
|
|
1105
|
-
five files that carry it,
|
|
1106
|
-
that version's files, commits and tags.
|
|
1105
|
+
five files that carry it, combines the waiting changelog and release-note entries
|
|
1106
|
+
into that version's files, commits and tags. Each change adds its entries as its
|
|
1107
|
+
own file in `docs/changelogs/upcoming/` and `docs/release-notes/upcoming/`, named
|
|
1108
|
+
after its work item, so changes finished on separate branches never edit the same
|
|
1109
|
+
file. Pushing the tag publishes the release to
|
|
1107
1110
|
PyPI. Details, including the one-time PyPI setup, are in
|
|
1108
1111
|
[`docs/releasing.md`](docs/releasing.md).
|
|
1109
1112
|
|
|
@@ -886,8 +886,11 @@ that an agent read them. Running it starts real agent sessions and costs money;
|
|
|
886
886
|
### Releasing
|
|
887
887
|
|
|
888
888
|
`python scripts/cut_version.py <patch|minor|major|X.Y.Z>` bumps the version in all
|
|
889
|
-
five files that carry it,
|
|
890
|
-
that version's files, commits and tags.
|
|
889
|
+
five files that carry it, combines the waiting changelog and release-note entries
|
|
890
|
+
into that version's files, commits and tags. Each change adds its entries as its
|
|
891
|
+
own file in `docs/changelogs/upcoming/` and `docs/release-notes/upcoming/`, named
|
|
892
|
+
after its work item, so changes finished on separate branches never edit the same
|
|
893
|
+
file. Pushing the tag publishes the release to
|
|
891
894
|
PyPI. Details, including the one-time PyPI setup, are in
|
|
892
895
|
[`docs/releasing.md`](docs/releasing.md).
|
|
893
896
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tcw-cli"
|
|
7
|
-
version = "2.6.
|
|
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
|
|
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
|
|
249
|
-
return lead +
|
|
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
|
-
|
|
3923
|
-
|
|
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
|
-
|
|
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.
|
|
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")
|
|
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 "
|