arctx-cli 0.3.1b2__tar.gz → 0.3.1b4__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 (104) hide show
  1. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/PKG-INFO +10 -7
  2. arctx_cli-0.3.1b4/README.md +26 -0
  3. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/pyproject.toml +2 -2
  4. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/append_batch.py +37 -0
  5. arctx_cli-0.3.1b4/src/arctx_cli/commands/_lane_context.py +91 -0
  6. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/add.py +59 -5
  7. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/asset.py +4 -0
  8. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/attach.py +11 -2
  9. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/cut.py +3 -1
  10. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/dump.py +10 -1
  11. arctx_cli-0.3.1b4/src/arctx_cli/commands/guide.py +208 -0
  12. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/lane.py +189 -76
  13. arctx_cli-0.3.1b4/src/arctx_cli/commands/log.py +331 -0
  14. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/reparent.py +5 -2
  15. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/uncut.py +3 -1
  16. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/__init__.py +4 -0
  17. arctx_cli-0.3.1b4/src/arctx_cli/lane_gate.py +25 -0
  18. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/main.py +37 -4
  19. arctx_cli-0.3.1b4/src/arctx_cli/post_write_check.py +72 -0
  20. arctx_cli-0.3.1b4/tests/cli/test_add_frontier.py +278 -0
  21. arctx_cli-0.3.1b4/tests/cli/test_guide_cli.py +149 -0
  22. arctx_cli-0.3.1b4/tests/cli/test_lane_lifecycle.py +279 -0
  23. arctx_cli-0.3.1b4/tests/cli/test_log_chronological.py +233 -0
  24. arctx_cli-0.3.1b4/tests/cli/test_main_guide_hint.py +85 -0
  25. arctx_cli-0.3.1b4/tests/cli/test_post_write_check.py +163 -0
  26. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_serve_api.py +63 -0
  27. arctx_cli-0.3.1b2/README.md +0 -23
  28. arctx_cli-0.3.1b2/src/arctx_cli/commands/guide.py +0 -135
  29. arctx_cli-0.3.1b2/src/arctx_cli/commands/log.py +0 -76
  30. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/.gitignore +0 -0
  31. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/__init__.py +0 -0
  32. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/alias.py +0 -0
  33. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/__init__.py +0 -0
  34. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/_targets.py +0 -0
  35. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/alias_cmd.py +0 -0
  36. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/current.py +0 -0
  37. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/export.py +0 -0
  38. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/ext.py +0 -0
  39. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/graph.py +0 -0
  40. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/init.py +0 -0
  41. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/list.py +0 -0
  42. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/migrate.py +0 -0
  43. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/node.py +0 -0
  44. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/outcomes.py +0 -0
  45. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/payload.py +0 -0
  46. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/pr.py +0 -0
  47. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/reachable.py +0 -0
  48. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/serve.py +0 -0
  49. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/show.py +0 -0
  50. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/step.py +0 -0
  51. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/sync_cmd.py +0 -0
  52. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/trace.py +0 -0
  53. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/use.py +0 -0
  54. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/commands/web.py +0 -0
  55. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/context.py +0 -0
  56. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/__init__.py +0 -0
  57. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/command/__init__.py +0 -0
  58. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/branch.py +0 -0
  59. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/cherry_pick.py +0 -0
  60. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/commit.py +0 -0
  61. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/hook.py +0 -0
  62. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/merge.py +0 -0
  63. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/repo.py +0 -0
  64. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/reset.py +0 -0
  65. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/revert.py +0 -0
  66. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/verify.py +0 -0
  67. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext/git/worktree.py +0 -0
  68. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/ext_registry.py +0 -0
  69. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/paths.py +0 -0
  70. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/payload_builder.py +0 -0
  71. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/src/arctx_cli/workspace.py +0 -0
  72. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/__init__.py +0 -0
  73. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/__init__.py +0 -0
  74. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_alias_cli.py +0 -0
  75. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_alias_resolve.py +0 -0
  76. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_asset_extension_cli.py +0 -0
  77. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_basic.py +0 -0
  78. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_branch.py +0 -0
  79. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_cherry_pick.py +0 -0
  80. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_command_extension.py +0 -0
  81. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_commit.py +0 -0
  82. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_commit_guard.py +0 -0
  83. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_dag_core_cli.py +0 -0
  84. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_ext_cli.py +0 -0
  85. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_git_namespace_cli.py +0 -0
  86. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_git_repo.py +0 -0
  87. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_hook_install.py +0 -0
  88. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_hook_post_merge.py +0 -0
  89. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_hook_post_rewrite.py +0 -0
  90. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_init_extension.py +0 -0
  91. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_init_hooks.py +0 -0
  92. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_init_stag_id.py +0 -0
  93. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_lane.py +0 -0
  94. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_merge.py +0 -0
  95. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_migrate.py +0 -0
  96. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_paths.py +0 -0
  97. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_pr.py +0 -0
  98. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_reset.py +0 -0
  99. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_revert.py +0 -0
  100. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_show_history.py +0 -0
  101. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_sync.py +0 -0
  102. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/cli/test_verify.py +0 -0
  103. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/tests/fixtures/__init__.py +0 -0
  104. {arctx_cli-0.3.1b2 → arctx_cli-0.3.1b4}/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.1b2
