tcw-cli 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/PKG-INFO +22 -5
  2. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/README.md +21 -4
  3. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/pyproject.toml +1 -1
  4. tcw_cli-2.0.1/tcw/__init__.py +1 -0
  5. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/PKG-INFO +22 -5
  6. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_plugin_manifests.py +49 -3
  7. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_skill_lifecycle_parity.py +103 -19
  8. tcw_cli-2.0.0/tcw/__init__.py +0 -1
  9. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/LICENSE +0 -0
  10. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/setup.cfg +0 -0
  11. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/capabilities/__init__.py +0 -0
  12. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/capabilities/cli.py +0 -0
  13. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/cli.py +0 -0
  14. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/refs.py +0 -0
  15. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/__init__.py +0 -0
  16. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
  17. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
  18. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/index.html +0 -0
  19. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/theme-init.js +0 -0
  20. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/server.cjs +0 -0
  21. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/runtime.py +0 -0
  22. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/stdin.py +0 -0
  23. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/__init__.py +0 -0
  24. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/base.py +0 -0
  25. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/checkouts.py +0 -0
  26. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/fs.py +0 -0
  27. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/project.py +0 -0
  28. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/taxonomy/__init__.py +0 -0
  29. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/taxonomy/cli.py +0 -0
  30. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/validate.py +0 -0
  31. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/__init__.py +0 -0
  32. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/cli.py +0 -0
  33. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/generate.py +0 -0
  34. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/hooks.py +0 -0
  35. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/projection.py +0 -0
  36. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/implement.md +0 -0
  37. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/inbox.md +0 -0
  38. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/plan.md +0 -0
  39. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/postmortem.md +0 -0
  40. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/request.md +0 -0
  41. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/spec.md +0 -0
  42. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/verify.md +0 -0
  43. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/recursion.py +0 -0
  44. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/resolve.py +0 -0
  45. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/templates.py +0 -0
  46. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/SOURCES.txt +0 -0
  47. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/dependency_links.txt +0 -0
  48. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/entry_points.txt +0 -0
  49. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/requires.txt +0 -0
  50. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/top_level.txt +0 -0
  51. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_body_prompt.py +0 -0
  52. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities.py +0 -0
  53. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_federation.py +0 -0
  54. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_reset.py +0 -0
  55. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_sidecar.py +0 -0
  56. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capability_ref_wording.py +0 -0
  57. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_cut_version.py +0 -0
  58. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_config.py +0 -0
  59. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_prompt.py +0 -0
  60. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_sync_wiring.py +0 -0
  61. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documented_cli_surface.py +0 -0
  62. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_environment_hardness.py +0 -0
  63. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_epic_completable.py +0 -0
  64. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_external_work_store.py +0 -0
  65. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_falsification_rule.py +0 -0
  66. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_generate_hook.py +0 -0
  67. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_inbox_title.py +0 -0
  68. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_baseline.py +0 -0
  69. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_hooks.py +0 -0
  70. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_inert.py +0 -0
  71. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_policy.py +0 -0
  72. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_validation.py +0 -0
  73. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_multiproject.py +0 -0
  74. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_non_git_writes.py +0 -0
  75. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_project_overrides.py +0 -0
  76. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_project_registry.py +0 -0
  77. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_projection.py +0 -0
  78. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_prompt_fallback.py +0 -0
  79. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_qualified_ref.py +0 -0
  80. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_recursion.py +0 -0
  81. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_refs.py +0 -0
  82. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_remote_session_setup.py +0 -0
  83. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_repo_lifecycle.py +0 -0
  84. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_resolve.py +0 -0
  85. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_retention.py +0 -0
  86. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_scaffold.py +0 -0
  87. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve.py +0 -0
  88. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_descendants.py +0 -0
  89. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_projection.py +0 -0
  90. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_resolve.py +0 -0
  91. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_runtime.py +0 -0
  92. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_write.py +0 -0
  93. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_session_bootstrap.py +0 -0
  94. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_shipped_prompts.py +0 -0
  95. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_show_json.py +0 -0
  96. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_skill_flow.py +0 -0
  97. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_smoke.py +0 -0
  98. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stage_verb.py +0 -0
  99. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_status_parity.py +0 -0
  100. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stdin.py +0 -0
  101. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stdin_cli.py +0 -0
  102. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_bounds.py +0 -0
  103. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_editor.py +0 -0
  104. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_nodes.py +0 -0
  105. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_provisioning.py +0 -0
  106. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_publication.py +0 -0
  107. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_subprocess_stdin.py +0 -0
  108. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_taxonomy.py +0 -0
  109. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_tombstone.py +0 -0
  110. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_unpushed_version_script.py +0 -0
  111. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_validate.py +0 -0
  112. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_validate_target.py +0 -0
  113. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work.py +0 -0
  114. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work_autocommit.py +0 -0
  115. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work_review.py +0 -0
  116. {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work_tags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tcw-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -483,8 +483,10 @@ Every command group also has `--help`, and a `check` that validates its tree.
483
483
 
484
484
  ## Skills — the judgment layer
485
485
 
486
- The CLI is the _mechanism_. Seven skills in [`skills/`](skills/) supply the
487
- _judgment_ that drives it — the parts a deterministic tool cannot decide.
486
+ The CLI is the _mechanism_. Fourteen skills in [`skills/`](skills/) supply the
487
+ _judgment_ that drives it — the parts a deterministic tool cannot decide. Eight
488
+ carry a distinct procedure; the other six all compose one lifecycle stage and
489
+ are listed together at the end.
488
490
 
489
491
  | Skill | What it does |
490
492
  | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
@@ -495,12 +497,27 @@ _judgment_ that drives it — the parts a deterministic tool cannot decide.
495
497
  | [`tcw-report`](skills/tcw-report/SKILL.md) | Reporting a `tcw` bug or suggestion upstream to [this project's issues](https://github.com/brocef/TCW/issues) |
496
498
  | [`tcw-triage-issues`](skills/tcw-triage-issues/SKILL.md) | Sweeps **your** project's GitHub issues and turns the ones worth doing into work items |
497
499
  | [`documentation-sync`](skills/documentation-sync/SKILL.md) | Keeps README, changelogs, release notes, and driving skills moving with the code that changes them |
500
+ | [`tcw-post-mortem`](skills/tcw-post-mortem/SKILL.md) | Finds which lifecycle stage could first have caught a problem, once one has surfaced |
498
501
 
499
502
  They name `tcw` commands and never reimplement tool logic: mechanism stays in the
500
503
  binary, judgment stays in the skills.
501
504
 
502
- Two read-only review agents ship alongside them — `tcw-verifier` and
503
- `tcw-post-mortem` — plus slash commands for each skill's main procedure
505
+ ### Reading a lifecycle stage
506
+
507
+ Six more skills do one job between them: hand you a stage's own working document
508
+ and the instructions your project resolves for it, as a single read rather than a
509
+ file you open and a command you run separately. They only read — `tcw work stage
510
+ gate` is still what refuses.
511
+
512
+ [`tcw-work-stage`](skills/tcw-work-stage/SKILL.md) is the general one and takes
513
+ the stage id, so it reaches all seven stages including `inbox` and `postmortem`.
514
+ The other five bake their stage in and ask only for the work item, which is
515
+ optional: `tcw-work-stage-request`, `tcw-work-stage-spec`,
516
+ `tcw-work-stage-plan`, `tcw-work-stage-implement`, `tcw-work-stage-verify`.
517
+
518
+ Three read-only review agents ship alongside them — `tcw-verifier`,
519
+ `tcw-backlog-auditor`, and `tcw-post-mortem`, which accelerates the skill of the
520
+ same name — plus slash commands for each skill's main procedure
504
521
  (`/tcw-plan-work`, `/tcw-drive-work-to-completion`, `/tcw-verify-work`,
505
522
  `/tcw-process-inbox`, `/tcw-triage-issues`, `/tcw-audit-work-backlog`,
506
523
  `/tcw-taxonomy-init`, `/tcw-capabilities-init`, `/tcw-docs-sync-setup`,
@@ -267,8 +267,10 @@ Every command group also has `--help`, and a `check` that validates its tree.
267
267
 
268
268
  ## Skills — the judgment layer
269
269
 
270
- The CLI is the _mechanism_. Seven skills in [`skills/`](skills/) supply the
271
- _judgment_ that drives it — the parts a deterministic tool cannot decide.
270
+ The CLI is the _mechanism_. Fourteen skills in [`skills/`](skills/) supply the
271
+ _judgment_ that drives it — the parts a deterministic tool cannot decide. Eight
272
+ carry a distinct procedure; the other six all compose one lifecycle stage and
273
+ are listed together at the end.
272
274
 
273
275
  | Skill | What it does |
274
276
  | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
@@ -279,12 +281,27 @@ _judgment_ that drives it — the parts a deterministic tool cannot decide.
279
281
  | [`tcw-report`](skills/tcw-report/SKILL.md) | Reporting a `tcw` bug or suggestion upstream to [this project's issues](https://github.com/brocef/TCW/issues) |
280
282
  | [`tcw-triage-issues`](skills/tcw-triage-issues/SKILL.md) | Sweeps **your** project's GitHub issues and turns the ones worth doing into work items |
281
283
  | [`documentation-sync`](skills/documentation-sync/SKILL.md) | Keeps README, changelogs, release notes, and driving skills moving with the code that changes them |
284
+ | [`tcw-post-mortem`](skills/tcw-post-mortem/SKILL.md) | Finds which lifecycle stage could first have caught a problem, once one has surfaced |
282
285
 
283
286
  They name `tcw` commands and never reimplement tool logic: mechanism stays in the
284
287
  binary, judgment stays in the skills.
285
288
 
286
- Two read-only review agents ship alongside them — `tcw-verifier` and
287
- `tcw-post-mortem` — plus slash commands for each skill's main procedure
289
+ ### Reading a lifecycle stage
290
+
291
+ Six more skills do one job between them: hand you a stage's own working document
292
+ and the instructions your project resolves for it, as a single read rather than a
293
+ file you open and a command you run separately. They only read — `tcw work stage
294
+ gate` is still what refuses.
295
+
296
+ [`tcw-work-stage`](skills/tcw-work-stage/SKILL.md) is the general one and takes
297
+ the stage id, so it reaches all seven stages including `inbox` and `postmortem`.
298
+ The other five bake their stage in and ask only for the work item, which is
299
+ optional: `tcw-work-stage-request`, `tcw-work-stage-spec`,
300
+ `tcw-work-stage-plan`, `tcw-work-stage-implement`, `tcw-work-stage-verify`.
301
+
302
+ Three read-only review agents ship alongside them — `tcw-verifier`,
303
+ `tcw-backlog-auditor`, and `tcw-post-mortem`, which accelerates the skill of the
304
+ same name — plus slash commands for each skill's main procedure
288
305
  (`/tcw-plan-work`, `/tcw-drive-work-to-completion`, `/tcw-verify-work`,
289
306
  `/tcw-process-inbox`, `/tcw-triage-issues`, `/tcw-audit-work-backlog`,
290
307
  `/tcw-taxonomy-init`, `/tcw-capabilities-init`, `/tcw-docs-sync-setup`,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tcw-cli"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1 @@
1
+ __version__ = "2.0.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tcw-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Taxonomy · Capabilities · Work — a storage-abstracted framework for describing and evolving a software project.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -483,8 +483,10 @@ Every command group also has `--help`, and a `check` that validates its tree.
483
483
 
484
484
  ## Skills — the judgment layer
485
485
 
486
- The CLI is the _mechanism_. Seven skills in [`skills/`](skills/) supply the
487
- _judgment_ that drives it — the parts a deterministic tool cannot decide.
486
+ The CLI is the _mechanism_. Fourteen skills in [`skills/`](skills/) supply the
487
+ _judgment_ that drives it — the parts a deterministic tool cannot decide. Eight
488
+ carry a distinct procedure; the other six all compose one lifecycle stage and
489
+ are listed together at the end.
488
490
 
489
491
  | Skill | What it does |
490
492
  | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
@@ -495,12 +497,27 @@ _judgment_ that drives it — the parts a deterministic tool cannot decide.
495
497
  | [`tcw-report`](skills/tcw-report/SKILL.md) | Reporting a `tcw` bug or suggestion upstream to [this project's issues](https://github.com/brocef/TCW/issues) |
496
498
  | [`tcw-triage-issues`](skills/tcw-triage-issues/SKILL.md) | Sweeps **your** project's GitHub issues and turns the ones worth doing into work items |
497
499
  | [`documentation-sync`](skills/documentation-sync/SKILL.md) | Keeps README, changelogs, release notes, and driving skills moving with the code that changes them |
500
+ | [`tcw-post-mortem`](skills/tcw-post-mortem/SKILL.md) | Finds which lifecycle stage could first have caught a problem, once one has surfaced |
498
501
 
499
502
  They name `tcw` commands and never reimplement tool logic: mechanism stays in the
500
503
  binary, judgment stays in the skills.
501
504
 
502
- Two read-only review agents ship alongside them — `tcw-verifier` and
503
- `tcw-post-mortem` — plus slash commands for each skill's main procedure
505
+ ### Reading a lifecycle stage
506
+
507
+ Six more skills do one job between them: hand you a stage's own working document
508
+ and the instructions your project resolves for it, as a single read rather than a
509
+ file you open and a command you run separately. They only read — `tcw work stage
510
+ gate` is still what refuses.
511
+
512
+ [`tcw-work-stage`](skills/tcw-work-stage/SKILL.md) is the general one and takes
513
+ the stage id, so it reaches all seven stages including `inbox` and `postmortem`.
514
+ The other five bake their stage in and ask only for the work item, which is
515
+ optional: `tcw-work-stage-request`, `tcw-work-stage-spec`,
516
+ `tcw-work-stage-plan`, `tcw-work-stage-implement`, `tcw-work-stage-verify`.
517
+
518
+ Three read-only review agents ship alongside them — `tcw-verifier`,
519
+ `tcw-backlog-auditor`, and `tcw-post-mortem`, which accelerates the skill of the
520
+ same name — plus slash commands for each skill's main procedure
504
521
  (`/tcw-plan-work`, `/tcw-drive-work-to-completion`, `/tcw-verify-work`,
505
522
  `/tcw-process-inbox`, `/tcw-triage-issues`, `/tcw-audit-work-backlog`,
506
523
  `/tcw-taxonomy-init`, `/tcw-capabilities-init`, `/tcw-docs-sync-setup`,
@@ -5,8 +5,11 @@ drift is `/tcw-doctor`'s job, not this test's).
5
5
  """
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import subprocess
9
10
  import tomllib
11
+
12
+ import yaml
10
13
  from pathlib import Path
11
14
 
12
15
  import pytest
@@ -65,9 +68,28 @@ def test_claude_agents_key_is_md_files_not_a_directory():
65
68
  NUMBER_WORDS = {
66
69
  1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
67
70
  7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve",
71
+ 13: "thirteen", 14: "fourteen", 15: "fifteen", 16: "sixteen",
72
+ 17: "seventeen", 18: "eighteen", 19: "nineteen", 20: "twenty",
68
73
  }
69
74
 
70
75
 
76
+ def _names_missing_from(blob: str, names) -> list[str]:
77
+ """Which of `names` the description does not mention, matching each as a
78
+ **whole token** rather than as a bare substring.
79
+
80
+ The distinction is load-bearing and was not before the per-stage skills
81
+ shipped: `tcw-work-stage` is a substring of `tcw-work-stage-spec`, so a plain
82
+ `n in blob` finds the generic skill inside every specialised one and reports
83
+ it present when the description never names it. The whole enumeration guard
84
+ for that skill would be dead — its name could be deleted from the
85
+ description with the suite green.
86
+
87
+ The lookahead is the fix: a trailing `-` or word character means the match
88
+ landed inside a longer name, not on the one being checked.
89
+ """
90
+ return [n for n in names if not re.search(re.escape(n) + r"(?![-\w])", blob)]
91
+
92
+
71
93
  def test_the_codex_description_counts_the_skills_it_ships():
72
94
  """The Codex manifest describes the plugin in prose and enumerates every
73
95
  skill by name. Prose does not check itself: it said "eight skills" and named
@@ -86,10 +108,25 @@ def test_the_codex_description_counts_the_skills_it_ships():
86
108
  assert f"{word} skills" in blob, (
87
109
  f"the Codex description does not say '{word} skills' for the "
88
110
  f"{len(names)} that ship: {', '.join(names)}")
89
- missing = [n for n in names if n not in blob]
111
+ missing = _names_missing_from(blob, names)
90
112
  assert not missing, f"shipped but unnamed in the description: {missing}"
91
113
 
92
114
 
115
+ def test_a_shared_name_prefix_cannot_stand_in_for_the_shorter_name():
116
+ """The guard above, guarded. `tcw-work-stage` is a prefix of all five
117
+ `tcw-work-stage-<stage>` skills, so under the substring match this test
118
+ replaces, dropping the generic skill from the description was invisible:
119
+ its name was still found, inside its own specialisations.
120
+
121
+ Asserting on the real description would not catch a revert — it names every
122
+ skill, so both matchers agree on it. This asserts the discrimination
123
+ directly, on a blob that names only the longer skill.
124
+ """
125
+ blob = "ships fourteen skills, among them tcw-work-stage-spec"
126
+ assert _names_missing_from(blob, ["tcw-work-stage-spec"]) == []
127
+ assert _names_missing_from(blob, ["tcw-work-stage"]) == ["tcw-work-stage"]
128
+
129
+
93
130
  @pytest.mark.parametrize("skill", sorted((REPO / "skills").glob("*/SKILL.md")), ids=lambda p: p.parent.name)
94
131
  def test_every_skill_has_name_and_description_frontmatter(skill):
95
132
  """Codex refuses to load a skill whose SKILL.md lacks `---` frontmatter with
@@ -98,8 +135,17 @@ def test_every_skill_has_name_and_description_frontmatter(skill):
98
135
  lines = skill.read_text(encoding="utf-8").splitlines()
99
136
  assert lines and lines[0] == "---", f"{skill} is missing YAML frontmatter"
100
137
  end = lines.index("---", 1)
101
- keys = {ln.split(":", 1)[0] for ln in lines[1:end] if not ln.startswith((" ", "\t"))}
102
- assert {"name", "description"} <= keys, f"{skill} frontmatter lacks name/description"
138
+
139
+ # Parsed as YAML, not scanned line by line. A plain scalar containing ": "
140
+ # is a YAML error, and every one of the five per-stage skills shipped with
141
+ # one on first write — a `when_to_use` reading "runs no gate: `tcw work
142
+ # stage gate` is what refuses". The line-based scan below sees the keys and
143
+ # passes; Codex, which actually parses this, refuses to load the skill. The
144
+ # scan cannot tell those apart, so it is no longer the only check.
145
+ front = yaml.safe_load("\n".join(lines[1:end]))
146
+ assert isinstance(front, dict), f"{skill} frontmatter is not a YAML mapping"
147
+ assert {"name", "description"} <= set(front), (
148
+ f"{skill} frontmatter lacks name/description")
103
149
 
104
150
 
105
151
  def test_hooks_manifest_wires_one_executable_session_start_script():
@@ -300,27 +300,108 @@ def test_the_router_routes_to_every_reference_file():
300
300
  assert not orphans, f"unreachable from SKILL.md: {orphans}"
301
301
 
302
302
 
303
- # ── the composing skill ──────────────────────────────────────────────────────
303
+ # ── the composing skills ─────────────────────────────────────────────────────
304
+ #
305
+ # Six documents compose a stage out of `cat <router>` + `tcw work stage prompt`:
306
+ # the generic `tcw-work-stage`, which takes the stage id as an argument, and one
307
+ # `tcw-work-stage-<stage>` per stage a person actually drives, which bakes it in.
308
+ #
309
+ # Every property below held for one file before the five shipped. Each is now
310
+ # parametrised over all six, because a guard that covers one of six near-identical
311
+ # documents has stopped being a guard.
304
312
 
305
313
  STAGE_SKILL = REPO / "skills/tcw-work-stage/SKILL.md"
306
314
 
307
-
308
- def test_the_composing_skill_reads_a_router_that_exists_for_every_stage():
309
- """`tcw-work-stage` interpolates the stage id into a path. Nothing at
310
- runtime checks that path — a failed `cat` is swallowed by `|| true` so the
311
- rest of the skill still renders, which is the right behaviour and also the
312
- reason a rename would go unnoticed. Resolve the template here instead."""
313
- body = STAGE_SKILL.read_text()
315
+ # Derived from STAGE_IDS, never written out as a second list — a stage added to
316
+ # the lifecycle shows up here rather than being silently skipped. Only the two
317
+ # exclusions are hand-written, and each is excluded for a reason that would have
318
+ # to stop being true before it gets a skill:
319
+ # inbox — runs before an item exists and takes no work item reference on
320
+ # either verb, so a per-stage skill would wrap a zero-argument
321
+ # command and add nothing.
322
+ # postmortem — already has `tcw-post-mortem`, with a read-only agent behind it;
323
+ # a second skill for the stage would compete with it.
324
+ NO_PER_STAGE_SKILL = {"inbox", "postmortem"}
325
+ PER_STAGE_IDS = tuple(s for s in STAGE_IDS if s not in NO_PER_STAGE_SKILL)
326
+ PER_STAGE_SKILLS = {s: REPO / f"skills/tcw-work-stage-{s}/SKILL.md"
327
+ for s in PER_STAGE_IDS}
328
+
329
+ # The generic skill keyed by None: it has no single stage, and every parametrised
330
+ # test below has to say what it does differently for that case anyway.
331
+ COMPOSING_SKILLS = {None: STAGE_SKILL, **PER_STAGE_SKILLS}
332
+
333
+
334
+ def _composing(stage):
335
+ """The path for a stage id, or for the generic skill when `stage` is None."""
336
+ return COMPOSING_SKILLS[stage]
337
+
338
+
339
+ composing = pytest.mark.parametrize(
340
+ "stage", sorted(COMPOSING_SKILLS, key=lambda s: s or ""),
341
+ ids=lambda s: s or "generic")
342
+
343
+
344
+ def test_the_per_stage_skills_are_exactly_the_stages_that_get_one():
345
+ """Both directions. A stage in `PER_STAGE_IDS` with no file is a skill that
346
+ was planned and never written; a `skills/tcw-work-stage-*/` directory with no
347
+ matching stage is one that appeared without the exclusion list being
348
+ revisited — most likely `postmortem`, whose exclusion is a judgement call
349
+ someone will eventually want to reverse. Reversing it should mean editing
350
+ `NO_PER_STAGE_SKILL`, not just adding a folder.
351
+
352
+ The glob discriminates on its own: every per-stage skill carries the
353
+ `tcw-work-stage-` prefix and the generic skill does not match it.
354
+ """
355
+ missing = sorted(s for s, p in PER_STAGE_SKILLS.items() if not p.is_file())
356
+ assert not missing, f"stages with no skill file: {missing}"
357
+ on_disk = {p.parent.name.removeprefix("tcw-work-stage-")
358
+ for p in REPO.glob("skills/tcw-work-stage-*/SKILL.md")}
359
+ assert on_disk == set(PER_STAGE_IDS), (
360
+ f"skills on disk {sorted(on_disk)} disagree with the stages that get one "
361
+ f"{sorted(PER_STAGE_IDS)}; excluded deliberately: {sorted(NO_PER_STAGE_SKILL)}")
362
+
363
+
364
+ @pytest.mark.parametrize("stage", PER_STAGE_IDS)
365
+ def test_a_per_stage_skill_takes_the_item_alone(stage):
366
+ """The whole point of splitting the generic skill. `arguments: [stage, item]`
367
+ made the caller restate a stage they already knew, and forced the item to be
368
+ supplied positionally after it; `arguments: [item]` leaves one argument, and
369
+ an omitted one interpolates to the empty string, which the CLI accepts."""
370
+ front = _composing(stage).read_text().split("---")[1]
371
+ args = next(l for l in front.splitlines() if l.startswith("arguments:"))
372
+ assert args.strip() == "arguments: [item]", args
373
+ assert "$stage" not in _composing(stage).read_text(), (
374
+ f"tcw-work-stage-{stage} still interpolates a stage argument")
375
+
376
+
377
+ @composing
378
+ def test_the_composing_skill_reads_a_router_that_exists(stage):
379
+ """Each of these skills names a router path. Nothing at runtime checks it —
380
+ a failed `cat` is swallowed by `|| true` so the rest of the skill still
381
+ renders, which is the right behaviour and also the reason a rename would go
382
+ unnoticed. Resolve the path here instead.
383
+
384
+ The generic skill's path is a template holding `$stage`, so it is resolved
385
+ against every stage id; a per-stage skill's names one stage literally and is
386
+ checked once. Same property, two shapes.
387
+ """
388
+ body = _composing(stage).read_text()
314
389
  m = re.search(r'cat "\$\{CLAUDE_PLUGIN_ROOT\}/(\S+?)"', body)
315
390
  assert m, "the skill no longer cats a router out of the plugin root"
316
391
  template = m.group(1)
317
- assert "$stage" in template, template
318
- for stage_id in STAGE_IDS:
319
- target = REPO / template.replace("$stage", stage_id)
392
+ if stage is None:
393
+ assert "$stage" in template, template
394
+ targets = {s: REPO / template.replace("$stage", s) for s in STAGE_IDS}
395
+ else:
396
+ assert f"stage-{stage}.md" in template, (
397
+ f"tcw-work-stage-{stage} cats {template}, not its own stage router")
398
+ targets = {stage: REPO / template}
399
+ for stage_id, target in targets.items():
320
400
  assert target.is_file(), f"{stage_id}: {target} does not exist"
321
401
 
322
402
 
323
- def test_the_composing_skill_names_the_gate_in_its_own_prose():
403
+ @composing
404
+ def test_the_composing_skill_names_the_gate_in_its_own_prose(stage):
324
405
  """The whole hazard of composing a stage out of `prompt`: it resolves the
325
406
  instructions without the gate. A skill that stopped naming `gate` would be
326
407
  a documented route around the legality check and the `pre` bindings.
@@ -333,15 +414,17 @@ def test_the_composing_skill_names_the_gate_in_its_own_prose():
333
414
  biting: the entire warning could be deleted with this test still green.
334
415
  Neither change was wrong on its own, which is why nothing caught it.
335
416
  """
336
- body = STAGE_SKILL.read_text()
337
- assert "tcw work stage prompt $stage $item" in body
417
+ slot = "$stage" if stage is None else stage
418
+ body = _composing(stage).read_text()
419
+ assert f"tcw work stage prompt {slot} $item" in body
338
420
  prose = re.sub(r"```.*?```", "", body, flags=re.DOTALL)
339
- assert "tcw work stage gate $stage $item" in prose, (
421
+ assert f"tcw work stage gate {slot} $item" in prose, (
340
422
  "the skill names the gate only inside its manual-fallback fence, so the "
341
423
  "prose warning against routing around the gate can be deleted freely")
342
424
 
343
425
 
344
- def test_every_injected_command_survives_its_own_failure():
426
+ @composing
427
+ def test_every_injected_command_survives_its_own_failure(stage):
345
428
  """The second of two ways this skill renders empty, and the one that shipped
346
429
  unguarded.
347
430
 
@@ -353,7 +436,7 @@ def test_every_injected_command_survives_its_own_failure():
353
436
 
354
437
  The sibling test covers the other cause, an undeclared command.
355
438
  """
356
- body = STAGE_SKILL.read_text()
439
+ body = _composing(stage).read_text()
357
440
  injected = re.findall(r"^!`(.+)`$", body, flags=re.MULTILINE)
358
441
  assert injected, "the skill no longer injects any command"
359
442
  for cmd in injected:
@@ -362,11 +445,12 @@ def test_every_injected_command_survives_its_own_failure():
362
445
  f"non-zero: {cmd}")
363
446
 
364
447
 
365
- def test_the_composing_skill_declares_the_commands_it_injects():
448
+ @composing
449
+ def test_the_composing_skill_declares_the_commands_it_injects(stage):
366
450
  """An injected command that is not pre-approved aborts the whole skill
367
451
  invocation — the model is shown nothing at all, not an error. Both commands
368
452
  have to be in `allowed-tools` or the skill silently renders empty."""
369
- body = STAGE_SKILL.read_text()
453
+ body = _composing(stage).read_text()
370
454
  front = body.split("---")[1]
371
455
  allowed = next(l for l in front.splitlines() if l.startswith("allowed-tools:"))
372
456
  assert "Bash(tcw *)" in allowed and "Bash(cat *)" in allowed, allowed
@@ -1 +0,0 @@
1
- __version__ = "2.0.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes