tcw-cli 1.2.3__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.
Files changed (116) hide show
  1. {tcw_cli-1.2.3/tcw_cli.egg-info → tcw_cli-1.3.0}/PKG-INFO +164 -10
  2. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/README.md +163 -9
  3. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/pyproject.toml +1 -1
  4. tcw_cli-1.3.0/tcw/__init__.py +1 -0
  5. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/cli.py +216 -3
  6. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/__init__.py +7 -0
  7. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/base.py +383 -9
  8. tcw_cli-1.3.0/tcw/store/checkouts.py +88 -0
  9. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/fs.py +916 -159
  10. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/project.py +234 -44
  11. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/validate.py +43 -1
  12. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/cli.py +241 -17
  13. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/hooks.py +42 -8
  14. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/recursion.py +30 -3
  15. {tcw_cli-1.2.3 → tcw_cli-1.3.0/tcw_cli.egg-info}/PKG-INFO +164 -10
  16. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/SOURCES.txt +2 -0
  17. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_federation.py +254 -0
  18. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_epic_completable.py +156 -0
  19. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_policy.py +17 -5
  20. tcw_cli-1.3.0/tests/test_multiproject.py +352 -0
  21. tcw_cli-1.3.0/tests/test_project_registry.py +579 -0
  22. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_qualified_ref.py +18 -0
  23. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_recursion.py +27 -0
  24. tcw_cli-1.3.0/tests/test_retention.py +978 -0
  25. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve.py +36 -0
  26. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_provisioning.py +544 -7
  27. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_publication.py +35 -0
  28. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_taxonomy.py +68 -0
  29. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_validate.py +83 -0
  30. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_validate_target.py +20 -1
  31. tcw_cli-1.2.3/tcw/__init__.py +0 -1
  32. tcw_cli-1.2.3/tests/test_multiproject.py +0 -54
  33. tcw_cli-1.2.3/tests/test_project_registry.py +0 -233
  34. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/LICENSE +0 -0
  35. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/setup.cfg +0 -0
  36. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/capabilities/__init__.py +0 -0
  37. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/capabilities/cli.py +0 -0
  38. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/refs.py +0 -0
  39. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
  40. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
  41. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/index.html +0 -0
  42. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/theme-init.js +0 -0
  43. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/server.cjs +0 -0
  44. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/runtime.py +0 -0
  45. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/stdin.py +0 -0
  46. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/__init__.py +0 -0
  47. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/taxonomy/__init__.py +0 -0
  48. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/taxonomy/cli.py +0 -0
  49. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/__init__.py +0 -0
  50. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/generate.py +0 -0
  51. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/projection.py +0 -0
  52. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/implement.md +0 -0
  53. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/plan.md +0 -0
  54. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/postmortem.md +0 -0
  55. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/request.md +0 -0
  56. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/spec.md +0 -0
  57. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/verify.md +0 -0
  58. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/resolve.py +0 -0
  59. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/templates.py +0 -0
  60. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/dependency_links.txt +0 -0
  61. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/entry_points.txt +0 -0
  62. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/requires.txt +0 -0
  63. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/top_level.txt +0 -0
  64. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_body_prompt.py +0 -0
  65. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities.py +0 -0
  66. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_reset.py +0 -0
  67. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_sidecar.py +0 -0
  68. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capability_ref_wording.py +0 -0
  69. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_cut_version.py +0 -0
  70. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_config.py +0 -0
  71. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_prompt.py +0 -0
  72. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_sync_wiring.py +0 -0
  73. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documented_cli_surface.py +0 -0
  74. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_environment_hardness.py +0 -0
  75. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_external_work_store.py +0 -0
  76. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_falsification_rule.py +0 -0
  77. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_generate_hook.py +0 -0
  78. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_inbox_title.py +0 -0
  79. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_baseline.py +0 -0
  80. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_hooks.py +0 -0
  81. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_inert.py +0 -0
  82. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_validation.py +0 -0
  83. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_non_git_writes.py +0 -0
  84. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_plugin_manifests.py +0 -0
  85. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_projection.py +0 -0
  86. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_prompt_fallback.py +0 -0
  87. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_refs.py +0 -0
  88. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_remote_session_setup.py +0 -0
  89. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_repo_lifecycle.py +0 -0
  90. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_resolve.py +0 -0
  91. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_scaffold.py +0 -0
  92. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_descendants.py +0 -0
  93. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_projection.py +0 -0
  94. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_resolve.py +0 -0
  95. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_runtime.py +0 -0
  96. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_write.py +0 -0
  97. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_session_bootstrap.py +0 -0
  98. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_shipped_prompts.py +0 -0
  99. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_show_json.py +0 -0
  100. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_skill_flow.py +0 -0
  101. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_skill_lifecycle_parity.py +0 -0
  102. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_smoke.py +0 -0
  103. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stage_verb.py +0 -0
  104. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_status_parity.py +0 -0
  105. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stdin.py +0 -0
  106. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stdin_cli.py +0 -0
  107. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_bounds.py +0 -0
  108. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_editor.py +0 -0
  109. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_nodes.py +0 -0
  110. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_subprocess_stdin.py +0 -0
  111. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_tombstone.py +0 -0
  112. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_unpushed_version_script.py +0 -0
  113. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work.py +0 -0
  114. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work_autocommit.py +0 -0
  115. {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work_review.py +0 -0
  116. {tcw_cli-1.2.3 → 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.2.3
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
- Scaffolding `work` also adds `.gitignore` rules for `docs/work/completed/` and
410
- `docs/work/discarded/`, keeping each folder's `.gitkeep` tracked. Resolved items
411
- therefore stay on your disk and in the history that tracked them while they were
412
- live, without piling up in the tree forever. Delete the rules to track resolved
413
- work instead; on a node that predates them, re-run `tcw work init` to add them
414
- and `git rm -r --cached docs/work/completed docs/work/discarded` to drop what git
415
- already tracks.
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, when it is not here yet
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
@@ -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
- Scaffolding `work` also adds `.gitignore` rules for `docs/work/completed/` and
194
- `docs/work/discarded/`, keeping each folder's `.gitkeep` tracked. Resolved items
195
- therefore stay on your disk and in the history that tracked them while they were
196
- live, without piling up in the tree forever. Delete the rules to track resolved
197
- work instead; on a node that predates them, re-run `tcw work init` to add them
198
- and `git rm -r --cached docs/work/completed docs/work/discarded` to drop what git
199
- already tracks.
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, when it is not here yet
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tcw-cli"
7
- version = "1.2.3"
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"