arctx-cli 0.3.1b2__tar.gz → 0.3.1b3__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.
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/PKG-INFO +2 -2
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/pyproject.toml +2 -2
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/add.py +6 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/asset.py +4 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/attach.py +6 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/guide.py +3 -2
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/lane.py +147 -75
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/__init__.py +4 -0
- arctx_cli-0.3.1b3/src/arctx_cli/lane_gate.py +25 -0
- arctx_cli-0.3.1b3/tests/cli/test_lane_lifecycle.py +155 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/.gitignore +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/README.md +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/alias.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/append_batch.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/_targets.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/alias_cmd.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/current.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/cut.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/dump.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/export.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/ext.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/graph.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/init.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/list.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/log.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/migrate.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/node.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/outcomes.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/payload.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/pr.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/reachable.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/reparent.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/serve.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/show.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/step.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/sync_cmd.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/trace.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/uncut.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/use.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/commands/web.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/context.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/command/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/branch.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/cherry_pick.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/commit.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/hook.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/merge.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/repo.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/reset.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/revert.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/verify.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext/git/worktree.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/ext_registry.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/main.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/paths.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/payload_builder.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/src/arctx_cli/workspace.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_alias_cli.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_alias_resolve.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_asset_extension_cli.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_basic.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_branch.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_cherry_pick.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_command_extension.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_commit.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_commit_guard.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_dag_core_cli.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_ext_cli.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_git_namespace_cli.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_git_repo.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_hook_install.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_hook_post_merge.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_hook_post_rewrite.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_init_extension.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_init_hooks.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_init_stag_id.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_lane.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_merge.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_migrate.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_paths.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_pr.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_reset.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_revert.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_serve_api.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_show_history.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_sync.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/cli/test_verify.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/fixtures/__init__.py +0 -0
- {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b3}/tests/fixtures/dummy_ext.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arctx-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1b3
|
|
4
4
|
Summary: CLI for ARCTX — append-only DAG editing and refactoring
|
|
5
5
|
Project-URL: Homepage, https://github.com/takumiecd/arctx
|
|
6
6
|
Project-URL: Repository, https://github.com/takumiecd/arctx
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: arctx>=0.3.
|
|
20
|
+
Requires-Dist: arctx>=0.3.1b3
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: black>=23.0; extra == 'dev'
|
|
23
23
|
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arctx-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1b3"
|
|
8
8
|
description = "CLI for ARCTX — append-only DAG editing and refactoring"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Utilities",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"arctx>=0.3.
|
|
28
|
+
"arctx>=0.3.1b3",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
@@ -14,6 +14,7 @@ from arctx_cli.context import (
|
|
|
14
14
|
resolve_user_id_from_args,
|
|
15
15
|
resolve_lane_id_from_args,
|
|
16
16
|
)
|
|
17
|
+
from arctx_cli.lane_gate import ensure_lane_open
|
|
17
18
|
from arctx_cli.payload_builder import build_payload, parse_field_args, parse_json_object
|
|
18
19
|
|
|
19
20
|
|
|
@@ -36,6 +37,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
36
37
|
parser.add_argument("--store-dir", default=None)
|
|
37
38
|
parser.add_argument("--user", default=None)
|
|
38
39
|
parser.add_argument("--lane", default=None)
|
|
40
|
+
parser.add_argument("--force", action="store_true",
|
|
41
|
+
help="Write even if the target lane is closed")
|
|
39
42
|
|
|
40
43
|
return parser
|
|
41
44
|
|
|
@@ -52,11 +55,13 @@ def run_add_step_command(
|
|
|
52
55
|
store_dir: str,
|
|
53
56
|
user_id: str | None = None,
|
|
54
57
|
lane_id: str | None = None,
|
|
58
|
+
force: bool = False,
|
|
55
59
|
) -> dict:
|
|
56
60
|
store = resolve_store(store_dir)
|
|
57
61
|
if not store.run_path(run_id).exists():
|
|
58
62
|
raise KeyError(f"unknown run_id: {run_id}")
|
|
59
63
|
handle = store.load_run(run_id)
|
|
64
|
+
ensure_lane_open(handle, lane_id, force=force)
|
|
60
65
|
data = dict(json_data or {})
|
|
61
66
|
data.update(field_data or {})
|
|
62
67
|
if title is not None:
|
|
@@ -105,6 +110,7 @@ def cli_add(args) -> int:
|
|
|
105
110
|
store_dir=args.store_dir,
|
|
106
111
|
user_id=resolve_user_id_from_args(args),
|
|
107
112
|
lane_id=resolve_lane_id_from_args(args),
|
|
113
|
+
force=args.force,
|
|
108
114
|
)
|
|
109
115
|
print(json.dumps(result["step"], ensure_ascii=False, indent=2))
|
|
110
116
|
return 0
|
|
@@ -13,6 +13,7 @@ from arctx_cli.context import (
|
|
|
13
13
|
resolve_user_id_from_args,
|
|
14
14
|
resolve_lane_id_from_args,
|
|
15
15
|
)
|
|
16
|
+
from arctx_cli.lane_gate import ensure_lane_open
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
@@ -27,6 +28,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
27
28
|
sp_attach.add_argument("--store-dir", default=None)
|
|
28
29
|
sp_attach.add_argument("--user", default=None)
|
|
29
30
|
sp_attach.add_argument("--lane", default=None)
|
|
31
|
+
sp_attach.add_argument("--force", action="store_true",
|
|
32
|
+
help="Write even if the target lane is closed")
|
|
30
33
|
|
|
31
34
|
sp_list = asset_sub.add_parser("list", help="List assets for a Node or Step")
|
|
32
35
|
sp_list.add_argument("--target", required=True, dest="target_id", help="Node or Step ID")
|
|
@@ -72,6 +75,7 @@ def _cli_asset_attach(args) -> int:
|
|
|
72
75
|
return 1
|
|
73
76
|
|
|
74
77
|
try:
|
|
78
|
+
ensure_lane_open(handle, lane_id, force=args.force)
|
|
75
79
|
before = graph_counts(handle)
|
|
76
80
|
payload = handle.attach_asset(
|
|
77
81
|
args.target_id,
|
|
@@ -15,6 +15,7 @@ from arctx_cli.context import (
|
|
|
15
15
|
resolve_lane_id_from_args,
|
|
16
16
|
)
|
|
17
17
|
from arctx_cli.payload_builder import build_payload, parse_field_args, parse_json_object
|
|
18
|
+
from arctx_cli.lane_gate import ensure_lane_open
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
@@ -28,6 +29,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
28
29
|
parser.add_argument("--store-dir", default=None)
|
|
29
30
|
parser.add_argument("--user", default=None)
|
|
30
31
|
parser.add_argument("--lane", default=None)
|
|
32
|
+
parser.add_argument("--force", action="store_true",
|
|
33
|
+
help="Write even if the target lane is closed")
|
|
31
34
|
return parser
|
|
32
35
|
|
|
33
36
|
|
|
@@ -42,11 +45,13 @@ def run_attach_command(
|
|
|
42
45
|
store_dir: str,
|
|
43
46
|
user_id: str | None = None,
|
|
44
47
|
lane_id: str | None = None,
|
|
48
|
+
force: bool = False,
|
|
45
49
|
) -> dict:
|
|
46
50
|
store = resolve_store(store_dir)
|
|
47
51
|
if not store.run_path(run_id).exists():
|
|
48
52
|
raise KeyError(f"unknown run_id: {run_id}")
|
|
49
53
|
handle = store.load_run(run_id)
|
|
54
|
+
ensure_lane_open(handle, lane_id, force=force)
|
|
50
55
|
target_kind = resolve_target_kind(handle, target_id)
|
|
51
56
|
if target_kind == "payload":
|
|
52
57
|
raise ValueError("cannot attach a payload to another payload")
|
|
@@ -107,6 +112,7 @@ def cli_attach(args) -> int:
|
|
|
107
112
|
store_dir=args.store_dir,
|
|
108
113
|
user_id=resolve_user_id_from_args(args),
|
|
109
114
|
lane_id=resolve_lane_id_from_args(args),
|
|
115
|
+
force=args.force,
|
|
110
116
|
)
|
|
111
117
|
print(json.dumps(result["payload"], ensure_ascii=False, indent=2))
|
|
112
118
|
return 0
|
|
@@ -27,7 +27,7 @@ def cli_guide(args) -> int:
|
|
|
27
27
|
1. **Understand Context**: Use `arctx dump` to view the graph. Check your **Current Lane** and **Active Frontiers** at the bottom of this guide to know where to resume work.
|
|
28
28
|
2. **Isolate Approaches**: Use lanes to explore different solutions independently.
|
|
29
29
|
3. **Record Everything**: Record your steps (e.g., via `arctx add` or `arctx git commit`). The graph should capture *how* you solved the problem, not just the final result.
|
|
30
|
-
4. **
|
|
30
|
+
4. **Close & Summarize**: When a lane's work is done, close it with `arctx lane close NAME --summary "..."`. This is the *close* action — use it even for a single linear chain, not only when several paths must be merged. The summary is **Markdown** (rendered as such in the web GUI) and becomes the closing node's payload, so **put your full synthesis there** (one leaf → it stamps that leaf; several leaves → it merges them into one node). Do **not** write a separate synthesis step and then close on top of it — the close *is* your synthesis-and-close. A closed lane refuses further writes; reopen it with `arctx lane open NAME` to resume.
|
|
31
31
|
|
|
32
32
|
## Essential Core Commands
|
|
33
33
|
|
|
@@ -35,7 +35,8 @@ def cli_guide(args) -> int:
|
|
|
35
35
|
* `arctx dump --format outline --lane` : View the history of the current lane.
|
|
36
36
|
* `arctx lane` : Show the currently active lane.
|
|
37
37
|
* `arctx show <ID>` : Show details of a specific Node, Step, or Payload.
|
|
38
|
-
* `arctx lane
|
|
38
|
+
* `arctx lane close <LANE> --summary "<your synthesis>"` : Close a lane. Attaches the summary (**Markdown**) to its terminal (its one leaf, or a fresh node merging several leaves) and marks it closed; writes are then refused until reopened. Put your findings in `--summary` here — not in a separate preceding step. (`arctx lane join` is a deprecated alias.)
|
|
39
|
+
* `arctx lane open <LANE>` : Reopen a closed lane to resume work.
|
|
39
40
|
"""
|
|
40
41
|
# Try to load execution context dynamically
|
|
41
42
|
try:
|
|
@@ -46,9 +46,12 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
46
46
|
"lane",
|
|
47
47
|
help="Switch/show the active lane (solo or collaborative unit of work)",
|
|
48
48
|
description=(
|
|
49
|
-
"`arctx lane create NAME` creates a lane. `arctx lane switch
|
|
50
|
-
"switches to an existing lane and pins it for this checkout. "
|
|
51
|
-
"`arctx lane NAME` is switch shorthand and errors when NAME is absent."
|
|
49
|
+
"`arctx lane create NAME` creates a lane (status open). `arctx lane switch "
|
|
50
|
+
"NAME` switches to an existing lane and pins it for this checkout. "
|
|
51
|
+
"`arctx lane NAME` is switch shorthand and errors when NAME is absent. "
|
|
52
|
+
"`arctx lane close NAME --summary TEXT` closes a lane (attaching the summary "
|
|
53
|
+
"to its terminal); `arctx lane open NAME` reopens it. Writes to a closed lane "
|
|
54
|
+
"are refused until it is reopened."
|
|
52
55
|
),
|
|
53
56
|
)
|
|
54
57
|
parser.add_argument(
|
|
@@ -57,7 +60,7 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
57
60
|
metavar="COMMAND|NAME",
|
|
58
61
|
help=(
|
|
59
62
|
"No args = current lane. Commands: create NAME, switch NAME, "
|
|
60
|
-
"adopt NAME, validate, list, show LANE, summaries LANE."
|
|
63
|
+
"close NAME, open NAME, adopt NAME, validate, list, show LANE, summaries LANE."
|
|
61
64
|
),
|
|
62
65
|
)
|
|
63
66
|
parser.add_argument("--list", action="store_true", dest="list_lanes",
|
|
@@ -86,18 +89,19 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
86
89
|
parser.add_argument(
|
|
87
90
|
"--reason",
|
|
88
91
|
default=None,
|
|
89
|
-
help="Reason recorded on a lane adoption event",
|
|
92
|
+
help="Reason recorded on a lane adoption / close / open event",
|
|
90
93
|
)
|
|
91
94
|
parser.add_argument(
|
|
92
95
|
"--summary",
|
|
93
96
|
default=None,
|
|
94
|
-
help="
|
|
97
|
+
help="Closing summary (Markdown), attached to the lane's terminal node (lane close)",
|
|
95
98
|
)
|
|
96
99
|
parser.add_argument(
|
|
97
100
|
"--node",
|
|
98
101
|
action="append",
|
|
99
102
|
default=None,
|
|
100
|
-
help="Specific node id to
|
|
103
|
+
help="Specific leaf node id to summarise on close "
|
|
104
|
+
"(repeatable; if omitted, uses all active leaf nodes of the lane)",
|
|
101
105
|
)
|
|
102
106
|
parser.add_argument("--user", default=None, help="Actor (created_by) when creating")
|
|
103
107
|
parser.add_argument("--run", default=None)
|
|
@@ -392,24 +396,76 @@ def validate_lane_run(*, run_id: str, store_dir: str | None) -> dict:
|
|
|
392
396
|
}
|
|
393
397
|
|
|
394
398
|
|
|
395
|
-
def
|
|
399
|
+
def _lane_active_leaves(handle, lane) -> list[str]:
|
|
400
|
+
"""Active leaf nodes of a lane (nodes in the lane with no step extending them)."""
|
|
401
|
+
from arctx.core.cuts import is_active_node
|
|
402
|
+
from arctx.core.lanes import lane_membership
|
|
403
|
+
|
|
404
|
+
membership = lane_membership(handle.run_graph)
|
|
405
|
+
group = next((g for g in membership.groups if g.lane_id == lane.lane_id), None)
|
|
406
|
+
if not group:
|
|
407
|
+
return []
|
|
408
|
+
leaves = [
|
|
409
|
+
nid
|
|
410
|
+
for nid in group.node_ids
|
|
411
|
+
if is_active_node(handle.run_graph, nid)
|
|
412
|
+
and not handle.run_graph.steps_from_node(nid)
|
|
413
|
+
]
|
|
414
|
+
return list(dict.fromkeys(leaves))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _attach_lane_summary(handle, *, lane, summary: str, node_ids, user_id: str):
|
|
418
|
+
"""Stamp a lane's terminal with ``summary``. Returns (summary_node, leaves).
|
|
419
|
+
|
|
420
|
+
- 1 leaf → attach the summary to that existing leaf (no extra node).
|
|
421
|
+
- ≥2 leaves → create one convergence (join) node and summarise that.
|
|
422
|
+
- 0 leaves → nothing to summarise (returns (None, [])).
|
|
423
|
+
"""
|
|
424
|
+
from arctx.core.schema.payloads import JoinPayload, SummaryPayload
|
|
425
|
+
|
|
426
|
+
leaves = list(dict.fromkeys(node_ids)) if node_ids else _lane_active_leaves(handle, lane)
|
|
427
|
+
if not leaves:
|
|
428
|
+
return None, []
|
|
429
|
+
|
|
430
|
+
if len(leaves) == 1:
|
|
431
|
+
target = leaves[0]
|
|
432
|
+
else:
|
|
433
|
+
join_payload = JoinPayload(payload_id=handle._next_id("pl"), target_id="")
|
|
434
|
+
step = handle.add_step(
|
|
435
|
+
input_node_ids=leaves,
|
|
436
|
+
payload=join_payload,
|
|
437
|
+
user_id=user_id,
|
|
438
|
+
lane_id=lane.lane_id,
|
|
439
|
+
)
|
|
440
|
+
target = step.output_node_id
|
|
441
|
+
|
|
442
|
+
handle.attach(
|
|
443
|
+
target,
|
|
444
|
+
SummaryPayload(payload_id=handle._next_id("pl"), target_id=target, text=summary),
|
|
445
|
+
user_id=user_id,
|
|
446
|
+
lane_id=lane.lane_id,
|
|
447
|
+
)
|
|
448
|
+
return target, leaves
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def run_lane_close_command(
|
|
396
452
|
*,
|
|
397
453
|
name_or_id: str,
|
|
398
|
-
summary: str,
|
|
454
|
+
summary: str | None,
|
|
399
455
|
node_ids: list[str] | None,
|
|
456
|
+
reason: str | None,
|
|
400
457
|
run_id: str,
|
|
401
458
|
user_id: str,
|
|
402
459
|
store_dir: str | None,
|
|
403
460
|
) -> dict:
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
If node_ids is not provided, this automatically finds all active leaf nodes
|
|
407
|
-
currently belonging to the lane.
|
|
408
|
-
"""
|
|
409
|
-
from arctx.core.cuts import is_active_node
|
|
410
|
-
from arctx.core.lanes import lane_membership
|
|
411
|
-
from arctx.core.schema.payloads import JoinPayload, SummaryPayload
|
|
461
|
+
"""Close a lane: attach the closing ``summary`` to its terminal, then mark it closed.
|
|
412
462
|
|
|
463
|
+
``summary`` is **Markdown** (rendered as such in the web GUI), so put the full
|
|
464
|
+
closing write-up here. It lands on the lane's single leaf when there is one (no
|
|
465
|
+
redundant node), or on a fresh convergence node when several leaves must be
|
|
466
|
+
merged. Once closed, writes to the lane are refused until it is reopened with
|
|
467
|
+
``lane open``.
|
|
468
|
+
"""
|
|
413
469
|
store = resolve_store(store_dir)
|
|
414
470
|
if not store.run_path(run_id).exists():
|
|
415
471
|
raise KeyError(f"unknown run_id: {run_id}")
|
|
@@ -418,64 +474,60 @@ def run_lane_join_command(
|
|
|
418
474
|
lane = _find_lane(handle, name_or_id)
|
|
419
475
|
if lane is None:
|
|
420
476
|
raise KeyError(f"unknown lane: {name_or_id!r}")
|
|
477
|
+
if lane.status == "closed":
|
|
478
|
+
raise ValueError(
|
|
479
|
+
f"lane already closed: {name_or_id!r}; "
|
|
480
|
+
f"reopen it with `arctx lane open {name_or_id}`"
|
|
481
|
+
)
|
|
421
482
|
|
|
422
|
-
|
|
423
|
-
if
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
candidates = []
|
|
431
|
-
for nid in group.node_ids:
|
|
432
|
-
if not is_active_node(handle.run_graph, nid):
|
|
433
|
-
continue
|
|
434
|
-
# Check if this node has no steps extending from it
|
|
435
|
-
if not handle.run_graph.steps_from_node(nid):
|
|
436
|
-
candidates.append(nid)
|
|
437
|
-
|
|
438
|
-
node_ids = candidates
|
|
439
|
-
|
|
440
|
-
if not node_ids:
|
|
441
|
-
raise ValueError(f"no active leaf nodes found in lane {name_or_id!r}")
|
|
442
|
-
|
|
443
|
-
# Deduplicate while preserving order
|
|
444
|
-
node_ids = list(dict.fromkeys(node_ids))
|
|
445
|
-
|
|
446
|
-
if len(node_ids) < 1:
|
|
447
|
-
raise ValueError("join requires at least one node")
|
|
448
|
-
|
|
449
|
-
# 1. Create Step with JoinPayload
|
|
450
|
-
join_payload = JoinPayload(
|
|
451
|
-
payload_id=handle._next_id("pl"),
|
|
452
|
-
target_id="",
|
|
453
|
-
)
|
|
454
|
-
step = handle.add_step(
|
|
455
|
-
input_node_ids=node_ids,
|
|
456
|
-
payload=join_payload,
|
|
457
|
-
user_id=user_id,
|
|
458
|
-
lane_id=lane.lane_id,
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
# 2. Attach SummaryPayload to the new output node
|
|
462
|
-
summary_payload = SummaryPayload(
|
|
463
|
-
payload_id=handle._next_id("pl"),
|
|
464
|
-
target_id=step.output_node_id,
|
|
465
|
-
text=summary,
|
|
466
|
-
)
|
|
467
|
-
handle.attach(
|
|
468
|
-
step.output_node_id,
|
|
469
|
-
summary_payload,
|
|
470
|
-
user_id=user_id,
|
|
471
|
-
lane_id=lane.lane_id,
|
|
483
|
+
summary_node, leaves = (None, [])
|
|
484
|
+
if summary:
|
|
485
|
+
summary_node, leaves = _attach_lane_summary(
|
|
486
|
+
handle, lane=lane, summary=summary, node_ids=node_ids, user_id=user_id
|
|
487
|
+
)
|
|
488
|
+
event = handle.set_lane_status(
|
|
489
|
+
lane.lane_id, status="closed", user_id=user_id, reason=reason
|
|
472
490
|
)
|
|
491
|
+
store.save_run(handle)
|
|
492
|
+
return {
|
|
493
|
+
"lane_id": lane.lane_id,
|
|
494
|
+
"name": lane.name,
|
|
495
|
+
"status": "closed",
|
|
496
|
+
"summary_node": summary_node,
|
|
497
|
+
"joined_nodes": leaves,
|
|
498
|
+
"event_id": event.event_id,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def run_lane_open_command(
|
|
503
|
+
*,
|
|
504
|
+
name_or_id: str,
|
|
505
|
+
reason: str | None,
|
|
506
|
+
run_id: str,
|
|
507
|
+
user_id: str,
|
|
508
|
+
store_dir: str | None,
|
|
509
|
+
) -> dict:
|
|
510
|
+
"""Reopen a closed lane so work can resume. Symmetric to ``lane close``."""
|
|
511
|
+
store = resolve_store(store_dir)
|
|
512
|
+
if not store.run_path(run_id).exists():
|
|
513
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
514
|
+
handle = store.load_run(run_id)
|
|
515
|
+
|
|
516
|
+
lane = _find_lane(handle, name_or_id)
|
|
517
|
+
if lane is None:
|
|
518
|
+
raise KeyError(f"unknown lane: {name_or_id!r}")
|
|
519
|
+
if lane.status != "closed":
|
|
520
|
+
raise ValueError(f"lane is already open: {name_or_id!r}")
|
|
473
521
|
|
|
522
|
+
event = handle.set_lane_status(
|
|
523
|
+
lane.lane_id, status="open", user_id=user_id, reason=reason
|
|
524
|
+
)
|
|
474
525
|
store.save_run(handle)
|
|
475
526
|
return {
|
|
476
|
-
"
|
|
477
|
-
"
|
|
478
|
-
"
|
|
527
|
+
"lane_id": lane.lane_id,
|
|
528
|
+
"name": lane.name,
|
|
529
|
+
"status": "open",
|
|
530
|
+
"event_id": event.event_id,
|
|
479
531
|
}
|
|
480
532
|
|
|
481
533
|
|
|
@@ -533,15 +585,35 @@ def cli_lane(args) -> int:
|
|
|
533
585
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
534
586
|
return 0
|
|
535
587
|
|
|
536
|
-
if command
|
|
588
|
+
if command in ("close", "join"):
|
|
589
|
+
if command == "join":
|
|
590
|
+
print(
|
|
591
|
+
"warning: `arctx lane join` is deprecated; use `arctx lane close` "
|
|
592
|
+
"(it closes the lane and attaches the summary to its terminal).",
|
|
593
|
+
file=sys.stderr,
|
|
594
|
+
)
|
|
537
595
|
if len(argv) != 2:
|
|
538
|
-
raise ValueError(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
result =
|
|
596
|
+
raise ValueError(
|
|
597
|
+
"usage: arctx lane close NAME_OR_ID --summary TEXT [--node ID...] [--reason TEXT]"
|
|
598
|
+
)
|
|
599
|
+
result = run_lane_close_command(
|
|
542
600
|
name_or_id=argv[1],
|
|
543
601
|
summary=args.summary,
|
|
544
602
|
node_ids=args.node,
|
|
603
|
+
reason=args.reason,
|
|
604
|
+
run_id=resolve_run_id_from_args(args),
|
|
605
|
+
user_id=resolve_user_id_from_args(args),
|
|
606
|
+
store_dir=args.store_dir,
|
|
607
|
+
)
|
|
608
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
609
|
+
return 0
|
|
610
|
+
|
|
611
|
+
if command == "open":
|
|
612
|
+
if len(argv) != 2:
|
|
613
|
+
raise ValueError("usage: arctx lane open NAME_OR_ID [--reason TEXT]")
|
|
614
|
+
result = run_lane_open_command(
|
|
615
|
+
name_or_id=argv[1],
|
|
616
|
+
reason=args.reason,
|
|
545
617
|
run_id=resolve_run_id_from_args(args),
|
|
546
618
|
user_id=resolve_user_id_from_args(args),
|
|
547
619
|
store_dir=args.store_dir,
|
|
@@ -14,6 +14,7 @@ from arctx_cli.context import (
|
|
|
14
14
|
resolve_user_id_from_args,
|
|
15
15
|
resolve_lane_id_from_args,
|
|
16
16
|
)
|
|
17
|
+
from arctx_cli.lane_gate import ensure_lane_open
|
|
17
18
|
|
|
18
19
|
# ---------------------------------------------------------------------------
|
|
19
20
|
# Parser registration
|
|
@@ -60,6 +61,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
|
60
61
|
sp_add.add_argument("--store-dir", default=None)
|
|
61
62
|
sp_add.add_argument("--user", default=None)
|
|
62
63
|
sp_add.add_argument("--lane", default=None)
|
|
64
|
+
sp_add.add_argument("--force", action="store_true",
|
|
65
|
+
help="Write even if the target lane is closed")
|
|
63
66
|
|
|
64
67
|
sp_show = git_sub.add_parser("show", help="Show git_change payloads for a Step")
|
|
65
68
|
sp_show.add_argument("--step", required=True, dest="step_id")
|
|
@@ -203,6 +206,7 @@ def _cli_git_attach(args) -> int:
|
|
|
203
206
|
from arctx.ext.git.helpers.attach import attach_commits_to_step
|
|
204
207
|
|
|
205
208
|
try:
|
|
209
|
+
ensure_lane_open(handle, lane_id, force=getattr(args, "force", False))
|
|
206
210
|
before = graph_counts(handle)
|
|
207
211
|
result = attach_commits_to_step(
|
|
208
212
|
handle,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Guard that refuses writes to a closed lane.
|
|
2
|
+
|
|
3
|
+
A lane's lifecycle is open → closed → (reopen) open. "Closed" only means
|
|
4
|
+
something if writes respect it: once a lane is closed, ``add`` / ``attach`` /
|
|
5
|
+
``asset attach`` / ``git add`` and friends refuse to append to it until it is
|
|
6
|
+
explicitly reopened with ``arctx lane open`` (or the caller passes ``--force``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_lane_open(handle, lane_id: str | None, *, force: bool = False) -> None:
|
|
13
|
+
"""Raise if *lane_id* names a closed lane, unless *force* is set.
|
|
14
|
+
|
|
15
|
+
No-op when *lane_id* is falsy (no lane context) or unknown — those are the
|
|
16
|
+
pre-existing default/lane-less write paths and are not this guard's business.
|
|
17
|
+
"""
|
|
18
|
+
if not lane_id or force:
|
|
19
|
+
return
|
|
20
|
+
lane = handle.run_graph.lanes.get(lane_id)
|
|
21
|
+
if lane is not None and getattr(lane, "status", "open") == "closed":
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"lane is closed: {lane_id}; reopen it with "
|
|
24
|
+
f"`arctx lane open {lane_id}` (or pass --force to write anyway)"
|
|
25
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Tests for the lane open/close lifecycle and the closed-lane write gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from arctx_cli.commands.add import run_add_step_command
|
|
11
|
+
from arctx_cli.commands.init import run_init_command
|
|
12
|
+
from arctx_cli.commands.lane import (
|
|
13
|
+
list_lanes,
|
|
14
|
+
run_lane_close_command,
|
|
15
|
+
run_lane_create_command,
|
|
16
|
+
run_lane_open_command,
|
|
17
|
+
)
|
|
18
|
+
from arctx_cli.context import resolve_store
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _store_dir(td: str) -> str:
|
|
22
|
+
return str(Path(td) / "runs")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _init(td: str) -> None:
|
|
26
|
+
run_init_command(
|
|
27
|
+
requirement_id="req",
|
|
28
|
+
target_type="task",
|
|
29
|
+
target_id="target",
|
|
30
|
+
run_id="run_lc",
|
|
31
|
+
store_dir=_store_dir(td),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _create_lane(sd: str) -> str:
|
|
36
|
+
res = run_lane_create_command(name="work", run_id="run_lc", user_id="alice", store_dir=sd)
|
|
37
|
+
return res["lane_id"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _root(sd: str) -> str:
|
|
41
|
+
return resolve_store(sd).load_run("run_lc").root_node_id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _add(sd: str, from_node: str, lane_id: str, *, title: str, force: bool = False) -> str:
|
|
45
|
+
res = run_add_step_command(
|
|
46
|
+
run_id="run_lc",
|
|
47
|
+
input_node_ids=[from_node],
|
|
48
|
+
title=title,
|
|
49
|
+
payload_kind=None,
|
|
50
|
+
payload_type="step_payload",
|
|
51
|
+
field_data={},
|
|
52
|
+
json_data={},
|
|
53
|
+
store_dir=sd,
|
|
54
|
+
user_id="alice",
|
|
55
|
+
lane_id=lane_id,
|
|
56
|
+
force=force,
|
|
57
|
+
)
|
|
58
|
+
return res["step"]["output_node_id"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _status(sd: str, lane_id: str) -> str:
|
|
62
|
+
return next(l["status"] for l in list_lanes(run_id="run_lc", store_dir=sd) if l["lane_id"] == lane_id)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_close_single_leaf_stamps_existing_leaf_no_extra_node():
|
|
66
|
+
with tempfile.TemporaryDirectory() as td:
|
|
67
|
+
sd = _store_dir(td)
|
|
68
|
+
_init(td)
|
|
69
|
+
lane_id = _create_lane(sd)
|
|
70
|
+
leaf = _add(sd, _root(sd), lane_id, title="s1")
|
|
71
|
+
|
|
72
|
+
res = run_lane_close_command(
|
|
73
|
+
name_or_id="work", summary="findings", node_ids=None, reason="done",
|
|
74
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
75
|
+
)
|
|
76
|
+
# The summary lands on the existing leaf — no convergence node is created.
|
|
77
|
+
assert res["status"] == "closed"
|
|
78
|
+
assert res["summary_node"] == leaf
|
|
79
|
+
assert res["joined_nodes"] == [leaf]
|
|
80
|
+
assert _status(sd, lane_id) == "closed"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_status_persists_across_reload():
|
|
84
|
+
with tempfile.TemporaryDirectory() as td:
|
|
85
|
+
sd = _store_dir(td)
|
|
86
|
+
_init(td)
|
|
87
|
+
lane_id = _create_lane(sd)
|
|
88
|
+
_add(sd, _root(sd), lane_id, title="s1")
|
|
89
|
+
run_lane_close_command(
|
|
90
|
+
name_or_id="work", summary="x", node_ids=None, reason=None,
|
|
91
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
92
|
+
)
|
|
93
|
+
# Fresh load → status is the projection of the lane_closed event.
|
|
94
|
+
assert _status(sd, lane_id) == "closed"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_write_to_closed_lane_is_blocked_then_force_overrides():
|
|
98
|
+
with tempfile.TemporaryDirectory() as td:
|
|
99
|
+
sd = _store_dir(td)
|
|
100
|
+
_init(td)
|
|
101
|
+
lane_id = _create_lane(sd)
|
|
102
|
+
leaf = _add(sd, _root(sd), lane_id, title="s1")
|
|
103
|
+
run_lane_close_command(
|
|
104
|
+
name_or_id="work", summary="x", node_ids=None, reason=None,
|
|
105
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
106
|
+
)
|
|
107
|
+
with pytest.raises(ValueError, match="closed"):
|
|
108
|
+
_add(sd, leaf, lane_id, title="s2")
|
|
109
|
+
# --force punches through.
|
|
110
|
+
forced = _add(sd, leaf, lane_id, title="s2", force=True)
|
|
111
|
+
assert forced
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_open_reopens_and_allows_writes():
|
|
115
|
+
with tempfile.TemporaryDirectory() as td:
|
|
116
|
+
sd = _store_dir(td)
|
|
117
|
+
_init(td)
|
|
118
|
+
lane_id = _create_lane(sd)
|
|
119
|
+
leaf = _add(sd, _root(sd), lane_id, title="s1")
|
|
120
|
+
run_lane_close_command(
|
|
121
|
+
name_or_id="work", summary="x", node_ids=None, reason=None,
|
|
122
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
123
|
+
)
|
|
124
|
+
res = run_lane_open_command(
|
|
125
|
+
name_or_id="work", reason="resume", run_id="run_lc",
|
|
126
|
+
user_id="alice", store_dir=sd,
|
|
127
|
+
)
|
|
128
|
+
assert res["status"] == "open"
|
|
129
|
+
assert _status(sd, lane_id) == "open"
|
|
130
|
+
# Writing now succeeds without --force.
|
|
131
|
+
assert _add(sd, leaf, lane_id, title="s2")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_open_when_open_and_close_when_closed_error():
|
|
135
|
+
with tempfile.TemporaryDirectory() as td:
|
|
136
|
+
sd = _store_dir(td)
|
|
137
|
+
_init(td)
|
|
138
|
+
_create_lane(sd)
|
|
139
|
+
# opening an already-open lane errors
|
|
140
|
+
with pytest.raises(ValueError, match="already open"):
|
|
141
|
+
run_lane_open_command(
|
|
142
|
+
name_or_id="work", reason=None, run_id="run_lc",
|
|
143
|
+
user_id="alice", store_dir=sd,
|
|
144
|
+
)
|
|
145
|
+
# close with no summary is allowed (closing an empty/dead-end lane)
|
|
146
|
+
run_lane_close_command(
|
|
147
|
+
name_or_id="work", summary=None, node_ids=None, reason=None,
|
|
148
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
149
|
+
)
|
|
150
|
+
# closing an already-closed lane errors
|
|
151
|
+
with pytest.raises(ValueError, match="already closed"):
|
|
152
|
+
run_lane_close_command(
|
|
153
|
+
name_or_id="work", summary=None, node_ids=None, reason=None,
|
|
154
|
+
run_id="run_lc", user_id="alice", store_dir=sd,
|
|
155
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|