arctx-cli 0.2.0b2__tar.gz → 0.2.0b3__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 (80) hide show
  1. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/PKG-INFO +8 -5
  2. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/pyproject.toml +8 -5
  3. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/__init__.py +3 -0
  4. arctx_cli-0.2.0b3/src/arctx_cli/commands/export.py +66 -0
  5. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/git.py +11 -0
  6. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/use.py +30 -4
  7. arctx_cli-0.2.0b3/src/arctx_cli/ext/git/repo.py +231 -0
  8. arctx_cli-0.2.0b3/tests/cli/test_git_repo.py +118 -0
  9. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_init_stag_id.py +52 -0
  10. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/.gitignore +0 -0
  11. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/README.md +0 -0
  12. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/__init__.py +0 -0
  13. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/alias.py +0 -0
  14. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/append_batch.py +0 -0
  15. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/alias_cmd.py +0 -0
  16. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/anchor.py +0 -0
  17. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/current.py +0 -0
  18. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/cut.py +0 -0
  19. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/dump.py +0 -0
  20. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/ext.py +0 -0
  21. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/graph.py +0 -0
  22. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/guide.py +0 -0
  23. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/init.py +0 -0
  24. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/list.py +0 -0
  25. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/migrate.py +0 -0
  26. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/node.py +0 -0
  27. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/outcomes.py +0 -0
  28. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/payload.py +0 -0
  29. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/reachable.py +0 -0
  30. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/show.py +0 -0
  31. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/sync.py +0 -0
  32. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/trace.py +0 -0
  33. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/transition.py +0 -0
  34. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/view.py +0 -0
  35. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/commands/work_session.py +0 -0
  36. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/context.py +0 -0
  37. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/__init__.py +0 -0
  38. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/command/__init__.py +0 -0
  39. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/__init__.py +0 -0
  40. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/branch.py +0 -0
  41. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/cherry_pick.py +0 -0
  42. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/commit.py +0 -0
  43. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/hook.py +0 -0
  44. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/merge.py +0 -0
  45. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/reset.py +0 -0
  46. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/revert.py +0 -0
  47. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/verify.py +0 -0
  48. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext/git/worktree.py +0 -0
  49. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/ext_registry.py +0 -0
  50. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/main.py +0 -0
  51. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/paths.py +0 -0
  52. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/payload_builder.py +0 -0
  53. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/src/arctx_cli/workspace.py +0 -0
  54. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/__init__.py +0 -0
  55. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/__init__.py +0 -0
  56. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_alias_cli.py +0 -0
  57. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_alias_resolve.py +0 -0
  58. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_basic.py +0 -0
  59. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_branch.py +0 -0
  60. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_cherry_pick.py +0 -0
  61. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_command_extension.py +0 -0
  62. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_commit.py +0 -0
  63. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_commit_guard.py +0 -0
  64. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_ext_cli.py +0 -0
  65. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_git_namespace_cli.py +0 -0
  66. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_git_worktree.py +0 -0
  67. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_hook_install.py +0 -0
  68. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_hook_post_merge.py +0 -0
  69. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_hook_post_rewrite.py +0 -0
  70. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_init_extension.py +0 -0
  71. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_init_hooks.py +0 -0
  72. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_merge.py +0 -0
  73. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_paths.py +0 -0
  74. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_reset.py +0 -0
  75. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_revert.py +0 -0
  76. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_show_history.py +0 -0
  77. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_verify.py +0 -0
  78. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/cli/test_work_session.py +0 -0
  79. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/fixtures/__init__.py +0 -0
  80. {arctx_cli-0.2.0b2 → arctx_cli-0.2.0b3}/tests/fixtures/dummy_ext.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arctx-cli
3
- Version: 0.2.0b2
4
- Summary: ARCTX CLI: command-line interface for arctx
5
- Project-URL: Homepage, https://github.com/takumiecd/stag
6
- Project-URL: Repository, https://github.com/takumiecd/stag
3
+ Version: 0.2.0b3
4
+ Summary: CLI for ARCTX append-only DAG for reasoning history and parallel agent work
5
+ Project-URL: Homepage, https://github.com/takumiecd/arctx
6
+ Project-URL: Repository, https://github.com/takumiecd/arctx
7
7
  Author: Takumi Ishida
