tcw-cli 2.5.1__tar.gz → 2.6.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.5.1/tcw_cli.egg-info → tcw_cli-2.6.1}/PKG-INFO +19 -10
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/README.md +18 -9
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/pyproject.toml +1 -1
- tcw_cli-2.6.1/tcw/__init__.py +1 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/base.py +60 -23
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/fs.py +5 -2
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/claim.py +18 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/intake.py +70 -8
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/ownership.py +51 -14
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/progress.py +2 -2
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/sync.py +453 -181
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/cli.py +327 -84
- {tcw_cli-2.5.1 → tcw_cli-2.6.1/tcw_cli.egg-info}/PKG-INFO +19 -10
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw_cli.egg-info/SOURCES.txt +3 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_claim.py +10 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_claimability.py +18 -1
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_cli.py +28 -2
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_comment.py +14 -1
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_config.py +42 -19
- tcw_cli-2.6.1/tests/test_tracker_gate.py +125 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_help.py +22 -5
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_import.py +67 -3
- tcw_cli-2.6.1/tests/test_tracker_import_returns.py +162 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_inheritance.py +10 -12
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_ownership.py +74 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_pre_backlog.py +105 -15
- tcw_cli-2.6.1/tests/test_tracker_strict.py +1687 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_sync.py +955 -176
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_validate.py +14 -2
- tcw_cli-2.6.1/tests/test_work_start.py +48 -0
- tcw_cli-2.5.1/tcw/__init__.py +0 -1
- tcw_cli-2.5.1/tests/test_tracker_strict.py +0 -834
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/LICENSE +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/setup.cfg +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/cli.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/harness.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/refs.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/dist/client/assets/index-CNxlcI8A.js +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/stdin.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/config_edit.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/store/project.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/create.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/tracker/jira.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/validate.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/generate.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/audit-backlog.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/consolidate-plans.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/create-work.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/decompose.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/delegation.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/documentation-sync.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/post-mortem.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/search.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/triage-issues.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/procedures/unattended-work.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/projection.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/inbox.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/recursion.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw/work/templates.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capabilities_rm.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capability_gate_children.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_check_versions.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_child_status.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_cli_help_coverage.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_committed_paths.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_config_edit.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_config_edit_writers.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_configuration_text_home.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_cut_version.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_dynamic_skill_marker.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_edit_type.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_eval_coverage.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_eval_files_changed.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_eval_fixture.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_eval_grading.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_eval_runner.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_external_work_store.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_harness.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_legacy_store_config.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_non_git_writes.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_procedure_config.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_procedure_verb.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_projection.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_recursion.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_refs.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_resolve.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_resolve_procedure.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_retention.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_shipped_procedures.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_show_json.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_skill_path_pointers.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_smoke.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_stage_validate.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_stage_verb.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_stdin.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_absent.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_binding.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_client.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_create.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_hold.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_link.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_replay.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_surface.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_tracker_sync_config.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_unattended_work_skill.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_validate.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_work.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_work_review.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/tests/test_work_tags.py +0 -0
- {tcw_cli-2.5.1 → tcw_cli-2.6.1}/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.6.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
|
|
@@ -727,14 +727,15 @@ to the transitions.
|
|
|
727
727
|
|
|
728
728
|
| Lifecycle step | Without Jira | With Jira configured |
|
|
729
729
|
| --------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
730
|
-
| an item is created | `tcw work inbox accept`, `new` | `tcw work tracker import <ticket>` claims the ticket (starts it and assigns it to you), then creates a backlog item whose intake is the ticket. With `inbox-query` set, `tcw work inbox accept <ticket>` does the same |
|
|
730
|
+
| an item is created | `tcw work inbox accept`, `new` | `tcw work tracker import <ticket>` claims the ticket (starts it and assigns it to you), then creates a backlog item whose intake is the ticket and puts the ticket back where it was, still assigned to you. With `inbox-query` set, `tcw work inbox accept <ticket>` does the same |
|
|
731
731
|
| `request`, `spec`, `plan` stages | unchanged | unchanged |
|
|
732
732
|
| at any time | — | `tcw work tracker link` / `unlink` records or removes the link between an existing item and a ticket; Jira itself is not touched |
|
|
733
733
|
| an item has no ticket | — | `tcw work tracker create <slug>` makes one for it and binds it; `--all` sweeps every open item without one. Needs `work.tracker.create` and `statuses.backlog`. With `create.on-new`, filing an item makes its ticket |
|
|
734
|
-
| `start` | moves the item | also claims a linked ticket
|
|
735
|
-
| `submit`, `rework
|
|
734
|
+
| `start` | moves the item | also claims a linked ticket (assigns it to you) and moves it to the status mapped for `active`; a ticket already past that stays where it is. Posts a comment if `comments: true` |
|
|
735
|
+
| `submit`, `rework` | moves the item | refused before the item moves unless a linked ticket is assigned to you (`tcw work tracker claim` takes it). Then moves the ticket to the Jira status mapped under `statuses`, and posts a comment if `comments: true` |
|
|
736
|
+
| `complete`, discard | moves the item | moves a linked ticket to its mapped status whoever holds it — finishing work needs no claim — and posts a comment if `comments: true` |
|
|
736
737
|
| the ticket could not follow | — | the item still moves; the command exits 1 and records the ticket as pending or conflicting; `tcw work tracker sync` retries |
|
|
737
|
-
| the ticket is behind its item | — |
|
|
738
|
+
| the ticket is behind its item | — | for a ticket linked to work already under way: `tcw work tracker claim <slug>`, then `tcw work tracker sync <slug>` |
|
|
738
739
|
| the ticket does not match its item | — | `tcw work tracker sync <slug>` puts it where the item's status says, whether the ticket is ahead of the item or behind it; a backwards move says so |
|
|
739
740
|
|
|
740
741
|
Setting `strict: true` changes several steps, so that no work happens without a
|
|
@@ -744,9 +745,17 @@ claimed ticket:
|
|
|
744
745
|
and point you at `tcw work tracker import`. Where `inbox-query` is set, `inbox accept`
|
|
745
746
|
of a ticket is allowed, because it claims the ticket.
|
|
746
747
|
- `start` refuses an item with no ticket. For a linked item it claims the ticket
|
|
747
|
-
first,
|
|
748
|
-
|
|
749
|
-
|
|
748
|
+
first, through the transition `exclusive-claim-transition` names, and moves the
|
|
749
|
+
item only if the claim worked and the workflow would refuse a second person —
|
|
750
|
+
that is, it does not offer that transition again once the ticket has it. Every
|
|
751
|
+
strict claim checks this, `tracker claim` and `tracker import` included.
|
|
752
|
+
- Strict mode needs `exclusive-claim-transition` and `transitions.start` set, and
|
|
753
|
+
`tcw validate` reports either one missing: `tracker import`, the way work is
|
|
754
|
+
created, claims through `transitions.start`.
|
|
755
|
+
- `submit` and `rework` read the ticket first, and refuse unless it is assigned to
|
|
756
|
+
you and where the item's last move left it. `complete --resolution done` refuses
|
|
757
|
+
only if the ticket is not where the item's last move left it: who holds it does
|
|
758
|
+
not matter for finishing work.
|
|
750
759
|
- Discarding is always allowed. `drop` refuses an item that was ever linked, so
|
|
751
760
|
the record stays.
|
|
752
761
|
- While Jira cannot be reached, those commands refuse, and `tcw serve` refuses
|
|
@@ -788,9 +797,9 @@ e-mail address or token themselves.
|
|
|
788
797
|
|
|
789
798
|
```sh
|
|
790
799
|
tcw work tracker list # TCW: lists tickets the query selects. Jira: unchanged
|
|
791
|
-
tcw work tracker import ENG-482 # TCW: creates a backlog item. Jira: ENG-482
|
|
800
|
+
tcw work tracker import ENG-482 # TCW: creates a backlog item. Jira: ENG-482 stays in To Do, assigned to you
|
|
792
801
|
# …the request, spec and plan stages run as usual…
|
|
793
|
-
tcw work start <slug> # TCW: backlog → active. Jira:
|
|
802
|
+
tcw work start <slug> # TCW: backlog → active. Jira: ENG-482 → In Progress
|
|
794
803
|
tcw work submit <slug> # TCW: active → review. Jira: ENG-482 → In Review
|
|
795
804
|
tcw work complete <slug> --resolution done --confirm
|
|
796
805
|
# TCW: review → completed. Jira: ENG-482 → Done
|
|
@@ -511,14 +511,15 @@ to the transitions.
|
|
|
511
511
|
|
|
512
512
|
| Lifecycle step | Without Jira | With Jira configured |
|
|
513
513
|
| --------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
514
|
-
| an item is created | `tcw work inbox accept`, `new` | `tcw work tracker import <ticket>` claims the ticket (starts it and assigns it to you), then creates a backlog item whose intake is the ticket. With `inbox-query` set, `tcw work inbox accept <ticket>` does the same |
|
|
514
|
+
| an item is created | `tcw work inbox accept`, `new` | `tcw work tracker import <ticket>` claims the ticket (starts it and assigns it to you), then creates a backlog item whose intake is the ticket and puts the ticket back where it was, still assigned to you. With `inbox-query` set, `tcw work inbox accept <ticket>` does the same |
|
|
515
515
|
| `request`, `spec`, `plan` stages | unchanged | unchanged |
|
|
516
516
|
| at any time | — | `tcw work tracker link` / `unlink` records or removes the link between an existing item and a ticket; Jira itself is not touched |
|
|
517
517
|
| an item has no ticket | — | `tcw work tracker create <slug>` makes one for it and binds it; `--all` sweeps every open item without one. Needs `work.tracker.create` and `statuses.backlog`. With `create.on-new`, filing an item makes its ticket |
|
|
518
|
-
| `start` | moves the item | also claims a linked ticket
|
|
519
|
-
| `submit`, `rework
|
|
518
|
+
| `start` | moves the item | also claims a linked ticket (assigns it to you) and moves it to the status mapped for `active`; a ticket already past that stays where it is. Posts a comment if `comments: true` |
|
|
519
|
+
| `submit`, `rework` | moves the item | refused before the item moves unless a linked ticket is assigned to you (`tcw work tracker claim` takes it). Then moves the ticket to the Jira status mapped under `statuses`, and posts a comment if `comments: true` |
|
|
520
|
+
| `complete`, discard | moves the item | moves a linked ticket to its mapped status whoever holds it — finishing work needs no claim — and posts a comment if `comments: true` |
|
|
520
521
|
| the ticket could not follow | — | the item still moves; the command exits 1 and records the ticket as pending or conflicting; `tcw work tracker sync` retries |
|
|
521
|
-
| the ticket is behind its item | — |
|
|
522
|
+
| the ticket is behind its item | — | for a ticket linked to work already under way: `tcw work tracker claim <slug>`, then `tcw work tracker sync <slug>` |
|
|
522
523
|
| the ticket does not match its item | — | `tcw work tracker sync <slug>` puts it where the item's status says, whether the ticket is ahead of the item or behind it; a backwards move says so |
|
|
523
524
|
|
|
524
525
|
Setting `strict: true` changes several steps, so that no work happens without a
|
|
@@ -528,9 +529,17 @@ claimed ticket:
|
|
|
528
529
|
and point you at `tcw work tracker import`. Where `inbox-query` is set, `inbox accept`
|
|
529
530
|
of a ticket is allowed, because it claims the ticket.
|
|
530
531
|
- `start` refuses an item with no ticket. For a linked item it claims the ticket
|
|
531
|
-
first,
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
first, through the transition `exclusive-claim-transition` names, and moves the
|
|
533
|
+
item only if the claim worked and the workflow would refuse a second person —
|
|
534
|
+
that is, it does not offer that transition again once the ticket has it. Every
|
|
535
|
+
strict claim checks this, `tracker claim` and `tracker import` included.
|
|
536
|
+
- Strict mode needs `exclusive-claim-transition` and `transitions.start` set, and
|
|
537
|
+
`tcw validate` reports either one missing: `tracker import`, the way work is
|
|
538
|
+
created, claims through `transitions.start`.
|
|
539
|
+
- `submit` and `rework` read the ticket first, and refuse unless it is assigned to
|
|
540
|
+
you and where the item's last move left it. `complete --resolution done` refuses
|
|
541
|
+
only if the ticket is not where the item's last move left it: who holds it does
|
|
542
|
+
not matter for finishing work.
|
|
534
543
|
- Discarding is always allowed. `drop` refuses an item that was ever linked, so
|
|
535
544
|
the record stays.
|
|
536
545
|
- While Jira cannot be reached, those commands refuse, and `tcw serve` refuses
|
|
@@ -572,9 +581,9 @@ e-mail address or token themselves.
|
|
|
572
581
|
|
|
573
582
|
```sh
|
|
574
583
|
tcw work tracker list # TCW: lists tickets the query selects. Jira: unchanged
|
|
575
|
-
tcw work tracker import ENG-482 # TCW: creates a backlog item. Jira: ENG-482
|
|
584
|
+
tcw work tracker import ENG-482 # TCW: creates a backlog item. Jira: ENG-482 stays in To Do, assigned to you
|
|
576
585
|
# …the request, spec and plan stages run as usual…
|
|
577
|
-
tcw work start <slug> # TCW: backlog → active. Jira:
|
|
586
|
+
tcw work start <slug> # TCW: backlog → active. Jira: ENG-482 → In Progress
|
|
578
587
|
tcw work submit <slug> # TCW: active → review. Jira: ENG-482 → In Review
|
|
579
588
|
tcw work complete <slug> --resolution done --confirm
|
|
580
589
|
# TCW: review → completed. Jira: ENG-482 → Done
|
|
@@ -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.6.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.6.1"
|
|
@@ -1259,11 +1259,11 @@ TRACKER_CREDENTIAL_KEYS = frozenset({"email-env", "token-env"})
|
|
|
1259
1259
|
# These name *how* it gets there, and are needed only where the status cannot say: a
|
|
1260
1260
|
# workflow offering two transitions into one status — a "finished" and an "abandoned"
|
|
1261
1261
|
# route both landing in `Done` is the common shape — is otherwise unreachable, since
|
|
1262
|
-
# TCW will not guess which. A move with no name here keeps the status-derived rule
|
|
1263
|
-
#
|
|
1264
|
-
#
|
|
1265
|
-
#
|
|
1266
|
-
#
|
|
1262
|
+
# TCW will not guess which. A move with no name here keeps the status-derived rule, and
|
|
1263
|
+
# that now includes `start`: a start goes through `assess_move` like the other four, so
|
|
1264
|
+
# it derives its transition from the status it is heading for when nobody names one. An
|
|
1265
|
+
# unknown key is reported, because silently ignoring a key someone set is silently not
|
|
1266
|
+
# doing what they asked.
|
|
1267
1267
|
TRACKER_TRANSITION_KEYS = frozenset({"start", "submit", "rework", "complete", "discard"})
|
|
1268
1268
|
TRACKER_MOVE_TRANSITION_KEYS = ("start", "submit", "rework", "complete", "discard")
|
|
1269
1269
|
# A key that used to exist, with what replaced it, so a config written for an older
|
|
@@ -1275,6 +1275,23 @@ TRACKER_RENAMED_KEYS = {
|
|
|
1275
1275
|
"renamed to work.tracker.transitions.start, the transition the start move "
|
|
1276
1276
|
"applies, alongside submit, rework, complete and discard",
|
|
1277
1277
|
}
|
|
1278
|
+
# Strict mode promises that only one person can take a ticket. A claim that applies
|
|
1279
|
+
# this transition first keeps that promise, because a workflow that will not apply
|
|
1280
|
+
# it to a ticket already taken stops the second person there.
|
|
1281
|
+
STRICT_NEEDS_EXCLUSIVE_CLAIM = (
|
|
1282
|
+
"work.tracker.exclusive-claim-transition: required when strict is true. Strict "
|
|
1283
|
+
"mode promises that only one person can take a ticket, and this transition, "
|
|
1284
|
+
"which your workflow will not apply to a ticket that has already been taken, "
|
|
1285
|
+
"is what stops a second person. Name the transition that takes a ticket into "
|
|
1286
|
+
"work.")
|
|
1287
|
+
# Strict mode creates work only from a ticket, and the two commands that do it claim
|
|
1288
|
+
# through this transition, so a strict project without it can create no work at all.
|
|
1289
|
+
STRICT_NEEDS_START_TRANSITION = (
|
|
1290
|
+
"work.tracker.transitions.start: required when strict is true. Strict mode "
|
|
1291
|
+
"creates work only from a ticket, and the two commands that do that — `tcw work "
|
|
1292
|
+
"tracker import` and `tcw work inbox accept` — claim the ticket through this "
|
|
1293
|
+
"transition. Name the transition that takes a ticket from the backlog status "
|
|
1294
|
+
"into work.")
|
|
1278
1295
|
# `backlog` first: it is where an item begins, and where creation puts its ticket.
|
|
1279
1296
|
# The other four are the statuses a *bound* ticket is moved through afterwards.
|
|
1280
1297
|
TRACKER_STATUS_KEYS = ("backlog", "active", "review", "completed", "discarded")
|
|
@@ -1431,7 +1448,13 @@ def parse_tracker_config(raw: Any) -> tuple["TrackerConfig | None", list[str]]:
|
|
|
1431
1448
|
return value
|
|
1432
1449
|
|
|
1433
1450
|
credentials = nested("credentials", TRACKER_CREDENTIAL_KEYS)
|
|
1434
|
-
|
|
1451
|
+
# Optional — every move derives its transition from the status when nobody names
|
|
1452
|
+
# one — so a lone `null` is a wrong value rather than a missing required one, the
|
|
1453
|
+
# shape `inbox-query` above and `exclusive-claim-transition` below both use.
|
|
1454
|
+
if "transitions" in raw and raw["transitions"] is None:
|
|
1455
|
+
problems.append("work.tracker.transitions: expected a mapping, got NoneType")
|
|
1456
|
+
transitions = (nested("transitions", TRACKER_TRANSITION_KEYS)
|
|
1457
|
+
if raw.get("transitions") is not None else {})
|
|
1435
1458
|
|
|
1436
1459
|
def nested_str(block: dict, key: str, path: str) -> str:
|
|
1437
1460
|
value = block.get(key)
|
|
@@ -1453,8 +1476,10 @@ def parse_tracker_config(raw: Any) -> tuple["TrackerConfig | None", list[str]]:
|
|
|
1453
1476
|
f"got {type(strict).__name__}")
|
|
1454
1477
|
strict = False
|
|
1455
1478
|
if strict:
|
|
1456
|
-
# Strict mode gates completing and discarding against these, so a
|
|
1457
|
-
# one would be a gate that can never pass or never check.
|
|
1479
|
+
# Strict mode gates completing and discarding against these statuses, so a
|
|
1480
|
+
# missing one would be a gate that can never pass or never check. It also
|
|
1481
|
+
# needs `exclusive-claim-transition`, the only thing that makes a claim
|
|
1482
|
+
# exclusive; that check is after these.
|
|
1458
1483
|
for key in ("active", "completed"):
|
|
1459
1484
|
if not statuses.get(key):
|
|
1460
1485
|
problems.append(f"work.tracker.statuses.{key}: required when "
|
|
@@ -1466,6 +1491,10 @@ def parse_tracker_config(raw: Any) -> tuple["TrackerConfig | None", list[str]]:
|
|
|
1466
1491
|
problems.append(f"work.tracker.statuses.discarded: required when strict "
|
|
1467
1492
|
f"is true, as one status or one for each of "
|
|
1468
1493
|
f"{', '.join(sorted(resolutions))}")
|
|
1494
|
+
# Absent only: a key written as `null` or blank already has its own problem
|
|
1495
|
+
# below, and one problem per key is enough.
|
|
1496
|
+
if "exclusive-claim-transition" not in raw:
|
|
1497
|
+
problems.append(STRICT_NEEDS_EXCLUSIVE_CLAIM)
|
|
1469
1498
|
|
|
1470
1499
|
comments = raw.get("comments", False)
|
|
1471
1500
|
if not isinstance(comments, bool):
|
|
@@ -1476,21 +1505,20 @@ def parse_tracker_config(raw: Any) -> tuple["TrackerConfig | None", list[str]]:
|
|
|
1476
1505
|
|
|
1477
1506
|
email_env = nested_str(credentials, "email-env", "credentials.email-env")
|
|
1478
1507
|
token_env = nested_str(credentials, "token-env", "credentials.token-env")
|
|
1479
|
-
# Optional
|
|
1480
|
-
# the same shape
|
|
1508
|
+
# Optional unless strict (required above, only when the key is absent), so a lone
|
|
1509
|
+
# `null` is a wrong value rather than a missing required one — the same shape
|
|
1510
|
+
# `inbox-query` uses above.
|
|
1481
1511
|
if "exclusive-claim-transition" in raw and raw["exclusive-claim-transition"] is None:
|
|
1482
1512
|
problems.append("work.tracker.exclusive-claim-transition: expected a "
|
|
1483
1513
|
"non-empty string, got NoneType")
|
|
1484
1514
|
exclusive_claim = (required_str("exclusive-claim-transition")
|
|
1485
1515
|
if raw.get("exclusive-claim-transition") is not None else "")
|
|
1486
1516
|
move_transitions = _parse_tracker_transitions(transitions, problems)
|
|
1487
|
-
# `start` is the one move whose transition is required: it is the only move with no
|
|
1488
|
-
# status-derived fallback, because a start applies its transition through the claim
|
|
1489
|
-
# rather than through `assess_move`. A wrong *value* is reported by the loop above,
|
|
1490
|
-
# like its four siblings; an absent *key* is reported here.
|
|
1491
|
-
if "start" not in transitions:
|
|
1492
|
-
problems.append("work.tracker.transitions.start: required")
|
|
1493
1517
|
start = move_transitions.get("start", "")
|
|
1518
|
+
# After `transitions` is parsed, and absent only — the same rule as
|
|
1519
|
+
# `exclusive-claim-transition`: `null` or blank already has its own problem.
|
|
1520
|
+
if strict and "start" not in transitions:
|
|
1521
|
+
problems.append(STRICT_NEEDS_START_TRANSITION)
|
|
1494
1522
|
|
|
1495
1523
|
timeout: Any = raw.get("timeout-seconds", TRACKER_DEFAULT_TIMEOUT)
|
|
1496
1524
|
# `bool` before `int`, because a bool *is* an int and `timeout-seconds: true`
|
|
@@ -1674,9 +1702,8 @@ def _parse_tracker_pre_backlog(raw: Any, statuses: dict, problems: list[str]) ->
|
|
|
1674
1702
|
def _parse_tracker_transitions(raw: dict, problems: list[str]) -> dict:
|
|
1675
1703
|
"""The per-move keys of `work.tracker.transitions`, appending a problem per defect.
|
|
1676
1704
|
|
|
1677
|
-
All five keys are parsed here, uniformly.
|
|
1678
|
-
|
|
1679
|
-
the status-derived rule.
|
|
1705
|
+
All five keys are parsed here, uniformly. Absent is `{}`, which leaves every
|
|
1706
|
+
move on the status-derived rule; no key here is required.
|
|
1680
1707
|
"""
|
|
1681
1708
|
where = "work.tracker.transitions"
|
|
1682
1709
|
|
|
@@ -1842,7 +1869,7 @@ def attribute_tracker_problems(problems: list[str], record: dict[TrackerKeyPath,
|
|
|
1842
1869
|
"""Prefix each parser problem with the label of the file that caused it.
|
|
1843
1870
|
|
|
1844
1871
|
Matched on the problem's exact key path, never the nearest enclosing
|
|
1845
|
-
mapping: `
|
|
1872
|
+
mapping: `credentials.token-env: required` under a `credentials` an ancestor
|
|
1846
1873
|
supplied is about a key nobody set, and goes to `own_label`. The parser
|
|
1847
1874
|
writes a path as its keys joined with `.` followed by `: `, so the longest
|
|
1848
1875
|
recorded path whose spelling prefixes the problem that way is the one it is
|
|
@@ -2925,13 +2952,21 @@ class MultipleMatch(Exception):
|
|
|
2925
2952
|
|
|
2926
2953
|
|
|
2927
2954
|
class AlreadyClaimed(IllegalTransition):
|
|
2928
|
-
"""A single-winner start lost to an existing active claim.
|
|
2955
|
+
"""A single-winner start lost to an existing active claim.
|
|
2956
|
+
|
|
2957
|
+
Names the command that takes the item over, because a refusal that stops somebody
|
|
2958
|
+
has to say what to do instead. `tcw work tracker claim --take-over` is the one
|
|
2959
|
+
for an item whose work is shared through a tracker; `start --take-over` stays for
|
|
2960
|
+
a project with none, and for recovering an interrupted claim."""
|
|
2929
2961
|
|
|
2930
2962
|
def __init__(self, slug: str, owner: str = "", started: str = ""):
|
|
2931
2963
|
self.slug, self.owner, self.started = slug, owner, started
|
|
2932
2964
|
who = owner or "an unknown owner"
|
|
2933
2965
|
when = started or "an unknown time"
|
|
2934
|
-
super().__init__(f"{slug} is already claimed by {who} since {when}"
|
|
2966
|
+
super().__init__(f"{slug} is already claimed by {who} since {when}. To take "
|
|
2967
|
+
f"it over, run `tcw work tracker claim {slug} --take-over` "
|
|
2968
|
+
f"(or `tcw work start {slug} --take-over` where no tracker "
|
|
2969
|
+
f"is configured)")
|
|
2935
2970
|
|
|
2936
2971
|
|
|
2937
2972
|
@dataclass
|
|
@@ -3876,7 +3911,9 @@ class WorkStore(ABC):
|
|
|
3876
3911
|
take_over: bool = False) -> WorkItem:
|
|
3877
3912
|
item = self._require(slug)
|
|
3878
3913
|
if item.status == "active":
|
|
3879
|
-
|
|
3914
|
+
# Active with nobody holding it — what `tcw work tracker release` leaves —
|
|
3915
|
+
# is taken, not refused: there is nobody to displace.
|
|
3916
|
+
if not take_over and item.owner:
|
|
3880
3917
|
raise AlreadyClaimed(slug, item.owner, item.started)
|
|
3881
3918
|
if not owner:
|
|
3882
3919
|
raise ValueError("takeover requires an owner")
|
|
@@ -4053,7 +4053,9 @@ class FsWorkStore(FsTreeStore, WorkStore):
|
|
|
4053
4053
|
if item is None:
|
|
4054
4054
|
raise ValueError(f"no such work item: {slug}")
|
|
4055
4055
|
if item.status == "active":
|
|
4056
|
-
|
|
4056
|
+
# Active with nobody holding it — what `tcw work tracker release` leaves —
|
|
4057
|
+
# is taken, not refused: there is nobody to displace.
|
|
4058
|
+
if not take_over and item.owner:
|
|
4057
4059
|
raise AlreadyClaimed(slug, item.owner, item.started)
|
|
4058
4060
|
if not owner:
|
|
4059
4061
|
raise ValueError("takeover requires an owner")
|
|
@@ -4061,7 +4063,8 @@ class FsWorkStore(FsTreeStore, WorkStore):
|
|
|
4061
4063
|
{"owner": owner, "started": started})
|
|
4062
4064
|
if self.auto_commit_transitions():
|
|
4063
4065
|
rel = str(self._require_dir(slug).relative_to(self.store_git_root))
|
|
4064
|
-
|
|
4066
|
+
what = "take over" if item.owner else "claim"
|
|
4067
|
+
err = git_commit_result(self.store_git_root, f"tcw work: {what} {slug}", rel)
|
|
4065
4068
|
if err:
|
|
4066
4069
|
raise TransitionCommitError(f"{slug} was taken over, but committing it failed:\n{err}")
|
|
4067
4070
|
return self._require(slug)
|
|
@@ -48,6 +48,12 @@ CLAIM_NOT_OFFERED = "claim not offered by this ticket"
|
|
|
48
48
|
# reporting one at all.
|
|
49
49
|
MISCONFIGURED = "misconfigured"
|
|
50
50
|
AMBIGUOUS = "ambiguous"
|
|
51
|
+
# The project named no start transition at all. `work.tracker.transitions.start` is
|
|
52
|
+
# optional — a lifecycle start works its transition out from the status it is heading
|
|
53
|
+
# for — but a claim has no target status of its own to derive from. Distinct from
|
|
54
|
+
# `CLAIM_NOT_OFFERED`, which is about a name this ticket does not offer: that one
|
|
55
|
+
# sends the reader to the ticket's workflow, and this one to their own configuration.
|
|
56
|
+
NOT_CONFIGURED = "no start transition configured"
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
@dataclass(frozen=True)
|
|
@@ -92,6 +98,18 @@ def assess(claim_transition: str, *, current_status: str, offered,
|
|
|
92
98
|
With no `landing_status` and no offered claim, the answer is `NOT_DETERMINED`.
|
|
93
99
|
That is a real limitation, reported rather than papered over.
|
|
94
100
|
"""
|
|
101
|
+
# Nothing configured. Held here rather than in each caller: every reader of
|
|
102
|
+
# `config.start_transition` that has no status to derive from goes through this
|
|
103
|
+
# function, so one guard covers all of them.
|
|
104
|
+
if not claim_transition.strip():
|
|
105
|
+
return Assessment(
|
|
106
|
+
claimable=NOT_CLAIMABLE,
|
|
107
|
+
exclusivity=NOT_DETERMINED,
|
|
108
|
+
verdict=NOT_CONFIGURED,
|
|
109
|
+
detail=("work.tracker.transitions.start names no transition, so there is "
|
|
110
|
+
"none to claim this ticket through."),
|
|
111
|
+
)
|
|
112
|
+
|
|
95
113
|
wanted = _normalize(claim_transition)
|
|
96
114
|
matches = [t for t in offered if _normalize(t.name) == wanted]
|
|
97
115
|
offered_names = [t.name for t in offered]
|
|
@@ -195,15 +195,17 @@ def find_binding(store, *, project: str, provider: str, ticket_id: str,
|
|
|
195
195
|
|
|
196
196
|
def binding_document(*, provider: str, project: str, part: str, ticket_id: str,
|
|
197
197
|
ticket_key: str, ticket_url: str, bound: str,
|
|
198
|
-
unlinked: list, status_synced: bool = True
|
|
199
|
-
catch_up: bool = False) -> str:
|
|
198
|
+
unlinked: list, status_synced: bool = True) -> str:
|
|
200
199
|
"""The `tracker.yaml` text for a new binding. No credential goes in it.
|
|
201
200
|
|
|
202
201
|
The document records that an item and a ticket are the same work and nothing
|
|
203
202
|
more: it names no account, because binding does not claim the ticket.
|
|
204
203
|
`status_synced=False` notes that the item was already past `backlog` and the
|
|
205
|
-
ticket was not brought along
|
|
206
|
-
|
|
204
|
+
ticket was not brought along.
|
|
205
|
+
|
|
206
|
+
Nothing writes `catch-up: true` any more — `link --sync-status` did, and is
|
|
207
|
+
retired — but a binding already carrying it is still read (`Bound.catch_up`), so
|
|
208
|
+
the multi-rung walk it asked for stays reachable until those bindings age out.
|
|
207
209
|
"""
|
|
208
210
|
document = {
|
|
209
211
|
"schema": 1,
|
|
@@ -216,8 +218,6 @@ def binding_document(*, provider: str, project: str, part: str, ticket_id: str,
|
|
|
216
218
|
}
|
|
217
219
|
if not status_synced:
|
|
218
220
|
document["status-synced"] = False
|
|
219
|
-
if catch_up:
|
|
220
|
-
document["catch-up"] = True
|
|
221
221
|
return yaml.safe_dump(document, sort_keys=False, allow_unicode=True)
|
|
222
222
|
|
|
223
223
|
|
|
@@ -445,6 +445,9 @@ class ClaimOutcome:
|
|
|
445
445
|
# when no such step ran. Set on success and on a refusal alike: either way the
|
|
446
446
|
# ticket has left triage, and the caller has to say so (`moved_out`).
|
|
447
447
|
left_status: str = ""
|
|
448
|
+
# The status the claim found the ticket in, after any `pre-backlog` step: where
|
|
449
|
+
# `import` puts a ticket its claim moved (`put_back`). Set only on a claim.
|
|
450
|
+
claimed_from: str = ""
|
|
448
451
|
|
|
449
452
|
|
|
450
453
|
def _fields(issue: dict) -> tuple[str, str, str, str]:
|
|
@@ -625,7 +628,8 @@ def claim(client, ticket: TicketRead) -> ClaimOutcome:
|
|
|
625
628
|
if left:
|
|
626
629
|
error.left_status = left
|
|
627
630
|
raise
|
|
628
|
-
return replace(outcome, left_status=left
|
|
631
|
+
return replace(outcome, left_status=left,
|
|
632
|
+
claimed_from=ticket.status if outcome.claimed else "")
|
|
629
633
|
|
|
630
634
|
|
|
631
635
|
def _claim_from(client, ticket: TicketRead) -> ClaimOutcome:
|
|
@@ -641,7 +645,7 @@ def _claim_from(client, ticket: TicketRead) -> ClaimOutcome:
|
|
|
641
645
|
Authentication, permission, rate-limit and not-found errors on the transition
|
|
642
646
|
propagate: the transition did not apply and there is nothing to read back.
|
|
643
647
|
"""
|
|
644
|
-
from tcw.tracker.claim import AMBIGUOUS, _normalize, assess
|
|
648
|
+
from tcw.tracker.claim import AMBIGUOUS, NOT_CONFIGURED, _normalize, assess
|
|
645
649
|
from tcw.tracker.jira import (TrackerAuthError, TrackerError, TrackerNotFound,
|
|
646
650
|
TrackerPermissionError, TrackerRateLimited,
|
|
647
651
|
TrackerRequestInvalid)
|
|
@@ -673,9 +677,23 @@ def _claim_from(client, ticket: TicketRead) -> ClaimOutcome:
|
|
|
673
677
|
f"is ambiguous on this ticket.", assessment.detail)
|
|
674
678
|
matches = [t for t in ticket.offered if _normalize(t.name) == _normalize(name)]
|
|
675
679
|
if not matches:
|
|
680
|
+
# Row `1e` first, and that order is load-bearing: a ticket this account
|
|
681
|
+
# already holds needs no transition, which is what lets a claim that
|
|
682
|
+
# succeeded and then failed locally finish on a re-run, and what lets
|
|
683
|
+
# somebody take a ticket with `tcw work tracker claim` — assignment only —
|
|
684
|
+
# and then import it. A refusal above this row would break both.
|
|
676
685
|
if ticket.assignee_id == ticket.me_id:
|
|
677
686
|
return claimed("1e", f"not claimed by this run: {key} is already in "
|
|
678
687
|
f"'{status}' and assigned to you.", status, False)
|
|
688
|
+
if assessment.verdict == NOT_CONFIGURED:
|
|
689
|
+
# Refused rather than derived: a claim is not a lifecycle move, it has no
|
|
690
|
+
# target status of its own to work a transition out from, and a wrong
|
|
691
|
+
# guess here assigns the ticket as well as moving it.
|
|
692
|
+
return refused("1d", f"{key} cannot be claimed: "
|
|
693
|
+
f"work.tracker.transitions.start is not set, and this "
|
|
694
|
+
f"command claims through it. Set it, or take the "
|
|
695
|
+
f"ticket with `tcw work tracker claim` and bind an "
|
|
696
|
+
f"item to it with `tcw work tracker link`.")
|
|
679
697
|
offers = ", ".join(repr(t.name) for t in ticket.offered) or "nothing"
|
|
680
698
|
return refused("1f", f"{key} is in '{status}', unassigned, and does not "
|
|
681
699
|
f"offer {name!r}. It offers: {offers}."
|
|
@@ -731,3 +749,47 @@ def _claim_from(client, ticket: TicketRead) -> ClaimOutcome:
|
|
|
731
749
|
return refused("3f", f"could not tell whether this run's claim applied: {key} is "
|
|
732
750
|
f"in '{now_status}', unassigned. Check the ticket's history "
|
|
733
751
|
f"in the tracker before assigning it.", detail)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def put_back(client, outcome: ClaimOutcome) -> tuple[str, str]:
|
|
755
|
+
"""Move a ticket this run's claim took back to `outcome.claimed_from`, where the
|
|
756
|
+
claim found it. `import` does this once its backlog item exists, so the ticket
|
|
757
|
+
does not say the work has started while the item says it has not.
|
|
758
|
+
|
|
759
|
+
The claim's own transition is what kept a second claimant out, so it is taken
|
|
760
|
+
first and only undone here, after the item is bound. The way back is the one
|
|
761
|
+
transition the ticket offers to that status, chosen as a lifecycle move with no
|
|
762
|
+
named transition chooses it (`assess_move`); none, or more than one, sends
|
|
763
|
+
nothing.
|
|
764
|
+
|
|
765
|
+
Returns `(the status the ticket is in, why it is not back or "")`. Never raises a
|
|
766
|
+
tracker error: by now the item exists, so a ticket left where the claim put it is
|
|
767
|
+
something to report, not a reason to fail.
|
|
768
|
+
"""
|
|
769
|
+
from tcw.tracker.claim import _normalize
|
|
770
|
+
from tcw.tracker.jira import TrackerError
|
|
771
|
+
from tcw.tracker.sync import CURRENT, assess_move
|
|
772
|
+
|
|
773
|
+
target = outcome.claimed_from
|
|
774
|
+
try:
|
|
775
|
+
ticket = read_ticket(client, outcome.issue_id)
|
|
776
|
+
state, found = assess_move(ticket, target=target, expected=(), move=None)
|
|
777
|
+
if state == CURRENT:
|
|
778
|
+
return ticket.status, ""
|
|
779
|
+
if state != "apply":
|
|
780
|
+
return ticket.status, found
|
|
781
|
+
client.apply_transition(ticket.issue_id, found.id)
|
|
782
|
+
status = _fields(client.issue(ticket.issue_id))[0]
|
|
783
|
+
except TrackerError as error:
|
|
784
|
+
# The transition may have landed with its answer lost, so where the ticket
|
|
785
|
+
# is has to be read rather than assumed.
|
|
786
|
+
try:
|
|
787
|
+
status = _fields(client.issue(outcome.issue_id))[0]
|
|
788
|
+
except TrackerError:
|
|
789
|
+
return outcome.status, f"{error}; where it is now could not be read"
|
|
790
|
+
if _normalize(status) == _normalize(target):
|
|
791
|
+
return status, ""
|
|
792
|
+
return status, str(error)
|
|
793
|
+
if _normalize(status) != _normalize(target):
|
|
794
|
+
return status, f"it is in '{status}' after '{found.name}'"
|
|
795
|
+
return status, ""
|