deepcell-cli 0.6.1__tar.gz → 0.6.2__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 (109) hide show
  1. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/PKG-INFO +6 -4
  2. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/README.md +5 -3
  3. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/pyproject.toml +1 -1
  4. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/__init__.py +1 -1
  5. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/capabilities.py +21 -5
  6. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/capability-contract.json +18 -18
  7. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/help_cmd.py +23 -0
  8. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/sync.py +6 -62
  9. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/version.py +73 -2
  10. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/main.py +25 -4
  11. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/mcp_server.py +7 -5
  12. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/stages.py +4 -1
  13. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/surface.py +17 -6
  14. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/upgrade_check.py +1 -1
  15. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/PKG-INFO +6 -4
  16. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_mcp_server.py +31 -2
  17. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_stages.py +25 -6
  18. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_upgrade_check.py +10 -10
  19. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/setup.cfg +0 -0
  20. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/__main__.py +0 -0
  21. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/_findings.py +0 -0
  22. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/client.py +0 -0
  23. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/__init__.py +0 -0
  24. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_batch_input.py +0 -0
  25. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_datatypes.py +0 -0
  26. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_negative_args.py +0 -0
  27. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_swapped_args.py +0 -0
  28. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_version_display.py +0 -0
  29. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/_write_opts.py +0 -0
  30. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/account.py +0 -0
  31. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/auth.py +0 -0
  32. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/changes.py +0 -0
  33. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/deck.py +0 -0
  34. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/defs.py +0 -0
  35. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/describe.py +0 -0
  36. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/doc.py +0 -0
  37. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/doctor.py +0 -0
  38. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/download.py +0 -0
  39. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/edit.py +0 -0
  40. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/example.py +0 -0
  41. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export.py +0 -0
  42. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_docx.py +0 -0
  43. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_pdf.py +0 -0
  44. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/export_pptx.py +0 -0
  45. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/files.py +0 -0
  46. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/grep.py +0 -0
  47. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/guide.py +0 -0
  48. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/impact.py +0 -0
  49. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/import_cmd.py +0 -0
  50. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/ingest.py +0 -0
  51. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/merge.py +0 -0
  52. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/query.py +0 -0
  53. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/reasoning.py +0 -0
  54. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/ref.py +0 -0
  55. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/replace.py +0 -0
  56. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/rules.py +0 -0
  57. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/share.py +0 -0
  58. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/upgrade.py +0 -0
  59. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/variant.py +0 -0
  60. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/viewer.py +0 -0
  61. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/commands/workspace.py +0 -0
  62. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/config.py +0 -0
  63. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/context.py +0 -0
  64. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/errors.py +0 -0
  65. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/logging_setup.py +0 -0
  66. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/oauth_provider.py +0 -0
  67. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/output.py +0 -0
  68. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/revision.py +0 -0
  69. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/sync_state.py +0 -0
  70. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli/xml_replace.py +0 -0
  71. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/SOURCES.txt +0 -0
  72. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/dependency_links.txt +0 -0
  73. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/entry_points.txt +0 -0
  74. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/requires.txt +0 -0
  75. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/src/deepcell_cli.egg-info/top_level.txt +0 -0
  76. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_anon.py +0 -0
  77. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_claim_strength.py +0 -0
  78. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_client.py +0 -0
  79. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_client_surface_header.py +0 -0
  80. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_config.py +0 -0
  81. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_conflict_reason.py +0 -0
  82. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_deck_help_contract.py +0 -0
  83. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_deck_op_shape.py +0 -0
  84. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_docs_catalog.py +0 -0
  85. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_doctor.py +0 -0
  86. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_download.py +0 -0
  87. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_findings_provenance.py +0 -0
  88. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_guide_eval_a4_a7.py +0 -0
  89. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_guide_flag_drift.py +0 -0
  90. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_help_flag_drift.py +0 -0
  91. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_impact_commands.py +0 -0
  92. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_ingest_command.py +0 -0
  93. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_logging_setup.py +0 -0
  94. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_mcp_auth.py +0 -0
  95. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_op_reachability.py +0 -0
  96. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_openapi_route_parity.py +0 -0
  97. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_output_export_notes.py +0 -0
  98. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_output_keeps_the_answer.py +0 -0
  99. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_reasoning_commands.py +0 -0
  100. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_reasoning_diff_command.py +0 -0
  101. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_ref_flag_drift.py +0 -0
  102. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_round6_signal_regressions.py +0 -0
  103. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_search_reach.py +0 -0
  104. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_shed_retry.py +0 -0
  105. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_surface_examples.py +0 -0
  106. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_surface_walker.py +0 -0
  107. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_version_parity.py +0 -0
  108. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_write_message_rule.py +0 -0
  109. {deepcell_cli-0.6.1 → deepcell_cli-0.6.2}/tests/test_xml_replace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepcell-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them
5
5
  Author-email: DeepCell <hello@deepcell.net>
6
6
  License-Expression: MIT
@@ -166,8 +166,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
166
166
  | Document surface | `doc` — outline, blocks, body, links, lint |
167
167
  | Deck surface | `deck` — slides, ordering, binding, branding |
168
168
  | Change review | `changes list / diff / revert`, `impact` |
