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.
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/PKG-INFO +22 -5
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/README.md +21 -4
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/pyproject.toml +1 -1
- tcw_cli-2.0.1/tcw/__init__.py +1 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/PKG-INFO +22 -5
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_plugin_manifests.py +49 -3
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_skill_lifecycle_parity.py +103 -19
- tcw_cli-2.0.0/tcw/__init__.py +0 -1
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/LICENSE +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/setup.cfg +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/capabilities/__init__.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/capabilities/cli.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/cli.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/refs.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/__init__.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/assets/index-CXNiOIeg.css +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/assets/index-Dz5B7M8G.js +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/index.html +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/client/theme-init.js +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/dist/server.cjs +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/serve/runtime.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/stdin.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/__init__.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/base.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/checkouts.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/fs.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/store/project.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/taxonomy/__init__.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/taxonomy/cli.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/validate.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/__init__.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/cli.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/generate.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/hooks.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/projection.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/implement.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/inbox.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/plan.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/postmortem.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/request.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/spec.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/prompts/verify.md +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/recursion.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/resolve.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw/work/templates.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/SOURCES.txt +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/dependency_links.txt +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/entry_points.txt +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/requires.txt +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tcw_cli.egg-info/top_level.txt +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_body_prompt.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_federation.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_reset.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capabilities_sidecar.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_capability_ref_wording.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_cut_version.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_config.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_prompt.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documentation_sync_wiring.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_documented_cli_surface.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_environment_hardness.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_epic_completable.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_external_work_store.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_falsification_rule.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_generate_hook.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_inbox_title.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_baseline.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_hooks.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_inert.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_policy.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_lifecycle_validation.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_multiproject.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_non_git_writes.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_project_overrides.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_project_registry.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_projection.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_prompt_fallback.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_qualified_ref.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_recursion.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_refs.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_remote_session_setup.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_repo_lifecycle.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_resolve.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_retention.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_scaffold.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_descendants.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_projection.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_resolve.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_runtime.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_serve_write.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_session_bootstrap.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_shipped_prompts.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_show_json.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_skill_flow.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_smoke.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stage_verb.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_status_parity.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stdin.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_stdin_cli.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_bounds.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_editor.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_nodes.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_provisioning.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_store_publication.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_subprocess_stdin.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_taxonomy.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_tombstone.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_unpushed_version_script.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_validate.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_validate_target.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work_autocommit.py +0 -0
- {tcw_cli-2.0.0 → tcw_cli-2.0.1}/tests/test_work_review.py +0 -0
- {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.
|
|
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_.
|
|
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
|
-
|
|
503
|
-
|
|
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_.
|
|
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
|
-
|
|
287
|
-
|
|
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.
|
|
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.
|
|
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_.
|
|
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
|
-
|
|
503
|
-
|
|
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 =
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
tcw_cli-2.0.0/tcw/__init__.py
DELETED
|
@@ -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
|
|
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
|
|
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
|