3
+ Version: 0.3.1b4
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.1b2
20
+ Requires-Dist: arctx==0.3.1b4
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: black>=23.0; extra == 'dev'
23
23
  Requires-Dist: mypy>=1.0; extra == 'dev'
@@ -26,9 +26,11 @@ Requires-Dist: pytest>=7.0; extra == 'dev'
26
26
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
27
  Description-Content-Type: text/markdown
28
28
 
29
- # arctx-cli
29
+ # ARCTX CLI
30
30
 
31
- Command-line interface and TUI for ARCTX (Arc + Context).
31
+ Command-line interface for ARCTX experiment graphs. Use it to initialize runs,
32
+ record steps, attach results, cut abandoned branches, export reports, and serve
33
+ the Web GUI.
32
34
 
33
35
  ## Installation
34
36
 
@@ -41,11 +43,12 @@ This also installs `arctx` as a dependency.
41
43
  ## Usage
42
44
 
43
45
  ```bash
44
- arctx init
45
- arctx transition create --from <node_id> --payload-type <type>
46
+ arctx init optimize --extension git --run-id demo
47
+ arctx add --from <node_id> --type experiment --field variant=A
46
48
  arctx dump
47
49
  ```
48
50
 
49
51
  ## Package layout
50
52
 
51
- This package provides the `arctx` CLI command and TUI. The core API is in the separate `arctx` package.
53
+ This package provides the `arctx` CLI command. The core API is in the separate
54
+ `arctx` package; the experimental TUI is shipped separately as `arctx-tui`.
@@ -0,0 +1,26 @@
1
+ # ARCTX CLI
2
+
3
+ Command-line interface for ARCTX experiment graphs. Use it to initialize runs,
4
+ record steps, attach results, cut abandoned branches, export reports, and serve
5
+ the Web GUI.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install arctx-cli
11
+ ```
12
+
13
+ This also installs `arctx` as a dependency.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ arctx init optimize --extension git --run-id demo
19
+ arctx add --from <node_id> --type experiment --field variant=A
20
+ arctx dump
21
+ ```
22
+
23
+ ## Package layout
24
+
25
+ This package provides the `arctx` CLI command. The core API is in the separate
26
+ `arctx` package; the experimental TUI is shipped separately as `arctx-tui`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arctx-cli"
7
- version = "0.3.1b2"
7
+ version = "0.3.1b4"
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.1b2",
28
+ "arctx==0.3.1b4",
29
29
  ]
30
30
 
31
31
  [project.optional-dependencies]
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from arctx.core.append import AppendBatch, GraphRecordEnvelope
6
+ from arctx.core.lanes import format_lane_validation_errors, lane_validation_errors
6
7
 
7
8
 
8
9
  def graph_counts(handle) -> dict[str, set[str]]:
@@ -12,6 +13,7 @@ def graph_counts(handle) -> dict[str, set[str]]:
12
13
  "steps": set(handle.run_graph.steps),
13
14
  "payloads": set(handle.run_graph.payloads),
14
15
  "work_events": {event.event_id for event in handle.run_graph.work_events},
16
+ "lane_error_count": {str(_lane_error_count(handle))},
15
17
  }
