gitwise-cli 0.24.2__py3-none-any.whl

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 (125) hide show
  1. gitwise/__init__.py +11 -0
  2. gitwise/__main__.py +113 -0
  3. gitwise/_cli_completions.py +88 -0
  4. gitwise/_cli_dispatch.py +469 -0
  5. gitwise/_cli_introspection.py +275 -0
  6. gitwise/_cli_parser.py +345 -0
  7. gitwise/_cli_setup_agents.py +439 -0
  8. gitwise/_i18n_data.json +1934 -0
  9. gitwise/_paths.py +22 -0
  10. gitwise/_runtime_config.py +246 -0
  11. gitwise/audit.py +338 -0
  12. gitwise/branches.py +183 -0
  13. gitwise/clean.py +197 -0
  14. gitwise/commit.py +142 -0
  15. gitwise/conflicts.py +112 -0
  16. gitwise/context.py +163 -0
  17. gitwise/design.py +383 -0
  18. gitwise/diff.py +309 -0
  19. gitwise/doctor.py +116 -0
  20. gitwise/git.py +254 -0
  21. gitwise/health.py +345 -0
  22. gitwise/i18n.py +99 -0
  23. gitwise/log.py +329 -0
  24. gitwise/merge.py +193 -0
  25. gitwise/optimize.py +212 -0
  26. gitwise/output.py +652 -0
  27. gitwise/pick.py +102 -0
  28. gitwise/pr.py +543 -0
  29. gitwise/py.typed +0 -0
  30. gitwise/schema.py +49 -0
  31. gitwise/setup.py +551 -0
  32. gitwise/setup_agents/__init__.py +36 -0
  33. gitwise/setup_agents/adapters/__init__.py +17 -0
  34. gitwise/setup_agents/adapters/aider.py +5 -0
  35. gitwise/setup_agents/adapters/base.py +5 -0
  36. gitwise/setup_agents/adapters/codex.py +5 -0
  37. gitwise/setup_agents/adapters/continue_adapter.py +5 -0
  38. gitwise/setup_agents/adapters/cursor.py +5 -0
  39. gitwise/setup_agents/adapters/opencode.py +5 -0
  40. gitwise/setup_agents/adapters/pi.py +5 -0
  41. gitwise/setup_agents/exec.py +449 -0
  42. gitwise/setup_agents/format.py +164 -0
  43. gitwise/setup_agents/plan.py +254 -0
  44. gitwise/setup_agents/plan_gitfiles.py +167 -0
  45. gitwise/setup_agents/plan_skills.py +256 -0
  46. gitwise/setup_agents/providers/__init__.py +96 -0
  47. gitwise/setup_agents/providers/aider.py +11 -0
  48. gitwise/setup_agents/providers/base.py +79 -0
  49. gitwise/setup_agents/providers/claude.py +408 -0
  50. gitwise/setup_agents/providers/codex.py +11 -0
  51. gitwise/setup_agents/providers/continue_adapter.py +11 -0
  52. gitwise/setup_agents/providers/cursor.py +11 -0
  53. gitwise/setup_agents/providers/opencode.py +11 -0
  54. gitwise/setup_agents/providers/pi.py +11 -0
  55. gitwise/setup_agents/state.py +141 -0
  56. gitwise/setup_agents/types.py +48 -0
  57. gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
  58. gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
  59. gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
  60. gitwise/share/aider/CONVENTIONS.md.template +8 -0
  61. gitwise/share/aider/aider.conf.yml.template +4 -0
  62. gitwise/share/claude/CLAUDE.md.template +9 -0
  63. gitwise/share/claude/rules/gitwise.md +16 -0
  64. gitwise/share/claude/settings.json.template +47 -0
  65. gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
  66. gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
  67. gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
  68. gitwise/share/codex/agents/gitwise.toml.template +18 -0
  69. gitwise/share/continue/rules/gitwise.md.template +14 -0
  70. gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
  71. gitwise/share/git-config-modern.txt +48 -0
  72. gitwise/share/hooks/commit-msg +22 -0
  73. gitwise/share/hooks/pre-commit +19 -0
  74. gitwise/share/opencode/agents/gitwise.md.template +14 -0
  75. gitwise/share/pi/skills/gitwise.md.template +14 -0
  76. gitwise/share/schemas/v1/input/audit.json +40 -0
  77. gitwise/share/schemas/v1/input/branches.json +51 -0
  78. gitwise/share/schemas/v1/input/clean.json +52 -0
  79. gitwise/share/schemas/v1/input/commands.json +36 -0
  80. gitwise/share/schemas/v1/input/commit.json +63 -0
  81. gitwise/share/schemas/v1/input/completions.json +51 -0
  82. gitwise/share/schemas/v1/input/conflicts.json +46 -0
  83. gitwise/share/schemas/v1/input/context.json +36 -0
  84. gitwise/share/schemas/v1/input/diff.json +56 -0
  85. gitwise/share/schemas/v1/input/doctor.json +36 -0
  86. gitwise/share/schemas/v1/input/health.json +36 -0
  87. gitwise/share/schemas/v1/input/log.json +71 -0
  88. gitwise/share/schemas/v1/input/merge.json +63 -0
  89. gitwise/share/schemas/v1/input/optimize.json +44 -0
  90. gitwise/share/schemas/v1/input/pick.json +63 -0
  91. gitwise/share/schemas/v1/input/pr.json +51 -0
  92. gitwise/share/schemas/v1/input/schema.json +48 -0
  93. gitwise/share/schemas/v1/input/setup-agents.json +108 -0
  94. gitwise/share/schemas/v1/input/setup.json +55 -0
  95. gitwise/share/schemas/v1/input/show.json +46 -0
  96. gitwise/share/schemas/v1/input/snapshot.json +36 -0
  97. gitwise/share/schemas/v1/input/stash.json +68 -0
  98. gitwise/share/schemas/v1/input/status.json +36 -0
  99. gitwise/share/schemas/v1/input/suggest.json +36 -0
  100. gitwise/share/schemas/v1/input/summarize.json +44 -0
  101. gitwise/share/schemas/v1/input/sync.json +55 -0
  102. gitwise/share/schemas/v1/input/tag.json +73 -0
  103. gitwise/share/schemas/v1/input/undo.json +60 -0
  104. gitwise/share/schemas/v1/input/update.json +40 -0
  105. gitwise/share/schemas/v1/input/worktree.json +50 -0
  106. gitwise/show.py +118 -0
  107. gitwise/snapshot.py +110 -0
  108. gitwise/stash.py +188 -0
  109. gitwise/status.py +93 -0
  110. gitwise/suggest.py +148 -0
  111. gitwise/summarize.py +202 -0
  112. gitwise/sync.py +257 -0
  113. gitwise/tag.py +252 -0
  114. gitwise/undo.py +145 -0
  115. gitwise/update.py +42 -0
  116. gitwise/utils/__init__.py +1 -0
  117. gitwise/utils/git_output.py +51 -0
  118. gitwise/utils/json_envelope.py +58 -0
  119. gitwise/utils/parsing.py +34 -0
  120. gitwise/worktree.py +182 -0
  121. gitwise_cli-0.24.2.dist-info/METADATA +151 -0
  122. gitwise_cli-0.24.2.dist-info/RECORD +125 -0
  123. gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
  124. gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
  125. gitwise_cli-0.24.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,96 @@