8
8
  License: MIT
9
+ Keywords: agents,arctx,cli,dag,developer-tools,workflow
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Intended Audience :: Developers
11
12
  Classifier: License :: OSI Approved :: MIT License
@@ -13,8 +14,10 @@ Classifier: Programming Language :: Python :: 3
13
14
  Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Utilities
16
19
  Requires-Python: >=3.10
17
- Requires-Dist: arctx>=0.2.0b2
20
+ Requires-Dist: arctx>=0.2.0b3
18
21
  Provides-Extra: dev
19
22
  Requires-Dist: black>=23.0; extra == 'dev'
20
23
  Requires-Dist: mypy>=1.0; extra == 'dev'
@@ -4,14 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arctx-cli"
7
- version = "0.2.0b2"
8
- description = "ARCTX CLI: command-line interface for arctx"
7
+ version = "0.2.0b3"
8
+ description = "CLI for ARCTX append-only DAG for reasoning history and parallel agent work"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = {text = "MIT"}
12
12
  authors = [
13
13
  {name = "Takumi Ishida"},
14
14
  ]
15
+ keywords = ["cli", "arctx", "dag", "agents", "developer-tools", "workflow"]
15
16
  classifiers = [
16
17
  "Development Status :: 4 - Beta",
17
18
  "Intended Audience :: Developers",
@@ -20,9 +21,11 @@ classifiers = [
20
21
  "Programming Language :: Python :: 3.10",
21
22
  "Programming Language :: Python :: 3.11",
22
23
  "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Topic :: Utilities",
23
26
  ]
24
27
  dependencies = [
25
- "arctx>=0.2.0b2",
28
+ "arctx>=0.2.0b3",
26
29
  ]
27
30
 
28
31
  [project.optional-dependencies]
@@ -38,8 +41,8 @@ dev = [
38
41
  arctx = "arctx_cli.main:main"
39
42
 
40
43
  [project.urls]
41
- Homepage = "https://github.com/takumiecd/stag"
42
- Repository = "https://github.com/takumiecd/stag"
44
+ Homepage = "https://github.com/takumiecd/arctx"
45
+ Repository = "https://github.com/takumiecd/arctx"
43
46
 
44
47
  [tool.hatch.build.targets.wheel]
45
48
  packages = ["src/arctx_cli"]
@@ -17,6 +17,8 @@ def core_cli_commands() -> list[CliCommand]:
17
17
  from arctx_cli.commands.cut import cli_cut
18
18
  from arctx_cli.commands.dump import add_parser as add_dump_parser
19
19
  from arctx_cli.commands.dump import cli_dump
20
+ from arctx_cli.commands.export import add_parser as add_export_parser
21
+ from arctx_cli.commands.export import cli_export
20
22
  from arctx_cli.commands.ext import add_parser as add_ext_parser
21
23
  from arctx_cli.commands.ext import cli_ext
22
24
  from arctx_cli.commands.graph import add_parser as add_graph_parser
@@ -58,6 +60,7 @@ def core_cli_commands() -> list[CliCommand]:
58
60
  CliCommand("current", add_current_parser, cli_current),
59
61
  CliCommand("ext", add_ext_parser, cli_ext),
60
62
  CliCommand("dump", add_dump_parser, cli_dump),
63
+ CliCommand("export", add_export_parser, cli_export),
61
64
  CliCommand("graph", add_graph_parser, cli_graph),
62
65
  CliCommand("guide", add_guide_parser, cli_guide),
63
66
  CliCommand("init", add_init_parser, cli_init),
@@ -0,0 +1,66 @@
1
+ """arctx CLI export command: render a run as markdown, LaTeX, or HTML.
2
+
3
+ Unlike ``dump`` (inspection / LLM), ``export`` produces a standalone document
4
+ to share. By default it strips machine-local data (repo ``local_path``); pass
5
+ ``--include-local`` to keep it. ``--exclude-cut`` drops cut history.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+
12
+ from arctx.core.run.export import ExportOptions, export
13
+ from arctx_cli.context import resolve_run_id_from_args, resolve_store
14
+
15
+
16
+ def add_parser(subparsers) -> argparse.ArgumentParser:
17
+ parser = subparsers.add_parser(
18
+ "export",
19
+ help="Export the run as a shareable document (md / tex / html)",
20
+ )
21
+ parser.add_argument(
22
+ "--format",
23
+ dest="fmt",
24
+ choices=["md", "tex", "html"],
25
+ default="md",
26
+ help="Output format (default: md)",
27
+ )
28
+ parser.add_argument("--node", dest="node_id", default=None,
29
+ help="Export only the subtree rooted at this node")
30
+ parser.add_argument("--depth", type=int, default=None,
31
+ help="Limit traversal depth")
32
+ parser.add_argument("--full-payloads", action="store_true",
33
+ help="Include full payload content")
34
+ parser.add_argument("--exclude-cut", action="store_true",
35
+ help="Drop cut (inactive) nodes and transitions")
36
+ parser.add_argument("--include-local", action="store_true",
37
+ help="Keep repo local_path in the output (off by default)")
38
+ parser.add_argument("--output", "-o", default=None,
39
+ help="Write to this file instead of stdout")
40
+ parser.add_argument("--run", default=None)
41
+ parser.add_argument("--store-dir", default=None)
42
+ return parser
43
+
44
+
45
+ def cli_export(args) -> int:
46
+ store = resolve_store(args.store_dir)
47
+ run_id = resolve_run_id_from_args(args)
48
+ if not store.run_path(run_id).exists():
49
+ raise KeyError(f"unknown run_id: {run_id}")
50
+ handle = store.load_run(run_id)
51
+
52
+ opts = ExportOptions(
53
+ node_id=args.node_id,
54
+ depth=args.depth,
55
+ full_payloads=args.full_payloads,
56
+ exclude_cut=args.exclude_cut,
57
+ include_local=args.include_local,
58
+ )
59
+ text = export(handle, args.fmt, opts)
60
+ if args.output:
61
+ with open(args.output, "w", encoding="utf-8") as f:
62
+ f.write(text)
63
+ print(f"wrote {args.output}")
64
+ else:
65
+ print(text)
66
+ return 0
@@ -31,6 +31,7 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
31
31
  from arctx_cli.ext.git.hook import add_parser as add_hook_parser
32
32
  from arctx_cli.ext.git.merge import add_parser as add_merge_parser
33
33
  from arctx_cli.ext.git.reset import add_parser as add_reset_parser
34
+ from arctx_cli.ext.git.repo import add_init_parser, add_repo_parser
34
35
  from arctx_cli.ext.git.revert import add_parser as add_revert_parser
35
36
  from arctx_cli.ext.git.verify import add_parser as add_verify_parser
36
37
  from arctx_cli.ext.git.worktree import add_parser as add_worktree_parser
@@ -39,7 +40,9 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
39
40
  add_cherry_pick_parser(git_sub)
40
41
  add_commit_parser(git_sub)
41
42
  add_hook_parser(git_sub)
43
+ add_init_parser(git_sub)
42
44
  add_merge_parser(git_sub)
45
+ add_repo_parser(git_sub)
43
46
  add_reset_parser(git_sub)
44
47
  add_revert_parser(git_sub)
45
48
  add_verify_parser(git_sub)
@@ -100,10 +103,18 @@ def cli_git(args) -> int:
100
103
  return cli_commit(args)
101
104
  if args.git_command == "hook":
102
105
  return cli_hook(args)
106
+ if args.git_command == "init":
107
+ from arctx_cli.ext.git.repo import cli_git_init # noqa: PLC0415
108
+
109
+ return cli_git_init(args)
103
110
  if args.git_command == "list":
104
111
  return _cli_git_list(args)
105
112
  if args.git_command == "merge":
106
113
  return cli_merge(args)
114
+ if args.git_command == "repo":
115
+ from arctx_cli.ext.git.repo import cli_repo # noqa: PLC0415
116
+
117
+ return cli_repo(args)
107
118
  if args.git_command == "reset":
108
119
  return cli_reset(args)
109
120
  if args.git_command == "revert":
@@ -19,6 +19,15 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
19
19
  default=None,
20
20
  help="Directory where runs are stored (default: <ARCTX_HOME>/runs)",
21
21
  )
22
+ parser.add_argument(
23
+ "--shell",
24
+ action="store_true",
25
+ help=(
26
+ "Print 'export ARCTX_RUN_ID=<run>' for this terminal instead of "
27
+ "writing the repo's persistent pointer. Use as: "
28
+ 'eval "$(arctx use <run> --shell)"'
29
+ ),
30
+ )
22
31
  return parser
23
32
 
24
33
 
@@ -26,8 +35,15 @@ def run_use_command(
26
35
  *,
27
36
  run_id: str,
28
37
  store_dir: str | None,
38
+ shell: bool = False,
29
39
  ) -> dict:
30
- """Set the current run by writing its id to ``<gitdir>/arctx-id``.
40
+ """Set the current run.
41
+
42
+ By default this writes the run id to ``<gitdir>/arctx-id`` — a persistent,
43
+ repo-scoped default that every terminal in that checkout sees. With
44
+ *shell* set, nothing is written; the caller is expected to ``eval`` the
45
+ emitted ``export ARCTX_RUN_ID=<run>`` line to pin the run for the current
46
+ terminal only (env beats the repo pointer in the resolution chain).
31
47
 
32
48
  Parameters
33
49
  ----------
@@ -35,23 +51,29 @@ def run_use_command(
35
51
  Identifier of the run.
36
52
  store_dir:
37
53
  Directory where runs are stored.
54
+ shell:
55
+ Emit a shell ``export`` line (terminal-scoped) instead of writing the
56
+ repo pointer.
38
57
 
39
58
  Returns
40
59
  -------
41
- dict with ``run_id`` and ``arctx_id_path`` keys.
60
+ dict with ``run_id``; ``arctx_id_path`` (repo mode) or ``export`` (shell
61
+ mode).
42
62
 
43
63
  Raises
44
64
  ------
45
65
  KeyError
46
66
  If the run_id does not exist in the store.
47
67
  RuntimeError
48
- If not inside a git repository.
68
+ If not inside a git repository (repo mode only).
49
69
  """
50
70
  resolved_store_dir = store_dir if store_dir is not None else resolve_store_dir()
51
71
  store = resolve_store(resolved_store_dir)
52
72
  run_path = store.run_path(run_id)
53
73
  if not run_path.exists():
54
74
  raise KeyError(f"unknown run_id: {run_id}")
75
+ if shell:
76
+ return {"run_id": run_id, "export": f"export ARCTX_RUN_ID={run_id}"}
55
77
  repo_root = find_repo_root()
56
78
  write_arctx_id(repo_root, run_id)
57
79
  return {"run_id": run_id, "arctx_id_path": str(arctx_id_path(repo_root))}
@@ -62,6 +84,10 @@ def cli_use(args) -> int:
62
84
  result = run_use_command(
63
85
  run_id=args.run_id,
64
86
  store_dir=args.store_dir,
87
+ shell=getattr(args, "shell", False),
65
88
  )
66
- print(result["run_id"])
89
+ if "export" in result:
90
+ print(result["export"])
91
+ else:
92
+ print(result["run_id"])
67
93
  return 0
@@ -0,0 +1,231 @@
1
+ """arctx git repo — manage the run's git repo registry (the repo 対応表).
2
+
3
+ One run can span several git repos. ``repo add`` is the "途中で入れる" verb:
4
+ it registers a repo into an already-running run (RepoPayload + ``.arctx-id``
5
+ pointer + ``.arctx-repo`` marker, and optionally installs hooks). ``arctx git
6
+ init`` is a thin wrapper that always installs hooks.
7
+
8
+ ``repo list`` / ``repo show`` inspect the registry. These are local-inspection
9
+ commands (like ``dump``), so they show ``local_path`` by default — export is
10
+ the outlet that strips it for sharing.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from arctx_cli.context import (
21
+ resolve_run_id_from_args,
22
+ resolve_store,
23
+ resolve_user_id_from_args,
24
+ resolve_work_session_id_from_args,
25
+ )
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Parser registration
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def add_repo_parser(git_sub) -> argparse.ArgumentParser:
34
+ p = git_sub.add_parser("repo", help="Manage the run's git repo registry (対応表)")
35
+ sub = p.add_subparsers(dest="repo_command", required=True)
36
+
37
+ add = sub.add_parser(
38
+ "add", help="Register a git repo into the current run (途中で入れる)"
39
+ )
40
+ _add_common(add)
41
+ add.add_argument("--repo-path", default=None, help="Repo working tree (default: cwd)")
42
+ add.add_argument("--slug", default=None, help="Override display slug (USER/REPO)")
43
+ add.add_argument("--no-hooks", action="store_true", help="Skip installing git hooks")
44
+ add.add_argument("--user", default=None)
45
+ add.add_argument("--work-session", default=None)
46
+
47
+ lst = sub.add_parser("list", help="List repos registered in the run")
48
+ _add_common(lst)
49
+
50
+ show = sub.add_parser("show", help="Show one repo registry entry as JSON")
51
+ _add_common(show)
52
+ show.add_argument("--repo-id", default=None, help="Repo id (default: resolve cwd)")
53
+ show.add_argument("--repo-path", default=None, help="Resolve by working tree instead")
54
+
55
+ return p
56
+
57
+
58
+ def add_init_parser(git_sub) -> argparse.ArgumentParser:
59
+ p = git_sub.add_parser(
60
+ "init",
61
+ help="Set up git integration for the current run on this repo "
62
+ "(registers the repo and installs hooks)",
63
+ )
64
+ _add_common(p)
65
+ p.add_argument("--repo-path", default=None, help="Repo working tree (default: cwd)")
66
+ p.add_argument("--slug", default=None, help="Override display slug (USER/REPO)")
67
+ p.add_argument("--no-hooks", action="store_true", help="Skip installing git hooks")
68
+ p.add_argument("--user", default=None)
69
+ p.add_argument("--work-session", default=None)
70
+ return p
71
+
72
+
73
+ def _add_common(p: argparse.ArgumentParser) -> None:
74
+ p.add_argument("--run", default=None)
75
+ p.add_argument("--store-dir", default=None)
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # add (the primitive; git init wraps it)
80
+ # ---------------------------------------------------------------------------
81
+
82
+
83
+ def run_repo_add(
84
+ *,
85
+ repo_path: str | None,
86
+ slug: str | None,
87
+ run_id: str | None,
88
+ store_dir: str | None,
89
+ user_id: str | None,
90
+ work_session_id: str | None,
91
+ install_hooks: bool,
92
+ ) -> dict:
93
+ from arctx.ext.git.helpers.repo import resolve_worktree_path
94
+ from arctx.ext.git.registry import list_repos, repo_by_id, resolve_repo_id
95
+ from arctx.paths import find_repo_root, write_arctx_id
96
+
97
+ store = resolve_store(store_dir)
98
+ handle = store.load_run(run_id)
99
+
100
+ resolved_path = resolve_worktree_path(repo_path)
101
+ existing_ids = {r.repo_id for r in list_repos(handle.run_graph)}
102
+ repo_id = resolve_repo_id(handle, resolved_path, slug=slug)
103
+ entry = repo_by_id(handle.run_graph, repo_id)
104
+
105
+ if repo_id not in existing_ids and entry is not None:
106
+ handle.record_work_event(
107
+ user_id=user_id,
108
+ work_session_id=work_session_id,
109
+ event_type="repo_added",
110
+ target_kind="node",
111
+ target_id=handle.root_node_id,
112
+ created_records=(entry.payload_id,),
113
+ summary=f"repo {entry.slug or repo_id} added",
114
+ data={"repo_id": repo_id, "slug": entry.slug, "canonical": entry.canonical},
115
+ )
116
+
117
+ store.save_run(handle)
118
+
119
+ # Point this repo's checkout at the run so future commands resolve it.
120
+ try:
121
+ repo_root = find_repo_root(resolved_path)
122
+ write_arctx_id(repo_root, handle.run_id)
123
+ except RuntimeError:
124
+ repo_root = Path(resolved_path)
125
+
126
+ hooks: dict | None = None
127
+ if install_hooks:
128
+ from arctx_cli.ext.git.hook import run_hook_install
129
+
130
+ hooks = run_hook_install(repo_path=repo_root)
131
+
132
+ result: dict = {
133
+ "run_id": handle.run_id,
134
+ "repo_id": repo_id,
135
+ "slug": entry.slug if entry else None,
136
+ "canonical": entry.canonical if entry else None,
137
+ "local_path": entry.local_path if entry else None,
138
+ }
139
+ if hooks is not None:
140
+ result["hooks"] = hooks.get("status")
141
+ return result
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Dispatch
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ def cli_repo(args) -> int:
150
+ if args.repo_command == "add":
151
+ return _cli_repo_add(args)
152
+ if args.repo_command == "list":
153
+ return _cli_repo_list(args)
154
+ if args.repo_command == "show":
155
+ return _cli_repo_show(args)
156
+ print(f"unknown repo subcommand: {args.repo_command}", file=sys.stderr)
157
+ return 1
158
+
159
+
160
+ def cli_git_init(args) -> int:
161
+ try:
162
+ result = run_repo_add(
163
+ repo_path=args.repo_path,
164
+ slug=args.slug,
165
+ run_id=resolve_run_id_from_args(args),
166
+ store_dir=args.store_dir,
167
+ user_id=resolve_user_id_from_args(args),
168
+ work_session_id=resolve_work_session_id_from_args(args),
169
+ install_hooks=not args.no_hooks,
170
+ )
171
+ except Exception as exc: # noqa: BLE001
172
+ print(f"error: {exc}", file=sys.stderr)
173
+ return 1
174
+ print(json.dumps(result, indent=2))
175
+ return 0
176
+
177
+
178
+ def _cli_repo_add(args) -> int:
179
+ try:
180
+ result = run_repo_add(
181
+ repo_path=args.repo_path,
182
+ slug=args.slug,
183
+ run_id=resolve_run_id_from_args(args),
184
+ store_dir=args.store_dir,
185
+ user_id=resolve_user_id_from_args(args),
186
+ work_session_id=resolve_work_session_id_from_args(args),
187
+ install_hooks=not args.no_hooks,
188
+ )
189
+ except Exception as exc: # noqa: BLE001
190
+ print(f"error: {exc}", file=sys.stderr)
191
+ return 1
192
+ print(json.dumps(result, indent=2))
193
+ return 0
194
+
195
+
196
+ def _cli_repo_list(args) -> int:
197
+ from arctx.ext.git.registry import list_repos
198
+
199
+ store = resolve_store(args.store_dir)
200
+ run_id = resolve_run_id_from_args(args)
201
+ handle = store.load_run(run_id)
202
+ entries = [r.to_dict() for r in list_repos(handle.run_graph)]
203
+ print(json.dumps(entries, indent=2))
204
+ return 0
205
+
206
+
207
+ def _cli_repo_show(args) -> int:
208
+ from arctx.ext.git.helpers.repo import resolve_worktree_path
209
+ from arctx.ext.git.registry import read_repo_marker, repo_by_id
210
+
211
+ store = resolve_store(args.store_dir)
212
+ run_id = resolve_run_id_from_args(args)
213
+ handle = store.load_run(run_id)
214
+
215
+ repo_id = args.repo_id
216
+ if repo_id is None:
217
+ marker = read_repo_marker(resolve_worktree_path(args.repo_path))
218
+ if marker is None:
219
+ print(
220
+ "error: no --repo-id given and no .arctx-repo marker found in cwd",
221
+ file=sys.stderr,
222
+ )
223
+ return 1
224
+ repo_id = marker
225
+
226
+ entry = repo_by_id(handle.run_graph, repo_id)
227
+ if entry is None:
228
+ print(f"error: repo not found in run: {repo_id}", file=sys.stderr)
229
+ return 1
230
+ print(json.dumps(entry.to_dict(), indent=2))
231
+ return 0
@@ -0,0 +1,118 @@
1
+ """Integration tests for `arctx git repo` / `arctx git init` with real repos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from arctx_cli.commands.init import run_init_command
9
+ from arctx_cli.context import resolve_store
10
+ from arctx_cli.ext.git.repo import run_repo_add
11
+
12
+
13
+ def _init_git_repo(path: Path, *, remote: str | None = None) -> Path:
14
+ path.mkdir(parents=True, exist_ok=True)
15
+ subprocess.run(["git", "init"], cwd=str(path), capture_output=True, check=True)
16
+ subprocess.run(["git", "config", "user.email", "t@e.com"], cwd=str(path),
17
+ capture_output=True, check=True)
18
+ subprocess.run(["git", "config", "user.name", "t"], cwd=str(path),
19
+ capture_output=True, check=True)
20
+ subprocess.run(["git", "checkout", "-b", "main"], cwd=str(path),
21
+ capture_output=True, check=True)
22
+ (path / "README.md").write_text("hello\n")
23
+ subprocess.run(["git", "add", "."], cwd=str(path), capture_output=True, check=True)
24
+ subprocess.run(["git", "commit", "-m", "init"], cwd=str(path),
25
+ capture_output=True, check=True)
26
+ if remote is not None:
27
+ subprocess.run(["git", "remote", "add", "origin", remote], cwd=str(path),
28
+ capture_output=True, check=True)
29
+ return path
30
+
31
+
32
+ def _store_dir(tmp_path: Path) -> str:
33
+ return str(tmp_path / "arctx_home" / "runs")
34
+
35
+
36
+ def _init_run(tmp_path: Path, run_id: str) -> None:
37
+ run_init_command(
38
+ requirement_id="req1",
39
+ target_type="task",
40
+ target_id="t",
41
+ run_id=run_id,
42
+ store_dir=_store_dir(tmp_path),
43
+ extensions=["git"],
44
+ )
45
+
46
+
47
+ class TestRepoAdd:
48
+ def test_add_registers_repo_and_persists(self, tmp_path, monkeypatch):
49
+ repo = _init_git_repo(tmp_path / "repo", remote="git@github.com:me/proj.git")
50
+ monkeypatch.setenv("ARCTX_HOME", str(tmp_path / "arctx_home"))
51
+ monkeypatch.chdir(repo)
52
+ _init_run(tmp_path, "run_a")
53
+
54
+ result = run_repo_add(
55
+ repo_path=str(repo),
56
+ slug=None,
57
+ run_id="run_a",
58
+ store_dir=_store_dir(tmp_path),
59
+ user_id="alice",
60
+ work_session_id="ws",
61
+ install_hooks=False,
62
+ )
63
+ assert result["repo_id"].startswith("repo_")
64
+ assert result["slug"] == "me/proj"
65
+ assert result["canonical"] == "github.com/me/proj"
66
+
67
+ # Reload from store: registry survived persistence.
68
+ store = resolve_store(_store_dir(tmp_path))
69
+ handle = store.load_run("run_a")
70
+ from arctx.ext.git.registry import list_repos
71
+
72
+ repos = list_repos(handle.run_graph)
73
+ assert [r.repo_id for r in repos] == [result["repo_id"]]
74
+ # .arctx-id pointer written for this repo.
75
+ from arctx.paths import read_arctx_id
76
+
77
+ assert read_arctx_id(repo) == "run_a"
78
+
79
+ def test_slug_override(self, tmp_path, monkeypatch):
80
+ repo = _init_git_repo(tmp_path / "repo", remote="git@github.com:me/proj.git")
81
+ monkeypatch.setenv("ARCTX_HOME", str(tmp_path / "arctx_home"))
82
+ monkeypatch.chdir(repo)
83
+ _init_run(tmp_path, "run_b")
84
+
85
+ result = run_repo_add(
86
+ repo_path=str(repo),
87
+ slug="custom/name",
88
+ run_id="run_b",
89
+ store_dir=_store_dir(tmp_path),
90
+ user_id="alice",
91
+ work_session_id="ws",
92
+ install_hooks=False,
93
+ )
94
+ assert result["slug"] == "custom/name"
95
+
96
+ def test_idempotent_re_add(self, tmp_path, monkeypatch):
97
+ repo = _init_git_repo(tmp_path / "repo", remote="git@github.com:me/proj.git")
98
+ monkeypatch.setenv("ARCTX_HOME", str(tmp_path / "arctx_home"))
99
+ monkeypatch.chdir(repo)
100
+ _init_run(tmp_path, "run_c")
101
+
102
+ first = run_repo_add(
103
+ repo_path=str(repo), slug=None, run_id="run_c",
104
+ store_dir=_store_dir(tmp_path), user_id="alice",
105
+ work_session_id="ws", install_hooks=False,
106
+ )
107
+ second = run_repo_add(
108
+ repo_path=str(repo), slug=None, run_id="run_c",
109
+ store_dir=_store_dir(tmp_path), user_id="alice",
110
+ work_session_id="ws", install_hooks=False,
111
+ )
112
+ assert first["repo_id"] == second["repo_id"]
113
+
114
+ store = resolve_store(_store_dir(tmp_path))
115
+ handle = store.load_run("run_c")
116
+ from arctx.ext.git.registry import list_repos
117
+
118
+ assert len(list_repos(handle.run_graph)) == 1
@@ -182,6 +182,58 @@ def test_use_raises_for_unknown_run(tmp_path, monkeypatch):
182
182
  run_use_command(run_id="no_such_run", store_dir=_store_dir(tmp_path))
183
183
 
184
184
 
185
+ def test_use_shell_emits_export_without_writing_pointer(tmp_path, monkeypatch):
186
+ repo = _fake_git_repo(tmp_path)
187
+ monkeypatch.setenv("ARCTX_HOME", str(_arctx_home(tmp_path)))
188
+ monkeypatch.chdir(repo)
189
+ run_init_command(
190
+ requirement_id="req1", target_type="task", target_id="t",
191
+ run_id="run_a", store_dir=_store_dir(tmp_path),
192
+ )
193
+ run_init_command(
194
+ requirement_id="req2", target_type="task", target_id="t",
195
+ run_id="run_b", store_dir=_store_dir(tmp_path),
196
+ )
197
+ # After the two inits the repo pointer is run_b.
198
+ assert read_arctx_id(repo) == "run_b"
199
+
200
+ result = run_use_command(
201
+ run_id="run_a", store_dir=_store_dir(tmp_path), shell=True
202
+ )
203
+
204
+ # Shell mode emits an export line for eval and leaves the repo pointer
205
+ # untouched (terminal-scoped, not repo-scoped).
206
+ assert result["export"] == "export ARCTX_RUN_ID=run_a"
207
+ assert read_arctx_id(repo) == "run_b"
208
+
209
+
210
+ def test_use_shell_validates_run_exists(tmp_path, monkeypatch):
211
+ repo = _fake_git_repo(tmp_path)
212
+ monkeypatch.setenv("ARCTX_HOME", str(_arctx_home(tmp_path)))
213
+ monkeypatch.chdir(repo)
214
+
215
+ with pytest.raises(KeyError, match="unknown run_id"):
216
+ run_use_command(run_id="nope", store_dir=_store_dir(tmp_path), shell=True)
217
+
218
+
219
+ def test_use_shell_works_outside_git_repo(tmp_path, monkeypatch):
220
+ # No .git here: shell mode must not require a repo (it writes no pointer).
221
+ monkeypatch.setenv("ARCTX_HOME", str(_arctx_home(tmp_path)))
222
+ monkeypatch.chdir(tmp_path)
223
+ run_init_command(
224
+ requirement_id="req1",
225
+ target_type="task",
226
+ target_id="t",
227
+ run_id="run_a",
228
+ store_dir=_store_dir(tmp_path),
229
+ )
230
+
231
+ result = run_use_command(
232
+ run_id="run_a", store_dir=_store_dir(tmp_path), shell=True
233
+ )
234
+ assert result["export"] == "export ARCTX_RUN_ID=run_a"
235
+
236
+
185
237
  # ---------------------------------------------------------------------------
186
238
  # ARCTX_HOME env overrides run storage location end-to-end
187
239
  # ---------------------------------------------------------------------------
File without changes
File without changes