tcw-cli 1.2.2__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tcw_cli-1.2.2/tcw_cli.egg-info → tcw_cli-1.3.0}/PKG-INFO +199 -10
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/README.md +198 -9
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/pyproject.toml +1 -1
- tcw_cli-1.3.0/tcw/__init__.py +1 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/capabilities/cli.py +35 -8
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/cli.py +216 -3
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/refs.py +29 -4
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/__init__.py +34 -3
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/store/base.py +431 -5
- tcw_cli-1.3.0/tcw/store/checkouts.py +88 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/store/fs.py +1328 -162
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/store/project.py +234 -44
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/validate.py +43 -1
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/cli.py +270 -17
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/hooks.py +42 -8
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/recursion.py +30 -3
- {tcw_cli-1.2.2 → tcw_cli-1.3.0/tcw_cli.egg-info}/PKG-INFO +199 -10
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw_cli.egg-info/SOURCES.txt +3 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_capabilities.py +135 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_capabilities_federation.py +254 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_epic_completable.py +156 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_lifecycle_policy.py +17 -5
- tcw_cli-1.3.0/tests/test_multiproject.py +352 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_non_git_writes.py +1 -0
- tcw_cli-1.3.0/tests/test_project_registry.py +579 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_qualified_ref.py +18 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_recursion.py +27 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_refs.py +62 -0
- tcw_cli-1.3.0/tests/test_retention.py +978 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve.py +36 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve_resolve.py +45 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_stage_verb.py +2 -1
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_store_provisioning.py +544 -7
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_store_publication.py +52 -3
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_taxonomy.py +68 -0
- tcw_cli-1.3.0/tests/test_tombstone.py +528 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_validate.py +145 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_validate_target.py +20 -1
- tcw_cli-1.2.2/tcw/__init__.py +0 -1
- tcw_cli-1.2.2/tests/test_multiproject.py +0 -54
- tcw_cli-1.2.2/tests/test_project_registry.py +0 -233
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/LICENSE +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/setup.cfg +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/serve/runtime.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/stdin.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/store/__init__.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/__init__.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/generate.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/projection.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/resolve.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw/work/templates.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_body_prompt.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_cut_version.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_documentation_config.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_external_work_store.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_generate_hook.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_inbox_title.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_projection.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_resolve.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_scaffold.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve_projection.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_serve_write.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_show_json.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_skill_flow.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_smoke.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_status_parity.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_stdin.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_store_bounds.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_store_editor.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_store_nodes.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_work.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_work_review.py +0 -0
- {tcw_cli-1.2.2 → tcw_cli-1.3.0}/tests/test_work_tags.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tcw-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project.
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -406,13 +406,86 @@ mirror — `tcw taxonomy init`, `tcw capabilities init`, `tcw work init` —
|
|
|
406
406
|
identical to `tcw init --id <project-id> <component>`. Existing configured nodes
|
|
407
407
|
may omit `--id`; legacy ID-less markers use it once to backfill their identity.
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
409
|
+
### What happens to resolved work
|
|
410
|
+
|
|
411
|
+
Three arrangements, and a project picks one per resolved status.
|
|
412
|
+
|
|
413
|
+
**Gitignored** is what scaffolding gives you and what every existing project
|
|
414
|
+
has: `tcw work init` writes `.gitignore` rules for `docs/work/completed/` and
|
|
415
|
+
`docs/work/discarded/`, keeping each folder's `.gitkeep` tracked. A resolved item
|
|
416
|
+
is untracked and left on disk, so it stays with the person who resolved it —
|
|
417
|
+
and, worth knowing, reaches nobody else. A fresh clone has no resolved items at
|
|
418
|
+
all.
|
|
419
|
+
|
|
420
|
+
**Retained** tracks them: delete the rules, and `git rm -r --cached
|
|
421
|
+
docs/work/completed docs/work/discarded` to drop what git already has.
|
|
422
|
+
|
|
423
|
+
**Auto-deleted** removes the folder and keeps the content in history:
|
|
424
|
+
|
|
425
|
+
```yaml
|
|
426
|
+
work:
|
|
427
|
+
retain:
|
|
428
|
+
completed: false # default: true, for both resolved statuses
|
|
429
|
+
discarded: false
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The resolving transition then writes **two commits** — the item lands in its
|
|
433
|
+
resolved folder and is committed, then the folder is removed — and the
|
|
434
|
+
graveyard entry records the first commit, so `tcw work show <slug>` on a
|
|
435
|
+
resolved item reports where its documents can still be fetched from. Nothing is
|
|
436
|
+
deleted unless you ask: the default retains everything, and a malformed
|
|
437
|
+
`retain` reads as the default and is reported by `tcw validate` rather than
|
|
438
|
+
quietly becoming a deletion.
|
|
439
|
+
|
|
440
|
+
**Auto-delete and the ignore rules cannot coexist**, and TCW refuses the
|
|
441
|
+
combination before anything moves. Git untracks rather than moves a path into an
|
|
442
|
+
ignored folder, so the first commit would record a removal and hold no item —
|
|
443
|
+
leaving the record pointing at a commit that never contained anything, and no
|
|
444
|
+
copy anywhere. Removing the rules is a precondition, not a companion change. Once
|
|
445
|
+
a status is named in `retain`, `tcw work init` stops writing rules for it.
|
|
446
|
+
|
|
447
|
+
**Hand the item to your own archive before it goes.** The removal is a bindable
|
|
448
|
+
lifecycle step, `auto-delete`, with `pre` and `post`:
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
work:
|
|
452
|
+
retain:
|
|
453
|
+
completed: false
|
|
454
|
+
lifecycle:
|
|
455
|
+
transitions:
|
|
456
|
+
auto-delete:
|
|
457
|
+
pre:
|
|
458
|
+
- command: tar -czf - -C "$TCW_ITEM_PATH" . |
|
|
459
|
+
aws s3 cp - "s3://my-bucket/$TCW_RESOLUTION/$TCW_SLUG.tgz"
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`pre` runs after the item is committed where it landed and before it is removed,
|
|
463
|
+
so your command sees a complete artifact that is already recorded. Two variables
|
|
464
|
+
join the usual four: `TCW_ITEM_PATH`, the store's own answer for where the item
|
|
465
|
+
is at the moment the hook runs, and `TCW_RESOLUTION`. Both are set on any
|
|
466
|
+
transition that has them — `TCW_ITEM_PATH` on all of them — and omitted rather
|
|
467
|
+
than blank when they do not, so a script can test for presence. **If your command fails, the item is not deleted** — it
|
|
468
|
+
stays resolved, recorded and committed, and `tcw work delete <slug>` finishes the
|
|
469
|
+
removal once you have fixed things. A command that moves the item away itself is
|
|
470
|
+
fine; an already-absent folder counts as removed.
|
|
471
|
+
|
|
472
|
+
Two things this does not promise. TCW cannot tell whether your command really
|
|
473
|
+
archived anything, and a `skill:` binding is reported for your agent to invoke
|
|
474
|
+
rather than run — so anything you need guaranteed belongs in a `command:`.
|
|
475
|
+
`tcw serve` runs no hooks, so an item resolved through the web UI waits for a CLI
|
|
476
|
+
`tcw work delete` rather than being removed without your archive.
|
|
477
|
+
|
|
478
|
+
Adopting auto-delete on an older board wants one more step first: the graveyard
|
|
479
|
+
is what keeps a deleted slug from being reissued, and a board that predates it
|
|
480
|
+
has none. `tcw work tombstone add <slug>` backfills the ones already resolved.
|
|
481
|
+
|
|
482
|
+
A history that gets rewritten takes the content with it. A squash-merge or a
|
|
483
|
+
shallow clone can leave a record whose commit no longer resolves — `tcw work
|
|
484
|
+
show` says so rather than printing a dead pointer, but it cannot get the content
|
|
485
|
+
back. That is the trade auto-delete makes, and it is why the default does not
|
|
486
|
+
make it for you.
|
|
487
|
+
|
|
488
|
+
### Where a component store lives
|
|
416
489
|
|
|
417
490
|
To keep a project's work in another Git repository while preserving its own ID
|
|
418
491
|
and lifecycle configuration, set `work.path` in its `tcw-config.yaml` or pass
|
|
@@ -447,6 +520,9 @@ taxonomy:
|
|
|
447
520
|
path: docs/taxonomy
|
|
448
521
|
```
|
|
449
522
|
|
|
523
|
+
A connected project takes the same block, in the same place its locator goes —
|
|
524
|
+
see [Connected projects](#connected-projects).
|
|
525
|
+
|
|
450
526
|
**A store that is already here always wins.** The declaration is consulted only
|
|
451
527
|
when the local store is absent, so the same config keeps working untouched on a
|
|
452
528
|
machine that has the folder, and answers for one that doesn't. Where the store is
|
|
@@ -455,17 +531,32 @@ instead of reporting that the project has no such component. That last part
|
|
|
455
531
|
matters most for the trees: a checkout that cloned only the code has no
|
|
456
532
|
`docs/taxonomy/` folder, which used to read as "this project has no taxonomy".
|
|
457
533
|
|
|
534
|
+
### Obtaining a declared store or project
|
|
535
|
+
|
|
458
536
|
`tcw provision` is what obtains it:
|
|
459
537
|
|
|
460
538
|
```sh
|
|
461
|
-
tcw provision # every declared store
|
|
539
|
+
tcw provision # every declared store and connected project
|
|
462
540
|
tcw provision --dry-run # print the plan; contact nothing
|
|
463
541
|
tcw provision --refresh # bring an existing copy to the declared version
|
|
464
542
|
tcw provision --component taxonomy
|
|
465
543
|
```
|
|
466
544
|
|
|
467
545
|
Each declared component is obtained on its own, so one bad declaration does not
|
|
468
|
-
suppress another's result.
|
|
546
|
+
suppress another's result. Connected projects are obtained after the components,
|
|
547
|
+
and **transitively**: a project obtained because it was declared may declare
|
|
548
|
+
others, and those are obtained in the same run. That is the one place `tcw`
|
|
549
|
+
contacts a URL you did not write yourself, so every remote is printed before it
|
|
550
|
+
is contacted — the transitive ones included — and `--dry-run` walks the whole
|
|
551
|
+
queue without touching the network, saying plainly that a project it has not
|
|
552
|
+
fetched may declare more. `--component` scopes the component pass only —
|
|
553
|
+
connected projects are still obtained, and every remote is still printed first. A
|
|
554
|
+
project this checkout can already reach is never fetched, however it is declared
|
|
555
|
+
— the same "already here wins" rule the stores follow — so declaring an edge on
|
|
556
|
+
both sides costs nothing. `--refresh` does not override that: it brings a copy
|
|
557
|
+
`tcw` itself provisioned back to the declared version, and a project you resolve
|
|
558
|
+
somewhere else has no such copy to bring anywhere — obtaining one would put a
|
|
559
|
+
second node in the graph under a single ID.
|
|
469
560
|
|
|
470
561
|
If the `repository` block itself is wrong — a missing `url`, a path that escapes
|
|
471
562
|
the repository root, a key that is not one of the four — every command says which
|
|
@@ -568,6 +659,24 @@ connected-projects:
|
|
|
568
659
|
orchestrator: ../orchestrator
|
|
569
660
|
```
|
|
570
661
|
|
|
662
|
+
An entry may also say where the project *comes from*, taking the same
|
|
663
|
+
`repository` block a component store takes (see [Where a component store
|
|
664
|
+
lives](#where-a-component-store-lives)) in the place a locator goes:
|
|
665
|
+
|
|
666
|
+
```yaml
|
|
667
|
+
id: project-a
|
|
668
|
+
connected-projects:
|
|
669
|
+
parent:
|
|
670
|
+
orchestrator:
|
|
671
|
+
path: ../orchestrator # optional; where it is here
|
|
672
|
+
repository:
|
|
673
|
+
url: https://github.com/me/orchestrator.git
|
|
674
|
+
ref: main
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
A bare locator string stays a locator, so nothing already written changes. The
|
|
678
|
+
ladder is the store's, and `tcw provision` is what walks it.
|
|
679
|
+
|
|
571
680
|
Relative locators resolve from the declaring config; absolute locators are also
|
|
572
681
|
allowed. `children` contains direct children only and `parent` has at most one
|
|
573
682
|
entry. TCW derives deeper descendants and ancestors transitively, never by
|
|
@@ -575,6 +684,51 @@ scanning directories to discover a project. `tcw work list --include-descendants
|
|
|
575
684
|
groups registered boards by project ID, and any work command accepts
|
|
576
685
|
`<descendant-project-id>/<slug>`.
|
|
577
686
|
|
|
687
|
+
**A locator is a fact about one machine, and a project that is not on it drops
|
|
688
|
+
out of the graph rather than failing your commands.** A checkout holding only
|
|
689
|
+
some of a graph's repositories — a fresh clone, a cloud session that cloned one
|
|
690
|
+
repo — keeps working: the absent project is simply not in the graph, and
|
|
691
|
+
everything that does not need it behaves normally. `tcw validate` names each
|
|
692
|
+
project it could not reach, every run. A project some other declaration did
|
|
693
|
+
resolve is not listed — in a
|
|
694
|
+
reciprocal graph both sides name every connection, and on a machine holding only
|
|
695
|
+
some of the repositories one of those two is routinely a path that is not here:
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
tcw-config.yaml: connected project 'orchestrator' is declared but not reachable
|
|
699
|
+
in this checkout (/home/you/orchestrator)
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
A command that *does* need the absent project says which one and where it was
|
|
703
|
+
declared, rather than reporting that it was never registered. This is the same
|
|
704
|
+
courtesy a declared store already gets when it has not been provisioned here.
|
|
705
|
+
`tcw work nodes` lists it as a parent or a child that is not in this checkout,
|
|
706
|
+
and `tcw work escalate` and `tcw work delegate` name it instead of calling the
|
|
707
|
+
node a root or a leaf.
|
|
708
|
+
|
|
709
|
+
`tcw validate` also reports a declared locator that does not resolve here for a
|
|
710
|
+
project it *does* have — declared there, found here — without calling it a
|
|
711
|
+
problem. Nothing on disk separates a typo from a path that is simply right for
|
|
712
|
+
another machine, and in a workspace whose repositories sit differently on
|
|
713
|
+
different disks the second is routine, so it states both facts and draws no
|
|
714
|
+
conclusion.
|
|
715
|
+
|
|
716
|
+
**A node need not keep a work store.** A repository root that only groups the
|
|
717
|
+
packages owning the boards is a registered project like any other, and relations
|
|
718
|
+
pass straight through it: an epic two levels up resolves, its slices below one
|
|
719
|
+
are found, and `tcw work escalate` reaches the nearest ancestor that does keep a
|
|
720
|
+
board. `tcw work nodes` says `parent: <id> (no work store)` for such a parent
|
|
721
|
+
rather than calling this node the root, and `(work store not provisioned here)`
|
|
722
|
+
for one whose declared board this machine has not obtained — the same two markers
|
|
723
|
+
it puts on the children lines.
|
|
724
|
+
|
|
725
|
+
Configuration that is genuinely wrong still fails closed, unchanged: an invalid
|
|
726
|
+
or duplicated project ID, a cycle, unparseable YAML, a registered key that
|
|
727
|
+
disagrees with the target it names. The one thing that relaxes with it is
|
|
728
|
+
reciprocity — two nodes that name each other at paths belonging to different
|
|
729
|
+
machines are correctly configured, and only a counterpart that is *present* and
|
|
730
|
+
points somewhere else is a non-reciprocal declaration.
|
|
731
|
+
|
|
578
732
|
Inside a **linked git worktree** a relative locator would otherwise be off by the
|
|
579
733
|
worktree's nesting depth, because it was written against the project's position
|
|
580
734
|
in its primary checkout. TCW re-anchors it against that project's own counterpart
|
|
@@ -947,6 +1101,41 @@ Blocked-ness is a **derived overlay**: an item is blocked when it has at least
|
|
|
947
1101
|
one unresolved blocker recorded in its data — there is no separate "blocked"
|
|
948
1102
|
folder or status.
|
|
949
1103
|
|
|
1104
|
+
### References to work that is finished
|
|
1105
|
+
|
|
1106
|
+
Resolving an item takes its documents out of the tracked tree, so a
|
|
1107
|
+
`tcw://W/<slug>` link to it would have nothing to resolve against. Completing
|
|
1108
|
+
and discarding therefore **record the slug**, in `graveyard.yaml` beside the
|
|
1109
|
+
status folders, and the record rides the same commit as the status change. A
|
|
1110
|
+
reference to finished work then keeps resolving: `tcw validate` says nothing
|
|
1111
|
+
about it, and `tcw serve` shows it inert rather than broken. A reference to a
|
|
1112
|
+
slug the project never held is still an error, in the same words as before —
|
|
1113
|
+
that distinction is the whole point of the record.
|
|
1114
|
+
|
|
1115
|
+
The record says the slug existed and how it was resolved. It deliberately does
|
|
1116
|
+
**not** say where the documents went: any such pointer stops working the moment
|
|
1117
|
+
history is squashed, rebased, or shallowly cloned, and a pointer that quietly
|
|
1118
|
+
breaks is worse than none. How long resolved documents are kept stays your
|
|
1119
|
+
call — `completed/` and `discarded/` are gitignored by default, and a project
|
|
1120
|
+
that wants them in the tracked tree simply does not ignore them.
|
|
1121
|
+
|
|
1122
|
+
For work resolved **before** your project kept these records — including
|
|
1123
|
+
everything resolved before this feature existed — record a slug by hand:
|
|
1124
|
+
|
|
1125
|
+
```bash
|
|
1126
|
+
tcw work tombstone add <slug> --resolution done --resolved 2026-09-01
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
Both flags are optional; omit them when nobody kept the detail, since the
|
|
1130
|
+
record's job is to say the slug existed.
|
|
1131
|
+
|
|
1132
|
+
Run it wherever you are — including on the machine that resolved the work, where
|
|
1133
|
+
the item's folder is usually still sitting on disk. It refuses only a slug that
|
|
1134
|
+
is *live*, and a slug already in the graveyard, so re-running it over a list is
|
|
1135
|
+
safe and will not quietly replace a good record with a blank one. It commits
|
|
1136
|
+
what it writes, and on a store with a configured remote it publishes too, since
|
|
1137
|
+
a record nobody else can see does not do its job.
|
|
1138
|
+
|
|
950
1139
|
### Binding your own skills and commands to the lifecycle
|
|
951
1140
|
|
|
952
1141
|
The lifecycle has named **stages** (each producing one document) and named
|
|
@@ -190,13 +190,86 @@ mirror — `tcw taxonomy init`, `tcw capabilities init`, `tcw work init` —
|
|
|
190
190
|
identical to `tcw init --id <project-id> <component>`. Existing configured nodes
|
|
191
191
|
may omit `--id`; legacy ID-less markers use it once to backfill their identity.
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
### What happens to resolved work
|
|
194
|
+
|
|
195
|
+
Three arrangements, and a project picks one per resolved status.
|
|
196
|
+
|
|
197
|
+
**Gitignored** is what scaffolding gives you and what every existing project
|
|
198
|
+
has: `tcw work init` writes `.gitignore` rules for `docs/work/completed/` and
|
|
199
|
+
`docs/work/discarded/`, keeping each folder's `.gitkeep` tracked. A resolved item
|
|
200
|
+
is untracked and left on disk, so it stays with the person who resolved it —
|
|
201
|
+
and, worth knowing, reaches nobody else. A fresh clone has no resolved items at
|
|
202
|
+
all.
|
|
203
|
+
|
|
204
|
+
**Retained** tracks them: delete the rules, and `git rm -r --cached
|
|
205
|
+
docs/work/completed docs/work/discarded` to drop what git already has.
|
|
206
|
+
|
|
207
|
+
**Auto-deleted** removes the folder and keeps the content in history:
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
work:
|
|
211
|
+
retain:
|
|
212
|
+
completed: false # default: true, for both resolved statuses
|
|
213
|
+
discarded: false
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The resolving transition then writes **two commits** — the item lands in its
|
|
217
|
+
resolved folder and is committed, then the folder is removed — and the
|
|
218
|
+
graveyard entry records the first commit, so `tcw work show <slug>` on a
|
|
219
|
+
resolved item reports where its documents can still be fetched from. Nothing is
|
|
220
|
+
deleted unless you ask: the default retains everything, and a malformed
|
|
221
|
+
`retain` reads as the default and is reported by `tcw validate` rather than
|
|
222
|
+
quietly becoming a deletion.
|
|
223
|
+
|
|
224
|
+
**Auto-delete and the ignore rules cannot coexist**, and TCW refuses the
|
|
225
|
+
combination before anything moves. Git untracks rather than moves a path into an
|
|
226
|
+
ignored folder, so the first commit would record a removal and hold no item —
|
|
227
|
+
leaving the record pointing at a commit that never contained anything, and no
|
|
228
|
+
copy anywhere. Removing the rules is a precondition, not a companion change. Once
|
|
229
|
+
a status is named in `retain`, `tcw work init` stops writing rules for it.
|
|
230
|
+
|
|
231
|
+
**Hand the item to your own archive before it goes.** The removal is a bindable
|
|
232
|
+
lifecycle step, `auto-delete`, with `pre` and `post`:
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
work:
|
|
236
|
+
retain:
|
|
237
|
+
completed: false
|
|
238
|
+
lifecycle:
|
|
239
|
+
transitions:
|
|
240
|
+
auto-delete:
|
|
241
|
+
pre:
|
|
242
|
+
- command: tar -czf - -C "$TCW_ITEM_PATH" . |
|
|
243
|
+
aws s3 cp - "s3://my-bucket/$TCW_RESOLUTION/$TCW_SLUG.tgz"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`pre` runs after the item is committed where it landed and before it is removed,
|
|
247
|
+
so your command sees a complete artifact that is already recorded. Two variables
|
|
248
|
+
join the usual four: `TCW_ITEM_PATH`, the store's own answer for where the item
|
|
249
|
+
is at the moment the hook runs, and `TCW_RESOLUTION`. Both are set on any
|
|
250
|
+
transition that has them — `TCW_ITEM_PATH` on all of them — and omitted rather
|
|
251
|
+
than blank when they do not, so a script can test for presence. **If your command fails, the item is not deleted** — it
|
|
252
|
+
stays resolved, recorded and committed, and `tcw work delete <slug>` finishes the
|
|
253
|
+
removal once you have fixed things. A command that moves the item away itself is
|
|
254
|
+
fine; an already-absent folder counts as removed.
|
|
255
|
+
|
|
256
|
+
Two things this does not promise. TCW cannot tell whether your command really
|
|
257
|
+
archived anything, and a `skill:` binding is reported for your agent to invoke
|
|
258
|
+
rather than run — so anything you need guaranteed belongs in a `command:`.
|
|
259
|
+
`tcw serve` runs no hooks, so an item resolved through the web UI waits for a CLI
|
|
260
|
+
`tcw work delete` rather than being removed without your archive.
|
|
261
|
+
|
|
262
|
+
Adopting auto-delete on an older board wants one more step first: the graveyard
|
|
263
|
+
is what keeps a deleted slug from being reissued, and a board that predates it
|
|
264
|
+
has none. `tcw work tombstone add <slug>` backfills the ones already resolved.
|
|
265
|
+
|
|
266
|
+
A history that gets rewritten takes the content with it. A squash-merge or a
|
|
267
|
+
shallow clone can leave a record whose commit no longer resolves — `tcw work
|
|
268
|
+
show` says so rather than printing a dead pointer, but it cannot get the content
|
|
269
|
+
back. That is the trade auto-delete makes, and it is why the default does not
|
|
270
|
+
make it for you.
|
|
271
|
+
|
|
272
|
+
### Where a component store lives
|
|
200
273
|
|
|
201
274
|
To keep a project's work in another Git repository while preserving its own ID
|
|
202
275
|
and lifecycle configuration, set `work.path` in its `tcw-config.yaml` or pass
|
|
@@ -231,6 +304,9 @@ taxonomy:
|
|
|
231
304
|
path: docs/taxonomy
|
|
232
305
|
```
|
|
233
306
|
|
|
307
|
+
A connected project takes the same block, in the same place its locator goes —
|
|
308
|
+
see [Connected projects](#connected-projects).
|
|
309
|
+
|
|
234
310
|
**A store that is already here always wins.** The declaration is consulted only
|
|
235
311
|
when the local store is absent, so the same config keeps working untouched on a
|
|
236
312
|
machine that has the folder, and answers for one that doesn't. Where the store is
|
|
@@ -239,17 +315,32 @@ instead of reporting that the project has no such component. That last part
|
|
|
239
315
|
matters most for the trees: a checkout that cloned only the code has no
|
|
240
316
|
`docs/taxonomy/` folder, which used to read as "this project has no taxonomy".
|
|
241
317
|
|
|
318
|
+
### Obtaining a declared store or project
|
|
319
|
+
|
|
242
320
|
`tcw provision` is what obtains it:
|
|
243
321
|
|
|
244
322
|
```sh
|
|
245
|
-
tcw provision # every declared store
|
|
323
|
+
tcw provision # every declared store and connected project
|
|
246
324
|
tcw provision --dry-run # print the plan; contact nothing
|
|
247
325
|
tcw provision --refresh # bring an existing copy to the declared version
|
|
248
326
|
tcw provision --component taxonomy
|
|
249
327
|
```
|
|
250
328
|
|
|
251
329
|
Each declared component is obtained on its own, so one bad declaration does not
|
|
252
|
-
suppress another's result.
|
|
330
|
+
suppress another's result. Connected projects are obtained after the components,
|
|
331
|
+
and **transitively**: a project obtained because it was declared may declare
|
|
332
|
+
others, and those are obtained in the same run. That is the one place `tcw`
|
|
333
|
+
contacts a URL you did not write yourself, so every remote is printed before it
|
|
334
|
+
is contacted — the transitive ones included — and `--dry-run` walks the whole
|
|
335
|
+
queue without touching the network, saying plainly that a project it has not
|
|
336
|
+
fetched may declare more. `--component` scopes the component pass only —
|
|
337
|
+
connected projects are still obtained, and every remote is still printed first. A
|
|
338
|
+
project this checkout can already reach is never fetched, however it is declared
|
|
339
|
+
— the same "already here wins" rule the stores follow — so declaring an edge on
|
|
340
|
+
both sides costs nothing. `--refresh` does not override that: it brings a copy
|
|
341
|
+
`tcw` itself provisioned back to the declared version, and a project you resolve
|
|
342
|
+
somewhere else has no such copy to bring anywhere — obtaining one would put a
|
|
343
|
+
second node in the graph under a single ID.
|
|
253
344
|
|
|
254
345
|
If the `repository` block itself is wrong — a missing `url`, a path that escapes
|
|
255
346
|
the repository root, a key that is not one of the four — every command says which
|
|
@@ -352,6 +443,24 @@ connected-projects:
|
|
|
352
443
|
orchestrator: ../orchestrator
|
|
353
444
|
```
|
|
354
445
|
|
|
446
|
+
An entry may also say where the project *comes from*, taking the same
|
|
447
|
+
`repository` block a component store takes (see [Where a component store
|
|
448
|
+
lives](#where-a-component-store-lives)) in the place a locator goes:
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
id: project-a
|
|
452
|
+
connected-projects:
|
|
453
|
+
parent:
|
|
454
|
+
orchestrator:
|
|
455
|
+
path: ../orchestrator # optional; where it is here
|
|
456
|
+
repository:
|
|
457
|
+
url: https://github.com/me/orchestrator.git
|
|
458
|
+
ref: main
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
A bare locator string stays a locator, so nothing already written changes. The
|
|
462
|
+
ladder is the store's, and `tcw provision` is what walks it.
|
|
463
|
+
|
|
355
464
|
Relative locators resolve from the declaring config; absolute locators are also
|
|
356
465
|
allowed. `children` contains direct children only and `parent` has at most one
|
|
357
466
|
entry. TCW derives deeper descendants and ancestors transitively, never by
|
|
@@ -359,6 +468,51 @@ scanning directories to discover a project. `tcw work list --include-descendants
|
|
|
359
468
|
groups registered boards by project ID, and any work command accepts
|
|
360
469
|
`<descendant-project-id>/<slug>`.
|
|
361
470
|
|
|
471
|
+
**A locator is a fact about one machine, and a project that is not on it drops
|
|
472
|
+
out of the graph rather than failing your commands.** A checkout holding only
|
|
473
|
+
some of a graph's repositories — a fresh clone, a cloud session that cloned one
|
|
474
|
+
repo — keeps working: the absent project is simply not in the graph, and
|
|
475
|
+
everything that does not need it behaves normally. `tcw validate` names each
|
|
476
|
+
project it could not reach, every run. A project some other declaration did
|
|
477
|
+
resolve is not listed — in a
|
|
478
|
+
reciprocal graph both sides name every connection, and on a machine holding only
|
|
479
|
+
some of the repositories one of those two is routinely a path that is not here:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
tcw-config.yaml: connected project 'orchestrator' is declared but not reachable
|
|
483
|
+
in this checkout (/home/you/orchestrator)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
A command that *does* need the absent project says which one and where it was
|
|
487
|
+
declared, rather than reporting that it was never registered. This is the same
|
|
488
|
+
courtesy a declared store already gets when it has not been provisioned here.
|
|
489
|
+
`tcw work nodes` lists it as a parent or a child that is not in this checkout,
|
|
490
|
+
and `tcw work escalate` and `tcw work delegate` name it instead of calling the
|
|
491
|
+
node a root or a leaf.
|
|
492
|
+
|
|
493
|
+
`tcw validate` also reports a declared locator that does not resolve here for a
|
|
494
|
+
project it *does* have — declared there, found here — without calling it a
|
|
495
|
+
problem. Nothing on disk separates a typo from a path that is simply right for
|
|
496
|
+
another machine, and in a workspace whose repositories sit differently on
|
|
497
|
+
different disks the second is routine, so it states both facts and draws no
|
|
498
|
+
conclusion.
|
|
499
|
+
|
|
500
|
+
**A node need not keep a work store.** A repository root that only groups the
|
|
501
|
+
packages owning the boards is a registered project like any other, and relations
|
|
502
|
+
pass straight through it: an epic two levels up resolves, its slices below one
|
|
503
|
+
are found, and `tcw work escalate` reaches the nearest ancestor that does keep a
|
|
504
|
+
board. `tcw work nodes` says `parent: <id> (no work store)` for such a parent
|
|
505
|
+
rather than calling this node the root, and `(work store not provisioned here)`
|
|
506
|
+
for one whose declared board this machine has not obtained — the same two markers
|
|
507
|
+
it puts on the children lines.
|
|
508
|
+
|
|
509
|
+
Configuration that is genuinely wrong still fails closed, unchanged: an invalid
|
|
510
|
+
or duplicated project ID, a cycle, unparseable YAML, a registered key that
|
|
511
|
+
disagrees with the target it names. The one thing that relaxes with it is
|
|
512
|
+
reciprocity — two nodes that name each other at paths belonging to different
|
|
513
|
+
machines are correctly configured, and only a counterpart that is *present* and
|
|
514
|
+
points somewhere else is a non-reciprocal declaration.
|
|
515
|
+
|
|
362
516
|
Inside a **linked git worktree** a relative locator would otherwise be off by the
|
|
363
517
|
worktree's nesting depth, because it was written against the project's position
|
|
364
518
|
in its primary checkout. TCW re-anchors it against that project's own counterpart
|
|
@@ -731,6 +885,41 @@ Blocked-ness is a **derived overlay**: an item is blocked when it has at least
|
|
|
731
885
|
one unresolved blocker recorded in its data — there is no separate "blocked"
|
|
732
886
|
folder or status.
|
|
733
887
|
|
|
888
|
+
### References to work that is finished
|
|
889
|
+
|
|
890
|
+
Resolving an item takes its documents out of the tracked tree, so a
|
|
891
|
+
`tcw://W/<slug>` link to it would have nothing to resolve against. Completing
|
|
892
|
+
and discarding therefore **record the slug**, in `graveyard.yaml` beside the
|
|
893
|
+
status folders, and the record rides the same commit as the status change. A
|
|
894
|
+
reference to finished work then keeps resolving: `tcw validate` says nothing
|
|
895
|
+
about it, and `tcw serve` shows it inert rather than broken. A reference to a
|
|
896
|
+
slug the project never held is still an error, in the same words as before —
|
|
897
|
+
that distinction is the whole point of the record.
|
|
898
|
+
|
|
899
|
+
The record says the slug existed and how it was resolved. It deliberately does
|
|
900
|
+
**not** say where the documents went: any such pointer stops working the moment
|
|
901
|
+
history is squashed, rebased, or shallowly cloned, and a pointer that quietly
|
|
902
|
+
breaks is worse than none. How long resolved documents are kept stays your
|
|
903
|
+
call — `completed/` and `discarded/` are gitignored by default, and a project
|
|
904
|
+
that wants them in the tracked tree simply does not ignore them.
|
|
905
|
+
|
|
906
|
+
For work resolved **before** your project kept these records — including
|
|
907
|
+
everything resolved before this feature existed — record a slug by hand:
|
|
908
|
+
|
|
909
|
+
```bash
|
|
910
|
+
tcw work tombstone add <slug> --resolution done --resolved 2026-09-01
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
Both flags are optional; omit them when nobody kept the detail, since the
|
|
914
|
+
record's job is to say the slug existed.
|
|
915
|
+
|
|
916
|
+
Run it wherever you are — including on the machine that resolved the work, where
|
|
917
|
+
the item's folder is usually still sitting on disk. It refuses only a slug that
|
|
918
|
+
is *live*, and a slug already in the graveyard, so re-running it over a list is
|
|
919
|
+
safe and will not quietly replace a good record with a blank one. It commits
|
|
920
|
+
what it writes, and on a store with a configured remote it publishes too, since
|
|
921
|
+
a record nobody else can see does not do its job.
|
|
922
|
+
|
|
734
923
|
### Binding your own skills and commands to the lifecycle
|
|
735
924
|
|
|
736
925
|
The lifecycle has named **stages** (each producing one document) and named
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tcw-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.0"
|
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
from tcw.stdin import read_piped_stdin
|
|
7
|
-
from tcw.store.base import Capability, RefError
|
|
7
|
+
from tcw.store.base import Capability, RefError, resolution_status
|
|
8
8
|
from tcw.store.base import AmbiguousRef
|
|
9
9
|
from tcw.store.fs import FsCapabilitiesStore, find_node, git_root
|
|
10
10
|
|
|
@@ -184,9 +184,15 @@ def _drift(args: argparse.Namespace) -> int:
|
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
def _shipped_but_missing(node, st) -> list[tuple[str, str]]:
|
|
187
|
-
"""Local Missing capabilities whose `Planning doc` names
|
|
187
|
+
"""Local Missing capabilities whose `Planning doc` names work that shipped.
|
|
188
|
+
|
|
188
189
|
Read-only follow of an existing capability→work forward pointer; degrades to
|
|
189
|
-
empty when no work node is present (no hard cross-axis dependency).
|
|
190
|
+
empty when no work node is present (no hard cross-axis dependency).
|
|
191
|
+
|
|
192
|
+
"Shipped" is answered from the live item where there is one and from its
|
|
193
|
+
tombstone where there is not, so the verdict is a property of the project
|
|
194
|
+
rather than of whose working directory it runs in.
|
|
195
|
+
"""
|
|
190
196
|
from tcw.store.fs import FsWorkStore
|
|
191
197
|
try:
|
|
192
198
|
work = FsWorkStore.open(node) # the *configured* store, wherever it is
|
|
@@ -199,16 +205,37 @@ def _shipped_but_missing(node, st) -> list[tuple[str, str]]:
|
|
|
199
205
|
slug = c.fields.get("Planning doc")
|
|
200
206
|
if not slug:
|
|
201
207
|
continue
|
|
202
|
-
try:
|
|
203
|
-
item = work.get(str(slug))
|
|
204
|
-
except Exception:
|
|
205
|
-
item = None
|
|
206
208
|
# `completed` alone, deliberately NOT `RESOLVED_STATUSES`: this asks
|
|
207
209
|
# "did it ship?", not "is it closed?". A discarded item's capability is
|
|
208
210
|
# *supposed* to stay Missing (or be marked Omitted) — reporting it as
|
|
209
211
|
# shipped-but-unreconciled would be a false positive, which is exactly
|
|
210
212
|
# what happened before `discarded` existed.
|
|
211
|
-
|
|
213
|
+
#
|
|
214
|
+
# Asked of the tombstone as well as the item, because `get()` alone made
|
|
215
|
+
# the answer depend on who was asking: a resolved item's folder is
|
|
216
|
+
# gitignored, so it is present for whoever ran the transition and reaches
|
|
217
|
+
# no other clone. `get()` answered there and returned None everywhere
|
|
218
|
+
# else, so this reported drift on one machine and "no capability drift"
|
|
219
|
+
# in CI — failing in the quiet direction, which is the one nobody
|
|
220
|
+
# notices. `resolution_status` maps the record onto the same status
|
|
221
|
+
# `complete()` derives, so the ship/abandon distinction above is drawn
|
|
222
|
+
# once rather than twice.
|
|
223
|
+
try:
|
|
224
|
+
item = work.get(str(slug))
|
|
225
|
+
if item is not None:
|
|
226
|
+
shipped = item.status == "completed"
|
|
227
|
+
else:
|
|
228
|
+
grave = work.tombstone(str(slug))
|
|
229
|
+
# No record: the slug names nothing this store ever held, which
|
|
230
|
+
# is a typo, not finished work. An unrecorded resolution: the
|
|
231
|
+
# store held it but nobody kept how it ended — report nothing
|
|
232
|
+
# rather than guess, because guessing wrong here means calling
|
|
233
|
+
# abandoned work shipped.
|
|
234
|
+
shipped = bool(grave and grave.resolution
|
|
235
|
+
and resolution_status(grave.resolution) == "completed")
|
|
236
|
+
except Exception:
|
|
237
|
+
shipped = False # a store that cannot answer is silent
|
|
238
|
+
if shipped:
|
|
212
239
|
out.append((c.path, str(slug)))
|
|
213
240
|
return out
|
|
214
241
|
|