evo-cli 0.30.0__tar.gz → 0.31.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {evo_cli-0.30.0 → evo_cli-0.31.0}/PKG-INFO +3 -1
  2. {evo_cli-0.30.0 → evo_cli-0.31.0}/README.md +2 -0
  3. evo_cli-0.31.0/evo_cli/VERSION +1 -0
  4. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/__init__.py +1 -1
  5. evo_cli-0.31.0/evo_cli/commands/harness/show.py +274 -0
  6. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/PKG-INFO +3 -1
  7. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_harness_show.py +195 -1
  8. evo_cli-0.30.0/evo_cli/VERSION +0 -1
  9. evo_cli-0.30.0/evo_cli/commands/harness/show.py +0 -144
  10. {evo_cli-0.30.0 → evo_cli-0.31.0}/Containerfile +0 -0
  11. {evo_cli-0.30.0 → evo_cli-0.31.0}/HISTORY.md +0 -0
  12. {evo_cli-0.30.0 → evo_cli-0.31.0}/LICENSE +0 -0
  13. {evo_cli-0.30.0 → evo_cli-0.31.0}/MANIFEST.in +0 -0
  14. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/__init__.py +0 -0
  15. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/__main__.py +0 -0
  16. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/base.py +0 -0
  17. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/cli.py +0 -0
  18. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/__init__.py +0 -0
  19. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/agent_toy.py +0 -0
  20. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/claude_code.py +0 -0
  21. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/cloudflare.py +0 -0
  22. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/cred.py +0 -0
  23. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/download.py +0 -0
  24. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/fix_claude.py +0 -0
  25. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/gdrive.py +0 -0
  26. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/gh.py +0 -0
  27. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_dag.py +0 -0
  28. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_git.py +0 -0
  29. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_hub.py +0 -0
  30. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_mirror.py +0 -0
  31. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_model.py +0 -0
  32. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_mutate.py +0 -0
  33. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_paths.py +0 -0
  34. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_render.py +0 -0
  35. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/_server.py +0 -0
  36. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/check.py +0 -0
  37. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/clone.py +0 -0
  38. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/edit.py +0 -0
  39. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/pull.py +0 -0
  40. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/serve.py +0 -0
  41. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/views.py +0 -0
  42. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
  43. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
  44. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  45. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  46. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  47. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  48. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  49. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  50. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  51. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  52. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  53. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  54. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  55. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  56. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/harness/web/index.html +0 -0
  57. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/hwid.py +0 -0
  58. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/hwid_reset.py +0 -0
  59. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/image.py +0 -0
  60. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/localproxy.py +0 -0
  61. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/mcp.py +0 -0
  62. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/miniconda.py +0 -0
  63. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/netcheck.py +0 -0
  64. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/ngrok.py +0 -0
  65. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/opencode.py +0 -0
  66. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/openvpn.py +0 -0
  67. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/plantuml.py +0 -0
  68. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/__init__.py +0 -0
  69. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/_dns.py +0 -0
  70. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/_dpi.py +0 -0
  71. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/_hosts.py +0 -0
  72. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/_probe.py +0 -0
  73. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/check.py +0 -0
  74. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/fix.py +0 -0
  75. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/probe.py +0 -0
  76. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/route/status.py +0 -0
  77. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/serp.py +0 -0
  78. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/site2s.py +0 -0
  79. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/ssh.py +0 -0
  80. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/storage.py +0 -0
  81. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/sysmon.py +0 -0
  82. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/tts.py +0 -0
  83. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/update.py +0 -0
  84. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/commands/wifi.py +0 -0
  85. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/console.py +0 -0
  86. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/__init__.py +0 -0
  87. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/doctor.py +0 -0
  88. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/google_oauth.py +0 -0
  89. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/migrate.py +0 -0
  90. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/oauth_flow.py +0 -0
  91. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/registry.py +0 -0
  92. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/store.py +0 -0
  93. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/credentials/sync.py +0 -0
  94. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/__init__.py +0 -0
  95. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/align.py +0 -0
  96. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/core.py +0 -0
  97. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/creds.py +0 -0
  98. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/errors.py +0 -0
  99. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/gemini.py +0 -0
  100. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/imaging/ncnn.py +0 -0
  101. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/mcp_registry.py +0 -0
  102. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/openvpn.py +0 -0
  103. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/__init__.py +0 -0
  104. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/client.py +0 -0
  105. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/creds.py +0 -0
  106. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/errors.py +0 -0
  107. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/install.py +0 -0
  108. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/serp/render.py +0 -0
  109. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/__init__.py +0 -0
  110. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/chunking.py +0 -0
  111. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/core.py +0 -0
  112. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/creds.py +0 -0
  113. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/errors.py +0 -0
  114. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/gemini.py +0 -0
  115. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/http.py +0 -0
  116. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/openai.py +0 -0
  117. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/player.py +0 -0
  118. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli/tts/vbee.py +0 -0
  119. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/SOURCES.txt +0 -0
  120. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  121. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/entry_points.txt +0 -0
  122. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/requires.txt +0 -0
  123. {evo_cli-0.30.0 → evo_cli-0.31.0}/evo_cli.egg-info/top_level.txt +0 -0
  124. {evo_cli-0.30.0 → evo_cli-0.31.0}/pyproject.toml +0 -0
  125. {evo_cli-0.30.0 → evo_cli-0.31.0}/setup.cfg +0 -0
  126. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/__init__.py +0 -0
  127. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/fake_evo_agents.py +0 -0
  128. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/fixtures/hub/contract-0.2.0.json +0 -0
  129. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/fixtures/hub/mirror-demo.yaml +0 -0
  130. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_agent_toy.py +0 -0
  131. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_claude_code.py +0 -0
  132. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_cli.py +0 -0
  133. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_console.py +0 -0
  134. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_cred.py +0 -0
  135. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_download.py +0 -0
  136. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_fix_claude.py +0 -0
  137. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_gdrive.py +0 -0
  138. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_gh.py +0 -0
  139. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_harness.py +0 -0
  140. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_harness_clone.py +0 -0
  141. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_harness_dag.py +0 -0
  142. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_harness_hub.py +0 -0
  143. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_image.py +0 -0
  144. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_mcp.py +0 -0
  145. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_ngrok.py +0 -0
  146. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_opencode.py +0 -0
  147. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_openvpn.py +0 -0
  148. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_plantuml.py +0 -0
  149. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_route.py +0 -0
  150. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_serp.py +0 -0
  151. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_storage.py +0 -0
  152. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_sysmon.py +0 -0
  153. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_tts.py +0 -0
  154. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_update.py +0 -0
  155. {evo_cli-0.30.0 → evo_cli-0.31.0}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -417,6 +417,8 @@ evo harness seams # contract seams; --graph for the owner -> consumer DAG