169
- | Version control | `log`, `diff`, `restore` |
170
- | Sync | `clone`, `status`, `pull`, `push`, `commit`, `merge` |
169
+ | Version control | `log`, `diff`, `restore`, `commit` |
170
+ | Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
171
171
  | Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
172
172
  | Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
173
173
  | Delivery | `viewer`, `share create / list / revoke` |
@@ -218,7 +218,9 @@ deepcell push -m "Updated assumptions" # upload
218
218
  deepcell pull # fetch cloud changes
219
219
  ```
220
220
 
221
- A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell guide revise/history`.
221
+ A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
222
+
223
+ Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
222
224
 
223
225
  ## Import & export
224
226
 
@@ -134,8 +134,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
134
134
  | Document surface | `doc` — outline, blocks, body, links, lint |
135
135
  | Deck surface | `deck` — slides, ordering, binding, branding |
136
136
  | Change review | `changes list / diff / revert`, `impact` |
137
- | Version control | `log`, `diff`, `restore` |
138
- | Sync | `clone`, `status`, `pull`, `push`, `commit`, `merge` |
137
+ | Version control | `log`, `diff`, `restore`, `commit` |
138
+ | Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
139
139
  | Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
140
140
  | Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
141
141
  | Delivery | `viewer`, `share create / list / revoke` |
@@ -186,7 +186,9 @@ deepcell push -m "Updated assumptions" # upload
186
186
  deepcell pull # fetch cloud changes
187
187
  ```
188
188
 
189
- A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell guide revise/history`.
189
+ A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
190
+
191
+ Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
190
192
 
191
193
  ## Import & export
192
194
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepcell-cli"
3
- version = "0.6.1"
3
+ version = "0.6.2"
4
4
  description = "Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -9,4 +9,4 @@ same thing, and a third is stamped into ``docs/cli-surface.json`` by
9
9
  reports a version it is not, and skipping the regen lands a red build.
