git-aware-coding-agent 1.0.0__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 (62) hide show
  1. avos_cli/__init__.py +3 -0
  2. avos_cli/agents/avos_ask_agent.md +47 -0
  3. avos_cli/agents/avos_ask_agent_JSON_converter.md +78 -0
  4. avos_cli/agents/avos_hisotry_agent_JSON_converter.md +92 -0
  5. avos_cli/agents/avos_history_agent.md +58 -0
  6. avos_cli/agents/git_diff_agent.md +63 -0
  7. avos_cli/artifacts/__init__.py +17 -0
  8. avos_cli/artifacts/base.py +47 -0
  9. avos_cli/artifacts/commit_builder.py +35 -0
  10. avos_cli/artifacts/doc_builder.py +30 -0
  11. avos_cli/artifacts/issue_builder.py +37 -0
  12. avos_cli/artifacts/pr_builder.py +50 -0
  13. avos_cli/cli/__init__.py +1 -0
  14. avos_cli/cli/main.py +504 -0
  15. avos_cli/commands/__init__.py +1 -0
  16. avos_cli/commands/ask.py +541 -0
  17. avos_cli/commands/connect.py +363 -0
  18. avos_cli/commands/history.py +549 -0
  19. avos_cli/commands/hook_install.py +260 -0
  20. avos_cli/commands/hook_sync.py +231 -0
  21. avos_cli/commands/ingest.py +506 -0
  22. avos_cli/commands/ingest_pr.py +239 -0
  23. avos_cli/config/__init__.py +1 -0
  24. avos_cli/config/hash_store.py +93 -0
  25. avos_cli/config/lock.py +122 -0
  26. avos_cli/config/manager.py +180 -0
  27. avos_cli/config/state.py +90 -0
  28. avos_cli/exceptions.py +272 -0
  29. avos_cli/models/__init__.py +58 -0
  30. avos_cli/models/api.py +75 -0
  31. avos_cli/models/artifacts.py +99 -0
  32. avos_cli/models/config.py +56 -0
  33. avos_cli/models/diff.py +117 -0
  34. avos_cli/models/query.py +234 -0
  35. avos_cli/parsers/__init__.py +21 -0
  36. avos_cli/parsers/artifact_ref_extractor.py +173 -0
  37. avos_cli/parsers/reference_parser.py +117 -0
  38. avos_cli/services/__init__.py +1 -0
  39. avos_cli/services/chronology_service.py +68 -0
  40. avos_cli/services/citation_validator.py +134 -0
  41. avos_cli/services/context_budget_service.py +104 -0
  42. avos_cli/services/diff_resolver.py +398 -0
  43. avos_cli/services/diff_summary_service.py +141 -0
  44. avos_cli/services/git_client.py +351 -0
  45. avos_cli/services/github_client.py +443 -0
  46. avos_cli/services/llm_client.py +312 -0
  47. avos_cli/services/memory_client.py +323 -0
  48. avos_cli/services/query_fallback_formatter.py +108 -0
  49. avos_cli/services/reply_output_service.py +341 -0
  50. avos_cli/services/sanitization_service.py +218 -0
  51. avos_cli/utils/__init__.py +1 -0
  52. avos_cli/utils/dotenv_load.py +50 -0
  53. avos_cli/utils/hashing.py +22 -0
  54. avos_cli/utils/logger.py +77 -0
  55. avos_cli/utils/output.py +232 -0
  56. avos_cli/utils/sanitization_diagnostics.py +81 -0
  57. avos_cli/utils/time_helpers.py +56 -0
  58. git_aware_coding_agent-1.0.0.dist-info/METADATA +390 -0
  59. git_aware_coding_agent-1.0.0.dist-info/RECORD +62 -0
  60. git_aware_coding_agent-1.0.0.dist-info/WHEEL +4 -0
  61. git_aware_coding_agent-1.0.0.dist-info/entry_points.txt +2 -0
  62. git_aware_coding_agent-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,363 @@
