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.
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/PKG-INFO +6 -3
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/README.md +5 -2
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/pyproject.toml +1 -1
- tcw_cli-2.7.0/tcw/__init__.py +1 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/cli.py +23 -11
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/__init__.py +51 -5
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/base.py +330 -32
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/fs.py +617 -130
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/project.py +287 -41
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/create.py +3 -2
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/intake.py +29 -11
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/jira.py +27 -8
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/sync.py +75 -12
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/cli.py +204 -94
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/recursion.py +64 -18
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/PKG-INFO +6 -3
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/SOURCES.txt +17 -0
- tcw_cli-2.7.0/tests/test_blocker_cycles_through_unreadable_items.py +194 -0
- tcw_cli-2.7.0/tests/test_boardless_delegate_and_slices.py +80 -0
- tcw_cli-2.7.0/tests/test_cross_node_blocker_cycles.py +239 -0
- tcw_cli-2.7.0/tests/test_init_configured_tree_path.py +119 -0
- tcw_cli-2.7.0/tests/test_initiative_node_qualified.py +151 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_interrupted_claim.py +194 -0
- tcw_cli-2.7.0/tests/test_local_qualified_blockers.py +133 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_non_git_writes.py +13 -0
- tcw_cli-2.7.0/tests/test_override_in_linked_worktree.py +45 -0
- tcw_cli-2.7.0/tests/test_override_letter_case.py +125 -0
- tcw_cli-2.7.0/tests/test_override_references.py +134 -0
- tcw_cli-2.7.0/tests/test_project_graph_paths.py +77 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_recursion.py +2 -1
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stage_verb.py +102 -22
- tcw_cli-2.7.0/tests/test_tracker_catch_up_moves.py +183 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_cli.py +5 -5
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_hardening.py +9 -1
- tcw_cli-2.7.0/tests/test_tracker_message_tidy.py +158 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_strict_gate.py +77 -10
- tcw_cli-2.7.0/tests/test_transition_hints.py +210 -0
- tcw_cli-2.7.0/tests/test_unreadable_item_files.py +261 -0
- tcw_cli-2.7.0/tests/test_unreadable_state_gates.py +148 -0
- tcw_cli-2.7.0/tests/test_untag_invalid_tags.py +82 -0
- tcw_cli-2.7.0/tests/test_upstream_projects.py +739 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work.py +9 -6
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_review.py +2 -2
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_worktree_sibling_nodes.py +15 -6
- tcw_cli-2.6.4/tcw/__init__.py +0 -1
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/LICENSE +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/setup.cfg +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/harness.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/refs.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/assets/index-C5_pngMt.css +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/assets/index-Dss8_ZG_.js +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/stdin.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/store/config_edit.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/__init__.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/claim.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/ownership.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/tracker/progress.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/validate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/generate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/audit-backlog.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/consolidate-plans.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/create-work.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/decompose.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/delegation.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/documentation-sync.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/post-mortem.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/search.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/triage-issues.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/procedures/unattended-work.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/projection.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/inbox.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw/work/templates.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_rm.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capability_gate_children.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_check_versions.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_child_status.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cli_help_coverage.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_committed_paths.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_config_edit.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_config_edit_writers.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_configuration_text_home.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cross_node_blockers.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_cut_version.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_dynamic_skill_marker.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_edit_refusal.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_edit_type.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_epic_completability_across_checkouts.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_coverage.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_files_changed.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_fixture.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_grading.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_eval_runner.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_external_work_store.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_harness.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_item_tags_read.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_legacy_store_config.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_config_tags.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_literal_store_names.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_procedure_config.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_procedure_verb.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_projection.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_refs.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_resolve.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_resolve_procedure.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_retention.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_routing_nodes.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_shipped_procedures.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_show_json.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_sidecar_paths_in_validate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_skill_path_pointers.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_smoke.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stage_validate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stdin.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_taxonomy_rm_gaps.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_absent.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_binding.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_claim.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_claimability.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_client.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_comment.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_config.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_create.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_gate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_help.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_hold.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_import.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_import_returns.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_inheritance.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_link.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_ownership.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_pre_backlog.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_replay.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_strict.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_surface.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_sync.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_sync_config.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_tracker_validate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unattended_work_skill.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unplanned_start.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_unreadable_capabilities_sidecar.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate_moved_stores.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_start.py +0 -0
- {tcw_cli-2.6.4 → tcw_cli-2.7.0}/tests/test_work_tags.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
215
|
-
|
|
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,
|
|
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
|
|
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
|
|
249
|
-
return lead +
|
|
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")
|