16
18
 
17
19
 
@@ -24,6 +26,8 @@ def maybe_append_or_save(
24
26
  before: dict[str, set[str]],
25
27
  ) -> None:
26
28
  """Use append_batch for capable stores, otherwise fall back to save_run."""
29
+ if _has_new_work_events(handle, before):
30
+ _ensure_no_new_lane_errors(handle, baseline=_baseline_lane_error_count(before))
27
31
  if user_id is None or lane_id is None or not hasattr(store, "append_batch"):
28
32
  store.save_run(handle)
29
33
  return
@@ -80,3 +84,36 @@ def build_append_batch(
80
84
 
81
85
  def _new_ids(current: dict[str, object], before: dict[str, set[str]], key: str) -> list[str]:
82
86
  return [record_id for record_id in current if record_id not in before.get(key, set())]
87
+
88
+
89
+ def _has_new_work_events(handle, before: dict[str, set[str]]) -> bool:
90
+ return any(
91
+ event.event_id not in before.get("work_events", set())
92
+ for event in handle.run_graph.work_events
93
+ )
94
+
95
+
96
+ def _lane_error_count(handle) -> int:
97
+ return len(
98
+ lane_validation_errors(
99
+ handle.run_graph,
100
+ root_node_id=handle.root_node_id,
101
+ )
102
+ )
103
+
104
+
105
+ def _baseline_lane_error_count(before: dict[str, set[str]]) -> int:
106
+ values = before.get("lane_error_count", {"0"})
107
+ try:
108
+ return int(next(iter(values)))
109
+ except (StopIteration, ValueError):
110
+ return 0
111
+
112
+
113
+ def _ensure_no_new_lane_errors(handle, *, baseline: int) -> None:
114
+ current = lane_validation_errors(
115
+ handle.run_graph,
116
+ root_node_id=handle.root_node_id,
117
+ )
118
+ if len(current) > baseline:
119
+ raise ValueError(format_lane_validation_errors(current[baseline:]))
@@ -0,0 +1,91 @@
1
+ """Shared helpers for resolving the current lane and its active frontiers.
2
+
3
+ Both ``arctx add`` (defaulting ``--from`` to the current lane's frontier) and
4
+ ``arctx guide`` (reporting "Active Frontiers in Lane") need the same three
5
+ steps: resolve the active lane id (env / --lane / repo pointer), look up the
6
+ ``Lane`` object by id or name, and compute its active frontier nodes. Keeping
7
+ this in one place means both commands agree on lane resolution order and on
8
+ what counts as a frontier.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+
16
+ def find_lane(handle, name_or_id: str):
17
+ """Resolve a Lane object by lane_id or by name. Returns None if absent."""
18
+ lane = handle.run_graph.lanes.get(name_or_id)
19
+ if lane is not None:
20
+ return lane
21
+ for candidate in handle.run_graph.lanes.values():
22
+ if candidate.name == name_or_id:
23
+ return candidate
24
+ return None
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class LaneFrontierContext:
29
+ """Resolution result: the lane id/name that was requested, the Lane object
30
+ found for it (if any), and its active frontier node ids."""
31
+
32
+ lane_id: str
33
+ lane_name: str | None
34
+ frontier_node_ids: tuple[str, ...]
35
+
36
+
37
+ def resolve_lane_frontiers(handle, lane_id: str) -> LaneFrontierContext:
38
+ """Resolve ``lane_id`` (id or name) to its active frontier nodes.
39
+
40
+ ``lane_id`` should already be resolved via the canonical chain (explicit
41
+ flag / ``ARCTX_LANE_ID`` / repo pointer) — see
42
+ ``arctx_cli.context.resolve_lane_id_from_args``. This helper only does the
43
+ graph-level lookup: Lane object + frontier computation.
44
+ """
45
+ from arctx.core.lanes import lane_active_frontiers
46
+
47
+ lane = find_lane(handle, lane_id)
48
+ if lane is None:
49
+ return LaneFrontierContext(lane_id=lane_id, lane_name=None, frontier_node_ids=())
50
+ frontiers = lane_active_frontiers(handle.run_graph, lane.lane_id)
51
+ return LaneFrontierContext(
52
+ lane_id=lane.lane_id, lane_name=lane.name, frontier_node_ids=frontiers
53
+ )
54
+
55
+
56
+ def root_frontier_fallback(handle) -> str | None:
57
+ """Return the run root node id if it is usable as a default ``--from``.
58
+
59
+ The run root is deliberately excluded from lane membership (it is run
60
+ metadata, not a lane-owned record — see ``lane_membership``), so
61
+ ``lane_active_frontiers`` never includes it and a brand-new run always
62
+ reports zero frontiers for every lane. That makes the very first
63
+ ``arctx add`` on a fresh run fail without ``--from`` even though the root
64
+ is the only possible input.
65
+
66
+ Returns the root node id when it is both active (not cut) and itself a
67
+ frontier (no outgoing steps yet, from *any* lane) — i.e. nothing has been
68
+ recorded against it yet. Returns ``None`` otherwise, so callers fall back
69
+ to the existing "no active frontiers" error.
70
+ """
71
+ from arctx.core.cuts import is_active_node
72
+
73
+ root_node_id = handle.root_node_id
74
+ if not root_node_id or root_node_id not in handle.run_graph.nodes:
75
+ return None
76
+ if not is_active_node(handle.run_graph, root_node_id):
77
+ return None
78
+ if handle.run_graph.steps_from_node(root_node_id):
79
+ return None
80
+ return root_node_id
81
+
82
+
83
+ def describe_frontier_candidates(handle, node_ids) -> list[str]:
84
+ """Format frontier node ids as ``n_xxx (type_a, type_b)`` lines, like guide."""
85
+ lines = []
86
+ for node_id in node_ids:
87
+ payloads = handle.run_graph.payloads_for_node(node_id)
88
+ types = [p.payload_type for p in payloads]
89
+ type_str = f" ({', '.join(types)})" if types else ""
90
+ lines.append(f"{node_id}{type_str}")
91
+ return lines
@@ -7,6 +7,11 @@ import json
7
7
  import sys
8
8
 
9
9
  from arctx_cli.append_batch import graph_counts, maybe_append_or_save
10
+ from arctx_cli.commands._lane_context import (
11
+ describe_frontier_candidates,
12
+ resolve_lane_frontiers,
13
+ root_frontier_fallback,
14
+ )
10
15
  from arctx_cli.commands._targets import step_view
11
16
  from arctx_cli.context import (
12
17
  resolve_run_id_from_args,
@@ -14,7 +19,9 @@ from arctx_cli.context import (
14
19
  resolve_user_id_from_args,
15
20
  resolve_lane_id_from_args,
16
21
  )
22
+ from arctx_cli.lane_gate import ensure_lane_open
17
23
  from arctx_cli.payload_builder import build_payload, parse_field_args, parse_json_object
24
+ from arctx_cli.post_write_check import warn_if_invalid
18
25
 
19
26
 
20
27
  def add_parser(subparsers) -> argparse.ArgumentParser:
@@ -22,10 +29,14 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
22
29
  parser.add_argument(
23
30
  "--from",
24
31
  action="append",
25
- required=True,
32
+ required=False,
33
+ default=None,
26
34
  dest="input_nodes",
27
35
  metavar="NODE_ID",
28
- help="Input node (repeatable for multi-input steps)",
36
+ help=(
37
+ "Input node (repeatable for multi-input steps). If omitted, "
38
+ "defaults to the current lane's single active frontier node."
39
+ ),
29
40
  )
30
41
  parser.add_argument("--title", default=None)
31
42
  parser.add_argument("--type", dest="payload_kind", default=None)
@@ -36,14 +47,50 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
36
47
  parser.add_argument("--store-dir", default=None)
37
48
  parser.add_argument("--user", default=None)
38
49
  parser.add_argument("--lane", default=None)
50
+ parser.add_argument("--force", action="store_true",
51
+ help="Write even if the target lane is closed")
39
52
 
40
53
  return parser
41
54
 
42
55
 
56
+ def _default_input_node_ids(handle, lane_id: str) -> list[str]:
57
+ """Default ``--from`` to the current lane's single active frontier node.
58
+
59
+ Raises ``ValueError`` (with a corrective, ready-to-copy command) when the
60
+ lane has zero or more than one active frontier, since there is no single
61
+ unambiguous node to continue from.
62
+ """
63
+ context = resolve_lane_frontiers(handle, lane_id)
64
+ frontiers = context.frontier_node_ids
65
+ lane_label = context.lane_name or context.lane_id
66
+
67
+ if len(frontiers) == 1:
68
+ return [frontiers[0]]
69
+
70
+ if not frontiers:
71
+ root_node_id = root_frontier_fallback(handle)
72
+ if root_node_id is not None:
73
+ return [root_node_id]
74
+ raise ValueError(
75
+ f"--from is required here: lane {lane_label!r} has no active frontiers. "
76
+ f"Pass --from NODE_ID explicitly. Use `arctx guide --context` or "
77
+ f"`arctx dump` to find a node, e.g.: arctx add --from n_xxx ..."
78
+ )
79
+
80
+ candidates = describe_frontier_candidates(handle, frontiers)
81
+ listing = "\n".join(f" - {line}" for line in candidates)
82
+ example = frontiers[0]
83
+ raise ValueError(
84
+ f"--from is required here: lane {lane_label!r} has {len(frontiers)} active "
85
+ f"frontiers. Pick one:\n{listing}\n"
86
+ f"arctx add --from {example} ..."
87
+ )
88
+
89
+
43
90
  def run_add_step_command(
44
91
  *,
45
92
  run_id: str,
46
- input_node_ids: list[str],
93
+ input_node_ids: list[str] | None,
47
94
  title: str | None,
48
95
  payload_kind: str | None,
49
96
  payload_type: str,
@@ -52,11 +99,15 @@ def run_add_step_command(
52
99
  store_dir: str,
53
100
  user_id: str | None = None,
54
101
  lane_id: str | None = None,
102
+ force: bool = False,
55
103
  ) -> dict:
56
104
  store = resolve_store(store_dir)
57
105
  if not store.run_path(run_id).exists():
58
106
  raise KeyError(f"unknown run_id: {run_id}")
59
107
  handle = store.load_run(run_id)
108
+ ensure_lane_open(handle, lane_id, force=force)
109
+ if not input_node_ids:
110
+ input_node_ids = _default_input_node_ids(handle, lane_id or "default")
60
111
  data = dict(json_data or {})
61
112
  data.update(field_data or {})
62
113
  if title is not None:
@@ -94,8 +145,9 @@ def run_add_step_command(
94
145
 
95
146
  def cli_add(args) -> int:
96
147
  try:
148
+ run_id = resolve_run_id_from_args(args)
97
149
  result = run_add_step_command(
98
- run_id=resolve_run_id_from_args(args),
150
+ run_id=run_id,
99
151
  input_node_ids=args.input_nodes,
100
152
  title=args.title,
101
153
  payload_kind=args.payload_kind,
@@ -105,9 +157,11 @@ def cli_add(args) -> int:
105
157
  store_dir=args.store_dir,
106
158
  user_id=resolve_user_id_from_args(args),
107
159
  lane_id=resolve_lane_id_from_args(args),
160
+ force=args.force,
108
161
  )
109
162
  print(json.dumps(result["step"], ensure_ascii=False, indent=2))
110
- return 0
163
+ strict_rc = warn_if_invalid(run_id, args.store_dir, command_name="add")
164
+ return strict_rc or 0
111
165
  except (KeyError, ValueError, json.JSONDecodeError) as exc:
112
166
  print(f"error: {exc}", file=sys.stderr)
113
167
  return 1
@@ -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,8 @@ 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
19
+ from arctx_cli.post_write_check import warn_if_invalid
18
20
 
19
21
 
20
22
  def add_parser(subparsers) -> argparse.ArgumentParser:
@@ -28,6 +30,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
28
30
  parser.add_argument("--store-dir", default=None)
29
31
  parser.add_argument("--user", default=None)
30
32
  parser.add_argument("--lane", default=None)
33
+ parser.add_argument("--force", action="store_true",
34
+ help="Write even if the target lane is closed")
31
35
  return parser
32
36
 
33
37
 
@@ -42,11 +46,13 @@ def run_attach_command(
42
46
  store_dir: str,
43
47
  user_id: str | None = None,
44
48
  lane_id: str | None = None,
49
+ force: bool = False,
45
50
  ) -> dict:
46
51
  store = resolve_store(store_dir)
47
52
  if not store.run_path(run_id).exists():
48
53
  raise KeyError(f"unknown run_id: {run_id}")
49
54
  handle = store.load_run(run_id)
55
+ ensure_lane_open(handle, lane_id, force=force)
50
56
  target_kind = resolve_target_kind(handle, target_id)
51
57
  if target_kind == "payload":
52
58
  raise ValueError("cannot attach a payload to another payload")
@@ -97,8 +103,9 @@ def run_attach_command(
97
103
 
98
104
  def cli_attach(args) -> int:
99
105
  try:
106
+ run_id = resolve_run_id_from_args(args)
100
107
  result = run_attach_command(
101
- run_id=resolve_run_id_from_args(args),
108
+ run_id=run_id,
102
109
  target_id=args.target_id,
103
110
  payload_kind=args.payload_kind,
104
111
  payload_type=args.payload_type,
@@ -107,9 +114,11 @@ def cli_attach(args) -> int:
107
114
  store_dir=args.store_dir,
108
115
  user_id=resolve_user_id_from_args(args),
109
116
  lane_id=resolve_lane_id_from_args(args),
117
+ force=args.force,
110
118
  )
111
119
  print(json.dumps(result["payload"], ensure_ascii=False, indent=2))
112
- return 0
120
+ strict_rc = warn_if_invalid(run_id, args.store_dir, command_name="attach")
121
+ return strict_rc or 0
113
122
  except (KeyError, ValueError, json.JSONDecodeError) as exc:
114
123
  print(f"error: {exc}", file=sys.stderr)
115
124
  return 1
@@ -13,6 +13,7 @@ from arctx_cli.context import (
13
13
  resolve_lane_id_from_args,
14
14
  )
15
15
  from arctx_cli.append_batch import graph_counts, maybe_append_or_save
16
+ from arctx_cli.post_write_check import warn_if_invalid
16
17
 
17
18
 
18
19
  def add_parser(subparsers) -> argparse.ArgumentParser:
@@ -99,4 +100,5 @@ def cli_cut(args) -> int:
99
100
  lane_id=resolve_lane_id_from_args(args),
100
101
  )
101
102
  print(json.dumps(result["cut"], ensure_ascii=False, indent=2))
102
- return 0
103
+ strict_rc = warn_if_invalid(run_id, args.store_dir, command_name="cut")
104
+ return strict_rc or 0
@@ -30,6 +30,8 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
30
30
  help="Hide observed (result) output steps")
31
31
  parser.add_argument("--full-payloads", action="store_true",
32
32
  help="Include full payload metrics / rationale")
33
+ parser.add_argument("--expand-closed-lanes", action="store_true",
34
+ help="Show closed lane internals instead of summary-only collapsed lanes")
33
35
  parser.add_argument("--run", default=None)
34
36
  parser.add_argument("--store-dir", default=None)
35
37
  return parser
@@ -43,12 +45,18 @@ def run_dump_command(
43
45
  node_id: str | None = None,
44
46
  depth: int | None = None,
45
47
  full_payloads: bool = False,
48
+ expand_closed_lanes: bool = False,
46
49
  ) -> str:
47
50
  store = resolve_store(store_dir)
48
51
  if not store.run_path(run_id).exists():
49
52
  raise KeyError(f"unknown run_id: {run_id}")
50
53
  handle = store.load_run(run_id)
51
- opts = DumpOptions(node_id=node_id, depth=depth, full_payloads=full_payloads)
54
+ opts = DumpOptions(
55
+ node_id=node_id,
56
+ depth=depth,
57
+ full_payloads=full_payloads,
58
+ expand_closed_lanes=expand_closed_lanes,
59
+ )
52
60
  return dump(handle, fmt, opts)
53
61
 
54
62
 
@@ -67,6 +75,7 @@ def cli_dump(args) -> int:
67
75
  observed_only=args.observed_only,
68
76
  predicted_only=args.predicted_only,
69
77
  full_payloads=args.full_payloads,
78
+ expand_closed_lanes=args.expand_closed_lanes,
70
79
  )
71
80
  print(dump(handle, args.fmt, opts))
72
81
  return 0