10
10
  """
11
11
 
12
- __version__ = "0.6.1"
12
+ __version__ = "0.6.2"
@@ -33,6 +33,7 @@ MCP_ALLOWED_COMMANDS = frozenset(
33
33
  "cell-meta",
34
34
  "changes",
35
35
  "claim",
36
+ "commit",
36
37
  "deck",
37
38
  "defs",
38
39
  "describe",
@@ -61,13 +62,23 @@ MCP_ALLOWED_COMMANDS = frozenset(
61
62
  "rm",
62
63
  "rules",
63
64
  "share",
64
- "variant",
65
65
  "viewer",
66
66
  "whoami",
67
+ # `workspace` is the pre-rename spelling of `project` — a hidden alias
68
+ # of the same Group. Hidden is not unreachable: `_parse_command`
69
+ # dispatches through Click, which runs a hidden command like any other,
70
+ # so the alias needs a policy of its own or it is reachable with none
71
+ # declared. It carries `project`'s policy because it IS `project`.
72
+ "workspace",
67
73
  "write",
68
74
  }
69
75
  )
70
76
 
77
+ #: Allowed spellings that are aliases of another allowed command. They need an
78
+ #: MCP policy (they run) but must NOT reach the tool catalog: documenting a
79
+ #: second spelling of `project` would put back the row the alias is hidden from.
80
+ MCP_ALIAS_COMMANDS = frozenset({"workspace"})
81
+
71
82
  # Reason codes are stable data rendered into help, guide, MCP errors, and the
72
83
  # for-agent page. The detail is for maintainers and structured clients; user
73
84
  # prose remains translated at its own surface.
@@ -80,10 +91,6 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
80
91
  "reason": "local_checkout",
81
92
  "detail": "creates and updates a local Git working copy",
82
93
  },
83
- "commit": {
84
- "reason": "local_checkout",
85
- "detail": "reads local sync state and commits a working copy",
86
- },
87
94
  "login": {
88
95
  "reason": "interactive_auth",
89
96
  "detail": "opens or polls an interactive browser authentication flow",
@@ -132,6 +139,15 @@ MCP_BLOCKED_COMMANDS: dict[str, dict[str, str]] = {
132
139
  "reason": "shared_host_install",
133
140
  "detail": "would inspect or replace the CLI installed on the MCP host",
134
141
  },
142
+ "variant": {
143
+ "reason": "local_checkout",
144
+ "detail": (
145
+ "branches the whole file set, and only a working copy can use one: "
146
+ "`checkout` resets a local tree, and no write command takes a "
147
+ "`--variant`, so a caller with no clone cannot put anything on a "
148
+ "branch for `diff` or `merge` to act on"
149
+ ),
150
+ },
135
151
  "verify-email": {
136
152
  "reason": "interactive_auth",
137
153
  "detail": "prompts for a verification token sent out of band",
@@ -427,9 +427,9 @@
427
427
  "reason": "not_dispatched"
428
428
  },
429
429
  "mcp": {
430
- "available": false,
431
- "detail": "reads local sync state and commits a working copy",
432
- "reason": "local_checkout"
430
+ "available": true,
431
+ "detail": null,
432
+ "reason": null
433
433
  }
434
434
  }
435
435
  },
@@ -5817,9 +5817,9 @@
5817
5817
  "reason": "not_dispatched"
5818
5818
  },
5819
5819
  "mcp": {
5820
- "available": true,
5821
- "detail": null,
5822
- "reason": null
5820
+ "available": false,
5821
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5822
+ "reason": "local_checkout"
5823
5823
  }
5824
5824
  }
5825
5825
  },
@@ -5848,9 +5848,9 @@
5848
5848
  "reason": "not_dispatched"
5849
5849
  },
5850
5850
  "mcp": {
5851
- "available": true,
5852
- "detail": null,
5853
- "reason": null
5851
+ "available": false,
5852
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5853
+ "reason": "local_checkout"
5854
5854
  }
5855
5855
  }
5856
5856
  },
@@ -5879,9 +5879,9 @@
5879
5879
  "reason": "not_dispatched"
5880
5880
  },
5881
5881
  "mcp": {
5882
- "available": true,
5883
- "detail": null,
5884
- "reason": null
5882
+ "available": false,
5883
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5884
+ "reason": "local_checkout"
5885
5885
  }
5886
5886
  }
5887
5887
  },
@@ -5910,9 +5910,9 @@
5910
5910
  "reason": "not_dispatched"
5911
5911
  },
5912
5912
  "mcp": {
5913
- "available": true,
5914
- "detail": null,
5915
- "reason": null
5913
+ "available": false,
5914
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5915
+ "reason": "local_checkout"
5916
5916
  }
5917
5917
  }
5918
5918
  },
@@ -5941,9 +5941,9 @@
5941
5941
  "reason": "not_dispatched"
5942
5942
  },
5943
5943
  "mcp": {
5944
- "available": true,
5945
- "detail": null,
5946
- "reason": null
5944
+ "available": false,
5945
+ "detail": "branches the whole file set, and only a working copy can use one: `checkout` resets a local tree, and no write command takes a `--variant`, so a caller with no clone cannot put anything on a branch for `diff` or `merge` to act on",
5946
+ "reason": "local_checkout"
5947
5947
  }
5948
5948
  }
5949
5949
  },
@@ -257,6 +257,19 @@ def help_cmd(
257
257
  if command:
258
258
  path = " ".join(command)
259
259
  entry = commands.get(path)
260
+ if entry is None:
261
+ # A HIDDEN command is absent from the listing on purpose but still
262
+ # runs, so asking for it by name must answer. `clone` is the case:
263
+ # `cli/README.md` points a person at `deepcell help clone`, and the
264
+ # listing it is hidden from is not the same question as the flags it
265
+ # takes. Only reached on an explicit name — the index, the stage
266
+ # filters and every generated surface still see the visible set.
267
+ hidden_entry = build_commands(include_hidden=True).get(path)
268
+ if hidden_entry is not None:
269
+ entry = {
270
+ **hidden_entry,
271
+ "capability": get_command_capability(path),
272
+ }
260
273
  if entry is None:
261
274
  # `build_commands()` keys leaf commands only, so a group path misses
262
275
  # the table. Asking what is under `defs` is a well-formed question
@@ -267,6 +280,16 @@ def help_cmd(
267
280
  children = sorted(
268
281
  name for name in commands if name.startswith(f"{path} ")
269
282
  )
283
+ if not children:
284
+ # A hidden GROUP is keyed nowhere — `build_commands` keys leaves
285
+ # — so `deepcell help variant` has to find its children the same
286
+ # way, in the hidden-inclusive tree.
287
+ all_commands = build_commands(include_hidden=True)
288
+ children = sorted(
289
+ name for name in all_commands if name.startswith(f"{path} ")
290
+ )
291
+ if children:
292
+ commands = {**all_commands, **commands}
270
293
  if children:
271
294
  if ctx.fmt == "json":
272
295
  output({"group": path, "commands": children}, ctx.fmt)
@@ -1,6 +1,10 @@
1
- """Sync commands: clone, pull, push, status, commit.
1
+ """Sync commands: clone, pull, push, status.
2
2
 
3
- Git-like sync between a local folder and a cloud DeepCell workspace.
3
+ Git-like sync between a local folder and a cloud DeepCell workspace. Every
4
+ command here genuinely reads or writes a local working copy — that is the
5
+ membership rule, and `commit` failed it: it posts to
6
+ `/workspaces/{slug}/versions/commit` and touches no local file, so it lives in
7
+ `version.py` with the rest of the server-side history commands.
4
8
  """
5
9
 
6
10
  from __future__ import annotations
@@ -742,63 +746,3 @@ def push(ctx: Ctx, message: str) -> None:
742
746
  "push again"
743
747
  )
744
748
  raise click.exceptions.Exit(1)