417
417
  evo harness plans # progress across every exec-plan
418
418
  evo harness show <plan> # every section, open questions with status and blocking
419
419
  evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
420
+ evo harness show <plan> --section acceptance # each criterion in full: id, criterion, verify, steps naming it
421
+ evo harness show <plan> --section non_goals # each non-goal in full
420
422
  evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
421
423
  evo harness check <plan> [--fetch] # what the plan claims vs what git says
422
424
  evo harness graph <plan>:steps # the same DAG as an adjacency list
@@ -384,6 +384,8 @@ evo harness seams # contract seams; --graph for the owner -> consumer DAG
384
384
  evo harness plans # progress across every exec-plan
385
385
  evo harness show <plan> # every section, open questions with status and blocking
386
386
  evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
387
+ evo harness show <plan> --section acceptance # each criterion in full: id, criterion, verify, steps naming it
388
+ evo harness show <plan> --section non_goals # each non-goal in full
387
389
  evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
388
390
  evo harness check <plan> [--fetch] # what the plan claims vs what git says
389
391
  evo harness graph <plan>:steps # the same DAG as an adjacency list
@@ -0,0 +1 @@
1
+ 0.31.0
@@ -18,7 +18,7 @@ def harness_group():
18
18
  evo harness repos repos in the manifest, with git state
