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.
- avos_cli/__init__.py +3 -0
- avos_cli/agents/avos_ask_agent.md +47 -0
- avos_cli/agents/avos_ask_agent_JSON_converter.md +78 -0
- avos_cli/agents/avos_hisotry_agent_JSON_converter.md +92 -0
- avos_cli/agents/avos_history_agent.md +58 -0
- avos_cli/agents/git_diff_agent.md +63 -0
- avos_cli/artifacts/__init__.py +17 -0
- avos_cli/artifacts/base.py +47 -0
- avos_cli/artifacts/commit_builder.py +35 -0
- avos_cli/artifacts/doc_builder.py +30 -0
- avos_cli/artifacts/issue_builder.py +37 -0
- avos_cli/artifacts/pr_builder.py +50 -0
- avos_cli/cli/__init__.py +1 -0
- avos_cli/cli/main.py +504 -0
- avos_cli/commands/__init__.py +1 -0
- avos_cli/commands/ask.py +541 -0
- avos_cli/commands/connect.py +363 -0
- avos_cli/commands/history.py +549 -0
- avos_cli/commands/hook_install.py +260 -0
- avos_cli/commands/hook_sync.py +231 -0
- avos_cli/commands/ingest.py +506 -0
- avos_cli/commands/ingest_pr.py +239 -0
- avos_cli/config/__init__.py +1 -0
- avos_cli/config/hash_store.py +93 -0
- avos_cli/config/lock.py +122 -0
- avos_cli/config/manager.py +180 -0
- avos_cli/config/state.py +90 -0
- avos_cli/exceptions.py +272 -0
- avos_cli/models/__init__.py +58 -0
- avos_cli/models/api.py +75 -0
- avos_cli/models/artifacts.py +99 -0
- avos_cli/models/config.py +56 -0
- avos_cli/models/diff.py +117 -0
- avos_cli/models/query.py +234 -0
- avos_cli/parsers/__init__.py +21 -0
- avos_cli/parsers/artifact_ref_extractor.py +173 -0
- avos_cli/parsers/reference_parser.py +117 -0
- avos_cli/services/__init__.py +1 -0
- avos_cli/services/chronology_service.py +68 -0
- avos_cli/services/citation_validator.py +134 -0
- avos_cli/services/context_budget_service.py +104 -0
- avos_cli/services/diff_resolver.py +398 -0
- avos_cli/services/diff_summary_service.py +141 -0
- avos_cli/services/git_client.py +351 -0
- avos_cli/services/github_client.py +443 -0
- avos_cli/services/llm_client.py +312 -0
- avos_cli/services/memory_client.py +323 -0
- avos_cli/services/query_fallback_formatter.py +108 -0
- avos_cli/services/reply_output_service.py +341 -0
- avos_cli/services/sanitization_service.py +218 -0
- avos_cli/utils/__init__.py +1 -0
- avos_cli/utils/dotenv_load.py +50 -0
- avos_cli/utils/hashing.py +22 -0
- avos_cli/utils/logger.py +77 -0
- avos_cli/utils/output.py +232 -0
- avos_cli/utils/sanitization_diagnostics.py +81 -0
- avos_cli/utils/time_helpers.py +56 -0
- git_aware_coding_agent-1.0.0.dist-info/METADATA +390 -0
- git_aware_coding_agent-1.0.0.dist-info/RECORD +62 -0
- git_aware_coding_agent-1.0.0.dist-info/WHEEL +4 -0
- git_aware_coding_agent-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|