745
-
746
-
747
- # -- commit (server-side pending) ---------------------------------------------
748
-
749
-
750
- @click.command()
751
- @click.option("-m", "--message", default=None, help="Commit message.")
752
- @pass_ctx
753
- def commit(ctx: Ctx, message: str | None) -> None:
754
- """Commit changes staged on the server by something other than you.
755
-
756
- You almost certainly do not need this. `defs`, `edit`, `write` and
757
- `reasoning` each commit their own change with their own `-m` — there is no
758
- staging area they queue into. This commits what a *different* actor left
759
- pending on the server, such as files created by an agent session.
760
-
761
- Nothing staged is a no-op, not an error.
762
- """
763
- root, state = _require_sync_root(ctx)
764
- slug = state.workspace_slug
765
-
766
- # `-m` is no longer `required=True`, so this reaches the server even when
767
- # the caller gave no message. The server checks "is anything staged"
768
- # first and answers the no-op; a commit that will actually write still
769
- # 422s without a message, and that is re-raised below as a usage error.
770
- #
771
- # It used to be rejected by Click during parsing, so a worker whose defs
772
- # ops had each already committed and who reasonably tried to finalize got
773
- # "Missing option '-m' / '--message'" — an argument-syntax error for a
774
- # command with nothing to do. That is what the 2026-08-02 CLI eval hit.
775
- data = ctx.client.post(
776
- f"/workspaces/{slug}/versions/commit",
777
- json={"message": message or ""},
778
- )
779
-
780
- # A commit with nothing staged is a benign no-op (the endpoint answers
781
- # 200 with committed=false, sha=null) — report it instead of slicing a
782
- # null sha.
783
- if isinstance(data, dict) and data.get("committed") is False:
784
- echo_info(
785
- "Nothing to commit — no pending changes. `defs`, `edit`, `write` "
786
- "and `reasoning` each commit their own change, so there is "
787
- "normally nothing here to finalize."
788
- )
789
- return
790
-
791
- sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
792
- # Staged content is committed either way (the server persists invalid
793
- # .deepcell on purpose), so report the errors rather than letting a clean
794
- # "Committed" line stand in for them — parity with write/push.
795
- invalid = echo_validation_map(
796
- data.get("validation") if isinstance(data, dict) else None
797
- )
798
- if invalid:
799
- echo_warning(
800
- f"Committed ({sha}), but {len(invalid)} file(s) have validation "
801
- f"error(s): {', '.join(invalid)} — fix the errors above"
802
- )
803
- raise click.exceptions.Exit(1)
804
- echo_success(f"Committed ({sha}): {message}")
@@ -1,4 +1,4 @@
1
- """Version control commands: log, diff, restore."""
1
+ """Version control commands: log, diff, restore, commit."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,7 +7,8 @@ import click
7
7
  from deepcell_cli.errors import APIError
8
8
  from deepcell_cli.context import Ctx, pass_ctx
9
9
  from deepcell_cli.output import (
10
- echo_error, echo_info, echo_success, echo_warning, output, output_mutation, print_plain,
10
+ echo_error, echo_info, echo_success, echo_validation_map, echo_warning,
11
+ output, output_mutation, print_plain,
11
12
  )
12
13
 
13
14
 
@@ -443,3 +444,73 @@ def restore(ctx: Ctx, revision: str, filename: str | None, yes: bool) -> None:
443
444
  "Recover them from a newer revision (`deepcell log`, then "
444
445
  "`deepcell download <file> --revision <sha>`)."
445
446
  )
447
+
448
+
449
+ # -- commit (finalize what another actor staged server-side) -------------------
450
+
451
+
452
+ @click.command()
453
+ @click.option("-m", "--message", default=None, help="Commit message.")
454
+ @pass_ctx
455
+ def commit(ctx: Ctx, message: str | None) -> None:
456
+ """Commit changes staged on the server by something other than you.
457
+
458
+ You almost certainly do not need this. `defs`, `edit`, `write` and
459
+ `reasoning` each commit their own change with their own `-m` — there is no
460
+ staging area they queue into. This commits what a *different* actor left
461
+ pending on the server, such as files created by an agent session.
462
+
463
+ Nothing staged is a no-op, not an error.
464
+ """
465
+ # Addressed by workspace, like everything else in this module. It used to
466
+ # live in `sync.py` and call `_require_sync_root`, which reads
467
+ # `.deepcell/sync.json` — for nothing but the slug, because the work is
468
+ # entirely server-side: `POST /workspaces/{slug}/versions/commit` stages
469
+ # nothing locally and reads no local file.
470
+ #
471
+ # That invented clone requirement cost the command the callers it exists
472
+ # for. It raised "Not inside a synced workspace" for anyone standing
473
+ # anywhere else, and it was blocked over MCP as `local_checkout` — a reason
474
+ # untrue of it. An agent, which is precisely who leaves changes staged,
475
+ # could not finalize them from any transport.
476
+ slug = ctx.require_workspace()
477
+
478
+ # `-m` is no longer `required=True`, so this reaches the server even when
479
+ # the caller gave no message. The server checks "is anything staged"
480
+ # first and answers the no-op; a commit that will actually write still
481
+ # 422s without a message, and that is re-raised below as a usage error.
482
+ #
483
+ # It used to be rejected by Click during parsing, so a worker whose defs
484
+ # ops had each already committed and who reasonably tried to finalize got
485
+ # "Missing option '-m' / '--message'" — an argument-syntax error for a
486
+ # command with nothing to do. That is what the 2026-08-02 CLI eval hit.
487
+ data = ctx.client.post(
488
+ f"/workspaces/{slug}/versions/commit",
489
+ json={"message": message or ""},
490
+ )
491
+
492
+ # A commit with nothing staged is a benign no-op (the endpoint answers
493
+ # 200 with committed=false, sha=null) — report it instead of slicing a
494
+ # null sha.
495
+ if isinstance(data, dict) and data.get("committed") is False:
496
+ echo_info(
497
+ "Nothing to commit — no pending changes. `defs`, `edit`, `write` "
498
+ "and `reasoning` each commit their own change, so there is "
499
+ "normally nothing here to finalize."
500
+ )
501
+ return
502
+
503
+ sha = (data.get("sha") or "")[:8] if isinstance(data, dict) else ""
504
+ # Staged content is committed either way (the server persists invalid
505
+ # .deepcell on purpose), so report the errors rather than letting a clean
506
+ # "Committed" line stand in for them — parity with write/push.
507
+ invalid = echo_validation_map(
508
+ data.get("validation") if isinstance(data, dict) else None
509
+ )
510
+ if invalid:
511
+ echo_warning(
512
+ f"Committed ({sha}), but {len(invalid)} file(s) have validation "
513
+ f"error(s): {', '.join(invalid)} — fix the errors above"
514
+ )
515
+ raise click.exceptions.Exit(1)
516
+ echo_success(f"Committed ({sha}): {message}")
@@ -442,11 +442,11 @@ from deepcell_cli.commands.impact import impact # noqa: E402
442
442
  from deepcell_cli.commands.export_docx import to_docx # noqa: E402
443
443
  from deepcell_cli.commands.export_pdf import to_pdf # noqa: E402
444
444
  from deepcell_cli.commands.export_pptx import to_pptx # noqa: E402
445
- from deepcell_cli.commands.version import log, diff, restore # noqa: E402
445
+ from deepcell_cli.commands.version import log, diff, restore, commit # noqa: E402
446
446
  from deepcell_cli.commands.changes import changes # noqa: E402
447
447
  from deepcell_cli.commands.variant import variant # noqa: E402
448
448
  from deepcell_cli.commands.grep import grep # noqa: E402
449
- from deepcell_cli.commands.sync import clone, pull, push, sync_status, commit # noqa: E402
449
+ from deepcell_cli.commands.sync import clone, pull, push, sync_status # noqa: E402
450
450
  from deepcell_cli.commands.merge import merge # noqa: E402
451
451
  from deepcell_cli.commands.import_cmd import import_cmd # noqa: E402
452
452
  from deepcell_cli.commands.ingest import ingest # noqa: E402
@@ -499,13 +499,34 @@ cli.add_command(log)
499
499
  cli.add_command(diff)
500
500
  cli.add_command(restore)
501
501
  cli.add_command(changes)
502
- cli.add_command(variant)
503
502
  cli.add_command(grep)
503
+ cli.add_command(commit)
504
+
505
+ # Local sync and variants: registered, working, and HIDDEN. Both are a person's
506
+ # workflow at a terminal with a clone, and neither is discoverable any more —
507
+ # `surface.py`, `main`'s own help and `docs/cli-commands.md`'s coverage test all
508
+ # skip hidden commands, so they produce no help row and no entry in any
509
+ # generated surface. Muscle memory and existing scripts keep working, and
510
+ # `deepcell help <name>` still answers for each.
511
+ #
512
+ # Sync moves BYTES while every read addresses the project on the server, so a
513
+ # clone is a working copy the read path cannot see. Variants branch the whole
514
+ # file set, and no write command takes a variant, so a branch is only reachable
515
+ # by checking it out onto disk. Both are therefore clone-shaped all the way
516
+ # down, which is precisely what the agent surfaces do not have.
517
+ #
518
+ # The cost is deliberate and was weighed: the `variants` and `merge` domains
519
+ # leave `docs/backend-cli-semantic-contracts.yaml`, because a hidden command is
520
+ # absent from the capability contract the domain resolves through. Those two
521
+ # backend domains now have no taught CLI command.
522
+ for _person_only in (variant, clone, pull, push, sync_status, merge):
523
+ _person_only.hidden = True
524
+
525
+ cli.add_command(variant)
504
526
  cli.add_command(clone)
505
527
  cli.add_command(pull)
506
528
  cli.add_command(push)
507
529
  cli.add_command(sync_status, "status")
508
- cli.add_command(commit)
509
530
  cli.add_command(merge)
510
531
  cli.add_command(import_cmd, "import")
511
532
  cli.add_command(ingest)
@@ -24,6 +24,7 @@ from click.testing import CliRunner
24
24
  from mcp.server.fastmcp import FastMCP
25
25
 
26
26
  from deepcell_cli.capabilities import (
27
+ MCP_ALIAS_COMMANDS,
27
28
  MCP_ALLOWED_COMMANDS,
28
29
  MCP_BLOCKED_COMMANDS as MCP_BLOCK_POLICY,
29
30
  blocked_mcp_commands,
@@ -48,7 +49,7 @@ AUTO_CONFIRM: dict[str, list[str]] = {
48
49
  # Top-level commands documented in TOOL_DESCRIPTION. Must stay equal to the
49
50
  # registered Click commands minus BLOCKED_COMMANDS — tests/test_mcp_server.py
50
51
  # cross-checks both directions so the catalog can't silently drift.
51
- DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS)
52
+ DOCUMENTED_COMMANDS = frozenset(MCP_ALLOWED_COMMANDS) - MCP_ALIAS_COMMANDS
52
53
 
53
54
  TOOL_DESCRIPTION = """\