19
19
  evo harness seams contract seams: owner, consumers, verify command
20
20
  evo harness plans progress across every exec-plan
21
- evo harness show <plan> read one plan (--step <id> prints one step in full)
21
+ evo harness show <plan> read one plan (--step <id>, --section acceptance|non_goals: in full)
22
22
  evo harness graph <plan> print a DAG as an adjacency list
23
23
  evo harness check <plan> check what the plan claims against real git
24
24
  evo harness step <plan> 3 done mark a step (--evidence and --note append)
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ import rich_click as click
6
+ import yaml
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from evo_cli.commands.harness._dag import resolve_step, step_key
11
+ from evo_cli.commands.harness._model import SECTIONS, find_plan, plain, tone_of
12
+ from evo_cli.commands.harness._paths import find_manifest, harness_option
13
+ from evo_cli.commands.harness._render import TONE_STYLE, as_text, bar, key_runs, one_line, title_width
14
+ from evo_cli.console import console
15
+
16
+ SECTION_TITLES = {
17
+ "references": "References",
18
+ "repos": "Repos and merge order",
19
+ "steps": "Steps",
20
+ "decisions": "Decisions",
21
+ "tech_debt": "Tech debt",
22
+ "open_questions": "Open questions",
23
+ "acceptance": "Acceptance",
24
+ "non_goals": "Non-goals",
25
+ }
26
+ # Plan-level lists `show` prints whole. They stay out of SECTIONS, which also shapes the
27
+ # dashboard's plan detail, where both are still read from `extra`.
28
+ PLAN_LISTS = ("acceptance", "non_goals")
29
+ SHOW_SECTIONS = (*SECTIONS, *PLAN_LISTS)
30
+ HIDE_KEYS = {"status", "order", "what", "repo", "id"}
31
+ # Every open question says whether it blocks, even one whose author left the key out.
32
+ ALWAYS_SHOWN = {"open_questions": ("blocking",)}
33
+
34
+ STEP_HEAD = ("id", "title", "status", "repo", "depends_on", "blocking", "done_at")
35
+ STEP_BODY = ("what", "verify", "note", "acceptance", "evidence")
36
+ INDENT = " "
37
+ FIELD_INDENT = " "
38
+ # A step's `acceptance` names criteria by id, as a list or as text like "a1; a9"; prose around them is not an id.
39
+ CRITERION_ID = re.compile(r"^a\d+$")
40
+ CRITERION_TEXT = ("criterion", "what")
41
+
42
+
43
+ def _render_item(section: str, index: int, item: dict, full: bool) -> None:
44
+ status = item.get("status")
45
+ style = TONE_STYLE[tone_of(section, status)]
46
+ authored = item.get("title")
47
+ titled = isinstance(authored, str) and bool(authored.strip())
48
+ if titled:
49
+ head, hidden = authored.strip(), (HIDE_KEYS - {"what"}) | {"title"}
50
+ else:
51
+ head = item.get("what") or item.get("repo") or item.get("issue") or "?"
52
+ hidden = HIDE_KEYS | ({"issue"} if head == item.get("issue") else set())
53
+ label = item.get("id", item.get("order", index))
54
+ badge = f"[{style}]{status}[/]" if status else ""
55
+ if not full:
56
+ head = one_line(head, title_width(reserved=18))
57
+ console.print(f" [bold]{label:>3}[/] {head} {badge}")
58
+ fields = {**item, **{key: item.get(key) for key in ALWAYS_SHOWN.get(section, ())}}
59
+ for key, value in fields.items():
60
+ if key in hidden:
61
+ continue
62
+ if isinstance(value, list):
63
+ value = "; ".join(as_text(v) for v in value) or "-"
64
+ text = " ".join(as_text(value).split())
65
+ console.print(f" [dim]{key}:[/] {text if full else one_line(text, title_width(reserved=18))}")
66
+
67
+
68
+ def _block(value: object) -> list[str]:
69
+ """Lines of a value printed under its key: text keeps its line breaks, a list becomes `- ` items."""
70
+ if value is None or value == "" or value == []:
71
+ return ["-"]
72
+ if isinstance(value, list):
73
+ lines = []
74
+ for entry in value:
75
+ first, *rest = _block(entry)
76
+ lines.append(f"- {first}")
77
+ lines.extend(f" {line}" if line else "" for line in rest)
78
+ return lines
79
+ if isinstance(value, dict):
80
+ dumped = yaml.safe_dump(plain(value), allow_unicode=True, sort_keys=False, width=1_000_000)
81
+ return dumped.rstrip("\n").split("\n")
82
+ return as_text(value).rstrip("\n").split("\n")
83
+
84
+
85
+ def _is_short(value: object) -> bool:
86
+ nested = isinstance(value, dict) or (isinstance(value, list) and any(isinstance(v, (dict, list)) for v in value))
87
+ text = as_text(value)
88
+ return not nested and "\n" not in text and len(text) <= 80
89
+
90
+
91
+ def _print(text: Text) -> None:
92
+ # Soft wrap: rich never breaks a line itself, so a verify command stays one copyable line.
93
+ console.print(text, soft_wrap=True)
94
+
95
+
96
+ def _print_step(item, key: str) -> None:
97
+ index = resolve_step(item, key)
98
+ entry = item.items("steps")[index]
99
+ key = step_key(entry, index)
100
+ _print(Text.assemble((item.id, "bold cyan"), " step ", (key, "bold"), (f" ({item.area})", "dim")))
101
+
102
+ # Any other key the step carries is printed too: short scalars beside the head, the rest as blocks.
103
+ extra = [k for k in entry if k not in STEP_HEAD and k not in STEP_BODY]
104
+ inline = [k for k in extra if _is_short(entry[k])]
105
+ head = [*STEP_HEAD, *inline]
106
+ width = max(len(k) for k in head) + 1
107
+ for name in head:
108
+ value = as_text(entry.get(name))
109
+ style = TONE_STYLE[tone_of("steps", value)] if name == "status" and value != "-" else ""
110
+ _print(Text.assemble((f"{name + ':':<{width}} ", "dim"), (value, style)))
111
+
112
+ for name in [*STEP_BODY, *(k for k in extra if k not in inline)]:
113
+ _print(Text())
114
+ _print(Text(f"{name}:", style="bold"))
115
+ for line in _block(entry.get(name)):
116
+ _print(Text(INDENT + line if line else ""))
117
+
118
+
119
+ def _plan_list(item, name: str) -> list:
120
+ """A plan-level list as written: `acceptance` holds mappings or plain strings, `non_goals` mostly strings."""
121
+ value = item.raw.get(name)
122
+ if value is None or value == "" or value == []:
123
+ return []
124
+ return [entry for entry in (value if isinstance(value, list) else [value]) if entry is not None]
125
+
126
+
127
+ def criterion_refs(value: object) -> list[str]:
128
+ """The criterion ids a step's `acceptance` names, once each, in written order."""
129
+ found: list[str] = []
130
+ for entry in value if isinstance(value, list) else [value]:
131
+ if entry is None or isinstance(entry, (dict, list)):
132
+ continue
133
+ for token in re.split(r"[\s,;]+", as_text(entry)):
134
+ if CRITERION_ID.match(token) and token not in found:
135
+ found.append(token)
136
+ return found
137
+
138
+
139
+ def _steps_by_criterion(item) -> dict[str, list[str]]:
140
+ named: dict[str, list[str]] = {}
141
+ for index, entry in enumerate(item.items("steps")):
142
+ for ref in criterion_refs(entry.get("acceptance")):
143
+ named.setdefault(ref, []).append(step_key(entry, index))
144
+ return named
145
+
146
+
147
+ def _print_whole(label: str, value: object, fields: list[tuple[str, object]], badge: Text | None = None) -> None:
148
+ """One entry laid out as the board lays it out, label then text then `key: value` lines, but nothing clipped."""
149
+ first, *rest = _block(value)
150
+ _print(Text.assemble(" ", (f"{label:>3}", "bold"), " ", first, *((" ", badge) if badge else ())))
151
+ for line in rest:
152
+ _print(Text(FIELD_INDENT + line if line else ""))
153
+ for key, field in fields:
154
+ lines = _block(field)
155
+ if len(lines) == 1:
156
+ _print(Text.assemble(FIELD_INDENT, (f"{key}:", "dim"), " ", lines[0]))
157
+ continue
158
+ _print(Text.assemble(FIELD_INDENT, (f"{key}:", "dim")))
159
+ for line in lines:
160
+ _print(Text(FIELD_INDENT + INDENT + line if line else ""))
161
+
162
+
163
+ def _render_acceptance(item, asked: bool) -> None:
164
+ criteria = _plan_list(item, "acceptance")
165
+ named = _steps_by_criterion(item)
166
+ if not criteria and not asked:
167
+ return
168
+ count = f"({len(criteria)})" if criteria else "(none in this plan)"
169
+ _print(Text.assemble("\n", ("Acceptance", "bold cyan"), " ", (count, "dim")))
170
+ ids: set[str] = set()
171
+ for index, entry in enumerate(criteria):
172
+ if not isinstance(entry, dict):
173
+ _print_whole(str(index), entry, [])
174
+ continue
175
+ text_key = next((key for key in CRITERION_TEXT if key in entry), None)
176
+ status = entry.get("status")
177
+ badge = Text(as_text(status), style=TONE_STYLE[tone_of("acceptance", as_text(status))]) if status else None
178
+ rest = [(key, value) for key, value in entry.items() if key not in ("id", "status", text_key)]
179
+ # verify first, the way a reader checks a criterion; any other key after it, as written.
180
+ fields = sorted(rest, key=lambda pair: pair[0] != "verify")
181
+ label = str(index) if entry.get("id") is None else str(entry["id"])
182
+ if entry.get("id") is not None:
183
+ ids.add(label)
184
+ fields.append(("steps", key_runs(named.get(label, []))))
185
+ _print_whole(label, entry.get(text_key) if text_key else "?", fields, badge)
186
+ unknown = [f"{ref} (step {key_runs(keys)})" for ref, keys in named.items() if ref not in ids]
187
+ if unknown:
188
+ _print(Text.assemble((" steps name criteria this plan does not have: ", "yellow"), "; ".join(unknown)))
189
+
190
+
191
+ def _render_non_goals(item, asked: bool) -> None:
192
+ goals = _plan_list(item, "non_goals")
193
+ if not goals and not asked:
194
+ return
195
+ count = f"({len(goals)})" if goals else "(none in this plan)"
196
+ _print(Text.assemble("\n", ("Non-goals", "bold cyan"), " ", (count, "dim")))
197
+ for index, entry in enumerate(goals):
198
+ _print_whole(str(index), entry, [])
199
+
200
+
201
+ def _count_plan_lists(item) -> None:
202
+ """The board stays short: one line per plan-level list, saying how to print it whole."""
203
+ for name in PLAN_LISTS:
204
+ entries = _plan_list(item, name)
205
+ if entries:
206
+ _print(
207
+ Text.assemble(
208
+ "\n", (SECTION_TITLES[name], "bold cyan"), (f" ({len(entries)}) in full: --section {name}", "dim")
209
+ )
210
+ )
211
+
212
+
213
+ @click.command(
214
+ "show",
215
+ help="Read one exec-plan in the terminal, or one of its steps in full with --step. "
216
+ "The board counts the plan's acceptance criteria and non-goals; --section acceptance or "
217
+ "--section non_goals prints them in full.",
218
+ )
219
+ @harness_option
220
+ @click.argument("plan_id")
221
+ @click.option(
222
+ "--section",
223
+ "sections",
224
+ multiple=True,
225
+ type=click.Choice(SHOW_SECTIONS),
226
+ help="Only print some sections. acceptance prints each criterion whole (id, criterion, verify, the steps "
227
+ "naming it); non_goals prints each non-goal whole.",
228
+ )
229
+ @click.option(
230
+ "--full",
231
+ is_flag=True,
232
+ help="Print notes verbatim instead of clipping to one line, and acceptance and non_goals in full.",
233
+ )
234
+ @click.option(
235
+ "--step",
236
+ "step_id",
237
+ metavar="ID",
238
+ help="Print only this step, every field as written: what, verify, note, acceptance and evidence unclipped.",
239
+ )
240
+ def show(harness_path, plan_id, sections, full, step_id):
241
+ item = find_plan(find_manifest(harness_path), plan_id)
242
+ if step_id is not None:
243
+ if sections:
244
+ raise click.UsageError("--step prints one step; leave out --section.")
245
+ _print_step(item, step_id)
246
+ return
247
+
248
+ p = item.progress()
249
+
250
+ console.print(
251
+ Panel(
252
+ " ".join(str(item.raw.get("goal") or "").split()),
253
+ title=f"[bold]{item.id}[/] [dim]({item.area})[/]",
254
+ subtitle=f"{bar(p['steps_done'], p['steps_total'])} {p['steps_done']}/{p['steps_total']} steps "
255
+ f"debt: {p['debt_open']} questions: {p['questions_open']}",
256
+ border_style="cyan",
257
+ )
258
+ )
259
+
260
+ for name in sections or (SHOW_SECTIONS if full else SECTIONS):
261
+ if name == "acceptance":
262
+ _render_acceptance(item, asked=bool(sections))
263
+ continue
264
+ if name == "non_goals":
265
+ _render_non_goals(item, asked=bool(sections))
266
+ continue
267
+ entries = item.items(name)
268
+ if not entries:
269
+ continue
270
+ console.print(f"\n[bold cyan]{SECTION_TITLES[name]}[/] [dim]({len(entries)})[/]")
271
+ for index, entry in enumerate(entries):
272
+ _render_item(name, index, entry, full)
273
+ if not sections and not full:
274
+ _count_plan_lists(item)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -417,6 +417,8 @@ evo harness seams # contract seams; --graph for the owner -> consumer DAG
417
417
  evo harness plans # progress across every exec-plan