1
+ """Connect command orchestrator for AVOS CLI.
2
+
3
+ Implements the `avos connect` flow: validates Git repo, optionally
4
+ accepts `org/repo` or derives it from `origin`, verifies GitHub access,
5
+ creates bootstrap note in Avos Memory, and writes .avos/config.json
6
+ (including `repo` for later use as default context).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ from avos_cli.config.manager import save_config
16
+ from avos_cli.config.state import read_json_safe
17
+ from avos_cli.exceptions import (
18
+ AuthError,
19
+ AvosError,
20
+ RepositoryContextError,
21
+ UpstreamUnavailableError,
22
+ )
23
+ from avos_cli.services.git_client import GitClient
24
+ from avos_cli.services.github_client import GitHubClient
25
+ from avos_cli.services.memory_client import AvosMemoryClient
26
+ from avos_cli.utils.logger import get_logger
27
+ from avos_cli.utils.output import (
28
+ print_error,
29
+ print_json,
30
+ render_kv_panel,
31
+ )
32
+
33
+ _log = get_logger("commands.connect")
34
+
35
+ _BOOTSTRAP_MARKER = "repo_connected"
36
+
37
+
38
+ class ConnectOrchestrator:
39
+ """Orchestrates the `avos connect` command.
40
+
41
+ Precondition order (per Q7): Git repo -> slug (explicit or from
42
+ origin) -> when explicit, remote must match -> GitHub API accessible
43
+ -> Avos API accessible -> write config with `repo` slug persisted.
44
+
45
+ Args:
46
+ git_client: Local git operations wrapper.
47
+ github_client: GitHub REST API client.
48
+ memory_client: Avos Memory API client.
49
+ repo_root: Path to the repository root.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ git_client: GitClient,
55
+ github_client: GitHubClient,
56
+ memory_client: AvosMemoryClient,
57
+ repo_root: Path,
58
+ ) -> None:
59
+ self._git = git_client
60
+ self._github = github_client
61
+ self._memory = memory_client
62
+ self._repo_root = repo_root
63
+
64
+ def run(self, repo_slug: str | None = None, json_output: bool = False) -> int:
65
+ """Execute the connect flow.
66
+
67
+ Args:
68
+ repo_slug: Repository identifier in 'org/repo' format. When
69
+ omitted, the slug is read from ``git remote origin`` (GitHub
70
+ HTTPS or SSH URL) and stored in config for later commands.
71
+ json_output: If True, emit JSON output instead of human UI.
72
+
73
+ Returns:
74
+ Exit code: 0=success, 1=local precondition, 2=hard external.
75
+ """
76
+ self._json_output = json_output
77
+
78
+ resolved = self._resolve_repo_slug(repo_slug)
79
+ if resolved is None:
80
+ return 1
81
+
82
+ owner, repo = resolved.split("/", 1)
83
+
84
+ if not self._verify_github_access(owner, repo):
85
+ return self._last_exit_code
86
+
87
+ memory_id = f"repo:{resolved}"
88
+
89
+ if not self._verify_avos_access(memory_id, _BOOTSTRAP_MARKER):
90
+ return self._last_exit_code
91
+
92
+ if not self._bootstrap_exists and not self._send_bootstrap_note(
93
+ memory_id, resolved, _BOOTSTRAP_MARKER
94
+ ):
95
+ return self._last_exit_code
96
+
97
+ self._write_config(resolved, memory_id)
98
+
99
+ # Auto-install pre-push hook for automatic commit sync
100
+ hook_installed = self._auto_install_hook()
101
+
102
+ config_path = str(self._repo_root / ".avos" / "config.json")
103
+ if json_output:
104
+ print_json(
105
+ success=True,
106
+ data={
107
+ "repo": resolved,
108
+ "memory_id": memory_id,
109
+ "config_path": config_path,
110
+ "hook_installed": hook_installed,
111
+ },
112
+ error=None,
113
+ )
114
+ else:
115
+ hook_status = "installed" if hook_installed else "skipped (existing hook found)"
116
+ render_kv_panel(
117
+ f"Connected to {resolved}",
118
+ [
119
+ ("Memory", memory_id),
120
+ ("Pre-push hook", hook_status),
121
+ ("Next step", "avos ingest"),
122
+ ],
123
+ style="success",
124
+ )
125
+ return 0
126
+
127
+ def _emit_error(
128
+ self, code: str, message: str, hint: str | None = None, retryable: bool = False
129
+ ) -> None:
130
+ """Emit error in JSON or human format based on mode."""
131
+ if self._json_output:
132
+ print_json(
133
+ success=False,
134
+ data=None,
135
+ error={"code": code, "message": message, "hint": hint, "retryable": retryable},
136
+ )
137
+ else:
138
+ print_error(f"[{code}] {message}")
139
+
140
+ def _validate_slug(self, slug: str) -> bool:
141
+ """Validate that slug is in 'org/repo' format."""
142
+ if not slug or "/" not in slug:
143
+ return False
144
+ parts = slug.split("/", 1)
145
+ return bool(parts[0]) and bool(parts[1])
146
+
147
+ def _resolve_repo_slug(self, repo_slug: str | None) -> str | None:
148
+ """Normalize user input or infer org/repo from ``origin``.
149
+
150
+ When ``repo_slug`` is None or whitespace-only, uses
151
+ ``GitClient.remote_origin`` (same parsing as explicit connect).
152
+
153
+ When a non-empty slug is provided, validates format and checks it
154
+ matches ``origin`` so forks cannot accidentally connect as upstream.
155
+
156
+ Args:
157
+ repo_slug: Explicit slug from CLI, or None / whitespace-only to
158
+ infer from git (same as omitting the argument).
159
+
160
+ Returns:
161
+ ``org/repo`` string, or None after emitting an error.
162
+ """
163
+ trimmed = repo_slug.strip() if repo_slug is not None else ""
164
+ if not trimmed:
165
+ return self._infer_repo_slug_from_origin()
166
+
167
+ if not self._validate_slug(trimmed):
168
+ self._emit_error(
169
+ "REPOSITORY_CONTEXT_ERROR",
170
+ "Invalid repo slug format. Expected 'org/repo'.",
171
+ )
172
+ return None
173
+
174
+ if not self._verify_git_remote(trimmed):
175
+ return None
176
+
177
+ return trimmed
178
+
179
+ def _infer_repo_slug_from_origin(self) -> str | None:
180
+ """Read ``org/repo`` from ``git remote get-url origin``.
181
+
182
+ Returns:
183
+ Valid slug, or None after emitting an error.
184
+ """
185
+ try:
186
+ remote = self._git.remote_origin(self._repo_root)
187
+ except (RepositoryContextError, AvosError) as e:
188
+ self._emit_error("REPOSITORY_CONTEXT_ERROR", str(e))
189
+ return None
190
+
191
+ if remote is None:
192
+ self._emit_error(
193
+ "REPOSITORY_CONTEXT_ERROR",
194
+ "No origin remote found, or origin URL could not be parsed as "
195
+ "org/repo. Add a GitHub remote or run: avos connect org/repo",
196
+ )
197
+ return None
198
+
199
+ if not self._validate_slug(remote):
200
+ self._emit_error(
201
+ "REPOSITORY_CONTEXT_ERROR",
202
+ f"Origin produced an invalid slug ({remote!r}). "
203
+ "Run: avos connect org/repo",
204
+ )
205
+ return None
206
+
207
+ return remote
208
+
209
+ def _verify_git_remote(self, repo_slug: str) -> bool:
210
+ """Verify git repo exists and remote matches the slug.
211
+
212
+ Returns:
213
+ True if valid, False with error output if not.
214
+ """
215
+ try:
216
+ remote = self._git.remote_origin(self._repo_root)
217
+ except (RepositoryContextError, AvosError) as e:
218
+ self._emit_error("REPOSITORY_CONTEXT_ERROR", str(e))
219
+ return False
220
+
221
+ if remote is None:
222
+ self._emit_error("REPOSITORY_CONTEXT_ERROR", "No origin remote found.")
223
+ return False
224
+
225
+ if remote != repo_slug:
226
+ self._emit_error(
227
+ "REPOSITORY_CONTEXT_ERROR",
228
+ f"Remote origin '{remote}' does not match '{repo_slug}'.",
229
+ )
230
+ return False
231
+
232
+ return True
233
+
234
+ def _verify_github_access(self, owner: str, repo: str) -> bool:
235
+ """Verify GitHub API access and repo existence.
236
+
237
+ Sets self._last_exit_code on failure.
238
+ """
239
+ try:
240
+ accessible = self._github.validate_repo(owner, repo)
241
+ except AuthError as e:
242
+ self._emit_error("AUTH_ERROR", f"GitHub: {e}")
243
+ self._last_exit_code = 1
244
+ return False
245
+ except UpstreamUnavailableError as e:
246
+ self._emit_error("UPSTREAM_UNAVAILABLE", f"GitHub: {e}", retryable=True)
247
+ self._last_exit_code = 2
248
+ return False
249
+
250
+ if not accessible:
251
+ self._emit_error(
252
+ "RESOURCE_NOT_FOUND",
253
+ f"Repository {owner}/{repo} not found on GitHub.",
254
+ )
255
+ self._last_exit_code = 1
256
+ return False
257
+
258
+ return True
259
+
260
+ def _verify_avos_access(self, memory_id: str, marker: str) -> bool:
261
+ """Check Avos Memory API access and whether bootstrap note exists.
262
+
263
+ Sets self._bootstrap_exists and self._last_exit_code.
264
+ """
265
+ try:
266
+ result = self._memory.search(
267
+ memory_id=memory_id,
268
+ query=f"[type: {marker}]",
269
+ k=1,
270
+ )
271
+ self._bootstrap_exists = bool(result.results)
272
+ return True
273
+ except AuthError as e:
274
+ self._emit_error("AUTH_ERROR", f"Avos Memory: {e}")
275
+ self._last_exit_code = 1
276
+ return False
277
+ except UpstreamUnavailableError as e:
278
+ self._emit_error("UPSTREAM_UNAVAILABLE", f"Avos Memory: {e}", retryable=True)
279
+ self._last_exit_code = 2
280
+ return False
281
+
282
+ def _send_bootstrap_note(
283
+ self, memory_id: str, repo_slug: str, marker: str
284
+ ) -> bool:
285
+ """Send the bootstrap note to Avos Memory.
286
+
287
+ Sets self._last_exit_code on failure.
288
+ """
289
+ content = (
290
+ f"[type: {marker}]\n"
291
+ f"Repository {repo_slug} connected to Avos Memory"
292
+ )
293
+ try:
294
+ self._memory.add_memory(memory_id=memory_id, content=content)
295
+ return True
296
+ except UpstreamUnavailableError as e:
297
+ self._emit_error(
298
+ "UPSTREAM_UNAVAILABLE",
299
+ f"Failed to store bootstrap note: {e}",
300
+ retryable=True,
301
+ )
302
+ self._last_exit_code = 2
303
+ return False
304
+ except AvosError as e:
305
+ self._emit_error(e.code, f"Failed to store bootstrap note: {e}")
306
+ self._last_exit_code = 2
307
+ return False
308
+
309
+ def _write_config(self, repo_slug: str, memory_id: str) -> None:
310
+ """Write .avos/config.json only if content would change.
311
+
312
+ Preserves connected_at from existing config to guarantee
313
+ strict idempotent rerun semantics (no observable mutation).
314
+ """
315
+ config_path = self._repo_root / ".avos" / "config.json"
316
+ existing = read_json_safe(config_path) if config_path.exists() else None
317
+
318
+ connected_at = datetime.now(tz=timezone.utc).isoformat()
319
+ if (
320
+ existing
321
+ and existing.get("repo") == repo_slug
322
+ and existing.get("memory_id") == memory_id
323
+ ):
324
+ connected_at = str(existing.get("connected_at", connected_at))
325
+
326
+ new_data = {
327
+ "repo": repo_slug,
328
+ "memory_id": memory_id,
329
+ "api_url": "",
330
+ "api_key": "",
331
+ "connected_at": connected_at,
332
+ "schema_version": "2",
333
+ }
334
+
335
+ if existing is not None:
336
+ existing_canonical = json.dumps(existing, indent=2, sort_keys=True)
337
+ new_canonical = json.dumps(new_data, indent=2, sort_keys=True)
338
+ if existing_canonical == new_canonical:
339
+ return
340
+
341
+ save_config(self._repo_root, new_data)
342
+
343
+ def _auto_install_hook(self) -> bool:
344
+ """Silently attempt to install pre-push hook after connect.
345
+
346
+ This is best-effort: connect succeeds even if hook install fails.
347
+ The hook enables automatic commit sync to Avos Memory on every
348
+ git push, keeping team memory up-to-date without manual ingest.
349
+
350
+ Returns:
351
+ True if hook was installed or already exists, False otherwise.
352
+ """
353
+ from avos_cli.commands.hook_install import HookInstallOrchestrator
354
+
355
+ hook_orch = HookInstallOrchestrator(
356
+ git_client=self._git,
357
+ repo_root=self._repo_root,
358
+ )
359
+ exit_code = hook_orch.run(force=False, quiet=True)
360
+ if exit_code != 0:
361
+ _log.debug("Hook auto-install skipped or failed (exit=%d)", exit_code)
362
+ return False
363
+ return True