54
55
  Run a DeepCell CLI command. Structured results are JSON; `cat` prints raw XML
@@ -136,9 +137,6 @@ Available commands:
136
137
  replace <file> <old> <new>
137
138
  String-replace in XML (last resort — prefer defs;
138
139
  `edit --replace` is a deprecated alias)
139
- variant list List variants
140
- variant create <name> Create a variant
141
- (Note: variant checkout/diff/merge need a LOCAL clone — blocked here)
142
140
  rm <file> Delete a file (auto-confirmed)
143
141
  log [--file F] [-n N] Show version history
144
142
  diff <rev_a> [rev_b] Diff between revisions
@@ -147,6 +145,9 @@ Available commands:
147
145
  changes diff <base> [head] What one change did, semantically
148
146
  changes revert <base> <head>
149
147
  Undo one change as a new commit (auto-confirmed)
148
+ commit [-m MSG] Finalize changes another actor left staged
149
+ (Rare: every writer commits its own change.
150
+ Nothing staged is a no-op, not an error.)
150
151
 
151
152
  VERIFY — change one thing, see everything it affects
152
153
  relationships <file> Show relationship graph
@@ -182,7 +183,8 @@ Available commands:
182
183
 
183
184
  SYNC — local <-> cloud
184
185
  download <file> -o <path> Download file to local path