418
418
  evo harness show <plan> # every section, open questions with status and blocking
419
419
  evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
420
+ evo harness show <plan> --section acceptance # each criterion in full: id, criterion, verify, steps naming it
421
+ evo harness show <plan> --section non_goals # each non-goal in full
420
422
  evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
421
423
  evo harness check <plan> [--fetch] # what the plan claims vs what git says
422
424
  evo harness graph <plan>:steps # the same DAG as an adjacency list
@@ -1,4 +1,5 @@
1
- """`evo harness show --step`, the open questions `show` lists, and the depends_on column of the step board."""
1
+ """`evo harness show --step`, `--section acceptance` and `--section non_goals`, the open questions `show` lists,
2
+ and the depends_on column of the step board."""
2
3
 
3
4
  from __future__ import annotations
4
5
 
@@ -11,6 +12,7 @@ from click.testing import CliRunner
11
12
  from evo_cli.cli import cli
12
13
  from evo_cli.commands.harness._dag import step_depends
13
14
  from evo_cli.commands.harness._render import key_runs
15
+ from evo_cli.commands.harness.show import criterion_refs
14
16
 
15
17
  MIRROR_FIXTURE = Path(__file__).parent / "fixtures" / "hub" / "mirror-demo.yaml"
16
18
 
