tcw-cli 2.1.1__tar.gz → 2.1.2__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.1.1 → tcw_cli-2.1.2}/PKG-INFO +46 -5
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/README.md +45 -4
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/pyproject.toml +1 -1
- tcw_cli-2.1.2/tcw/__init__.py +1 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/capabilities/cli.py +19 -1
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/store/base.py +35 -8
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/store/fs.py +78 -4
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/tracker/claim.py +7 -4
- tcw_cli-2.1.2/tcw/tracker/intake.py +373 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/tracker/jira.py +52 -3
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/cli.py +265 -2
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/recursion.py +16 -7
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/PKG-INFO +46 -5
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/SOURCES.txt +7 -0
- tcw_cli-2.1.2/tests/test_capabilities_rm.py +406 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_capabilities_sidecar.py +18 -5
- tcw_cli-2.1.2/tests/test_eval_files_changed.py +177 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_eval_fixture.py +82 -1
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_eval_grading.py +57 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_eval_runner.py +49 -4
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_recursion.py +10 -0
- tcw_cli-2.1.2/tests/test_tracker_binding.py +250 -0
- tcw_cli-2.1.2/tests/test_tracker_claim.py +247 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_claimability.py +28 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_client.py +83 -0
- tcw_cli-2.1.2/tests/test_tracker_import.py +354 -0
- tcw_cli-2.1.2/tests/test_tracker_link.py +192 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_work.py +23 -0
- tcw_cli-2.1.1/tcw/__init__.py +0 -1
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/LICENSE +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/setup.cfg +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/cli.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/refs.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/stdin.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/store/project.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/tracker/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/validate.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/generate.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/projection.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/inbox.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw/work/templates.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_committed_paths.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_cut_version.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_eval_coverage.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_external_work_store.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_non_git_writes.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_projection.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_refs.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_resolve.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_retention.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_show_json.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_smoke.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_stage_verb.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_stdin.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_absent.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_cli.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_config.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_replay.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_tracker_validate.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_validate.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_work_review.py +0 -0
- {tcw_cli-2.1.1 → tcw_cli-2.1.2}/tests/test_work_tags.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tcw-cli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
@@ -229,6 +229,28 @@ They link by one-directional pointers — a capability names the taxonomy terms
|
|
|
229
229
|
involves, a work item names the capability it changes — and never copy each
|
|
230
230
|
other's content.
|
|
231
231
|
|
|
232
|
+
## Contents
|
|
233
|
+
|
|
234
|
+
- [The problem](#the-problem)
|
|
235
|
+
- [What it looks like](#what-it-looks-like)
|
|
236
|
+
- [Why many repositories is the case it is built for](#why-many-repositories-is-the-case-it-is-built-for)
|
|
237
|
+
- [What it deliberately refuses](#what-it-deliberately-refuses)
|
|
238
|
+
- [What adopting it costs](#what-adopting-it-costs)
|
|
239
|
+
- [Who it's for](#who-its-for)
|
|
240
|
+
- [Storage abstraction (the prime directive)](#storage-abstraction-the-prime-directive)
|
|
241
|
+
- [Install](#install)
|
|
242
|
+
- [As a plugin (recommended)](#as-a-plugin-recommended)
|
|
243
|
+
- [As a Python package](#as-a-python-package)
|
|
244
|
+
- [In a cloud environment](#in-a-cloud-environment)
|
|
245
|
+
- [Quickstart](#quickstart)
|
|
246
|
+
- [Working from your Jira tickets](#working-from-your-jira-tickets)
|
|
247
|
+
- [Documentation](#documentation)
|
|
248
|
+
- [Skills — the judgment layer](#skills--the-judgment-layer)
|
|
249
|
+
- [Reading a lifecycle stage](#reading-a-lifecycle-stage)
|
|
250
|
+
- [Review agents and slash commands](#review-agents-and-slash-commands)
|
|
251
|
+
- [Status](#status)
|
|
252
|
+
- [Further reading](#further-reading)
|
|
253
|
+
|
|
232
254
|
---
|
|
233
255
|
|
|
234
256
|
## The problem
|
|
@@ -548,15 +570,17 @@ studies your code, proposes a first draft, refines it with you, and writes it.
|
|
|
548
570
|
|
|
549
571
|
---
|
|
550
572
|
|
|
551
|
-
##
|
|
573
|
+
## Working from your Jira tickets
|
|
552
574
|
|
|
553
|
-
A project can name the Jira Cloud site it uses,
|
|
554
|
-
|
|
555
|
-
ticket into a work item.
|
|
575
|
+
A project can name the Jira Cloud site it uses, read its tickets, and take one as
|
|
576
|
+
a work item.
|
|
556
577
|
|
|
557
578
|
```sh
|
|
558
579
|
tcw work tracker list # tickets the configured query selects
|
|
559
580
|
tcw work tracker show ENG-482 # one ticket, and whether it is yours to take
|
|
581
|
+
tcw work tracker import ENG-482 # take the ticket and get a work item for it
|
|
582
|
+
tcw work tracker link <slug> ENG-482 # take it for an item you already have
|
|
583
|
+
tcw work tracker unlink <slug> --reason "wrong ticket"
|
|
560
584
|
```
|
|
561
585
|
|
|
562
586
|
Configuration goes in the node's `tcw-config.yaml`:
|
|
@@ -594,6 +618,23 @@ reports `not determined`, because it cannot show you what happens to the second
|
|
|
594
618
|
person. Making a workflow exclusive is a Jira administration change, not something
|
|
595
619
|
TCW can do for you.
|
|
596
620
|
|
|
621
|
+
**Taking a ticket.** `tracker import` moves the ticket through your configured
|
|
622
|
+
claim transition, assigns it to you if nobody had it, and then checks Jira again:
|
|
623
|
+
only when the ticket really is started and yours does it create a backlog item. The
|
|
624
|
+
ticket's text goes into the item as its raw input, with a link back, and the item's
|
|
625
|
+
request still gets written the usual way. A ticket assigned to someone else is
|
|
626
|
+
refused, and so is one that is already closed.
|
|
627
|
+
|
|
628
|
+
Running `import` again for the same ticket gives you the same item rather than a
|
|
629
|
+
second one, and if the first run took the ticket but stopped before creating the
|
|
630
|
+
item, the second run finishes the job. One ticket can deliberately become several
|
|
631
|
+
items with `--part api`, `--part web`, and so on. `unlink` removes a wrong binding
|
|
632
|
+
and keeps a record of it with your reason; it never changes the ticket in Jira.
|
|
633
|
+
|
|
634
|
+
Two limits to know. On a workflow that lets anyone start a ticket from any status,
|
|
635
|
+
two people can both take the same ticket, and TCW does not stop that. And two runs
|
|
636
|
+
by the same Jira account at the same moment can both create an item.
|
|
637
|
+
|
|
597
638
|
Two guarantees worth stating plainly. A project with no `tracker` block behaves
|
|
598
639
|
exactly as before, with no new required setting and no network access. And
|
|
599
640
|
`tcw validate` never contacts the tracker, which matters because projects commonly
|
|
@@ -13,6 +13,28 @@ They link by one-directional pointers — a capability names the taxonomy terms
|
|
|
13
13
|
involves, a work item names the capability it changes — and never copy each
|
|
14
14
|
other's content.
|
|
15
15
|
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [The problem](#the-problem)
|
|
19
|
+
- [What it looks like](#what-it-looks-like)
|
|
20
|
+
- [Why many repositories is the case it is built for](#why-many-repositories-is-the-case-it-is-built-for)
|
|
21
|
+
- [What it deliberately refuses](#what-it-deliberately-refuses)
|
|
22
|
+
- [What adopting it costs](#what-adopting-it-costs)
|
|
23
|
+
- [Who it's for](#who-its-for)
|
|
24
|
+
- [Storage abstraction (the prime directive)](#storage-abstraction-the-prime-directive)
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [As a plugin (recommended)](#as-a-plugin-recommended)
|
|
27
|
+
- [As a Python package](#as-a-python-package)
|
|
28
|
+
- [In a cloud environment](#in-a-cloud-environment)
|
|
29
|
+
- [Quickstart](#quickstart)
|
|
30
|
+
- [Working from your Jira tickets](#working-from-your-jira-tickets)
|
|
31
|
+
- [Documentation](#documentation)
|
|
32
|
+
- [Skills — the judgment layer](#skills--the-judgment-layer)
|
|
33
|
+
- [Reading a lifecycle stage](#reading-a-lifecycle-stage)
|
|
34
|
+
- [Review agents and slash commands](#review-agents-and-slash-commands)
|
|
35
|
+
- [Status](#status)
|
|
36
|
+
- [Further reading](#further-reading)
|
|
37
|
+
|
|
16
38
|
---
|
|
17
39
|
|
|
18
40
|
## The problem
|
|
@@ -332,15 +354,17 @@ studies your code, proposes a first draft, refines it with you, and writes it.
|
|
|
332
354
|
|
|
333
355
|
---
|
|
334
356
|
|
|
335
|
-
##
|
|
357
|
+
## Working from your Jira tickets
|
|
336
358
|
|
|
337
|
-
A project can name the Jira Cloud site it uses,
|
|
338
|
-
|
|
339
|
-
ticket into a work item.
|
|
359
|
+
A project can name the Jira Cloud site it uses, read its tickets, and take one as
|
|
360
|
+
a work item.
|
|
340
361
|
|
|
341
362
|
```sh
|
|
342
363
|
tcw work tracker list # tickets the configured query selects
|
|
343
364
|
tcw work tracker show ENG-482 # one ticket, and whether it is yours to take
|
|
365
|
+
tcw work tracker import ENG-482 # take the ticket and get a work item for it
|
|
366
|
+
tcw work tracker link <slug> ENG-482 # take it for an item you already have
|
|
367
|
+
tcw work tracker unlink <slug> --reason "wrong ticket"
|
|
344
368
|
```
|
|
345
369
|
|
|
346
370
|
Configuration goes in the node's `tcw-config.yaml`:
|
|
@@ -378,6 +402,23 @@ reports `not determined`, because it cannot show you what happens to the second
|
|
|
378
402
|
person. Making a workflow exclusive is a Jira administration change, not something
|
|
379
403
|
TCW can do for you.
|
|
380
404
|
|
|
405
|
+
**Taking a ticket.** `tracker import` moves the ticket through your configured
|
|
406
|
+
claim transition, assigns it to you if nobody had it, and then checks Jira again:
|
|
407
|
+
only when the ticket really is started and yours does it create a backlog item. The
|
|
408
|
+
ticket's text goes into the item as its raw input, with a link back, and the item's
|
|
409
|
+
request still gets written the usual way. A ticket assigned to someone else is
|
|
410
|
+
refused, and so is one that is already closed.
|
|
411
|
+
|
|
412
|
+
Running `import` again for the same ticket gives you the same item rather than a
|
|
413
|
+
second one, and if the first run took the ticket but stopped before creating the
|
|
414
|
+
item, the second run finishes the job. One ticket can deliberately become several
|
|
415
|
+
items with `--part api`, `--part web`, and so on. `unlink` removes a wrong binding
|
|
416
|
+
and keeps a record of it with your reason; it never changes the ticket in Jira.
|
|
417
|
+
|
|
418
|
+
Two limits to know. On a workflow that lets anyone start a ticket from any status,
|
|
419
|
+
two people can both take the same ticket, and TCW does not stop that. And two runs
|
|
420
|
+
by the same Jira account at the same moment can both create an item.
|
|
421
|
+
|
|
381
422
|
Two guarantees worth stating plainly. A project with no `tracker` block behaves
|
|
382
423
|
exactly as before, with no new required setting and no network access. And
|
|
383
424
|
`tcw validate` never contacts the tracker, which matters because projects commonly
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tcw-cli"
|
|
7
|
-
version = "2.1.
|
|
7
|
+
version = "2.1.2"
|
|
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.1.2"
|
|
@@ -9,7 +9,8 @@ from tcw.store.base import AmbiguousRef
|
|
|
9
9
|
from tcw.store.fs import FsCapabilitiesStore, find_node, git_root
|
|
10
10
|
|
|
11
11
|
NAME = "capabilities"
|
|
12
|
-
SUBCOMMANDS = {"init", "list", "show", "path", "add", "search", "check", "set", "reset", "
|
|
12
|
+
SUBCOMMANDS = {"init", "list", "show", "path", "add", "search", "check", "set", "reset", "rm",
|
|
13
|
+
"extends", "drift"}
|
|
13
14
|
DEFAULT_SUBCOMMAND = "show" # `tcw capabilities <path>` == `tcw capabilities show <path>`
|
|
14
15
|
|
|
15
16
|
|
|
@@ -129,6 +130,19 @@ def _reset(args: argparse.Namespace) -> int:
|
|
|
129
130
|
return 0
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
def _rm(args: argparse.Namespace) -> int:
|
|
134
|
+
st = _store()
|
|
135
|
+
if st is None:
|
|
136
|
+
return 1
|
|
137
|
+
try:
|
|
138
|
+
st.remove(args.id)
|
|
139
|
+
except (ValueError, RefError) as e: # AmbiguousRef carries its own message
|
|
140
|
+
print(f"tcw capabilities rm: {e}", file=sys.stderr)
|
|
141
|
+
return 1
|
|
142
|
+
print(f"Removed capability {args.id}")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
132
146
|
def _search(args: argparse.Namespace) -> int:
|
|
133
147
|
st = _store()
|
|
134
148
|
if st is None:
|
|
@@ -294,6 +308,10 @@ def add_subparser(sub: argparse._SubParsersAction) -> None:
|
|
|
294
308
|
prst.add_argument("id", metavar="path")
|
|
295
309
|
prst.set_defaults(func=_reset)
|
|
296
310
|
|
|
311
|
+
prm = g.add_parser("rm", help="remove a local capability")
|
|
312
|
+
prm.add_argument("id", metavar="path")
|
|
313
|
+
prm.set_defaults(func=_rm)
|
|
314
|
+
|
|
297
315
|
pse = g.add_parser("search", help="search names + bodies")
|
|
298
316
|
pse.add_argument("query")
|
|
299
317
|
pse.set_defaults(func=_search)
|
|
@@ -287,24 +287,27 @@ class ProjectRegistry(ABC):
|
|
|
287
287
|
|
|
288
288
|
def declared_capabilities(capabilities: Any) -> dict[str, list[str]]:
|
|
289
289
|
"""Canonical read of a work item's ``capabilities.yaml`` into
|
|
290
|
-
``{"new": [...], "changed": [...]}`` — the work→capability
|
|
291
|
-
DoD gate enforces.
|
|
290
|
+
``{"new": [...], "changed": [...], "removed": [...]}`` — the work→capability
|
|
291
|
+
back-pointers the DoD gate enforces.
|
|
292
292
|
|
|
293
293
|
``capabilities`` is the already-parsed sidecar object (``WorkItem.capabilities``):
|
|
294
|
-
a mapping with ``new:``/``changed:`` lists of canonical
|
|
295
|
-
strings. ``
|
|
294
|
+
a mapping with ``new:``/``changed:``/``removed:`` lists of canonical
|
|
295
|
+
``namespace/path`` strings. ``removed:`` names capabilities the item deleted,
|
|
296
|
+
which cannot sit under ``changed:`` because they no longer resolve.
|
|
297
|
+
``added:`` is accepted as a deprecated alias of ``new:``. A trailing
|
|
296
298
|
`` # comment`` on a value is stripped (YAML strips it already; belt and
|
|
297
299
|
suspenders). The reconcile list-form sidecar and any other shape declare
|
|
298
300
|
nothing here. The ``_tcw_parse_error`` sentinel the FS adapter produces on bad
|
|
299
301
|
YAML raises ``SidecarError`` so the gate fails closed rather than reading
|
|
300
302
|
"no deltas".
|
|
301
303
|
"""
|
|
302
|
-
out: dict[str, list[str]] = {"new": [], "changed": []}
|
|
304
|
+
out: dict[str, list[str]] = {"new": [], "changed": [], "removed": []}
|
|
303
305
|
if not capabilities or not isinstance(capabilities, dict):
|
|
304
306
|
return out
|
|
305
307
|
if "_tcw_parse_error" in capabilities:
|
|
306
308
|
raise SidecarError(str(capabilities["_tcw_parse_error"]))
|
|
307
|
-
for key, bucket in (("new", "new"), ("added", "new"), ("changed", "changed")
|
|
309
|
+
for key, bucket in (("new", "new"), ("added", "new"), ("changed", "changed"),
|
|
310
|
+
("removed", "removed")):
|
|
308
311
|
vals = capabilities.get(key)
|
|
309
312
|
if vals is None:
|
|
310
313
|
continue
|
|
@@ -564,13 +567,27 @@ class CapabilitiesStore(ABC):
|
|
|
564
567
|
|
|
565
568
|
@abstractmethod
|
|
566
569
|
def remove(self, identifier: str) -> None:
|
|
567
|
-
|
|
570
|
+
"""Delete the local capability at `identifier`, writing nothing when
|
|
571
|
+
refused. Raise `AmbiguousRef` when a bare ref matches multiple extended
|
|
572
|
+
stores, and `ValueError` when:
|
|
573
|
+
|
|
574
|
+
- nothing resolves;
|
|
575
|
+
- the capability is inherited, overridden locally or not (an override
|
|
576
|
+
is dropped with `reset`, never with `remove`);
|
|
577
|
+
- another capability or override is nested under its path;
|
|
578
|
+
- another local capability or override still references it through
|
|
579
|
+
`Superseded by`, `Blocked by`, `Roles` or `When`.
|
|
580
|
+
|
|
581
|
+
Refusing rather than cascading is the contract: a delete never takes a
|
|
582
|
+
second capability with it or leaves a reference pointing at nothing.
|
|
583
|
+
Never mutates an extended store."""
|
|
568
584
|
|
|
569
585
|
@abstractmethod
|
|
570
586
|
def reset(self, identifier: str) -> None:
|
|
571
587
|
"""Drop the local override at `identifier`, re-inheriting the upstream
|
|
572
588
|
capability verbatim. Raise `ValueError` when there is no override (a
|
|
573
|
-
standalone local capability is not an override —
|
|
589
|
+
standalone local capability is not an override — the `remove` operation,
|
|
590
|
+
driven by `tcw capabilities rm`, deletes it; a bare
|
|
574
591
|
inherited path has nothing to drop), or `AmbiguousRef` when a bare ref
|
|
575
592
|
matches multiple extended stores. Never mutates an extended store."""
|
|
576
593
|
|
|
@@ -1940,6 +1957,16 @@ WORK_SIDECARS: dict[str, dict[str, str]] = {
|
|
|
1940
1957
|
"media_type": "text/markdown",
|
|
1941
1958
|
"generated": "yes",
|
|
1942
1959
|
},
|
|
1960
|
+
# The binding between an item and the tracker ticket it answers. Written by
|
|
1961
|
+
# `tcw work tracker import | link | unlink`, never by hand, so it is
|
|
1962
|
+
# `generated` like `rollup.md` and edit surfaces must not offer to write it.
|
|
1963
|
+
# Nothing treats it as proof that a claim was made: every command that acts on
|
|
1964
|
+
# a binding re-reads the ticket from the tracker.
|
|
1965
|
+
"tracker.yaml": {
|
|
1966
|
+
"media_type": "application/yaml",
|
|
1967
|
+
"validation": "yaml_mapping",
|
|
1968
|
+
"generated": "yes",
|
|
1969
|
+
},
|
|
1943
1970
|
}
|
|
1944
1971
|
|
|
1945
1972
|
# Taxonomy term fields that the abstract ``update_term`` operation may modify.
|
|
@@ -565,7 +565,10 @@ def git_stage(node_root: Path, *paths: Path) -> None:
|
|
|
565
565
|
|
|
566
566
|
def git_rm(node_root: Path, path: Path) -> None:
|
|
567
567
|
# -f so a term staged-but-not-yet-committed (just `add`ed) can still be removed.
|
|
568
|
-
|
|
568
|
+
# --literal-pathspecs: `--` ends options but a path is still a glob to git, so
|
|
569
|
+
# removing a folder named `a*` would also delete `abc`.
|
|
570
|
+
_git(["git", "-C", str(node_root), "--literal-pathspecs", "rm", "-rfq", "--", str(path)],
|
|
571
|
+
check=True)
|
|
569
572
|
|
|
570
573
|
|
|
571
574
|
NOT_A_REPOSITORY = "not inside a git repository. Run `git init` first."
|
|
@@ -1077,8 +1080,13 @@ _UniqueKeyLoader.add_constructor(
|
|
|
1077
1080
|
#: the Definition-of-Done gate reads, and the list form `reconcile` writes — so
|
|
1078
1081
|
#: holding it to one of them would report a sound file, and worse, make that
|
|
1079
1082
|
#: gate fail closed on it. See `declared_capabilities` in `tcw/store/base.py`.
|
|
1083
|
+
#:
|
|
1084
|
+
#: `tracker.yaml`, an item's binding to a tracker ticket, has one shape — a mapping,
|
|
1085
|
+
#: which `write_sidecar` already enforces — so it is held to the contract like
|
|
1086
|
+
#: `state.yaml`.
|
|
1080
1087
|
OWNED_YAML_NAMES = frozenset({
|
|
1081
1088
|
"state.yaml", "meta.yaml", "graveyard.yaml", "config.yaml", ".config.yaml",
|
|
1089
|
+
"tracker.yaml",
|
|
1082
1090
|
})
|
|
1083
1091
|
|
|
1084
1092
|
|
|
@@ -2480,18 +2488,84 @@ class FsCapabilitiesStore(FsTreeStore, _FederationCycles, CapabilitiesStore):
|
|
|
2480
2488
|
|
|
2481
2489
|
def remove(self, identifier: str) -> None:
|
|
2482
2490
|
cap = self.get(identifier)
|
|
2491
|
+
# The exact listed spelling only. `get` resolves `routes/`, `./routes`,
|
|
2492
|
+
# or `Routes` on a case-insensitive disk, to the `routes` folder but
|
|
2493
|
+
# echoes the spelling back as the path, so everything below that works
|
|
2494
|
+
# from the path would be working from the wrong one.
|
|
2495
|
+
if cap is not None and cap.origin == "local" and cap.path not in self._local_paths():
|
|
2496
|
+
cap = None
|
|
2483
2497
|
if cap is None:
|
|
2484
2498
|
raise ValueError(f"no such capability: {identifier}")
|
|
2485
2499
|
if cap.origin != "local":
|
|
2486
2500
|
raise ValueError(f"cannot remove inherited capability '{cap.qualified}' "
|
|
2487
|
-
f"(edit it at its source
|
|
2488
|
-
|
|
2501
|
+
f"(edit it at its source; to drop a local override use "
|
|
2502
|
+
f"`tcw capabilities reset`)")
|
|
2503
|
+
d = self.root / cap.path
|
|
2504
|
+
# Refused, never cascaded: `_rm` deletes the whole folder. Asked of the
|
|
2505
|
+
# folder itself rather than `_all_meta_dirs`, which skips dot-directories
|
|
2506
|
+
# and unreadable nodes that `git rm -rf` would delete all the same.
|
|
2507
|
+
nested = sorted(str(m.parent.relative_to(self.root))
|
|
2508
|
+
for m in d.rglob("meta.yaml") if m.parent != d)
|
|
2509
|
+
if nested:
|
|
2510
|
+
raise ValueError(f"cannot remove '{cap.path}': nested under it: "
|
|
2511
|
+
f"{', '.join(nested)} (remove those first)")
|
|
2512
|
+
referrers = self._referrers(d)
|
|
2513
|
+
if referrers:
|
|
2514
|
+
raise ValueError(f"cannot remove '{cap.path}': still referenced by "
|
|
2515
|
+
f"{', '.join(referrers)} (repoint or clear those fields first)")
|
|
2516
|
+
self._rm(d)
|
|
2517
|
+
|
|
2518
|
+
def _referrers(self, target: Path) -> list[str]:
|
|
2519
|
+
"""`<folder> (<field>)` for every other local capability or override whose
|
|
2520
|
+
reference fields resolve to the local capability folder `target`.
|
|
2521
|
+
|
|
2522
|
+
Each field is read exactly as `check` reads it (`_ref_problems`,
|
|
2523
|
+
`_check_globals`), so this refuses on precisely the references `check`
|
|
2524
|
+
would report as dangling afterwards. A hit is compared by folder identity,
|
|
2525
|
+
not spelling: `a/b/`, `x/../a/b` and a differently-cased `A/B` all
|
|
2526
|
+
resolve, and `set` accepts each.
|
|
2527
|
+
"""
|
|
2528
|
+
def tokens(field: str, raw) -> list[str]:
|
|
2529
|
+
if field in ("Superseded by", "Blocked by"):
|
|
2530
|
+
return [str(raw)]
|
|
2531
|
+
ns = "roles" if field == "Roles" else "conditions"
|
|
2532
|
+
toks = raw if isinstance(raw, list) else str(raw).split(",")
|
|
2533
|
+
refs = [str(s).strip().lstrip("!") for s in toks if str(s).strip()]
|
|
2534
|
+
return [r for r in refs if r.startswith(f"{ns}/")]
|
|
2535
|
+
|
|
2536
|
+
def same(a: Path) -> bool:
|
|
2537
|
+
try:
|
|
2538
|
+
return a.samefile(target)
|
|
2539
|
+
# Gone since it was listed or resolved: not the target. Only that —
|
|
2540
|
+
# any other error leaves the question open, so it propagates and
|
|
2541
|
+
# the delete stops.
|
|
2542
|
+
except FileNotFoundError:
|
|
2543
|
+
return False
|
|
2544
|
+
|
|
2545
|
+
out = []
|
|
2546
|
+
for p in self._all_meta_dirs():
|
|
2547
|
+
folder = self.root / p
|
|
2548
|
+
if same(folder):
|
|
2549
|
+
continue
|
|
2550
|
+
meta = load_yaml(folder / "meta.yaml")
|
|
2551
|
+
for field in ("Superseded by", "Blocked by", "Roles", "When"):
|
|
2552
|
+
if field not in meta:
|
|
2553
|
+
continue
|
|
2554
|
+
for ref in tokens(field, meta[field]):
|
|
2555
|
+
try:
|
|
2556
|
+
hit = self.get(ref)
|
|
2557
|
+
except RefError:
|
|
2558
|
+
continue
|
|
2559
|
+
if hit is not None and hit.origin == "local" and same(self.root / hit.path):
|
|
2560
|
+
out.append(f"{p} ({field})")
|
|
2561
|
+
break
|
|
2562
|
+
return out
|
|
2489
2563
|
|
|
2490
2564
|
def reset(self, identifier: str) -> None:
|
|
2491
2565
|
# A standalone local capability is not an override — `remove` deletes it.
|
|
2492
2566
|
if self.get_local(identifier) is not None:
|
|
2493
2567
|
raise ValueError(f"'{identifier}' is a local capability, not an override "
|
|
2494
|
-
f"(use `
|
|
2568
|
+
f"(use `tcw capabilities rm` to delete it)")
|
|
2495
2569
|
cap = self.get(identifier) # federated; may raise AmbiguousRef
|
|
2496
2570
|
if cap is None:
|
|
2497
2571
|
raise ValueError(f"no such capability: {identifier}")
|
|
@@ -127,8 +127,10 @@ def assess(claim_transition: str, *, current_status: str, offered,
|
|
|
127
127
|
exclusivity=NOT_DETERMINED,
|
|
128
128
|
verdict=OK,
|
|
129
129
|
landing_status=landing,
|
|
130
|
-
detail=(f"{matches[0].name!r} leads to {landing!r}.
|
|
131
|
-
f"
|
|
130
|
+
detail=(f"{matches[0].name!r} leads to {landing!r}. A ticket already in "
|
|
131
|
+
f"that status can show a workflow that is not exclusive, but never "
|
|
132
|
+
f"one that is. That is confirmed only when a claim is made, or "
|
|
133
|
+
f"from the workflow definition."),
|
|
132
134
|
)
|
|
133
135
|
|
|
134
136
|
# The claim is not offered. Two very different situations.
|
|
@@ -164,8 +166,9 @@ def assess(claim_transition: str, *, current_status: str, offered,
|
|
|
164
166
|
detail=(f"work.tracker.transitions.claim is {claim_transition!r}, which "
|
|
165
167
|
f"this ticket does not offer. It offers: "
|
|
166
168
|
f"{', '.join(repr(n) for n in offered_names)}. Either the name "
|
|
167
|
-
f"is wrong, or this ticket is
|
|
168
|
-
f"
|
|
169
|
+
f"is wrong, or this ticket is not at the point where it applies — "
|
|
170
|
+
f"it has not reached it yet, or is already past it. One ticket "
|
|
171
|
+
f"cannot tell those apart."),
|
|
169
172
|
)
|
|
170
173
|
|
|
171
174
|
# No transitions at all: a resolved ticket, or one this account cannot move.
|