185
- (the rest of sync is blocked here — see below)
186
+ (Sync proper is blocked here — it needs a clone.
187
+ Note `commit` is NOT sync: see REVISE.)
186
188
 
187
189
  LEARN — the five reference surfaces
188
190
  help [command] Every command, flag, exit code and example.
@@ -99,6 +99,10 @@ COMMAND_TIER: dict[str, str] = {
99
99
  "log": "revise",
100
100
  "diff": "revise",
101
101
  "restore": "revise",
102
+ # Filed under SYNC until it was read closely: it posts to
103
+ # `/workspaces/{slug}/versions/commit` and touches no local working copy,
104
+ # so it belongs with the other history commands, not with `clone`/`push`.
105
+ "commit": "revise",
102
106
  "rm": "revise",
103
107
  # VERIFY — the connectedness story: change one thing, see what it affects.
104
108
  "relationships": "verify",
@@ -132,7 +136,6 @@ COMMAND_TIER: dict[str, str] = {
132
136
  "status": "sync",
133
137
  "pull": "sync",
134
138
  "push": "sync",
135
- "commit": "sync",
136
139
  "merge": "sync",
137
140
  "download": "sync",
138
141
  # LEARN — the five reference surfaces.
@@ -502,7 +502,12 @@ def _describe_command(path: str, command: click.Command) -> dict[str, Any]:
502
502
  }
503
503
 
504
504
 
505
- def _walk(command: click.Command, path: str, out: dict[str, dict[str, Any]]) -> None:
505
+ def _walk(
506
+ command: click.Command,
507
+ path: str,
508
+ out: dict[str, dict[str, Any]],
509
+ include_hidden: bool = False,
510
+ ) -> None:
506
511
  if isinstance(command, click.Group):
507
512
  # An `invoke_without_command=True` group is a command *as well as* a
508
513
  # group: `deepcell example --pack finance` runs, and the `--pack` is
@@ -515,13 +520,13 @@ def _walk(command: click.Command, path: str, out: dict[str, dict[str, Any]]) ->
515
520
  ctx = click.Context(command)
516
521
  for name in command.list_commands(ctx):
517
522
  sub = command.get_command(ctx, name)
518
- if sub is not None and not sub.hidden:
519
- _walk(sub, f"{path} {name}".strip(), out)
523
+ if sub is not None and (include_hidden or not sub.hidden):
524
+ _walk(sub, f"{path} {name}".strip(), out, include_hidden)
520
525
  return
521
526
  out[path] = _describe_command(path, command)
522
527
 
523
528
 
524
- def build_commands() -> dict[str, dict[str, Any]]:
529
+ def build_commands(include_hidden: bool = False) -> dict[str, dict[str, Any]]:
525
530
  """Every invocable command, keyed by its full invocation path.
526
531
 
527
532
  Groups are walked rather than listed: ``defs`` is not a command, ``defs
@@ -534,6 +539,12 @@ def build_commands() -> dict[str, dict[str, Any]]:
534
539
  its own. ``test_surface_group_options`` fails on that shape rather than
535
540
  letting the option vanish the way ``example --pack`` did — the fix is to
536
541
  move the option onto the leaves, or make the group invocable.
542
+
543
+ ``include_hidden`` is for lookup by name only, never for codegen: a hidden
544
+ command is deliberately absent from every generated surface, but it still
545
+ runs, so ``deepcell help <name>`` must still answer for it. Hidden from the
546
+ index, answerable when asked for — the alternative is a command a person
547
+ can invoke and cannot read the flags of.
537
548
  """
538
549
  from deepcell_cli.main import cli
539
550
 
@@ -541,8 +552,8 @@ def build_commands() -> dict[str, dict[str, Any]]:
541
552
  ctx = click.Context(cli)
542
553
  for name in cli.list_commands(ctx):
543
554
  sub = cli.get_command(ctx, name)
544
- if sub is not None and not sub.hidden:
545
- _walk(sub, name, out)
555
+ if sub is not None and (include_hidden or not sub.hidden):
556
+ _walk(sub, name, out, include_hidden)
546
557
  return dict(sorted(out.items()))
547
558
 
548
559
 
@@ -95,7 +95,7 @@ class Index(NamedTuple):
95
95
  # first-answer-wins would then quietly stop noticing releases whenever the
96
96
  # earlier index lagged the later one. Nothing would fail; the check would just
97
97
  # say nothing forever, which is the failure mode this whole module is built to
98
- # avoid. (It is also what the TestPyPI era needed, when production PyPI 404'd.)
98
+ # avoid.
99
99
  DEFAULT_INDEXES = (
100
100
  Index(
101
101
  f"https://pypi.org/pypi/{PACKAGE_NAME}/json",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepcell-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Build and revise .deepcell documents from a terminal — the reasoning, the calculations, the document and the deck, with the links between them
5
5
  Author-email: DeepCell <hello@deepcell.net>
6
6
  License-Expression: MIT
@@ -166,8 +166,8 @@ deepcell defs apply model.deepcell --ops ops.json # many ops, atomically
166
166
  | Document surface | `doc` — outline, blocks, body, links, lint |
167
167
  | Deck surface | `deck` — slides, ordering, binding, branding |
168
168
  | Change review | `changes list / diff / revert`, `impact` |
169
- | Version control | `log`, `diff`, `restore` |
170
- | Sync | `clone`, `status`, `pull`, `push`, `commit`, `merge` |
169
+ | Version control | `log`, `diff`, `restore`, `commit` |
170
+ | Sync (people, not agents) | `clone`, `status`, `pull`, `push`, `merge` |
171
171
  | Import / export | `import`, `ingest`, `to-excel`, `to-docx`, `to-pptx`, `to-pdf` |
172
172
  | Reasoning | `reasoning`, `reasoning-diff`, `claim`, `assumption` |
173
173
  | Delivery | `viewer`, `share create / list / revoke` |
@@ -218,7 +218,9 @@ deepcell push -m "Updated assumptions" # upload
218
218
  deepcell pull # fetch cloud changes
219
219
  ```
220
220
 
221
- A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell guide revise/history`.
221
+ A `.deepcell/` metadata directory tracks the project link and sync state. `push` refuses to clobber cloud changes — it asks you to `pull` first. See `deepcell help clone`.
222
+
223
+ Sync moves *bytes*, not the commands that read them: `query`, `describe --lint`, `cat` and every other read address the project on the server, so inside a clone they answer from the cloud's HEAD and not from the file you just edited. `push` first, then read back. Sync is a human workflow for this reason and is unavailable over MCP.
222
224
 
223
225
  ## Import & export
224
226
 
@@ -63,11 +63,40 @@ class TestParseCommand:
63
63
  with pytest.raises(ValueError, match="verify-email.*blocked"):
64
64
  _parse_command("verify-email")
65
65
 
66
- @pytest.mark.parametrize("cmd", ["clone", "status", "pull", "push", "commit", "merge"])
66
+ @pytest.mark.parametrize("cmd", ["clone", "status", "pull", "push", "merge"])
67
67
  def test_blocked_sync_commands(self, cmd):
68
68
  with pytest.raises(ValueError, match=f"{cmd}.*blocked"):
69
69
  _parse_command(cmd)
70
70
 
71
+ def test_variant_is_blocked(self):
72
+ """A branch you cannot write to is not a capability, it is a trap.
73
+
74
+ `variant checkout` resets a local working tree the MCP caller does not
75
+ have, and `variant diff` / `variant merge` resolve their workspace out
76
+ of `.deepcell/sync.json` — so the family refused itself here anyway,
77
+ with "Not inside a synced workspace" instead of a capability answer.
78
+ The deeper reason it stays blocked rather than being made to work: no
79
+ write command takes a variant, so an agent cannot put anything ON a
80
+ branch it creates, leaving `diff` and `merge` nothing to act on.
81
+ Scenarios are the in-place answer (`guide revise/scenarios`).
82
+ """
83
+ with pytest.raises(ValueError, match="variant.*blocked"):
84
+ _parse_command("variant create bear-case")
85
+ with pytest.raises(ValueError, match="variant.*blocked"):
86
+ _parse_command("variant list")
87
+ assert "variant" not in DOCUMENTED_COMMANDS
88
+
89
+ def test_commit_is_not_a_sync_command(self):
90
+ """`commit` sat in this list for its module, not its behaviour.
91
+
92
+ It posts to `/workspaces/{slug}/versions/commit` and reads no local
93
+ file, so `local_checkout` was never true of it. Blocking it kept the
94
+ one caller who needs it — an agent finalizing what it left staged —
95
+ from reaching it on any transport.
96
+ """
97
+ assert "commit" not in BLOCKED_COMMANDS
98
+ assert "commit" in _parse_command("commit -m 'finalize'")
99
+
71
100
  def test_auto_confirm_rm(self):
72
101
  result = _parse_command("rm model.deepcell")
73
102
  assert "-y" in result
@@ -275,7 +304,7 @@ class TestDeepcellToolOutput:
275
304
  # Blocked commands are refused with a clear, non-silent error (not run).
276
305
  for cmd in (
277
306
  "login", "logout", "register", "verify-email", "to-excel",
278
- "clone", "status", "pull", "push", "commit", "merge",
307
+ "clone", "status", "pull", "push", "merge",
279
308
  ):
280
309
  out = deepcell(cmd)
281
310
  # The failure MARKER must lead: `guide exit-codes` tells agents to
@@ -113,12 +113,26 @@ def _lead_commands(bullet: str) -> list[str]:
113
113
 
114
114
 
115
115
  def _top_level() -> list[str]:
116
+ """Every registered top-level command, HIDDEN ONES INCLUDED.
117
+
118
+ Both questions below are about existence, not about the help index.
119
+ `clone`, `pull`, `push`, `status`, `merge` and `variant` are `hidden=True`
120
+ — retired from discovery, still registered and still runnable — so reading
121
+ them as "no longer exist" would delete their stage entries and, the next
122
+ time one is un-hidden, land it in a bucket `assert_fully_staged` exists to
123
+ forbid. A hidden command keeps its stage for the same reason it keeps its
124
+ MCP policy: it still runs.
125
+
126
+ An ALIAS is still excluded, and that is a different case: `workspace` is
127
+ the pre-rename spelling of `project`, the same Group under a second name.
128
+ It has no stage and no catalog row of its own because it is not a second
129
+ command — staging it would put a duplicate of every `project` row back into
130
+ the help index the alias exists to stay out of.
131
+ """
132
+ from deepcell_cli.capabilities import MCP_ALIAS_COMMANDS
133
+
116
134
  ctx = click.Context(cli)
117
- return [
118
- name
119
- for name in cli.list_commands(ctx)
120
- if not (cli.get_command(ctx, name) or click.Command(name)).hidden
121
- ]
135
+ return [n for n in cli.list_commands(ctx) if n not in MCP_ALIAS_COMMANDS]
122
136
 
123
137
 
124
138
  def test_every_command_has_a_stage():
@@ -360,7 +374,12 @@ def test_the_docs_lead_list_reader_actually_reads_commands():
360
374
  for b in bullets
361
375
  for n in _lead_commands(b)
362
376
  }
363
- assert {"query", "defs", "deck", "changes", "clone"} <= found, sorted(found)
377
+ # One sentinel per section, so a reader that went blind in any single one
378
+ # is caught. `download` heads SYNC's only bullet since `clone` and the rest
379
+ # of the sync family became hidden commands, which are named in the
380
+ # preamble's prose rather than in a stage-headed list — and prose is
381
+ # exactly what this reader is built NOT to read.
382
+ assert {"query", "defs", "deck", "changes", "download"} <= found, sorted(found)
364
383
 
365
384
 
366
385
  def test_the_lead_list_reader_stops_where_the_grouping_claim_stops():
@@ -25,12 +25,12 @@ from deepcell_cli.main import cli
25
25
 
26
26
  PYPI = upgrade_check.DEFAULT_INDEXES[0].json_url
27
27
 
28
- # Two stand-ins for the multi-index tests below. `DEFAULT_INDEXES` names one
29
- # index (production PyPI) now that the package no longer ships to TestPyPI, but
30
- # `fetch_latest` still asks every index and takes the highest — the tuple is the
31
- # seam a mirror or a pre-release channel gets added at. Patching the module
32
- # global is what keeps that loop covered without inventing a second real index;
33
- # it is a plain attribute, so monkeypatch restores it cleanly.
28
+ # Two stand-ins for the multi-index tests below. `DEFAULT_INDEXES` names a
29
+ # single index, production PyPI, but `fetch_latest` still asks every index and
30
+ # takes the highest — the tuple is the seam a mirror or a pre-release channel
31
+ # gets added at. Patching the module global is what keeps that loop covered
32
+ # without inventing a second real index; it is a plain attribute, so
33
+ # monkeypatch restores it cleanly.
34
34
  MIRROR_A = "https://mirror-a.test/pypi/deepcell-cli/json"
35
35
  MIRROR_B = "https://mirror-b.test/pypi/deepcell-cli/json"
36
36
  _INSTALL_A = "pip install --upgrade --index-url https://mirror-a.test/simple/ deepcell-cli"
@@ -184,10 +184,10 @@ def _index_payload(version: str) -> dict:
184
184
  class TestFetchLatest:
185
185
  def test_the_default_index_is_production_pypi(self) -> None:
186
186
  # The package ships to production PyPI, which carries its dependencies
187
- # too so the notice's install line takes no --index-url and no
188
- # --extra-index-url. Both were mandatory in the TestPyPI era, and a
189
- # notice that prints them again sends the reader to an index this
190
- # project no longer publishes to.
187
+ # too, so the notice's install line takes no --index-url and no
188
+ # --extra-index-url. A notice that pins an index sends the reader
189
+ # somewhere this project does not publish, and every later release is
190
+ # then invisible to them.
191
191
  assert len(upgrade_check.DEFAULT_INDEXES) == 1
192
192
  (index,) = upgrade_check.DEFAULT_INDEXES
193
193
  assert index.json_url == "https://pypi.org/pypi/deepcell-cli/json"
File without changes