1
+ """Provider registry: multi-agent tool support for setup-agents."""
2
+
3
+ from pathlib import Path
4
+
5
+ from gitwise.i18n import t
6
+ from gitwise.setup_agents.plan_skills import _SKILLS
7
+ from gitwise.setup_agents.providers.aider import ADAPTER as AIDER
8
+ from gitwise.setup_agents.providers.base import AdapterConfig, AdapterContext
9
+ from gitwise.setup_agents.providers.claude import ADAPTER as CLAUDE
10
+ from gitwise.setup_agents.providers.codex import ADAPTER as CODEX
11
+ from gitwise.setup_agents.providers.continue_adapter import ADAPTER as CONTINUE
12
+ from gitwise.setup_agents.providers.cursor import ADAPTER as CURSOR
13
+ from gitwise.setup_agents.providers.opencode import ADAPTER as OPENCODE
14
+ from gitwise.setup_agents.providers.pi import ADAPTER as PI
15
+ from gitwise.setup_agents.state import _AGENTS_MD, _detect_state, _gpg_ready
16
+
17
+ ADAPTERS: dict[str, AdapterConfig] = {
18
+ "claude": CLAUDE,
19
+ "cursor": CURSOR,
20
+ "continue": CONTINUE,
21
+ "opencode": OPENCODE,
22
+ "codex": CODEX,
23
+ "aider": AIDER,
24
+ "pi": PI,
25
+ }
26
+
27
+
28
+ def list_adapters() -> list[str]:
29
+ return sorted(ADAPTERS.keys())
30
+
31
+
32
+ def list_providers() -> list[str]:
33
+ return list_adapters()
34
+
35
+
36
+ def resolve_adapter_selection(
37
+ names: list[str] | None,
38
+ ) -> tuple[list[AdapterConfig], list[str]]:
39
+ if not names:
40
+ return [], []
41
+ normalized = ["claude" if name == "claude-only" else name for name in names]
42
+ if "none" in normalized:
43
+ if len(normalized) > 1:
44
+ return [], [t("adapters_none_with_others")]
45
+ return [], []
46
+ resolved: list[AdapterConfig] = []
47
+ seen: set[str] = set()
48
+ errors: list[str] = []
49
+ for name in normalized:
50
+ if name in seen:
51
+ continue
52
+ seen.add(name)
53
+ cfg = ADAPTERS.get(name)
54
+ if cfg is None:
55
+ errors.append(t("unknown_adapter", name=name))
56
+ else:
57
+ resolved.append(cfg)
58
+ return resolved, errors
59
+
60
+
61
+ def detect_global_skills(home: Path | None = None) -> frozenset[str]:
62
+ home_dir = home or Path.home()
63
+ return frozenset(
64
+ skill_name
65
+ for skill_name in _SKILLS
66
+ if (home_dir / ".claude" / "skills" / skill_name / "SKILL.md").exists()
67
+ )
68
+
69
+
70
+ def plan_adapter_actions(
71
+ adapter_names: list[str] | None,
72
+ root: Path,
73
+ context: AdapterContext | None = None,
74
+ ) -> tuple[list[dict], list[str], list[str]]:
75
+ if not adapter_names:
76
+ return [], [], []
77
+ selected, errors = resolve_adapter_selection(adapter_names)
78
+ if errors:
79
+ return [], errors, []
80
+ if context is None:
81
+ state = _detect_state(root)
82
+ context = {
83
+ "state": state,
84
+ "canonical_doc_path": _AGENTS_MD,
85
+ "global_skills": detect_global_skills(),
86
+ "supports_symlinks": state["supports_symlinks"],
87
+ "gpg_ready": _gpg_ready(root),
88
+ "flags": {},
89
+ }
90
+ actions: list[dict] = []
91
+ warnings: list[str] = []
92
+ for cfg in selected:
93
+ adapter_actions, adapter_warnings = cfg.plan(root, context)
94
+ actions.extend(adapter_actions)
95
+ warnings.extend(adapter_warnings)
96
+ return actions, [], warnings
@@ -0,0 +1,11 @@
1
+ """Aider provider — .aider.conf.yml + CONVENTIONS.md."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="aider",
7
+ display_name="Aider",
8
+ config_paths=(".aider.conf.yml", "CONVENTIONS.md"),
9
+ template_paths=("aider.conf.yml.template", "CONVENTIONS.md.template"),
10
+ template_dir="share/aider",
11
+ )
@@ -0,0 +1,79 @@
1
+ """Base types and planning behavior for the provider registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TypedDict
7
+
8
+ from gitwise.i18n import t
9
+ from gitwise.setup_agents.state import _classify_path
10
+ from gitwise.setup_agents.types import ActionDict, StateDict
11
+
12
+
13
+ class AdapterFlags(TypedDict, total=False):
14
+ no_symlinks: bool
15
+ replace_claude_with_symlink: bool
16
+ migrate_legacy_claude: bool
17
+ frozen_time: bool
18
+ no_git_files: bool
19
+ core_claude_planned: bool
20
+
21
+
22
+ class AdapterContext(TypedDict):
23
+ state: StateDict
24
+ canonical_doc_path: str
25
+ global_skills: frozenset[str]
26
+ supports_symlinks: bool
27
+ gpg_ready: bool
28
+ flags: AdapterFlags
29
+
30
+
31
+ class AdapterConfig:
32
+ def __init__(
33
+ self,
34
+ *,
35
+ name: str,
36
+ display_name: str,
37
+ config_paths: tuple[str, ...],
38
+ template_paths: tuple[str, ...],
39
+ template_dir: str,
40
+ ) -> None:
41
+ self.name = name
42
+ self.display_name = display_name
43
+ self.config_paths = config_paths
44
+ self.template_paths = template_paths
45
+ self.template_dir = template_dir
46
+
47
+ def _read_template(self, template_name: str) -> str:
48
+ from gitwise._paths import share_dir
49
+
50
+ template_dir_str = str(self.template_dir)
51
+ relative = (
52
+ template_dir_str.removeprefix("share/")
53
+ if template_dir_str.startswith("share/")
54
+ else template_dir_str
55
+ )
56
+ template_path = share_dir() / relative / template_name
57
+ if not template_path.exists():
58
+ raise FileNotFoundError(t("adapter_no_template", path=str(template_path)))
59
+ return template_path.read_text(encoding="utf-8")
60
+
61
+ def plan(self, root: Path, _context: AdapterContext) -> tuple[list[ActionDict], list[str]]:
62
+ actions: list[ActionDict] = []
63
+ warnings: list[str] = []
64
+ for config_path, template_path in zip(self.config_paths, self.template_paths, strict=True):
65
+ target_path = root / config_path
66
+ state = _classify_path(target_path)
67
+ if state == "absent":
68
+ content = self._read_template(template_path)
69
+ actions.append(
70
+ {
71
+ "action": "adapter-create",
72
+ "file": config_path,
73
+ "content": content,
74
+ "adapter": self.display_name,
75
+ }
76
+ )
77
+ else:
78
+ warnings.append(t("adapter_exists", adapter=self.display_name, file=config_path))
79
+ return actions, warnings
@@ -0,0 +1,408 @@
1
+ """Claude provider wrapper for staged migration from plan.py."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from gitwise.i18n import t
9
+ from gitwise.setup_agents.plan_skills import _read_template, plan_global_skills
10
+ from gitwise.setup_agents.providers.base import AdapterConfig, AdapterContext
11
+ from gitwise.setup_agents.state import (
12
+ _AGENTS_MD,
13
+ _CLAUDE_MD,
14
+ _files_equal,
15
+ _gpg_ready,
16
+ _has_marker,
17
+ )
18
+ from gitwise.setup_agents.types import ActionDict, StateDict
19
+
20
+
21
+ class ClaudeAdapter(AdapterConfig):
22
+ def __init__(self) -> None:
23
+ super().__init__(
24
+ name="claude",
25
+ display_name="Claude",
26
+ config_paths=(),
27
+ template_paths=(),
28
+ template_dir="share/claude",
29
+ )
30
+
31
+ def plan_settings(self, root: Path) -> tuple[list[ActionDict], list[str]]:
32
+ settings_path = root / ".claude" / "settings.json"
33
+ settings_template: dict = json.loads(_read_template("settings.json.template"))
34
+ if settings_path.exists():
35
+ try:
36
+ existing_settings: dict = json.loads(settings_path.read_text(encoding="utf-8"))
37
+ except json.JSONDecodeError:
38
+ return [
39
+ {
40
+ "file": ".claude/settings.json",
41
+ "action": "create",
42
+ "data": settings_template,
43
+ }
44
+ ], [t("invalid_json")]
45
+
46
+ existing_deny: list = existing_settings.get("permissions", {}).get("deny", [])
47
+ new_deny: list = settings_template.get("permissions", {}).get("deny", [])
48
+ merged_deny = list(dict.fromkeys(existing_deny + new_deny))
49
+ existing_settings.setdefault("permissions", {})["deny"] = merged_deny
50
+ gpg_rules = [r for r in merged_deny if "gpgsign" in r or "no-gpg-sign" in r]
51
+ warnings: list[str] = []
52
+ if not gpg_rules:
53
+ warnings.append(t("settings_sin_gpg_deny"))
54
+ return [
55
+ {
56
+ "file": ".claude/settings.json",
57
+ "action": "merge",
58
+ "data": existing_settings,
59
+ }
60
+ ], warnings
61
+
62
+ return [
63
+ {
64
+ "file": ".claude/settings.json",
65
+ "action": "create",
66
+ "data": settings_template,
67
+ }
68
+ ], []
69
+
70
+ def pointer_template(self) -> str:
71
+ return t("conventions_heading") + t("conventions_pointer")
72
+
73
+ def compute_backup_path(self, path: Path) -> Path:
74
+ backup = path.with_suffix(".md.bak")
75
+ if backup.exists():
76
+ suffix = int(time.time())
77
+ backup = path.parent / f"{path.stem}.md.bak.{suffix}"
78
+ return backup
79
+
80
+ def build_template(self, root: Path) -> str:
81
+ raw = _read_template("CLAUDE.md.template")
82
+ if _gpg_ready(root):
83
+ return raw
84
+ return (
85
+ "\n".join(
86
+ line
87
+ for line in raw.splitlines()
88
+ if "GPG" not in line and "gpg-sign" not in line.lower()
89
+ )
90
+ + "\n"
91
+ )
92
+
93
+ def bucket1_no_agents(
94
+ self,
95
+ state: StateDict,
96
+ root: Path,
97
+ template: str,
98
+ ) -> tuple[int, list[ActionDict], list[str]]:
99
+ c_state = state["c_state"]
100
+ claude_md = root / _CLAUDE_MD
101
+ if c_state == "absent":
102
+ return 1, [{"file": _CLAUDE_MD, "action": "create", "content": template}], []
103
+ if c_state in ("symlink_valid", "regular"):
104
+ if _has_marker(claude_md):
105
+ return (
106
+ 1,
107
+ [
108
+ {
109
+ "file": _CLAUDE_MD,
110
+ "action": "skip",
111
+ "reason": t("already_contains_conventions"),
112
+ }
113
+ ],
114
+ [],
115
+ )
116
+ return 1, [{"file": _CLAUDE_MD, "action": "append", "content": template}], []
117
+ return 1, [], []
118
+
119
+ def bucket2_agents_no_claude(
120
+ self,
121
+ root: Path,
122
+ supports_symlinks: bool,
123
+ template: str,
124
+ ) -> tuple[int, list[ActionDict], list[str]]:
125
+ agents_actions: list[ActionDict] = []
126
+ agents_md = root / _AGENTS_MD
127
+ if not _has_marker(agents_md):
128
+ agents_actions.append({"file": _AGENTS_MD, "action": "append", "content": template})
129
+ if supports_symlinks:
130
+ claude_actions: list[ActionDict] = [
131
+ {"file": _CLAUDE_MD, "action": "symlink-create", "target_relative": _AGENTS_MD}
132
+ ]
133
+ else:
134
+ claude_actions = [
135
+ {"file": _CLAUDE_MD, "action": "create", "content": self.pointer_template()}
136
+ ]
137
+ return 2, agents_actions + claude_actions, []
138
+
139
+ def _migrate_legacy_claude_only(
140
+ self,
141
+ *,
142
+ root: Path,
143
+ state: StateDict,
144
+ template: str,
145
+ supports_symlinks: bool,
146
+ ) -> tuple[int, list[ActionDict], list[str]]:
147
+ claude_md = root / _CLAUDE_MD
148
+ c_state = state["c_state"]
149
+
150
+ if c_state == "regular":
151
+ try:
152
+ agents_content = claude_md.read_text(encoding="utf-8")
153
+ except OSError:
154
+ agents_content = template
155
+ else:
156
+ agents_content = template
157
+
158
+ agents_actions: list[ActionDict] = [
159
+ {
160
+ "file": _AGENTS_MD,
161
+ "action": "create",
162
+ "content": agents_content,
163
+ }
164
+ ]
165
+
166
+ if c_state == "absent":
167
+ if supports_symlinks:
168
+ return (
169
+ 2,
170
+ agents_actions
171
+ + [
172
+ {
173
+ "file": _CLAUDE_MD,
174
+ "action": "symlink-create",
175
+ "target_relative": _AGENTS_MD,
176
+ }
177
+ ],
178
+ [],
179
+ )
180
+ return (
181
+ 2,
182
+ agents_actions
183
+ + [
184
+ {
185
+ "file": _CLAUDE_MD,
186
+ "action": "create",
187
+ "content": self.pointer_template(),
188
+ }
189
+ ],
190
+ [],
191
+ )
192
+
193
+ if c_state == "regular":
194
+ _bucket, replace_actions, replace_warnings = self.bucket4_replace(
195
+ agents_actions, claude_md
196
+ )
197
+ return 4, replace_actions, replace_warnings
198
+
199
+ _bucket, fallback_actions, fallback_warnings = self.bucket4_default(
200
+ state, claude_md, agents_actions
201
+ )
202
+ return 4, fallback_actions, fallback_warnings
203
+
204
+ def bucket4_default(
205
+ self,
206
+ state: StateDict,
207
+ claude_md: Path,
208
+ agents_actions: list[ActionDict],
209
+ ) -> tuple[int, list[ActionDict], list[str]]:
210
+ c_state = state["c_state"]
211
+ if c_state == "symlink_valid":
212
+ try:
213
+ link_target = os.readlink(claude_md)
214
+ except OSError:
215
+ link_target = ""
216
+ return (
217
+ 4,
218
+ agents_actions,
219
+ [
220
+ t(
221
+ "claude_md_symlink_other",
222
+ file=_CLAUDE_MD,
223
+ existing=link_target,
224
+ expected=_AGENTS_MD,
225
+ )
226
+ ],
227
+ )
228
+ if c_state == "regular":
229
+ return 4, agents_actions, [t("claude_md_separate", c=_CLAUDE_MD, a=_AGENTS_MD)]
230
+ return 5, [], []
231
+
232
+ def bucket4_replace(
233
+ self,
234
+ agents_actions: list[ActionDict],
235
+ claude_md: Path,
236
+ ) -> tuple[int, list[ActionDict], list[str]]:
237
+ backup_path = self.compute_backup_path(claude_md)
238
+ return (
239
+ 4,
240
+ agents_actions
241
+ + [
242
+ {
243
+ "file": _CLAUDE_MD,
244
+ "action": "claude-md-replace-with-symlink",
245
+ "target_relative": _AGENTS_MD,
246
+ "backup_path": str(backup_path),
247
+ }
248
+ ],
249
+ [
250
+ t(
251
+ "claude_md_replaced",
252
+ file=_CLAUDE_MD,
253
+ target=_AGENTS_MD,
254
+ backup=backup_path.name,
255
+ )
256
+ ],
257
+ )
258
+
259
+ def bucket3(
260
+ self,
261
+ state: StateDict,
262
+ claude_md: Path,
263
+ agents_md: Path,
264
+ agents_actions: list[ActionDict],
265
+ ) -> tuple[int, list[ActionDict], list[str]]:
266
+ c_state = state["c_state"]
267
+ if c_state == "symlink_valid":
268
+ try:
269
+ link_target = os.readlink(claude_md)
270
+ except OSError:
271
+ link_target = ""
272
+ points_to_agents = link_target == _AGENTS_MD or Path(
273
+ os.path.realpath(str(claude_md.parent / link_target))
274
+ ) == Path(os.path.realpath(str(agents_md)))
275
+ if points_to_agents:
276
+ return (
277
+ 3,
278
+ agents_actions
279
+ + [
280
+ {
281
+ "file": _CLAUDE_MD,
282
+ "action": "symlink-skip",
283
+ "reason": t("already_points_to_agents"),
284
+ }
285
+ ],
286
+ [],
287
+ )
288
+ if c_state == "regular" and _files_equal(claude_md, agents_md):
289
+ return (
290
+ 3,
291
+ agents_actions
292
+ + [
293
+ {
294
+ "file": _CLAUDE_MD,
295
+ "action": "skip",
296
+ "reason": t("claude_md_identical_content"),
297
+ }
298
+ ],
299
+ [],
300
+ )
301
+ return self.bucket4_default(state, claude_md, agents_actions)
302
+
303
+ def resolve_canonical_doc(
304
+ self,
305
+ root: Path,
306
+ state: StateDict,
307
+ *,
308
+ no_symlinks: bool = False,
309
+ replace_claude_with_symlink: bool = False,
310
+ migrate_legacy_claude: bool = False,
311
+ ) -> tuple[int, list[ActionDict], list[str]]:
312
+ a_state = state["a_state"]
313
+ c_state = state["c_state"]
314
+ claude_md = root / _CLAUDE_MD
315
+ agents_md = root / _AGENTS_MD
316
+ supports_symlinks = state["supports_symlinks"] and not no_symlinks
317
+ template = self.build_template(root)
318
+
319
+ if a_state == "absent":
320
+ if migrate_legacy_claude:
321
+ return self._migrate_legacy_claude_only(
322
+ root=root,
323
+ state=state,
324
+ template=template,
325
+ supports_symlinks=supports_symlinks,
326
+ )
327
+ return self.bucket1_no_agents(state, root, template)
328
+
329
+ agents_actions: list[ActionDict] = []
330
+ if not _has_marker(agents_md):
331
+ agents_actions.append({"file": _AGENTS_MD, "action": "append", "content": template})
332
+
333
+ if c_state == "absent":
334
+ return self.bucket2_agents_no_claude(root, supports_symlinks, template)
335
+
336
+ if c_state in ("symlink_valid", "regular"):
337
+ if c_state == "regular" and (replace_claude_with_symlink or migrate_legacy_claude):
338
+ return self.bucket4_replace(agents_actions, claude_md)
339
+ return self.bucket3(state, claude_md, agents_md, agents_actions)
340
+
341
+ return 5, [], []
342
+
343
+ def plan_rules(self, root: Path) -> tuple[list[ActionDict], list[str]]:
344
+ rule_path = root / ".claude" / "rules" / "gitwise.md"
345
+ if rule_path.exists():
346
+ return [
347
+ {
348
+ "file": ".claude/rules/gitwise.md",
349
+ "action": "skip",
350
+ "reason": t("already_exists"),
351
+ }
352
+ ], []
353
+ return [
354
+ {
355
+ "file": ".claude/rules/gitwise.md",
356
+ "action": "create",
357
+ "content": _read_template("rules/gitwise.md"),
358
+ }
359
+ ], []
360
+
361
+ def plan_snapshot(self, *, frozen_time: bool = False) -> list[ActionDict]:
362
+ return [
363
+ {"file": ".claude/git-snapshot.md", "action": "generate", "frozen_time": frozen_time}
364
+ ]
365
+
366
+ def plan_global(
367
+ self,
368
+ home: Path,
369
+ *,
370
+ no_skills: bool = False,
371
+ ) -> tuple[list[ActionDict], list[str], list[ActionDict]]:
372
+ actions: list[ActionDict] = []
373
+ warnings: list[str] = []
374
+
375
+ settings_actions, settings_warnings = self.plan_settings(home)
376
+ actions += settings_actions
377
+ warnings += settings_warnings
378
+
379
+ rules_actions, rules_warnings = self.plan_rules(home)
380
+ actions += rules_actions
381
+ warnings += rules_warnings
382
+
383
+ if not no_skills:
384
+ skills_actions, skills_warnings = plan_global_skills(home)
385
+ actions += skills_actions
386
+ warnings += skills_warnings
387
+
388
+ return actions, warnings, []
389
+
390
+ def plan(self, root: Path, context: AdapterContext) -> tuple[list[ActionDict], list[str]]:
391
+ if context["flags"].get("core_claude_planned", False):
392
+ return [], []
393
+ actions: list[ActionDict] = []
394
+ warnings: list[str] = []
395
+
396
+ settings_actions, settings_warnings = self.plan_settings(root)
397
+ rules_actions, rules_warnings = self.plan_rules(root)
398
+
399
+ actions += settings_actions
400
+ actions += rules_actions
401
+ actions += self.plan_snapshot(frozen_time=context["flags"].get("frozen_time", False))
402
+
403
+ warnings += settings_warnings
404
+ warnings += rules_warnings
405
+ return actions, warnings
406
+
407
+
408
+ ADAPTER = ClaudeAdapter()
@@ -0,0 +1,11 @@
1
+ """Codex (OpenAI) provider — .codex/agents/*.toml."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="codex",
7
+ display_name="Codex",
8
+ config_paths=(".codex/agents/gitwise.toml",),
9
+ template_paths=("agents/gitwise.toml.template",),
10
+ template_dir="share/codex",
11
+ )
@@ -0,0 +1,11 @@
1
+ """Continue provider — .continue/rules/*.md."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="continue",
7
+ display_name="Continue",
8
+ config_paths=(".continue/rules/gitwise.md",),
9
+ template_paths=("rules/gitwise.md.template",),
10
+ template_dir="share/continue",
11
+ )
@@ -0,0 +1,11 @@
1
+ """Cursor provider — .cursor/rules/*.mdc (MDC format with YAML frontmatter)."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="cursor",
7
+ display_name="Cursor",
8
+ config_paths=(".cursor/rules/gitwise.mdc",),
9
+ template_paths=("rules/gitwise.mdc.template",),
10
+ template_dir="share/cursor",
11
+ )
@@ -0,0 +1,11 @@
1
+ """OpenCode provider — .opencode/agents/*.md."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="opencode",
7
+ display_name="OpenCode",
8
+ config_paths=(".opencode/agents/gitwise.md",),
9
+ template_paths=("agents/gitwise.md.template",),
10
+ template_dir="share/opencode",
11
+ )
@@ -0,0 +1,11 @@
1
+ """Pi provider — .pi/agent/skills/gitwise.md."""
2
+
3
+ from gitwise.setup_agents.providers.base import AdapterConfig
4
+
5
+ ADAPTER = AdapterConfig(
6
+ name="pi",
7
+ display_name="Pi",
8
+ config_paths=(".pi/agent/skills/gitwise.md",),
9
+ template_paths=("skills/gitwise.md.template",),
10
+ template_dir="share/pi",
11
+ )