@@ -265,3 +267,195 @@ def test_step_depends_reads_every_forward_spelling_once_in_written_order():
265
267
  "4",
266
268
  "x",
267
269
  ]
270
+
271
+
272
+ # show --section acceptance / non_goals
273
+
274
+ LONG_CRITERION = (
275
+ "A criterion long enough to run past the sixty columns rich is given, so the acceptance section has to "
276
+ "print it whole on one line, with [Tính năng AI] in brackets that are not markup."
277
+ )
278
+ CRITERIA_PLAN = (
279
+ textwrap.dedent(
280
+ """\
281
+ id: graded
282
+ goal: A plan whose steps cite its acceptance criteria.
283
+ steps:
284
+ - id: 1
285
+ what: Build it.
286
+ acceptance: [a1, a2, Prose that mentions no id.]
287
+ status: done
288
+ - id: 2
289
+ what: Check it.
290
+ acceptance: "a1; a3"
291
+ - id: 3
292
+ what: Cites a criterion the plan lacks.
293
+ acceptance: [a9]
294
+ - id: 4
295
+ what: No acceptance at all.
296
+ acceptance:
297
+ - id: a1
298
+ criterion: >
299
+ LONG_HERE
300
+ verify: >
301
+ VERIFY_HERE
302
+ - id: a2
303
+ criterion: |
304
+ Two lines,
305
+ kept apart.
306
+ verify: pytest -q tests/test_two.py
307
+ invariant: true
308
+ - id: a3
309
+ what: Written with what instead of criterion.
310
+ status: met-in-code
311
+ evidence: abc1234 merged
312
+ - id: a4
313
+ criterion: Nobody names this one.
314
+ verify: make check
315
+ non_goals:
316
+ - Rewriting the scheduler.
317
+ - |
318
+ A non-goal over
319
+ two lines.
320
+ - Renderer for drag and drop: belongs to another plan.
321
+ """
322
+ )
323
+ .replace("LONG_HERE", LONG_CRITERION)
324
+ .replace("VERIFY_HERE", VERIFY)
325
+ )
326
+
327
+ STRING_PLAN = textwrap.dedent(
328
+ """\
329
+ id: plain
330
+ goal: An older plan whose criteria are plain strings.
331
+ steps:
332
+ - id: 1
333
+ what: Only step.
334
+ acceptance:
335
+ - The first criterion.
336
+ - |
337
+ A criterion over
338
+ two lines.
339
+ non_goals:
340
+ - Nothing else.
341
+ """
342
+ )
343
+
344
+
345
+ def test_show_acceptance_prints_each_criterion_whole_with_verify_and_the_steps_naming_it(tmp_path):
346
+ root = _harness(tmp_path, body=CRITERIA_PLAN, name="graded")
347
+
348
+ result = _run("show", "graded", "--section", "acceptance", "--harness", str(root), columns=60)
349
+
350
+ assert result.exit_code == 0, result.output
351
+ lines = _lines(result.output)
352
+ start = lines.index("Acceptance (4)")
353
+ body = lines[start + 1 :]
354
+ # Longer than the 60 columns rich was given, yet whole on one line; the brackets survive as text.
355
+ assert body[0] == " a1 " + LONG_CRITERION
356
+ assert body[1] == " verify: " + VERIFY
357
+ assert body[2] == " steps: 1, 2"
358
+ assert body[3:7] == [
359
+ " a2 Two lines,",
360
+ " kept apart.",
361
+ " verify: pytest -q tests/test_two.py",
362
+ " invariant: true",
363
+ ]
364
+ assert body[7] == " steps: 1"
365
+ assert body[8].split() == ["a3", "Written", "with", "what", "instead", "of", "criterion.", "met-in-code"]
366
+ assert body[9:11] == [" evidence: abc1234 merged", " steps: 2"]
367
+ assert body[11:14] == [" a4 Nobody names this one.", " verify: make check", " steps: -"]
368
+ assert body[14] == " steps name criteria this plan does not have: a9 (step 3)"
369
+ assert len(body) == 15
370
+ # Only the asked section: no step board, no non-goals.
371
+ assert "Steps" not in " ".join(lines) and "Non-goals" not in result.output
372
+
373
+
374
+ def test_show_acceptance_prints_plain_string_criteria_by_position(tmp_path):
375
+ root = _harness(tmp_path, body=STRING_PLAN, name="plain")
376
+
377
+ result = _run("show", "plain", "--section", "acceptance", "--harness", str(root), columns=60)
378
+
379
+ assert result.exit_code == 0, result.output
380
+ lines = _lines(result.output)
381
+ start = lines.index("Acceptance (2)")
382
+ assert lines[start + 1 :] == [
383
+ " 0 The first criterion.",
384
+ " 1 A criterion over",
385
+ " two lines.",
386
+ ]
387
+
388
+
389
+ def test_show_non_goals_prints_each_non_goal_whole(tmp_path):
390
+ root = _harness(tmp_path, body=CRITERIA_PLAN, name="graded")
391
+
392
+ result = _run("show", "graded", "--section", "non_goals", "--harness", str(root), columns=40)
393
+
394
+ assert result.exit_code == 0, result.output
395
+ lines = _lines(result.output)
396
+ start = lines.index("Non-goals (3)")
397
+ assert lines[start + 1 :] == [
398
+ " 0 Rewriting the scheduler.",
399
+ " 1 A non-goal over",
400
+ " two lines.",
401
+ # An unquoted "key: value" in YAML parses as a mapping; it prints back as the line it was.
402
+ " 2 Renderer for drag and drop: belongs to another plan.",
403
+ ]
404
+ assert "Acceptance" not in result.output
405
+
406
+
407
+ def test_show_acceptance_and_non_goals_say_so_when_the_plan_has_none(tmp_path):
408
+ root = _harness(
409
+ tmp_path, body="id: bare\ngoal: Nothing to accept.\nsteps:\n - id: 1\n what: Only step.\n", name="bare"
410
+ )
411
+
412
+ result = _run(
413
+ "show", "bare", "--section", "acceptance", "--section", "non_goals", "--harness", str(root), columns=2000
414
+ )
415
+
416
+ assert result.exit_code == 0, result.output
417
+ lines = _lines(result.output)
418
+ assert "Acceptance (none in this plan)" in lines
419
+ assert "Non-goals (none in this plan)" in lines
420
+ # Without --section, a plan with neither list prints no line for them.
421
+ default = _run("show", "bare", "--harness", str(root), columns=2000)
422
+ assert default.exit_code == 0, default.output
423
+ assert "Acceptance" not in default.output and "Non-goals" not in default.output
424
+
425
+
426
+ def test_show_board_counts_acceptance_and_non_goals_and_full_prints_them(tmp_path):
427
+ root = _harness(tmp_path, body=CRITERIA_PLAN, name="graded")
428
+
429
+ board = _run("show", "graded", "--harness", str(root), columns=200)
430
+
431
+ assert board.exit_code == 0, board.output
432
+ lines = [line.strip() for line in _lines(board.output)]
433
+ assert lines[-3:] == [
434
+ "Acceptance (4) in full: --section acceptance",
435
+ "",
436
+ "Non-goals (3) in full: --section non_goals",
437
+ ]
438
+ assert LONG_CRITERION not in board.output
439
+
440
+ full = _run("show", "graded", "--full", "--harness", str(root), columns=200)
441
+
442
+ assert full.exit_code == 0, full.output
443
+ lines = _lines(full.output)
444
+ assert " a1 " + LONG_CRITERION in lines
445
+ assert " 0 Rewriting the scheduler." in lines
446
+ assert "in full:" not in full.output
447
+
448
+
449
+ def test_section_choice_lists_acceptance_and_non_goals_in_help():
450
+ result = _run("show", "--help", columns=200)
451
+
452
+ assert result.exit_code == 0, result.output
453
+ assert "acceptance" in result.output and "non_goals" in result.output
454
+
455
+
456
+ def test_criterion_refs_reads_ids_from_a_list_or_text_and_skips_prose():
457
+ assert criterion_refs(None) == []
458
+ assert criterion_refs("a1; a9") == ["a1", "a9"]
459
+ assert criterion_refs("a1, a2 a1") == ["a1", "a2"]
460
+ assert criterion_refs(["a3", "Prose naming a10 in passing.", "a2;a4", {"a5": "x"}]) == ["a3", "a10", "a2", "a4"]
461
+ assert criterion_refs(["A1", "a1.", "ab1", "a"]) == []
@@ -1 +0,0 @@
1
- 0.30.0