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.
- {tcw_cli-1.2.3/tcw_cli.egg-info → tcw_cli-1.3.0}/PKG-INFO +164 -10
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/README.md +163 -9
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/pyproject.toml +1 -1
- tcw_cli-1.3.0/tcw/__init__.py +1 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/cli.py +216 -3
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/__init__.py +7 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/base.py +383 -9
- tcw_cli-1.3.0/tcw/store/checkouts.py +88 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/fs.py +916 -159
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/project.py +234 -44
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/validate.py +43 -1
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/cli.py +241 -17
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/hooks.py +42 -8
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/recursion.py +30 -3
- {tcw_cli-1.2.3 → tcw_cli-1.3.0/tcw_cli.egg-info}/PKG-INFO +164 -10
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/SOURCES.txt +2 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_federation.py +254 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_epic_completable.py +156 -0
- {tcw_cli-1.2.3 → 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.3.0/tests/test_project_registry.py +579 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_qualified_ref.py +18 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_recursion.py +27 -0
- tcw_cli-1.3.0/tests/test_retention.py +978 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve.py +36 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_provisioning.py +544 -7
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_publication.py +35 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_taxonomy.py +68 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_validate.py +83 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_validate_target.py +20 -1
- tcw_cli-1.2.3/tcw/__init__.py +0 -1
- tcw_cli-1.2.3/tests/test_multiproject.py +0 -54
- tcw_cli-1.2.3/tests/test_project_registry.py +0 -233
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/LICENSE +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/setup.cfg +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/refs.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/serve/runtime.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/stdin.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/store/__init__.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/__init__.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/generate.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/projection.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/resolve.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw/work/templates.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_body_prompt.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_cut_version.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_config.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_external_work_store.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_generate_hook.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_inbox_title.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_non_git_writes.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_plugin_manifests.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_projection.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_refs.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_resolve.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_scaffold.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_projection.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_serve_write.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_show_json.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_skill_flow.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_skill_lifecycle_parity.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_smoke.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stage_verb.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_status_parity.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stdin.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_bounds.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_editor.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_store_nodes.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_tombstone.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-1.2.3 → tcw_cli-1.3.0}/tests/test_work_review.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
@@ -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"
|