evo-cli 0.29.2__tar.gz → 0.30.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.
- {evo_cli-0.29.2 → evo_cli-0.30.0}/PKG-INFO +4 -2
- {evo_cli-0.29.2 → evo_cli-0.30.0}/README.md +3 -1
- evo_cli-0.30.0/evo_cli/VERSION +1 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/__init__.py +1 -1
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_dag.py +23 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_render.py +34 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/edit.py +3 -13
- evo_cli-0.30.0/evo_cli/commands/harness/show.py +144 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/views.py +8 -4
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/PKG-INFO +4 -2
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/SOURCES.txt +1 -0
- evo_cli-0.30.0/tests/test_harness_show.py +267 -0
- evo_cli-0.29.2/evo_cli/VERSION +0 -1
- evo_cli-0.29.2/evo_cli/commands/harness/show.py +0 -68
- {evo_cli-0.29.2 → evo_cli-0.30.0}/Containerfile +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/HISTORY.md +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/LICENSE +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/MANIFEST.in +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/base.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_hub.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_mirror.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/image.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/ngrok.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/openvpn.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/_dns.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/_dpi.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/_hosts.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/_probe.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/check.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/fix.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/probe.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/route/status.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/storage.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/console.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/align.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/core.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/creds.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/errors.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/gemini.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/imaging/ncnn.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/openvpn.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/gemini.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/pyproject.toml +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/setup.cfg +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/__init__.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/fake_evo_agents.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/fixtures/hub/contract-0.2.0.json +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/fixtures/hub/mirror-demo.yaml +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_cli.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_console.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_cred.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_download.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_gdrive.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_gh.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_harness.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_harness_hub.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_image.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_ngrok.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_opencode.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_openvpn.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_route.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_serp.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_storage.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_tts.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.0}/tests/test_update.py +0 -0
- {evo_cli-0.29.2 → evo_cli-0.30.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.
|
|
3
|
+
Version: 0.30.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
|
|
@@ -415,7 +415,9 @@ evo harness serve # dashboard on http://127.0.0.1:8788
|
|
|
415
415
|
evo harness repos # repos in the manifest, with branch and dirty state
|
|
416
416
|
evo harness seams # contract seams; --graph for the owner -> consumer DAG
|
|
417
417
|
evo harness plans # progress across every exec-plan
|
|
418
|
-
evo harness show <plan>
|
|
418
|
+
evo harness show <plan> # every section, open questions with status and blocking
|
|
419
|
+
evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
|
|
420
|
+
evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
|
|
419
421
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
420
422
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
421
423
|
evo harness clone # clone every declared repo into its declared path
|
|
@@ -382,7 +382,9 @@ evo harness serve # dashboard on http://127.0.0.1:8788
|
|
|
382
382
|
evo harness repos # repos in the manifest, with branch and dirty state
|
|
383
383
|
evo harness seams # contract seams; --graph for the owner -> consumer DAG
|
|
384
384
|
evo harness plans # progress across every exec-plan
|
|
385
|
-
evo harness show <plan>
|
|
385
|
+
evo harness show <plan> # every section, open questions with status and blocking
|
|
386
|
+
evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
|
|
387
|
+
evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
|
|
386
388
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
387
389
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
388
390
|
evo harness clone # clone every declared repo into its declared path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.30.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
|
|
21
|
+
evo harness show <plan> read one plan (--step <id> prints one step 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)
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import itertools
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import rich_click as click
|
|
7
|
+
|
|
6
8
|
from evo_cli.commands.harness._model import Plan, cluster, load_seams, step_title, tone_of
|
|
7
9
|
|
|
8
10
|
CLUSTER_REPO = "__cluster__"
|
|
@@ -285,6 +287,17 @@ def step_key(entry: dict, index: int) -> str:
|
|
|
285
287
|
return str(index)
|
|
286
288
|
|
|
287
289
|
|
|
290
|
+
def resolve_step(plan: Plan, key) -> int:
|
|
291
|
+
"""Index of the step whose key is KEY. An unknown key fails with every key the plan holds."""
|
|
292
|
+
entries = plan.items("steps")
|
|
293
|
+
wanted = str(key)
|
|
294
|
+
for index, entry in enumerate(entries):
|
|
295
|
+
if step_key(entry, index) == wanted:
|
|
296
|
+
return index
|
|
297
|
+
known = ", ".join(step_key(e, i) for i, e in enumerate(entries)) or "(the plan has no steps)"
|
|
298
|
+
raise click.ClickException(f"No step {wanted!r} in {plan.id}. Available: {known}")
|
|
299
|
+
|
|
300
|
+
|
|
288
301
|
def _as_list(value) -> list:
|
|
289
302
|
if value is None:
|
|
290
303
|
return []
|
|
@@ -293,6 +306,16 @@ def _as_list(value) -> list:
|
|
|
293
306
|
return [value]
|
|
294
307
|
|
|
295
308
|
|
|
309
|
+
def step_depends(entry: dict) -> list[str]:
|
|
310
|
+
"""Keys of the steps this one waits on, in written order, from every spelling the step graph reads."""
|
|
311
|
+
found: list[str] = []
|
|
312
|
+
for name in FORWARD_KEYS:
|
|
313
|
+
for raw in _as_list(entry.get(name)):
|
|
314
|
+
if str(raw) not in found:
|
|
315
|
+
found.append(str(raw))
|
|
316
|
+
return found
|
|
317
|
+
|
|
318
|
+
|
|
296
319
|
def plan_step_graph(plan: Plan) -> dict:
|
|
297
320
|
steps = plan.items("steps")
|
|
298
321
|
repo_entries = plan.items("repos")
|
|
@@ -31,6 +31,40 @@ def one_line(value: object, width: int) -> str:
|
|
|
31
31
|
return text if len(text) <= width else text[: max(1, width - 3)] + "..."
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def as_text(value: object) -> str:
|
|
35
|
+
"""A plan value the way YAML spells it: booleans as true/false, lists joined, nothing as `-`."""
|
|
36
|
+
if value is None or value == "" or value == []:
|
|
37
|
+
return "-"
|
|
38
|
+
if isinstance(value, bool):
|
|
39
|
+
return "true" if value else "false"
|
|
40
|
+
if isinstance(value, (list, tuple)):
|
|
41
|
+
return ", ".join(as_text(v) for v in value)
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def key_runs(keys: list[str]) -> str:
|
|
46
|
+
"""Step keys in written order, with a run of three or more consecutive integers shown as `first-last`."""
|
|
47
|
+
parts: list[str] = []
|
|
48
|
+
run: list[int] = []
|
|
49
|
+
|
|
50
|
+
def flush() -> None:
|
|
51
|
+
parts.extend([f"{run[0]}-{run[-1]}"] if len(run) >= 3 else [str(n) for n in run])
|
|
52
|
+
run.clear()
|
|
53
|
+
|
|
54
|
+
for key in keys:
|
|
55
|
+
number = int(key) if key.isascii() and key.isdigit() and str(int(key)) == key else None
|
|
56
|
+
if number is not None and run and number == run[-1] + 1:
|
|
57
|
+
run.append(number)
|
|
58
|
+
continue
|
|
59
|
+
flush()
|
|
60
|
+
if number is None:
|
|
61
|
+
parts.append(key)
|
|
62
|
+
else:
|
|
63
|
+
run.append(number)
|
|
64
|
+
flush()
|
|
65
|
+
return ", ".join(parts) or "-"
|
|
66
|
+
|
|
67
|
+
|
|
34
68
|
def title_width(reserved: int = 26) -> int:
|
|
35
69
|
return max(28, min(console.width, 140) - reserved)
|
|
36
70
|
|
|
@@ -5,7 +5,7 @@ from datetime import datetime
|
|
|
5
5
|
import rich_click as click
|
|
6
6
|
from rich.markup import escape
|
|
7
7
|
|
|
8
|
-
from evo_cli.commands.harness._dag import step_key
|
|
8
|
+
from evo_cli.commands.harness._dag import resolve_step, step_key
|
|
9
9
|
from evo_cli.commands.harness._model import find_plan, step_title, tone_of
|
|
10
10
|
from evo_cli.commands.harness._mutate import update_item
|
|
11
11
|
from evo_cli.commands.harness._paths import find_manifest, harness_option
|
|
@@ -70,16 +70,6 @@ def _write(manifest_path, plan_id, section, index, status, note, no_date, eviden
|
|
|
70
70
|
console.print(f" [yellow]note:[/] {escape(text)}")
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def _resolve_step(item, key) -> int:
|
|
74
|
-
entries = item.items("steps")
|
|
75
|
-
wanted = str(key)
|
|
76
|
-
for index, entry in enumerate(entries):
|
|
77
|
-
if step_key(entry, index) == wanted:
|
|
78
|
-
return index
|
|
79
|
-
known = ", ".join(step_key(e, i) for i, e in enumerate(entries))
|
|
80
|
-
raise click.ClickException(f"No step {wanted!r} in {item.id}. Available: {known}")
|
|
81
|
-
|
|
82
|
-
|
|
83
73
|
def _pick(item, key, status):
|
|
84
74
|
"""Interactive picker, but only when questionary is installed and we own a real terminal."""
|
|
85
75
|
try:
|
|
@@ -104,7 +94,7 @@ def _pick(item, key, status):
|
|
|
104
94
|
raise click.Abort()
|
|
105
95
|
|
|
106
96
|
if status is None:
|
|
107
|
-
index =
|
|
97
|
+
index = resolve_step(item, key)
|
|
108
98
|
current = str(entries[index].get("status") or "")
|
|
109
99
|
suggested = NEXT_STATUS.get(current, STEP_STATUS[0])
|
|
110
100
|
status = questionary.select(
|
|
@@ -150,7 +140,7 @@ def step(harness_path, plan_id, key, status, note, evidence, no_date, no_input):
|
|
|
150
140
|
if key is None or status is None:
|
|
151
141
|
return
|
|
152
142
|
|
|
153
|
-
_write(manifest_path, plan_id, "steps",
|
|
143
|
+
_write(manifest_path, plan_id, "steps", resolve_step(item, key), status, note, no_date, evidence)
|
|
154
144
|
console.print()
|
|
155
145
|
step_board(find_plan(manifest_path, plan_id), highlight=key)
|
|
156
146
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import rich_click as click
|
|
4
|
+
import yaml
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from evo_cli.commands.harness._dag import resolve_step, step_key
|
|
9
|
+
from evo_cli.commands.harness._model import SECTIONS, find_plan, plain, tone_of
|
|
10
|
+
from evo_cli.commands.harness._paths import find_manifest, harness_option
|
|
11
|
+
from evo_cli.commands.harness._render import TONE_STYLE, as_text, bar, one_line, title_width
|
|
12
|
+
from evo_cli.console import console
|
|
13
|
+
|
|
14
|
+
SECTION_TITLES = {
|
|
15
|
+
"references": "References",
|
|
16
|
+
"repos": "Repos and merge order",
|
|
17
|
+
"steps": "Steps",
|
|
18
|
+
"decisions": "Decisions",
|
|
19
|
+
"tech_debt": "Tech debt",
|
|
20
|
+
"open_questions": "Open questions",
|
|
21
|
+
}
|
|
22
|
+
HIDE_KEYS = {"status", "order", "what", "repo", "id"}
|
|
23
|
+
# Every open question says whether it blocks, even one whose author left the key out.
|
|
24
|
+
ALWAYS_SHOWN = {"open_questions": ("blocking",)}
|
|
25
|
+
|
|
26
|
+
STEP_HEAD = ("id", "title", "status", "repo", "depends_on", "blocking", "done_at")
|
|
27
|
+
STEP_BODY = ("what", "verify", "note", "acceptance", "evidence")
|
|
28
|
+
INDENT = " "
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _render_item(section: str, index: int, item: dict, full: bool) -> None:
|
|
32
|
+
status = item.get("status")
|
|
33
|
+
style = TONE_STYLE[tone_of(section, status)]
|
|
34
|
+
authored = item.get("title")
|
|
35
|
+
titled = isinstance(authored, str) and bool(authored.strip())
|
|
36
|
+
if titled:
|
|
37
|
+
head, hidden = authored.strip(), (HIDE_KEYS - {"what"}) | {"title"}
|
|
38
|
+
else:
|
|
39
|
+
head = item.get("what") or item.get("repo") or item.get("issue") or "?"
|
|
40
|
+
hidden = HIDE_KEYS | ({"issue"} if head == item.get("issue") else set())
|
|
41
|
+
label = item.get("id", item.get("order", index))
|
|
42
|
+
badge = f"[{style}]{status}[/]" if status else ""
|
|
43
|
+
if not full:
|
|
44
|
+
head = one_line(head, title_width(reserved=18))
|
|
45
|
+
console.print(f" [bold]{label:>3}[/] {head} {badge}")
|
|
46
|
+
fields = {**item, **{key: item.get(key) for key in ALWAYS_SHOWN.get(section, ())}}
|
|
47
|
+
for key, value in fields.items():
|
|
48
|
+
if key in hidden:
|
|
49
|
+
continue
|
|
50
|
+
if isinstance(value, list):
|
|
51
|
+
value = "; ".join(as_text(v) for v in value) or "-"
|
|
52
|
+
text = " ".join(as_text(value).split())
|
|
53
|
+
console.print(f" [dim]{key}:[/] {text if full else one_line(text, title_width(reserved=18))}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _block(value: object) -> list[str]:
|
|
57
|
+
"""Lines of a value printed under its key: text keeps its line breaks, a list becomes `- ` items."""
|
|
58
|
+
if value is None or value == "" or value == []:
|
|
59
|
+
return ["-"]
|
|
60
|
+
if isinstance(value, list):
|
|
61
|
+
lines = []
|
|
62
|
+
for entry in value:
|
|
63
|
+
first, *rest = _block(entry)
|
|
64
|
+
lines.append(f"- {first}")
|
|
65
|
+
lines.extend(f" {line}" if line else "" for line in rest)
|
|
66
|
+
return lines
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
dumped = yaml.safe_dump(plain(value), allow_unicode=True, sort_keys=False, width=1_000_000)
|
|
69
|
+
return dumped.rstrip("\n").split("\n")
|
|
70
|
+
return as_text(value).rstrip("\n").split("\n")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_short(value: object) -> bool:
|
|
74
|
+
nested = isinstance(value, dict) or (isinstance(value, list) and any(isinstance(v, (dict, list)) for v in value))
|
|
75
|
+
text = as_text(value)
|
|
76
|
+
return not nested and "\n" not in text and len(text) <= 80
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _print(text: Text) -> None:
|
|
80
|
+
# Soft wrap: rich never breaks a line itself, so a verify command stays one copyable line.
|
|
81
|
+
console.print(text, soft_wrap=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _print_step(item, key: str) -> None:
|
|
85
|
+
index = resolve_step(item, key)
|
|
86
|
+
entry = item.items("steps")[index]
|
|
87
|
+
key = step_key(entry, index)
|
|
88
|
+
_print(Text.assemble((item.id, "bold cyan"), " step ", (key, "bold"), (f" ({item.area})", "dim")))
|
|
89
|
+
|
|
90
|
+
# Any other key the step carries is printed too: short scalars beside the head, the rest as blocks.
|
|
91
|
+
extra = [k for k in entry if k not in STEP_HEAD and k not in STEP_BODY]
|
|
92
|
+
inline = [k for k in extra if _is_short(entry[k])]
|
|
93
|
+
head = [*STEP_HEAD, *inline]
|
|
94
|
+
width = max(len(k) for k in head) + 1
|
|
95
|
+
for name in head:
|
|
96
|
+
value = as_text(entry.get(name))
|
|
97
|
+
style = TONE_STYLE[tone_of("steps", value)] if name == "status" and value != "-" else ""
|
|
98
|
+
_print(Text.assemble((f"{name + ':':<{width}} ", "dim"), (value, style)))
|
|
99
|
+
|
|
100
|
+
for name in [*STEP_BODY, *(k for k in extra if k not in inline)]:
|
|
101
|
+
_print(Text())
|
|
102
|
+
_print(Text(f"{name}:", style="bold"))
|
|
103
|
+
for line in _block(entry.get(name)):
|
|
104
|
+
_print(Text(INDENT + line if line else ""))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.command("show", help="Read one exec-plan in the terminal, or one of its steps in full with --step.")
|
|
108
|
+
@harness_option
|
|
109
|
+
@click.argument("plan_id")
|
|
110
|
+
@click.option("--section", "sections", multiple=True, type=click.Choice(SECTIONS), help="Only print some sections.")
|
|
111
|
+
@click.option("--full", is_flag=True, help="Print notes verbatim instead of clipping to one line.")
|
|
112
|
+
@click.option(
|
|
113
|
+
"--step",
|
|
114
|
+
"step_id",
|
|
115
|
+
metavar="ID",
|
|
116
|
+
help="Print only this step, every field as written: what, verify, note, acceptance and evidence unclipped.",
|
|
117
|
+
)
|
|
118
|
+
def show(harness_path, plan_id, sections, full, step_id):
|
|
119
|
+
item = find_plan(find_manifest(harness_path), plan_id)
|
|
120
|
+
if step_id is not None:
|
|
121
|
+
if sections:
|
|
122
|
+
raise click.UsageError("--step prints one step; leave out --section.")
|
|
123
|
+
_print_step(item, step_id)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
p = item.progress()
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
Panel(
|
|
130
|
+
" ".join(str(item.raw.get("goal") or "").split()),
|
|
131
|
+
title=f"[bold]{item.id}[/] [dim]({item.area})[/]",
|
|
132
|
+
subtitle=f"{bar(p['steps_done'], p['steps_total'])} {p['steps_done']}/{p['steps_total']} steps "
|
|
133
|
+
f"debt: {p['debt_open']} questions: {p['questions_open']}",
|
|
134
|
+
border_style="cyan",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for name in sections or SECTIONS:
|
|
139
|
+
entries = item.items(name)
|
|
140
|
+
if not entries:
|
|
141
|
+
continue
|
|
142
|
+
console.print(f"\n[bold cyan]{SECTION_TITLES[name]}[/] [dim]({len(entries)})[/]")
|
|
143
|
+
for index, entry in enumerate(entries):
|
|
144
|
+
_render_item(name, index, entry, full)
|
|
@@ -2,12 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import rich_click as click
|
|
4
4
|
|
|
5
|
-
from evo_cli.commands.harness._dag import plan_repo_graph, plan_step_graph, seam_graph, step_key
|
|
5
|
+
from evo_cli.commands.harness._dag import plan_repo_graph, plan_step_graph, seam_graph, step_depends, step_key
|
|
6
6
|
from evo_cli.commands.harness._model import cluster, find_plan, load_plans, load_seams, step_title, tone_of
|
|
7
7
|
from evo_cli.commands.harness._paths import find_manifest, git, harness_option
|
|
8
8
|
from evo_cli.commands.harness._render import (
|
|
9
9
|
TONE_STYLE,
|
|
10
10
|
bar,
|
|
11
|
+
key_runs,
|
|
11
12
|
one_line,
|
|
12
13
|
print_graph,
|
|
13
14
|
print_warnings,
|
|
@@ -144,18 +145,21 @@ def step_board(item, highlight=None) -> None:
|
|
|
144
145
|
console.print("[dim] This plan has no steps yet.[/]")
|
|
145
146
|
return
|
|
146
147
|
|
|
147
|
-
width = title_width()
|
|
148
|
-
grid = table("", "#", "REPO", "STATUS", "STEP")
|
|
148
|
+
width = title_width(reserved=38)
|
|
149
|
+
grid = table("", "#", "REPO", "STATUS", "DEPENDS ON", "STEP")
|
|
149
150
|
for index, entry in enumerate(steps):
|
|
150
151
|
status = entry.get("status")
|
|
151
152
|
style = TONE_STYLE[tone_of("steps", status)]
|
|
152
153
|
key = step_key(entry, index)
|
|
153
154
|
marker = "[bold cyan]>[/]" if highlight is not None and key == str(highlight) else " "
|
|
155
|
+
# Nearly every step is blocking, so only the exception is marked.
|
|
156
|
+
optional = " [dim](non-blocking)[/]" if entry.get("blocking") is False else ""
|
|
154
157
|
grid.add_row(
|
|
155
158
|
marker,
|
|
156
159
|
f"[bold]{key}[/]",
|
|
157
160
|
str(entry.get("repo") or "-"),
|
|
158
161
|
f"[{style}]{status or '-'}[/]",
|
|
159
|
-
|
|
162
|
+
key_runs(step_depends(entry)),
|
|
163
|
+
step_title(entry, width) + optional,
|
|
160
164
|
)
|
|
161
165
|
console.print(grid)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.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
|
|
@@ -415,7 +415,9 @@ evo harness serve # dashboard on http://127.0.0.1:8788
|
|
|
415
415
|
evo harness repos # repos in the manifest, with branch and dirty state
|
|
416
416
|
evo harness seams # contract seams; --graph for the owner -> consumer DAG
|
|
417
417
|
evo harness plans # progress across every exec-plan
|
|
418
|
-
evo harness show <plan>
|
|
418
|
+
evo harness show <plan> # every section, open questions with status and blocking
|
|
419
|
+
evo harness show <plan> --step 3 # one step in full: what, verify, note, acceptance, evidence
|
|
420
|
+
evo harness step <plan> --no-input # the step board: repo, status, depends_on, title
|
|
419
421
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
420
422
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
421
423
|
evo harness clone # clone every declared repo into its declared path
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""`evo harness show --step`, the open questions `show` lists, and the depends_on column of the step board."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import textwrap
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from evo_cli.cli import cli
|
|
12
|
+
from evo_cli.commands.harness._dag import step_depends
|
|
13
|
+
from evo_cli.commands.harness._render import key_runs
|
|
14
|
+
|
|
15
|
+
MIRROR_FIXTURE = Path(__file__).parent / "fixtures" / "hub" / "mirror-demo.yaml"
|
|
16
|
+
|
|
17
|
+
LONG_WHAT = (
|
|
18
|
+
"A long folded sentence that runs on well past any terminal width, so the step view has to print it whole "
|
|
19
|
+
"rather than cut it at the end of the line, with tiếng Việt có dấu đầy đủ at the very end."
|
|
20
|
+
)
|
|
21
|
+
VERIFY = (
|
|
22
|
+
"V=$(git -C ~/github/alpha show origin/main:alpha/__init__.py | sed -n 's/x/\\1/p') "
|
|
23
|
+
'&& test "$(curl -fsS https://example.test/health | jq -r .version)" = "$V"'
|
|
24
|
+
)
|
|
25
|
+
PLAN = textwrap.dedent(
|
|
26
|
+
"""\
|
|
27
|
+
id: demo
|
|
28
|
+
goal: A plan to read one step at a time.
|
|
29
|
+
steps:
|
|
30
|
+
- id: 1
|
|
31
|
+
title: First
|
|
32
|
+
repo: alpha
|
|
33
|
+
what: Start.
|
|
34
|
+
status: done
|
|
35
|
+
done_at: '2026-10-01'
|
|
36
|
+
evidence: 'alpha@abc1234: 12 passed'
|
|
37
|
+
- id: 2
|
|
38
|
+
title: Second
|
|
39
|
+
repo: alpha
|
|
40
|
+
what: Two.
|
|
41
|
+
status: done
|
|
42
|
+
- id: 3
|
|
43
|
+
title: Third
|
|
44
|
+
repo: beta
|
|
45
|
+
what: Three.
|
|
46
|
+
depends_on: [1, 2]
|
|
47
|
+
status: done
|
|
48
|
+
- id: 4
|
|
49
|
+
title: Wrap up
|
|
50
|
+
repo: beta
|
|
51
|
+
what: >
|
|
52
|
+
A long folded sentence that runs on well past any terminal width, so the step view has to print it
|
|
53
|
+
whole rather than cut it at the end of the line, with tiếng Việt có dấu đầy đủ at the very end.
|
|
54
|
+
depends_on: [1, 2, 3]
|
|
55
|
+
verify: >
|
|
56
|
+
VERIFY_HERE
|
|
57
|
+
status: pending
|
|
58
|
+
blocking: true
|
|
59
|
+
note: |
|
|
60
|
+
First line of the note.
|
|
61
|
+
Indented second line [bold]not markup[/bold].
|
|
62
|
+
acceptance:
|
|
63
|
+
- The first criterion.
|
|
64
|
+
- |
|
|
65
|
+
A criterion over
|
|
66
|
+
two lines.
|
|
67
|
+
why: Because the board needs it.
|
|
68
|
+
links: {pr: 12, commits: [abc1234]}
|
|
69
|
+
- id: 5
|
|
70
|
+
title: Optional polish
|
|
71
|
+
repo: beta
|
|
72
|
+
what: Polish.
|
|
73
|
+
depends_on: [4]
|
|
74
|
+
status: pending
|
|
75
|
+
blocking: false
|
|
76
|
+
open_questions:
|
|
77
|
+
- issue: Should the board show blocking?
|
|
78
|
+
status: open
|
|
79
|
+
blocking: false
|
|
80
|
+
note: Only when it is the exception.
|
|
81
|
+
- issue: Which width is enough?
|
|
82
|
+
status: answered
|
|
83
|
+
"""
|
|
84
|
+
).replace("VERIFY_HERE", VERIFY)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _harness(tmp_path: Path, body: str = PLAN, name: str = "demo", hub: bool = False) -> Path:
|
|
88
|
+
root = tmp_path / "cluster"
|
|
89
|
+
(root / "plans" / "active").mkdir(parents=True)
|
|
90
|
+
project = "hub:\n project: demo\n" if hub else ""
|
|
91
|
+
(root / "harness.yaml").write_text(f"name: test-cluster\nrepos:\n - name: alpha\n{project}", encoding="utf-8")
|
|
92
|
+
(root / "plans" / "active" / f"{name}.yaml").write_text(body, encoding="utf-8")
|
|
93
|
+
return root
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run(*args: str, columns: int = 80, env: dict | None = None):
|
|
97
|
+
return CliRunner().invoke(cli, ["harness", *args], env={"COLUMNS": str(columns), **(env or {})})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _lines(output: str) -> list[str]:
|
|
101
|
+
"""Output lines with rich's colour codes taken out (CI makes rich colour even under CliRunner)."""
|
|
102
|
+
return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", output).splitlines()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _flat(output: str) -> str:
|
|
106
|
+
return " ".join(re.sub(r"[─-╿]", " ", "\n".join(_lines(output))).split())
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# show --step
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_show_step_prints_every_field_of_one_step_without_clipping_or_wrapping(tmp_path):
|
|
113
|
+
root = _harness(tmp_path)
|
|
114
|
+
|
|
115
|
+
result = _run("show", "demo", "--step", "4", "--harness", str(root), columns=60)
|
|
116
|
+
|
|
117
|
+
assert result.exit_code == 0, result.output
|
|
118
|
+
lines = _lines(result.output)
|
|
119
|
+
assert lines[0].startswith("demo step 4")
|
|
120
|
+
head = [line.split(":", 1)[0] for line in lines[1:9]]
|
|
121
|
+
assert head == ["id", "title", "status", "repo", "depends_on", "blocking", "done_at", "why"]
|
|
122
|
+
assert any(line.split() == ["depends_on:", "1,", "2,", "3"] for line in lines)
|
|
123
|
+
assert any(line.split() == ["blocking:", "true"] for line in lines)
|
|
124
|
+
assert any(line.split() == ["done_at:", "-"] for line in lines)
|
|
125
|
+
assert any(line.split() == ["why:", "Because", "the", "board", "needs", "it."] for line in lines)
|
|
126
|
+
|
|
127
|
+
body = [line for line in lines if line.endswith(":") and not line.startswith(" ")]
|
|
128
|
+
assert body == ["what:", "verify:", "note:", "acceptance:", "evidence:", "links:"]
|
|
129
|
+
# Longer than the 60 columns rich was given, yet each sits on one line, whole.
|
|
130
|
+
assert " " + VERIFY in lines
|
|
131
|
+
assert " " + LONG_WHAT in lines
|
|
132
|
+
# A literal block keeps its line breaks and indentation, and brackets are not read as markup.
|
|
133
|
+
note = lines.index("note:")
|
|
134
|
+
assert lines[note + 1 : note + 3] == [
|
|
135
|
+
" First line of the note.",
|
|
136
|
+
" Indented second line [bold]not markup[/bold].",
|
|
137
|
+
]
|
|
138
|
+
acceptance = lines.index("acceptance:")
|
|
139
|
+
assert lines[acceptance + 1 : acceptance + 4] == [
|
|
140
|
+
" - The first criterion.",
|
|
141
|
+
" - A criterion over",
|
|
142
|
+
" two lines.",
|
|
143
|
+
]
|
|
144
|
+
assert lines[lines.index("evidence:") + 1] == " -"
|
|
145
|
+
# A key outside the schema is printed as well, a mapping as the YAML it is.
|
|
146
|
+
assert lines[lines.index("links:") + 1 :] == [" pr: 12", " commits:", " - abc1234"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_show_step_prints_the_evidence_and_done_at_a_finished_step_carries(tmp_path):
|
|
150
|
+
root = _harness(tmp_path)
|
|
151
|
+
|
|
152
|
+
result = _run("show", "demo", "--step", "1", "--harness", str(root))
|
|
153
|
+
|
|
154
|
+
assert result.exit_code == 0, result.output
|
|
155
|
+
lines = _lines(result.output)
|
|
156
|
+
assert any(line.split() == ["done_at:", "2026-10-01"] for line in lines)
|
|
157
|
+
assert any(line.split() == ["depends_on:", "-"] for line in lines)
|
|
158
|
+
assert lines[lines.index("evidence:") + 1] == " alpha@abc1234: 12 passed"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_show_step_with_an_unknown_id_names_the_ids_the_plan_holds(tmp_path):
|
|
162
|
+
root = _harness(tmp_path)
|
|
163
|
+
|
|
164
|
+
result = _run("show", "demo", "--step", "99", "--harness", str(root), columns=2000)
|
|
165
|
+
|
|
166
|
+
assert result.exit_code == 1
|
|
167
|
+
assert "No step '99' in demo. Available: 1, 2, 3, 4, 5" in _flat(result.output)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_show_step_refuses_to_mix_with_section(tmp_path):
|
|
171
|
+
root = _harness(tmp_path)
|
|
172
|
+
|
|
173
|
+
result = _run("show", "demo", "--step", "1", "--section", "steps", "--harness", str(root), columns=2000)
|
|
174
|
+
|
|
175
|
+
assert result.exit_code == 2
|
|
176
|
+
assert "--step prints one step; leave out --section." in _flat(result.output)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_show_step_reads_the_copy_of_a_hub_plan_without_calling_the_hub(tmp_path):
|
|
180
|
+
# The copy evo-agents wrote, with its hub key and digest, in a harness whose plans live on the hub.
|
|
181
|
+
root = _harness(tmp_path, body=MIRROR_FIXTURE.read_text(encoding="utf-8"), name="mirror-demo", hub=True)
|
|
182
|
+
empty = tmp_path / "no-tools"
|
|
183
|
+
empty.mkdir()
|
|
184
|
+
|
|
185
|
+
# Nothing on PATH, so a call out to evo-agents could not succeed.
|
|
186
|
+
result = _run("show", "mirror-demo", "--step", "2", "--harness", str(root), env={"PATH": str(empty)})
|
|
187
|
+
|
|
188
|
+
assert result.exit_code == 0, result.output
|
|
189
|
+
lines = _lines(result.output)
|
|
190
|
+
assert lines[0].startswith("mirror-demo step 2")
|
|
191
|
+
assert any(line.split() == ["title:", "Ghi", "chú", "nhiều", "dòng"] for line in lines)
|
|
192
|
+
assert any(line.split() == ["depends_on:", "1"] for line in lines)
|
|
193
|
+
note = lines.index("note:")
|
|
194
|
+
assert lines[note + 1 : note + 3] == [
|
|
195
|
+
" First line of the note.",
|
|
196
|
+
" Second line: \"quoted\" and 'single'.",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# show lists the open questions
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_show_lists_each_open_question_with_its_status_and_blocking_once(tmp_path):
|
|
204
|
+
root = _harness(tmp_path)
|
|
205
|
+
|
|
206
|
+
result = _run("show", "demo", "--harness", str(root), columns=2000)
|
|
207
|
+
|
|
208
|
+
assert result.exit_code == 0, result.output
|
|
209
|
+
lines = [line.strip() for line in _lines(result.output)]
|
|
210
|
+
start = lines.index("Open questions (2)")
|
|
211
|
+
assert lines[start + 1 :] == [
|
|
212
|
+
"0 Should the board show blocking? open",
|
|
213
|
+
"blocking: false",
|
|
214
|
+
"note: Only when it is the exception.",
|
|
215
|
+
"1 Which width is enough? answered",
|
|
216
|
+
"blocking: -",
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_show_has_no_open_questions_heading_for_a_plan_without_any(tmp_path):
|
|
221
|
+
root = _harness(
|
|
222
|
+
tmp_path, body="id: bare\ngoal: Nothing open.\nsteps:\n - id: 1\n what: Only step.\n", name="bare"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result = _run("show", "bare", "--harness", str(root), columns=2000)
|
|
226
|
+
|
|
227
|
+
assert result.exit_code == 0, result.output
|
|
228
|
+
assert "Open questions" not in result.output
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# The step board
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_step_board_shows_what_each_step_depends_on_and_marks_a_non_blocking_step(tmp_path):
|
|
235
|
+
root = _harness(tmp_path)
|
|
236
|
+
|
|
237
|
+
result = _run("step", "demo", "--no-input", "--harness", str(root), columns=200)
|
|
238
|
+
|
|
239
|
+
assert result.exit_code == 0, result.output
|
|
240
|
+
rows = {line.split()[0]: line.split() for line in _lines(result.output)[2:] if line.strip()}
|
|
241
|
+
assert _lines(result.output)[1].split() == ["#", "REPO", "STATUS", "DEPENDS", "ON", "STEP"]
|
|
242
|
+
assert rows["1"] == ["1", "alpha", "done", "-", "First"]
|
|
243
|
+
assert rows["3"] == ["3", "beta", "done", "1,", "2", "Third"]
|
|
244
|
+
assert rows["4"] == ["4", "beta", "pending", "1-3", "Wrap", "up"]
|
|
245
|
+
assert rows["5"] == ["5", "beta", "pending", "4", "Optional", "polish", "(non-blocking)"]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_key_runs_collapses_only_runs_of_three_or_more_consecutive_integers():
|
|
249
|
+
assert key_runs([]) == "-"
|
|
250
|
+
assert key_runs(["11"]) == "11"
|
|
251
|
+
assert key_runs(["1", "2"]) == "1, 2"
|
|
252
|
+
assert key_runs(["1", "2", "3", "5"]) == "1-3, 5"
|
|
253
|
+
assert key_runs(["5", "6", "8", "9", "10"]) == "5, 6, 8-10"
|
|
254
|
+
assert key_runs(["3", "2", "1"]) == "3, 2, 1"
|
|
255
|
+
assert key_runs(["07", "8", "9"]) == "07, 8, 9"
|
|
256
|
+
assert key_runs(["a", "1", "2", "3", "b"]) == "a, 1-3, b"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_step_depends_reads_every_forward_spelling_once_in_written_order():
|
|
260
|
+
assert step_depends({}) == []
|
|
261
|
+
assert step_depends({"depends_on": 3}) == ["3"]
|
|
262
|
+
assert step_depends({"depends_on": [2, 1], "depends_on_step": [1, 4], "blocked_by": "x", "blocks": [9]}) == [
|
|
263
|
+
"2",
|
|
264
|
+
"1",
|
|
265
|
+
"4",
|
|
266
|
+
"x",
|
|
267
|
+
]
|
evo_cli-0.29.2/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.29.2
|