tcw-cli 2.7.0__tar.gz → 2.8.1__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.7.0 → tcw_cli-2.8.1}/PKG-INFO +5 -5
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/README.md +4 -4
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/pyproject.toml +1 -1
- tcw_cli-2.8.1/tcw/__init__.py +1 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/cli.py +17 -7
- tcw_cli-2.8.1/tcw/cli_suggest.py +199 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/refs.py +4 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/__init__.py +35 -3
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/base.py +174 -14
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/config_edit.py +4 -3
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/fs.py +1041 -159
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/project.py +3 -1
- tcw_cli-2.8.1/tcw/store/yaml_source.py +25 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/validate.py +7 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/cli.py +343 -74
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/create-work.md +9 -4
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/implement.md +2 -1
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/inbox.md +2 -1
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/request.md +2 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/verify.md +3 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/recursion.py +11 -3
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/PKG-INFO +5 -5
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/SOURCES.txt +13 -0
- tcw_cli-2.8.1/tests/test_already_integrated_check.py +365 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_blocker_cycles_through_unreadable_items.py +1 -1
- tcw_cli-2.8.1/tests/test_command_suggestions.py +154 -0
- tcw_cli-2.8.1/tests/test_creation_commits.py +262 -0
- tcw_cli-2.8.1/tests/test_damaged_state_on_edit.py +125 -0
- tcw_cli-2.8.1/tests/test_detached_worktree_teardown.py +145 -0
- tcw_cli-2.8.1/tests/test_duplicate_slug_refusals.py +139 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_external_work_store.py +7 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_non_git_writes.py +24 -4
- tcw_cli-2.8.1/tests/test_planning_gates_after_start.py +144 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_prompt_fallback.py +5 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_recursion.py +4 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_refs.py +8 -4
- tcw_cli-2.8.1/tests/test_rename_work_item.py +453 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_retention.py +4 -1
- tcw_cli-2.8.1/tests/test_serve_duplicate_slug.py +47 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_stage_verb.py +4 -3
- tcw_cli-2.8.1/tests/test_stale_item_path.py +283 -0
- tcw_cli-2.8.1/tests/test_store_lock.py +287 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_comment.py +6 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_strict.py +17 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_sync.py +20 -1
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_transition_hints.py +6 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unreadable_capabilities_sidecar.py +2 -1
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_upstream_projects.py +4 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_work.py +6 -2
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_work_autocommit.py +19 -8
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_work_review.py +4 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_worktree_completion.py +12 -0
- tcw_cli-2.7.0/tcw/__init__.py +0 -1
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/LICENSE +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/setup.cfg +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/harness.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/dist/client/assets/index-C5_pngMt.css +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/dist/client/assets/index-Dss8_ZG_.js +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/stdin.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/__init__.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/claim.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/create.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/intake.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/jira.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/ownership.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/progress.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/tracker/sync.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/generate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/audit-backlog.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/consolidate-plans.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/decompose.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/delegation.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/documentation-sync.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/post-mortem.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/search.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/triage-issues.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/procedures/unattended-work.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/projection.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw/work/templates.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_boardless_delegate_and_slices.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capabilities_rm.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capability_gate_children.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_check_versions.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_child_status.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_cli_help_coverage.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_committed_paths.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_config_edit.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_config_edit_writers.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_configuration_text_home.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_cross_node_blocker_cycles.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_cross_node_blockers.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_cut_version.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_dynamic_skill_marker.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_edit_refusal.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_edit_type.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_epic_completability_across_checkouts.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_eval_coverage.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_eval_files_changed.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_eval_fixture.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_eval_grading.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_eval_runner.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_harness.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_init_configured_tree_path.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_initiative_node_qualified.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_interrupted_claim.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_item_tags_read.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_legacy_store_config.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_config_tags.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_literal_store_names.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_local_qualified_blockers.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_override_in_linked_worktree.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_override_letter_case.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_override_references.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_procedure_config.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_procedure_verb.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_project_graph_paths.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_projection.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_resolve.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_resolve_procedure.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_routing_nodes.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_shipped_procedures.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_show_json.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_sidecar_paths_in_validate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_skill_path_pointers.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_smoke.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_stage_validate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_stdin.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_taxonomy_rm_gaps.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_absent.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_binding.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_catch_up_moves.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_claim.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_claimability.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_cli.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_client.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_config.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_create.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_gate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_hardening.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_help.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_hold.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_import.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_import_returns.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_inheritance.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_link.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_message_tidy.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_ownership.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_pre_backlog.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_replay.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_strict_gate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_surface.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_sync_config.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_tracker_validate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unattended_work_skill.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unplanned_start.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unreadable_item_files.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_unreadable_state_gates.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_untag_invalid_tags.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_validate.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_validate_moved_stores.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_work_start.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_work_tags.py +0 -0
- {tcw_cli-2.7.0 → tcw_cli-2.8.1}/tests/test_worktree_sibling_nodes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tcw-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.1
|
|
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
|
|
@@ -688,9 +688,9 @@ The stages, and the document each one writes:
|
|
|
688
688
|
| Stage | Runs while the item is | Writes |
|
|
689
689
|
| ------------ | ---------------------- | ----------------------------------------------------------------- |
|
|
690
690
|
| `inbox` | not yet an item | nothing; it creates the item, keeping the raw text as `intake.md` |
|
|
691
|
-
| `request` | backlog
|
|
692
|
-
| `spec` | backlog
|
|
693
|
-
| `plan` | backlog
|
|
691
|
+
| `request` | backlog (or active) | `initial-request.md`: what is asked for, and why |
|
|
692
|
+
| `spec` | backlog (or active) | `spec.md`: what to build, with acceptance criteria |
|
|
693
|
+
| `plan` | backlog (or active) | `plan.md`: how to build it, as ordered tasks |
|
|
694
694
|
| `implement` | active | `outcome.md`: what was built, and what the plan got wrong |
|
|
695
695
|
| `verify` | review (or active) | `refined-outcome.md` if accepted, `rework.md` if not |
|
|
696
696
|
| `postmortem` | review or completed | `post-mortem.md`: which stage could first have caught a problem |
|
|
@@ -702,7 +702,7 @@ The transitions, and what each one checks before it moves an item:
|
|
|
702
702
|
| `start` | `tcw work start <slug>` | backlog → active | a blocker is unresolved; the item belongs to an epic that is not active |
|
|
703
703
|
| `submit` | `tcw work submit <slug>` | active → review | never |
|
|
704
704
|
| `rework` | `tcw work rework <slug>` | review → active | `refined-outcome.md` is present (it says the work was accepted) |
|
|
705
|
-
| `complete` | `tcw work complete <slug> --resolution done --confirm` | review or active → completed | a blocker is unresolved; an epic has open children; declared capabilities are not reconciled; a worktree merge-back fails; the item's folder in its worktree has uncommitted changes; no `--confirm` |
|
|
705
|
+
| `complete` | `tcw work complete <slug> --resolution done --confirm` | review or active → completed | a blocker is unresolved; an epic has open children; declared capabilities are not reconciled; a worktree merge-back fails; with `--already-integrated`, the branch is not merged into `HEAD`; the item's folder in its worktree has uncommitted changes; its worktree holds commits on no branch; no `--confirm` |
|
|
706
706
|
| discard | `tcw work complete <slug> --resolution wontfix\|duplicate\|superseded --confirm` | backlog, active or review → discarded | no `--confirm` |
|
|
707
707
|
| `drop` | `tcw work drop <slug> --confirm` | backlog → removed | the item is not in backlog; no `--confirm` |
|
|
708
708
|
| not retained | happens during `complete` or discard | completed or discarded → removed | only happens when `work.retain` says that status is not kept; `tcw work delete` finishes one that was interrupted |
|
|
@@ -472,9 +472,9 @@ The stages, and the document each one writes:
|
|
|
472
472
|
| Stage | Runs while the item is | Writes |
|
|
473
473
|
| ------------ | ---------------------- | ----------------------------------------------------------------- |
|
|
474
474
|
| `inbox` | not yet an item | nothing; it creates the item, keeping the raw text as `intake.md` |
|
|
475
|
-
| `request` | backlog
|
|
476
|
-
| `spec` | backlog
|
|
477
|
-
| `plan` | backlog
|
|
475
|
+
| `request` | backlog (or active) | `initial-request.md`: what is asked for, and why |
|
|
476
|
+
| `spec` | backlog (or active) | `spec.md`: what to build, with acceptance criteria |
|
|
477
|
+
| `plan` | backlog (or active) | `plan.md`: how to build it, as ordered tasks |
|
|
478
478
|
| `implement` | active | `outcome.md`: what was built, and what the plan got wrong |
|
|
479
479
|
| `verify` | review (or active) | `refined-outcome.md` if accepted, `rework.md` if not |
|
|
480
480
|
| `postmortem` | review or completed | `post-mortem.md`: which stage could first have caught a problem |
|
|
@@ -486,7 +486,7 @@ The transitions, and what each one checks before it moves an item:
|
|
|
486
486
|
| `start` | `tcw work start <slug>` | backlog → active | a blocker is unresolved; the item belongs to an epic that is not active |
|
|
487
487
|
| `submit` | `tcw work submit <slug>` | active → review | never |
|
|
488
488
|
| `rework` | `tcw work rework <slug>` | review → active | `refined-outcome.md` is present (it says the work was accepted) |
|
|
489
|
-
| `complete` | `tcw work complete <slug> --resolution done --confirm` | review or active → completed | a blocker is unresolved; an epic has open children; declared capabilities are not reconciled; a worktree merge-back fails; the item's folder in its worktree has uncommitted changes; no `--confirm` |
|
|
489
|
+
| `complete` | `tcw work complete <slug> --resolution done --confirm` | review or active → completed | a blocker is unresolved; an epic has open children; declared capabilities are not reconciled; a worktree merge-back fails; with `--already-integrated`, the branch is not merged into `HEAD`; the item's folder in its worktree has uncommitted changes; its worktree holds commits on no branch; no `--confirm` |
|
|
490
490
|
| discard | `tcw work complete <slug> --resolution wontfix\|duplicate\|superseded --confirm` | backlog, active or review → discarded | no `--confirm` |
|
|
491
491
|
| `drop` | `tcw work drop <slug> --confirm` | backlog → removed | the item is not in backlog; no `--confirm` |
|
|
492
492
|
| not retained | happens during `complete` or discard | completed or discarded → removed | only happens when `work.retain` says that status is not kept; `tcw work delete` finishes one that was interrupted |
|
|
@@ -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.8.1"
|
|
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.8.1"
|
|
@@ -10,6 +10,7 @@ import subprocess
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
from tcw.cli_suggest import SuggestingParser, attach_index
|
|
13
14
|
from tcw import __version__
|
|
14
15
|
from tcw.capabilities import cli as capabilities_cli
|
|
15
16
|
from tcw.serve import DEFAULT_PORT, serve
|
|
@@ -18,7 +19,9 @@ from tcw.store.fs import (
|
|
|
18
19
|
FsWorkStore, declared_repository, find_node_root, git_root, init,
|
|
19
20
|
provisioned_store_root,
|
|
20
21
|
)
|
|
22
|
+
from tcw.store.base import MultipleMatch
|
|
21
23
|
from tcw.store.project import FsProjectRegistry
|
|
24
|
+
from tcw.store.yaml_source import load as _load_named_yaml
|
|
22
25
|
import yaml
|
|
23
26
|
from tcw.taxonomy import cli as taxonomy_cli
|
|
24
27
|
from tcw.work import cli as work_cli
|
|
@@ -55,7 +58,8 @@ def run_init(components: list[str], project_id: str | None = None,
|
|
|
55
58
|
sentinel = root / SENTINEL
|
|
56
59
|
if project_id is None:
|
|
57
60
|
try:
|
|
58
|
-
configured =
|
|
61
|
+
configured = (_load_named_yaml(sentinel.read_text(encoding="utf-8"), sentinel)
|
|
62
|
+
if sentinel.exists() else {})
|
|
59
63
|
except yaml.YAMLError as error:
|
|
60
64
|
print(f"tcw init: invalid {SENTINEL}: {error}", file=sys.stderr)
|
|
61
65
|
return 1
|
|
@@ -479,7 +483,7 @@ def _cmd_serve(args: argparse.Namespace) -> int:
|
|
|
479
483
|
|
|
480
484
|
|
|
481
485
|
def build_parser() -> argparse.ArgumentParser:
|
|
482
|
-
parser =
|
|
486
|
+
parser = SuggestingParser(prog="tcw", description="Taxonomy · Capabilities · Work.")
|
|
483
487
|
parser.add_argument("--version", action="version", version=f"tcw {__version__}")
|
|
484
488
|
sub = parser.add_subparsers(dest="group", required=True)
|
|
485
489
|
|
|
@@ -528,6 +532,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
528
532
|
p = sub.add_parser(name, help=f"{name} commands (not yet implemented)")
|
|
529
533
|
p.set_defaults(func=_not_yet(name))
|
|
530
534
|
|
|
535
|
+
attach_index(parser)
|
|
531
536
|
return parser
|
|
532
537
|
|
|
533
538
|
|
|
@@ -546,7 +551,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
546
551
|
args = build_parser().parse_args(_normalize(argv))
|
|
547
552
|
try:
|
|
548
553
|
return args.func(args)
|
|
549
|
-
except ValueError as error:
|
|
554
|
+
except (ValueError, MultipleMatch) as error:
|
|
555
|
+
# `MultipleMatch` is not a `ValueError`, so a verb that does not catch it
|
|
556
|
+
# itself — `start`, `submit` — printed a traceback for a slug two folders
|
|
557
|
+
# hold. Its message names both folders; this is only the safety net.
|
|
550
558
|
print(f"tcw: {error}", file=sys.stderr)
|
|
551
559
|
return 1
|
|
552
560
|
except yaml.YAMLError as error:
|
|
@@ -555,14 +563,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
555
563
|
# twenty-odd `load_yaml` calls that do not catch it: refusing is already
|
|
556
564
|
# the right answer at every one of them — `init` must not overwrite a
|
|
557
565
|
# config it cannot read — and only the presentation was wrong. The
|
|
558
|
-
# loader's message names the file.
|
|
566
|
+
# loader's message names the file (`tcw.store.yaml_source.load`).
|
|
559
567
|
print(f"tcw: {error}", file=sys.stderr)
|
|
560
568
|
return 1
|
|
561
569
|
except subprocess.CalledProcessError as error:
|
|
562
570
|
# Deliberately generic: the only policy here is "a git subprocess
|
|
563
|
-
# failed", which is true of every component.
|
|
564
|
-
#
|
|
565
|
-
#
|
|
571
|
+
# failed", which is true of every component. The index-touching calls
|
|
572
|
+
# capture git's output (so a retried failure does not print once per
|
|
573
|
+
# try), and it is printed here; an uncaptured call has already shown it.
|
|
566
574
|
# `cmd` is a sequence *or* a string (stdlib contract); joining a string
|
|
567
575
|
# iterates its characters. No shipped raiser passes one, but a handler
|
|
568
576
|
# whose whole justification is that it assumes nothing about its caller
|
|
@@ -571,6 +579,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
571
579
|
shlex.join(str(a) for a in error.cmd)
|
|
572
580
|
print(f"tcw: git command failed (exit {error.returncode}): {cmd}",
|
|
573
581
|
file=sys.stderr)
|
|
582
|
+
if isinstance(error.stderr, str) and error.stderr.strip():
|
|
583
|
+
print(error.stderr.rstrip("\n"), file=sys.stderr)
|
|
574
584
|
return 1
|
|
575
585
|
|
|
576
586
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Answer a mistyped subcommand with the command that was probably meant.
|
|
2
|
+
|
|
3
|
+
argparse reports a wrong subcommand by listing every valid choice at that level,
|
|
4
|
+
which says nothing when the word exists one level down (`tcw tracker` for
|
|
5
|
+
`tcw work tracker`) or is a synonym (`status` for `show`). `SuggestingParser`
|
|
6
|
+
keeps argparse's message word for word and adds one line after it.
|
|
7
|
+
|
|
8
|
+
Three rules, first match wins:
|
|
9
|
+
|
|
10
|
+
1. The word is a subcommand somewhere else in the tree: name each full command,
|
|
11
|
+
and complete it with the word that followed when that is wrong there too —
|
|
12
|
+
`tcw tracker status` → `tcw work tracker show`.
|
|
13
|
+
2. A small table of synonyms, offered only where their target is valid.
|
|
14
|
+
3. Close spellings among the choices valid at that level.
|
|
15
|
+
|
|
16
|
+
Only subcommands that `--help` lists are ever offered. That is what keeps the
|
|
17
|
+
removed `tcw work stage <id>` spellings — registered without `help=` so that the
|
|
18
|
+
old form can print its replacement — out of every suggestion, without naming
|
|
19
|
+
them here.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import difflib
|
|
26
|
+
|
|
27
|
+
# One clear target each. Words for removal are left out on purpose: `drop`,
|
|
28
|
+
# `delete` and `rm` destroy things, and pointing at one is worse than no hint.
|
|
29
|
+
SYNONYMS: dict[str, tuple[str, ...]] = {
|
|
30
|
+
"status": ("show", "list"),
|
|
31
|
+
"info": ("show",),
|
|
32
|
+
"view": ("show",),
|
|
33
|
+
"get": ("show",),
|
|
34
|
+
"cat": ("show",),
|
|
35
|
+
"ls": ("list",),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Never offered by any rule, and never answered either: pointing somebody at a
|
|
39
|
+
# command that destroys things, when they typed something else, is worse than
|
|
40
|
+
# no hint — and so is guessing what a mistyped removal was meant to remove
|
|
41
|
+
# (`tcw work tracker delete` is not `tcw work delete`, which deletes an item).
|
|
42
|
+
REMOVAL = frozenset({"rm", "drop", "delete"})
|
|
43
|
+
|
|
44
|
+
_MAX = 3
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _subparsers(parser: argparse.ArgumentParser) -> argparse._SubParsersAction | None:
|
|
48
|
+
for action in parser._actions:
|
|
49
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
50
|
+
return action
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def visible_choices(action: argparse._SubParsersAction) -> list[str]:
|
|
55
|
+
"""The subcommands this action lists in its help, in its own order."""
|
|
56
|
+
listed = {choice.dest for choice in action._choices_actions}
|
|
57
|
+
return [name for name in action.choices if name in listed]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _offered(word: str, choices: list[str]) -> list[str]:
|
|
61
|
+
"""Rules 2 and 3 for one level: synonyms valid here, else close spellings."""
|
|
62
|
+
safe = [c for c in choices if c not in REMOVAL]
|
|
63
|
+
synonyms = [s for s in SYNONYMS.get(word, ()) if s in safe]
|
|
64
|
+
if synonyms:
|
|
65
|
+
return synonyms
|
|
66
|
+
return difflib.get_close_matches(word, safe, n=_MAX, cutoff=0.6)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _command(path: tuple[str, ...]) -> str:
|
|
70
|
+
return "`" + " ".join(path) + "`"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _phrase(commands: list[str]) -> str:
|
|
74
|
+
if len(commands) == 1:
|
|
75
|
+
return f"did you mean {commands[0]}?"
|
|
76
|
+
if len(commands) == 2:
|
|
77
|
+
return f"did you mean {commands[0]} or {commands[1]}?"
|
|
78
|
+
return f"did you mean one of {', '.join(commands)}?"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SuggestingParser(argparse.ArgumentParser):
|
|
82
|
+
"""An `ArgumentParser` whose wrong-subcommand error suggests what was meant.
|
|
83
|
+
|
|
84
|
+
Every subparser gets this class too, since `add_subparsers` defaults to the
|
|
85
|
+
parent's. `attach_index` gives the whole tree what the hint needs."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *args, **kwargs):
|
|
88
|
+
super().__init__(*args, **kwargs)
|
|
89
|
+
# Python 3.14 has argparse's own close-spelling hint. Off, explicitly,
|
|
90
|
+
# so a later default cannot print a second, different suggestion.
|
|
91
|
+
self.suggest_on_error = False
|
|
92
|
+
|
|
93
|
+
def parse_known_args(self, args=None, namespace=None):
|
|
94
|
+
# The root keeps what it was asked to parse: a nested parser reports its
|
|
95
|
+
# own error and needs the word after the one it rejected.
|
|
96
|
+
if getattr(self, "_tcw_root", None) is self:
|
|
97
|
+
self._tcw_argv = list(args) if args is not None else None
|
|
98
|
+
return super().parse_known_args(args, namespace)
|
|
99
|
+
|
|
100
|
+
def _check_value(self, action, value):
|
|
101
|
+
if not isinstance(action, argparse._SubParsersAction) or value in action.choices:
|
|
102
|
+
return super()._check_value(action, value)
|
|
103
|
+
choices = visible_choices(action)
|
|
104
|
+
hint = self._hint(str(value), choices)
|
|
105
|
+
if len(choices) == len(action.choices):
|
|
106
|
+
# Nothing hidden: argparse's own message, exactly as this Python
|
|
107
|
+
# version words it (3.12 and 3.13 leave the choices unquoted).
|
|
108
|
+
try:
|
|
109
|
+
super()._check_value(action, value)
|
|
110
|
+
except argparse.ArgumentError as error:
|
|
111
|
+
if not hint:
|
|
112
|
+
raise
|
|
113
|
+
raise argparse.ArgumentError(action, f"{error.message}\n{hint}") from None
|
|
114
|
+
message = (f"invalid choice: {value!r} (choose from "
|
|
115
|
+
f"{', '.join(repr(c) for c in choices)})")
|
|
116
|
+
if hint:
|
|
117
|
+
message += f"\n{hint}"
|
|
118
|
+
raise argparse.ArgumentError(action, message)
|
|
119
|
+
|
|
120
|
+
def _hint(self, word: str, choices: list[str]) -> str:
|
|
121
|
+
root = getattr(self, "_tcw_root", None)
|
|
122
|
+
here = getattr(self, "_tcw_path", None)
|
|
123
|
+
if root is None or here is None or word in REMOVAL:
|
|
124
|
+
return ""
|
|
125
|
+
# Same component first — `tcw work tags show` means a `tcw work …` show,
|
|
126
|
+
# not `tcw taxonomy show` — then shallowest: `tcw show` means
|
|
127
|
+
# `tcw work show` before `tcw work inbox show`. `sorted` is stable, so
|
|
128
|
+
# ties keep tree order.
|
|
129
|
+
elsewhere = sorted(
|
|
130
|
+
(p for p in root._tcw_index.get(word, ()) if p[:-1] != here),
|
|
131
|
+
key=lambda p: (-_shared(p, here), len(p)))
|
|
132
|
+
if elsewhere:
|
|
133
|
+
following = _following(root, here, word)
|
|
134
|
+
commands = []
|
|
135
|
+
for path in elsewhere[:_MAX]:
|
|
136
|
+
commands += [_command(path + tail) for tail in _completion(root, path, following)]
|
|
137
|
+
return _phrase(commands[:_MAX])
|
|
138
|
+
offered = _offered(word, choices)
|
|
139
|
+
return _phrase([_command(here + (o,)) for o in offered]) if offered else ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _shared(path: tuple[str, ...], here: tuple[str, ...]) -> int:
|
|
143
|
+
"""How many leading words `path` shares with `here`."""
|
|
144
|
+
n = 0
|
|
145
|
+
for a, b in zip(path, here):
|
|
146
|
+
if a != b:
|
|
147
|
+
break
|
|
148
|
+
n += 1
|
|
149
|
+
return n
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _following(root: SuggestingParser, here: tuple[str, ...], word: str) -> str:
|
|
153
|
+
"""The word after `word` on the command line, if it is not an option."""
|
|
154
|
+
argv = getattr(root, "_tcw_argv", None) or []
|
|
155
|
+
start = len(here) - 1 # `here` includes "tcw"
|
|
156
|
+
for index in range(start, len(argv)):
|
|
157
|
+
if argv[index] == word:
|
|
158
|
+
after = argv[index + 1] if index + 1 < len(argv) else ""
|
|
159
|
+
return "" if after.startswith("-") else after
|
|
160
|
+
return ""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _completion(root: SuggestingParser, path: tuple[str, ...],
|
|
164
|
+
following: str) -> list[tuple[str, ...]]:
|
|
165
|
+
"""How to finish `path` with the word typed after it: kept when it is valid
|
|
166
|
+
there or there is nothing to finish, else what that word was likely meant as."""
|
|
167
|
+
parser = root._tcw_parsers.get(path)
|
|
168
|
+
action = _subparsers(parser) if parser is not None else None
|
|
169
|
+
if not following or action is None:
|
|
170
|
+
return [()]
|
|
171
|
+
choices = visible_choices(action)
|
|
172
|
+
if following in choices:
|
|
173
|
+
return [(following,)]
|
|
174
|
+
offered = _offered(following, choices)
|
|
175
|
+
return [(o,) for o in offered] or [()]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def attach_index(root: SuggestingParser) -> None:
|
|
179
|
+
"""Walk the finished tree once: every parser learns its own path and the
|
|
180
|
+
root, and the root learns every listed subcommand's full paths."""
|
|
181
|
+
index: dict[str, list[tuple[str, ...]]] = {}
|
|
182
|
+
parsers: dict[tuple[str, ...], argparse.ArgumentParser] = {}
|
|
183
|
+
|
|
184
|
+
def walk(parser: argparse.ArgumentParser, path: tuple[str, ...]) -> None:
|
|
185
|
+
parser._tcw_root = root
|
|
186
|
+
parser._tcw_path = path
|
|
187
|
+
parsers[path] = parser
|
|
188
|
+
action = _subparsers(parser)
|
|
189
|
+
if action is None:
|
|
190
|
+
return
|
|
191
|
+
listed = set(visible_choices(action))
|
|
192
|
+
for name, child in action.choices.items():
|
|
193
|
+
if name in listed:
|
|
194
|
+
index.setdefault(name, []).append(path + (name,))
|
|
195
|
+
walk(child, path + (name,))
|
|
196
|
+
|
|
197
|
+
walk(root, (root.prog,))
|
|
198
|
+
root._tcw_index = index
|
|
199
|
+
root._tcw_parsers = parsers
|
|
@@ -135,6 +135,10 @@ def resolve_tcw_ref(node_root: Path | None, uri: str) -> ResolveResult:
|
|
|
135
135
|
return ResolveResult(
|
|
136
136
|
False, "W", None, qualified_work_ref_problem(node_root, ns_ref))
|
|
137
137
|
store, bare = resolved
|
|
138
|
+
# A renamed item answers to its old slug too: a link is exactly the kind
|
|
139
|
+
# of reference a rename cannot rewrite.
|
|
140
|
+
if store.get(bare) is None and (renamed := store.renamed(bare)):
|
|
141
|
+
bare = renamed
|
|
138
142
|
# Locating the store that would hold the item is not the item existing.
|
|
139
143
|
# `resolve_qualified_work_ref` answers "which store addresses this ref",
|
|
140
144
|
# which is what `tcw serve`'s routing wants; `ok` answers "does this
|
|
@@ -20,7 +20,8 @@ from urllib.parse import unquote, urlparse
|
|
|
20
20
|
from tcw.work.projection import work_item_json
|
|
21
21
|
from tcw.store.base import (
|
|
22
22
|
CAP_FIELDS, CAP_STATUSES, WORK_ARTIFACTS, WORK_SIDECARS, _UNSET,
|
|
23
|
-
IllegalTransition, PublicationError, RefError, StaleRevision,
|
|
23
|
+
IllegalTransition, MultipleMatch, PublicationError, RefError, StaleRevision,
|
|
24
|
+
TransitionCommitError,
|
|
24
25
|
)
|
|
25
26
|
from tcw.store.fs import (
|
|
26
27
|
FsCapabilitiesStore, FsTaxonomyStore, FsWorkStore, descendant_nodes,
|
|
@@ -300,6 +301,9 @@ def _map_store_error(e: Exception) -> tuple[int, bytes]:
|
|
|
300
301
|
# because strict-tracker and generated-sidecar refusals are 409 too and
|
|
301
302
|
# must show their own message instead.
|
|
302
303
|
return _err(HTTPStatus.CONFLICT, str(e), code="stale-revision")
|
|
304
|
+
if isinstance(e, MultipleMatch):
|
|
305
|
+
# Two folders hold the slug: the data needs fixing, not the request.
|
|
306
|
+
return _err(HTTPStatus.CONFLICT, str(e))
|
|
303
307
|
if isinstance(e, IllegalTransition):
|
|
304
308
|
return _err(HTTPStatus.UNPROCESSABLE_ENTITY, str(e))
|
|
305
309
|
if isinstance(e, RefError):
|
|
@@ -551,8 +555,17 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
551
555
|
for root, prefix in self._board_roots():
|
|
552
556
|
work = FsWorkStore.open(root)
|
|
553
557
|
for it in work.board():
|
|
554
|
-
|
|
555
|
-
|
|
558
|
+
try:
|
|
559
|
+
items.append(_item_payload(work, it.slug, it,
|
|
560
|
+
f"{prefix}{it.slug}" if prefix else None))
|
|
561
|
+
except MultipleMatch:
|
|
562
|
+
# Two folders hold the slug, so its artifacts cannot be
|
|
563
|
+
# read. Listed with none, so one duplicate does not take the
|
|
564
|
+
# board down; the item's own route says why.
|
|
565
|
+
data = work_item_json(it, [])
|
|
566
|
+
if prefix:
|
|
567
|
+
data["slug"] = f"{prefix}{it.slug}"
|
|
568
|
+
items.append(data)
|
|
556
569
|
return items
|
|
557
570
|
|
|
558
571
|
def _board_roots(self) -> list[tuple[Path, str]]:
|
|
@@ -580,6 +593,8 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
580
593
|
return
|
|
581
594
|
try:
|
|
582
595
|
self._get()
|
|
596
|
+
except MultipleMatch as e:
|
|
597
|
+
self._send(HTTPStatus.CONFLICT, str(e).encode("utf-8"))
|
|
583
598
|
except Exception as e:
|
|
584
599
|
self._send(HTTPStatus.INTERNAL_SERVER_ERROR, str(e).encode("utf-8"))
|
|
585
600
|
|
|
@@ -588,6 +603,8 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
588
603
|
return
|
|
589
604
|
try:
|
|
590
605
|
self._post()
|
|
606
|
+
except MultipleMatch as e:
|
|
607
|
+
self._send(HTTPStatus.CONFLICT, str(e).encode("utf-8"))
|
|
591
608
|
except Exception as e:
|
|
592
609
|
self._send(HTTPStatus.INTERNAL_SERVER_ERROR, str(e).encode("utf-8"))
|
|
593
610
|
|
|
@@ -596,6 +613,8 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
596
613
|
return
|
|
597
614
|
try:
|
|
598
615
|
self._patch()
|
|
616
|
+
except MultipleMatch as e:
|
|
617
|
+
self._send(HTTPStatus.CONFLICT, str(e).encode("utf-8"))
|
|
599
618
|
except Exception as e:
|
|
600
619
|
self._send(HTTPStatus.INTERNAL_SERVER_ERROR, str(e).encode("utf-8"))
|
|
601
620
|
|
|
@@ -604,6 +623,8 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
604
623
|
return
|
|
605
624
|
try:
|
|
606
625
|
self._put()
|
|
626
|
+
except MultipleMatch as e:
|
|
627
|
+
self._send(HTTPStatus.CONFLICT, str(e).encode("utf-8"))
|
|
607
628
|
except Exception as e:
|
|
608
629
|
self._send(HTTPStatus.INTERNAL_SERVER_ERROR, str(e).encode("utf-8"))
|
|
609
630
|
|
|
@@ -612,6 +633,8 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
612
633
|
return
|
|
613
634
|
try:
|
|
614
635
|
self._delete()
|
|
636
|
+
except MultipleMatch as e:
|
|
637
|
+
self._send(HTTPStatus.CONFLICT, str(e).encode("utf-8"))
|
|
615
638
|
except Exception as e:
|
|
616
639
|
self._send(HTTPStatus.INTERNAL_SERVER_ERROR, str(e).encode("utf-8"))
|
|
617
640
|
|
|
@@ -967,6 +990,7 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
967
990
|
initiative = body.get("initiative", "")
|
|
968
991
|
type_val = body.get("type", "")
|
|
969
992
|
tags = body.get("tags") or None
|
|
993
|
+
work.refresh_for_creation()
|
|
970
994
|
detail = work.create_work(
|
|
971
995
|
title=title,
|
|
972
996
|
created=created,
|
|
@@ -987,6 +1011,14 @@ class TcwHandler(BaseHTTPRequestHandler):
|
|
|
987
1011
|
# stale — which the next sidecar write from the page would
|
|
988
1012
|
# be rejected on.
|
|
989
1013
|
detail = work.get_detail(detail.item.slug) or detail
|
|
1014
|
+
# After the owed record, so it is in the same commit. A refusal
|
|
1015
|
+
# is logged, not returned: the item exists, and an error here
|
|
1016
|
+
# would invite the browser to create it a second time.
|
|
1017
|
+
folder = work.path(detail.item.slug)
|
|
1018
|
+
if folder is not None and (reason := work.commit_writes(
|
|
1019
|
+
f"tcw work: new {detail.item.slug}", folder)):
|
|
1020
|
+
print(f"tcw serve: created {detail.item.slug}, but {reason}",
|
|
1021
|
+
file=sys.stderr)
|
|
990
1022
|
response = {
|
|
991
1023
|
"item": _item_payload(work, detail.item.slug, detail.item),
|
|
992
1024
|
"coreRevision": detail.core_revision,
|