@memorax/memorax-code 0.1.8 → 0.1.10
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.
- package/README.md +10 -0
- package/bin/memorax-code-codebuddy.mjs +4 -0
- package/bin/memorax-code-setup.mjs +182 -65
- package/bin/memorax-code.mjs +74 -45
- package/docs/configuration.md +82 -22
- package/docs/troubleshooting.md +69 -5
- package/lib/automatic-update.mjs +233 -0
- package/lib/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
- package/lib/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
- package/lib/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
- package/lib/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
- package/lib/memorax-code-adapter-common/src/config-utils.mjs +11 -0
- package/lib/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
- package/lib/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
- package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
- package/lib/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
- package/lib/memorax-code-backend/dist/app/backend-server.js +2 -2
- package/lib/memorax-code-backend/dist/app/memory-observability.js +2 -2
- package/lib/memorax-code-backend/dist/clients/codebuddy/jsonl-history.js +208 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/lifecycle.js +44 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/memory-hook-runtime.js +250 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/turn-id.js +16 -0
- package/lib/memorax-code-backend/dist/clients/codex/plugin-install.js +129 -7
- package/lib/memorax-code-backend/dist/clients/codex/rollout-turn.js +52 -2
- package/lib/memorax-code-backend/dist/config/memorax-code.js +14 -1
- package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +43 -6
- package/lib/memorax-code-backend/dist/lifecycle/active-clients.js +3 -0
- package/lib/memorax-code-backend/dist/lifecycle/automatic-update-scheduler.js +133 -0
- package/lib/memorax-code-backend/dist/lifecycle/client-plugin-removal.js +14 -2
- package/lib/memorax-code-backend/dist/lifecycle/client-selection.js +6 -4
- package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +48 -9
- package/lib/memorax-code-backend/dist/memory/cli.js +4 -2
- package/lib/memorax-code-backend/dist/memory/hook-command.js +31 -1
- package/lib/memorax-code-backend/dist/memory/reminder-trace-recorder.js +5 -1
- package/lib/memorax-code-backend/dist/memory/service.js +12 -0
- package/lib/memorax-code-backend/dist/shared/windows-cli-invocation.js +10 -1
- package/lib/memorax-code-backend/dist/trace/config.js +12 -0
- package/lib/memorax-code-backend/dist/trace/context.js +21 -1
- package/lib/memorax-code-backend/dist/trace/store.js +12 -1
- package/lib/memorax-code-backend/package.json +1 -1
- package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
- package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +11 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
- package/lib/memorax-code-codebuddy-adapter/.codebuddy-plugin/plugin.json +7 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/common-runtime.mjs +13 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/hooks.json +38 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/repo-memory-job.mjs +40 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/runtime-hook.mjs +279 -0
- package/lib/memorax-code-codebuddy-adapter/package.json +9 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/SKILL.md +85 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/claude.yaml +10 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/openai.yaml +7 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/defaults.json +12 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-add.md +88 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-search.md +93 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-read.md +46 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-write.md +120 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-build.md +319 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-read.md +103 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-templates.md +390 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-update.md +127 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/collect_all.py +579 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/detect_updates.py +919 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/git_commit_facets.py +222 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/github_resource_facets.py +512 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/gitlab_resource_facets.py +517 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/prepare_repo_memory.py +411 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/user_profile_memory.py +528 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/validate_memory.py +248 -0
- package/lib/memorax-code-codebuddy-adapter/src/cli.mjs +58 -0
- package/lib/memorax-code-codebuddy-adapter/src/config.mjs +277 -0
- package/lib/memorax-code-codebuddy-adapter/src/hook-manifest.mjs +47 -0
- package/lib/memorax-code-codebuddy-adapter/src/runtime-observation.mjs +65 -0
- package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
- package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-codex-adapter/package.json +1 -1
- package/lib/memorax-code-codex-adapter/runtime-hooks/ensure-backend.mjs +4 -1
- package/lib/memorax-code-dsh-adapter/package.json +2 -1
- package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +1 -0
- package/lib/memorax-code-opencode-adapter/package.json +1 -1
- package/lib/npm-invocation.mjs +51 -8
- package/lib/resolve-codebuddy-command.mjs +112 -0
- package/lib/run-entrypoint.mjs +25 -1
- package/lib/windows-cli-invocation.mjs +11 -1
- package/lib/windows-user-path.mjs +218 -0
- package/package.json +5 -3
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the mechanical repo-memory collection steps."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
14
|
+
DEFAULTS_PATH = SCRIPT_DIR.parent / "defaults.json"
|
|
15
|
+
FALLBACK_DEFAULTS = {
|
|
16
|
+
"repoHistory": {
|
|
17
|
+
"mode": "provider",
|
|
18
|
+
"limits": {
|
|
19
|
+
"commits": 30,
|
|
20
|
+
"prs": 30,
|
|
21
|
+
"issues": 30,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
# Kept for compatibility with defaults.json v1.
|
|
25
|
+
"limits": {
|
|
26
|
+
"commits": 30,
|
|
27
|
+
"prs": 30,
|
|
28
|
+
"issues": 30,
|
|
29
|
+
},
|
|
30
|
+
"summaryChars": 4000,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
HISTORY_MODES = {"none", "commits-only", "local-only", "provider", "provider-required"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProgressBar:
|
|
37
|
+
def __init__(self, enabled: bool, total: int) -> None:
|
|
38
|
+
self.enabled = enabled
|
|
39
|
+
self.total = total
|
|
40
|
+
self.width = 20
|
|
41
|
+
self._last_line_len = 0
|
|
42
|
+
self._is_tty = sys.stderr.isatty()
|
|
43
|
+
|
|
44
|
+
def update(self, completed: int, label: str) -> None:
|
|
45
|
+
if not self.enabled:
|
|
46
|
+
return
|
|
47
|
+
completed = max(0, min(completed, self.total))
|
|
48
|
+
filled = round((completed / self.total) * self.width) if self.total else self.width
|
|
49
|
+
bar = "#" * filled + "-" * (self.width - filled)
|
|
50
|
+
line = f"memorax-code repo-build [{bar}] {completed}/{self.total} {label}"
|
|
51
|
+
if self._is_tty:
|
|
52
|
+
padding = " " * max(0, self._last_line_len - len(line))
|
|
53
|
+
print(f"\r{line}{padding}", end="", file=sys.stderr, flush=True)
|
|
54
|
+
self._last_line_len = len(line)
|
|
55
|
+
else:
|
|
56
|
+
print(line, file=sys.stderr, flush=True)
|
|
57
|
+
|
|
58
|
+
def fail(self, completed: int, label: str) -> None:
|
|
59
|
+
self.update(completed, label)
|
|
60
|
+
self.finish()
|
|
61
|
+
|
|
62
|
+
def finish(self) -> None:
|
|
63
|
+
if self.enabled and self._is_tty:
|
|
64
|
+
print(file=sys.stderr, flush=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_step(cmd: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
68
|
+
return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def json_step(result: subprocess.CompletedProcess[str]) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"ok": result.returncode == 0,
|
|
74
|
+
"exit_code": result.returncode,
|
|
75
|
+
"stdout": result.stdout.strip(),
|
|
76
|
+
"stderr": result.stderr.strip(),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_json(path: Path) -> Any:
|
|
81
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def raw_source_counts(path: Path) -> dict[str, int]:
|
|
85
|
+
if not path.exists():
|
|
86
|
+
return {}
|
|
87
|
+
data = load_json(path)
|
|
88
|
+
if not isinstance(data, list):
|
|
89
|
+
return {}
|
|
90
|
+
counts = Counter(str(item.get("sourceType") or "unknown") for item in data if isinstance(item, dict))
|
|
91
|
+
return dict(sorted(counts.items()))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def emit_process_output(result: subprocess.CompletedProcess[str]) -> None:
|
|
95
|
+
if result.stderr:
|
|
96
|
+
print(result.stderr, file=sys.stderr)
|
|
97
|
+
if result.stdout:
|
|
98
|
+
print(result.stdout, file=sys.stderr)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def write_report(report: dict[str, Any], pretty: bool) -> None:
|
|
102
|
+
print(json.dumps(report, ensure_ascii=False, indent=2 if pretty else None))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def write_failure(
|
|
106
|
+
failed_step: str,
|
|
107
|
+
result: subprocess.CompletedProcess[str],
|
|
108
|
+
pretty: bool,
|
|
109
|
+
*,
|
|
110
|
+
repo: Optional[Path] = None,
|
|
111
|
+
memory: Optional[Path] = None,
|
|
112
|
+
provider: Optional[dict[str, Any]] = None,
|
|
113
|
+
steps: Optional[dict[str, Any]] = None,
|
|
114
|
+
outputs: Optional[dict[str, str]] = None,
|
|
115
|
+
notices: Optional[list[dict[str, Any]]] = None,
|
|
116
|
+
effective_settings: Optional[dict[str, Any]] = None,
|
|
117
|
+
) -> int:
|
|
118
|
+
emit_process_output(result)
|
|
119
|
+
step_reports = dict(steps or {})
|
|
120
|
+
step_reports[failed_step] = json_step(result)
|
|
121
|
+
report = {
|
|
122
|
+
"ok": False,
|
|
123
|
+
"failed_step": failed_step,
|
|
124
|
+
"steps": step_reports,
|
|
125
|
+
}
|
|
126
|
+
if repo is not None:
|
|
127
|
+
report["repo_path"] = str(repo)
|
|
128
|
+
if memory is not None:
|
|
129
|
+
report["memory_path"] = str(memory)
|
|
130
|
+
if provider is not None:
|
|
131
|
+
report["provider"] = provider
|
|
132
|
+
if outputs:
|
|
133
|
+
report["outputs"] = outputs
|
|
134
|
+
if notices:
|
|
135
|
+
report["notices"] = notices
|
|
136
|
+
if effective_settings is not None:
|
|
137
|
+
report["effective_settings"] = effective_settings
|
|
138
|
+
write_report(report, pretty)
|
|
139
|
+
return result.returncode or 1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def provider_report(prepare_report: dict[str, Any]) -> dict[str, Any]:
|
|
143
|
+
return {
|
|
144
|
+
"name": prepare_report.get("git_provider", ""),
|
|
145
|
+
"repo": prepare_report.get("git_remote_repo", ""),
|
|
146
|
+
"host": prepare_report.get("git_remote_host", ""),
|
|
147
|
+
"remote_name": prepare_report.get("git_remote_name", ""),
|
|
148
|
+
"selection_reason": prepare_report.get("git_remote_selection_reason", ""),
|
|
149
|
+
"cli": prepare_report.get("provider_cli", ""),
|
|
150
|
+
"cli_available": bool(prepare_report.get("provider_cli_available")),
|
|
151
|
+
"authenticated": bool(prepare_report.get("provider_authenticated")),
|
|
152
|
+
"auth_status": prepare_report.get("provider_auth_status", ""),
|
|
153
|
+
"evidence_state": prepare_report.get("provider_evidence_state", ""),
|
|
154
|
+
"login_hint": prepare_report.get("provider_login_hint", ""),
|
|
155
|
+
"notice_level": prepare_report.get("provider_notice_level", ""),
|
|
156
|
+
"user_notice": prepare_report.get("provider_user_notice", ""),
|
|
157
|
+
"notice_markdown": prepare_report.get("provider_notice_markdown", ""),
|
|
158
|
+
"next_steps": prepare_report.get("provider_next_steps", []),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def provider_notices(prepare_report: dict[str, Any]) -> list[dict[str, Any]]:
|
|
163
|
+
if prepare_report.get("provider_notice_level") != "warning":
|
|
164
|
+
return []
|
|
165
|
+
message = str(prepare_report.get("provider_user_notice", "")).replace("`", "")
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
"level": "warning",
|
|
169
|
+
"title": "Provider Evidence Unavailable",
|
|
170
|
+
"message": message,
|
|
171
|
+
"command": prepare_report.get("provider_login_hint", ""),
|
|
172
|
+
"next_steps": prepare_report.get("provider_next_steps", []),
|
|
173
|
+
"render_as": "assistant_message",
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def provider_failure_notice(provider: dict[str, Any], result: subprocess.CompletedProcess[str], *, continuing: bool) -> dict[str, Any]:
|
|
179
|
+
stderr = result.stderr.strip()
|
|
180
|
+
stdout = result.stdout.strip()
|
|
181
|
+
detail = (stderr or stdout or "provider facet collection failed").replace("`", "")
|
|
182
|
+
if len(detail) > 800:
|
|
183
|
+
detail = f"{detail[:800]}..."
|
|
184
|
+
label = str(provider.get("name") or "provider")
|
|
185
|
+
evidence = "GitHub PR/issue" if label == "github" else "GitLab MR/issue" if label == "gitlab" else "provider"
|
|
186
|
+
continuation = (
|
|
187
|
+
"Continuing with local-only repo memory now."
|
|
188
|
+
if continuing
|
|
189
|
+
else "Provider evidence was required, so the run stopped before authoring provider resources."
|
|
190
|
+
)
|
|
191
|
+
return {
|
|
192
|
+
"level": "warning",
|
|
193
|
+
"title": "Provider Evidence Unavailable",
|
|
194
|
+
"message": (
|
|
195
|
+
f"{evidence} evidence could not be collected even though the provider CLI appeared ready. "
|
|
196
|
+
f"{continuation} Provider error: {detail}"
|
|
197
|
+
),
|
|
198
|
+
"command": "",
|
|
199
|
+
"next_steps": [
|
|
200
|
+
"Fix provider repository access or remote URL.",
|
|
201
|
+
f"Rerun $memorax-code repo-build to collect {evidence} evidence.",
|
|
202
|
+
],
|
|
203
|
+
"render_as": "assistant_message",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def provider_script(provider: str) -> str:
|
|
208
|
+
if provider == "github":
|
|
209
|
+
return "github_resource_facets.py"
|
|
210
|
+
if provider == "gitlab":
|
|
211
|
+
return "gitlab_resource_facets.py"
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def provider_raw_name(provider: str) -> str:
|
|
216
|
+
if provider == "github":
|
|
217
|
+
return "github-facets.json"
|
|
218
|
+
if provider == "gitlab":
|
|
219
|
+
return "gitlab-facets.json"
|
|
220
|
+
return "provider-facets.json"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load_default_settings() -> tuple[dict[str, Any], str]:
|
|
224
|
+
if not DEFAULTS_PATH.exists():
|
|
225
|
+
return FALLBACK_DEFAULTS, "hardcoded_fallback"
|
|
226
|
+
try:
|
|
227
|
+
data = json.loads(DEFAULTS_PATH.read_text(encoding="utf-8"))
|
|
228
|
+
except json.JSONDecodeError as exc:
|
|
229
|
+
raise ValueError(f"{DEFAULTS_PATH}: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc
|
|
230
|
+
if not isinstance(data, dict):
|
|
231
|
+
raise ValueError(f"{DEFAULTS_PATH}: expected a JSON object")
|
|
232
|
+
return data, str(DEFAULTS_PATH)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def default_int(settings: dict[str, Any], path: list[str], fallback: int) -> int:
|
|
236
|
+
value: Any = settings
|
|
237
|
+
for key in path:
|
|
238
|
+
value = value.get(key) if isinstance(value, dict) else None
|
|
239
|
+
if value is None:
|
|
240
|
+
return fallback
|
|
241
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
242
|
+
raise ValueError(f"{DEFAULTS_PATH}: {'.'.join(path)} must be an integer")
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_range(parser: argparse.ArgumentParser, name: str, value: int, minimum: int, maximum: int) -> None:
|
|
247
|
+
if value < minimum or value > maximum:
|
|
248
|
+
parser.error(f"{name} must be from {minimum} to {maximum}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def default_history_mode(settings: dict[str, Any]) -> str:
|
|
252
|
+
history = settings.get("repoHistory")
|
|
253
|
+
if not isinstance(history, dict):
|
|
254
|
+
return "provider"
|
|
255
|
+
mode = history.get("mode", "provider")
|
|
256
|
+
if not isinstance(mode, str) or mode not in HISTORY_MODES:
|
|
257
|
+
raise ValueError(f"{DEFAULTS_PATH}: repoHistory.mode must be one of {', '.join(sorted(HISTORY_MODES))}")
|
|
258
|
+
return mode
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def default_limit(settings: dict[str, Any], key: str) -> int:
|
|
262
|
+
history = settings.get("repoHistory")
|
|
263
|
+
if isinstance(history, dict) and isinstance(history.get("limits"), dict) and history["limits"].get(key) is not None:
|
|
264
|
+
return default_int(settings, ["repoHistory", "limits", key], FALLBACK_DEFAULTS["repoHistory"]["limits"][key])
|
|
265
|
+
return default_int(settings, ["limits", key], FALLBACK_DEFAULTS["limits"][key])
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def history_collect(mode: str) -> dict[str, bool]:
|
|
269
|
+
return {
|
|
270
|
+
"commits": mode != "none",
|
|
271
|
+
"provider": mode in {"provider", "provider-required"},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def resolve_history_mode(args: argparse.Namespace, default_mode: str) -> str:
|
|
276
|
+
mode = args.history_mode or default_mode
|
|
277
|
+
if args.skip_provider:
|
|
278
|
+
mode = "local-only"
|
|
279
|
+
if args.require_provider:
|
|
280
|
+
mode = "provider-required"
|
|
281
|
+
return mode
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def apply_effective_settings(args: argparse.Namespace, parser: argparse.ArgumentParser) -> argparse.Namespace:
|
|
285
|
+
try:
|
|
286
|
+
defaults, source = load_default_settings()
|
|
287
|
+
default_mode = default_history_mode(defaults)
|
|
288
|
+
default_commit_limit = default_limit(defaults, "commits")
|
|
289
|
+
default_pr_limit = default_limit(defaults, "prs")
|
|
290
|
+
default_issue_limit = default_limit(defaults, "issues")
|
|
291
|
+
default_summary_chars = default_int(defaults, ["summaryChars"], FALLBACK_DEFAULTS["summaryChars"])
|
|
292
|
+
except ValueError as exc:
|
|
293
|
+
parser.error(str(exc))
|
|
294
|
+
|
|
295
|
+
history_mode = resolve_history_mode(args, default_mode)
|
|
296
|
+
collect = history_collect(history_mode)
|
|
297
|
+
|
|
298
|
+
commit_limit = args.commit_limit if args.commit_limit is not None else default_commit_limit
|
|
299
|
+
pr_limit = args.pr_limit if args.pr_limit is not None else default_pr_limit
|
|
300
|
+
issue_limit = args.issue_limit if args.issue_limit is not None else default_issue_limit
|
|
301
|
+
summary_chars = args.summary_chars if args.summary_chars is not None else default_summary_chars
|
|
302
|
+
|
|
303
|
+
validate_range(parser, "--commit-limit", commit_limit, 1, 500)
|
|
304
|
+
validate_range(parser, "--pr-limit", pr_limit, 1, 100)
|
|
305
|
+
validate_range(parser, "--issue-limit", issue_limit, 1, 100)
|
|
306
|
+
if summary_chars < 100:
|
|
307
|
+
parser.error("--summary-chars must be at least 100")
|
|
308
|
+
|
|
309
|
+
overrides: dict[str, Any] = {}
|
|
310
|
+
if args.history_mode is not None:
|
|
311
|
+
overrides["history_mode"] = args.history_mode
|
|
312
|
+
if args.skip_provider:
|
|
313
|
+
overrides["skip_provider"] = True
|
|
314
|
+
if args.require_provider:
|
|
315
|
+
overrides["require_provider"] = True
|
|
316
|
+
if args.commit_limit is not None:
|
|
317
|
+
overrides["commit_limit"] = args.commit_limit
|
|
318
|
+
if args.pr_limit is not None:
|
|
319
|
+
overrides["pr_limit"] = args.pr_limit
|
|
320
|
+
if args.issue_limit is not None:
|
|
321
|
+
overrides["issue_limit"] = args.issue_limit
|
|
322
|
+
if args.summary_chars is not None:
|
|
323
|
+
overrides["summary_chars"] = args.summary_chars
|
|
324
|
+
|
|
325
|
+
args.commit_limit = commit_limit
|
|
326
|
+
args.pr_limit = pr_limit
|
|
327
|
+
args.issue_limit = issue_limit
|
|
328
|
+
args.summary_chars = summary_chars
|
|
329
|
+
args.history_mode = history_mode
|
|
330
|
+
args.history_collect = collect
|
|
331
|
+
args.effective_settings = {
|
|
332
|
+
"history": {
|
|
333
|
+
"mode": history_mode,
|
|
334
|
+
"collect": collect,
|
|
335
|
+
},
|
|
336
|
+
"limits": {
|
|
337
|
+
"commits": commit_limit,
|
|
338
|
+
"prs": pr_limit,
|
|
339
|
+
"issues": issue_limit,
|
|
340
|
+
},
|
|
341
|
+
"summary_chars": summary_chars,
|
|
342
|
+
"source": source,
|
|
343
|
+
"overrides": overrides,
|
|
344
|
+
}
|
|
345
|
+
return args
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
349
|
+
parser = argparse.ArgumentParser(description="Prepare repo memory and collect raw facets for agent-authored resources.")
|
|
350
|
+
parser.add_argument("--repo-path", default=".", help="Local git repository path")
|
|
351
|
+
parser.add_argument("--reuse", action="store_true", help="Reuse/update an existing .repo_memory directory")
|
|
352
|
+
parser.add_argument("--snapshot-ref", default="HEAD", help="Local git ref whose history should be collected")
|
|
353
|
+
parser.add_argument("--commit-limit", type=int, default=None, help="Maximum local commits retained from the snapshot history")
|
|
354
|
+
parser.add_argument("--pr-limit", type=int, default=None, help="Maximum snapshot-filtered PRs/MRs retained")
|
|
355
|
+
parser.add_argument("--issue-limit", type=int, default=None, help="Maximum provider issues retained")
|
|
356
|
+
parser.add_argument("--summary-chars", type=int, default=None)
|
|
357
|
+
parser.add_argument("--history-mode", choices=sorted(HISTORY_MODES), default=None, help="History evidence policy for this build")
|
|
358
|
+
parser.add_argument("--skip-provider", action="store_true", help="Skip GitHub/GitLab PR/MR/issue collection and build local-only memory")
|
|
359
|
+
parser.add_argument("--require-provider", action="store_true", help="Fail if provider PR/MR/issue evidence cannot be collected")
|
|
360
|
+
parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON")
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"--progress",
|
|
363
|
+
action="store_true",
|
|
364
|
+
help="Render a stderr progress bar while keeping stdout as the final JSON report",
|
|
365
|
+
)
|
|
366
|
+
args = parser.parse_args(argv)
|
|
367
|
+
if args.skip_provider and args.require_provider:
|
|
368
|
+
parser.error("--skip-provider and --require-provider cannot be used together")
|
|
369
|
+
if args.history_mode and args.skip_provider and args.history_mode != "local-only":
|
|
370
|
+
parser.error(f"--skip-provider cannot be combined with --history-mode {args.history_mode}")
|
|
371
|
+
if args.history_mode and args.require_provider and args.history_mode != "provider-required":
|
|
372
|
+
parser.error("--require-provider cannot be combined with a non-required history mode")
|
|
373
|
+
args.repo_path = Path(args.repo_path).expanduser().resolve()
|
|
374
|
+
return apply_effective_settings(args, parser)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def main(argv: list[str]) -> int:
|
|
378
|
+
args = parse_args(argv)
|
|
379
|
+
repo = args.repo_path
|
|
380
|
+
progress = ProgressBar(args.progress, 3)
|
|
381
|
+
command_cwd = repo if repo.exists() else SCRIPT_DIR
|
|
382
|
+
prepare_cmd = [sys.executable, str(SCRIPT_DIR / "prepare_repo_memory.py"), str(repo)]
|
|
383
|
+
if args.reuse:
|
|
384
|
+
prepare_cmd.append("--reuse")
|
|
385
|
+
progress.update(0, "prepare")
|
|
386
|
+
prepare = run_step(prepare_cmd, command_cwd)
|
|
387
|
+
if prepare.returncode != 0:
|
|
388
|
+
progress.fail(0, "prepare failed")
|
|
389
|
+
return write_failure("prepare", prepare, args.pretty, repo=repo)
|
|
390
|
+
if prepare.stderr:
|
|
391
|
+
print(prepare.stderr, file=sys.stderr)
|
|
392
|
+
progress.update(1, "prepare")
|
|
393
|
+
|
|
394
|
+
memory = repo / ".repo_memory"
|
|
395
|
+
raw_dir = memory / "raw"
|
|
396
|
+
prepare_report_path = raw_dir / "prepare-report.json"
|
|
397
|
+
prepare_data = load_json(prepare_report_path)
|
|
398
|
+
notices = provider_notices(prepare_data)
|
|
399
|
+
|
|
400
|
+
git_commits_path = raw_dir / "git-commits.json"
|
|
401
|
+
outputs = {
|
|
402
|
+
"prepare_report": str(prepare_report_path),
|
|
403
|
+
"git_commits": str(git_commits_path),
|
|
404
|
+
}
|
|
405
|
+
if args.history_collect["commits"]:
|
|
406
|
+
git_commits = run_step(
|
|
407
|
+
[
|
|
408
|
+
sys.executable,
|
|
409
|
+
str(SCRIPT_DIR / "git_commit_facets.py"),
|
|
410
|
+
"--repo-path",
|
|
411
|
+
str(repo),
|
|
412
|
+
"--snapshot-ref",
|
|
413
|
+
args.snapshot_ref,
|
|
414
|
+
"--limit",
|
|
415
|
+
str(args.commit_limit),
|
|
416
|
+
"--summary-chars",
|
|
417
|
+
str(args.summary_chars),
|
|
418
|
+
"--out",
|
|
419
|
+
str(git_commits_path),
|
|
420
|
+
],
|
|
421
|
+
repo,
|
|
422
|
+
)
|
|
423
|
+
git_commits_step = json_step(git_commits)
|
|
424
|
+
if git_commits.returncode != 0:
|
|
425
|
+
progress.fail(1, "git commits failed")
|
|
426
|
+
return write_failure(
|
|
427
|
+
"git_commits",
|
|
428
|
+
git_commits,
|
|
429
|
+
args.pretty,
|
|
430
|
+
repo=repo,
|
|
431
|
+
memory=memory,
|
|
432
|
+
steps={"prepare": json_step(prepare)},
|
|
433
|
+
outputs=outputs,
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
git_commits_path.write_text("[]\n", encoding="utf-8")
|
|
437
|
+
git_commits_step = {
|
|
438
|
+
"ok": True,
|
|
439
|
+
"exit_code": 0,
|
|
440
|
+
"stdout": "",
|
|
441
|
+
"stderr": "",
|
|
442
|
+
"skipped": True,
|
|
443
|
+
"reason": "history_disabled_by_policy",
|
|
444
|
+
}
|
|
445
|
+
progress.update(2, "git commits")
|
|
446
|
+
|
|
447
|
+
provider = provider_report(prepare_data)
|
|
448
|
+
provider_name = str(provider["name"])
|
|
449
|
+
provider_facets: dict[str, Any]
|
|
450
|
+
provider_output = raw_dir / provider_raw_name(provider_name)
|
|
451
|
+
provider_script_name = provider_script(provider_name)
|
|
452
|
+
if not args.history_collect["provider"]:
|
|
453
|
+
reason = "provider_skipped_by_user" if args.skip_provider else "history_disabled_by_policy" if args.history_mode == "none" else "history_provider_disabled_by_policy"
|
|
454
|
+
provider_facets = {
|
|
455
|
+
"ok": True,
|
|
456
|
+
"skipped": True,
|
|
457
|
+
"reason": reason,
|
|
458
|
+
"output": "",
|
|
459
|
+
}
|
|
460
|
+
elif provider["evidence_state"] == "ready" and provider_script_name and provider["repo"]:
|
|
461
|
+
provider_cmd = [
|
|
462
|
+
sys.executable,
|
|
463
|
+
str(SCRIPT_DIR / provider_script_name),
|
|
464
|
+
"--repo",
|
|
465
|
+
str(provider["repo"]),
|
|
466
|
+
"--repo-path",
|
|
467
|
+
str(repo),
|
|
468
|
+
"--snapshot-ref",
|
|
469
|
+
args.snapshot_ref,
|
|
470
|
+
"--include",
|
|
471
|
+
"prs,issues",
|
|
472
|
+
"--pr-limit",
|
|
473
|
+
str(args.pr_limit),
|
|
474
|
+
"--issue-limit",
|
|
475
|
+
str(args.issue_limit),
|
|
476
|
+
"--state",
|
|
477
|
+
"all",
|
|
478
|
+
"--summary-chars",
|
|
479
|
+
str(args.summary_chars),
|
|
480
|
+
"--out",
|
|
481
|
+
str(provider_output),
|
|
482
|
+
]
|
|
483
|
+
if provider.get("host"):
|
|
484
|
+
provider_cmd.extend(["--hostname", str(provider["host"])])
|
|
485
|
+
provider_result = run_step(provider_cmd, repo)
|
|
486
|
+
provider_facets = {
|
|
487
|
+
**json_step(provider_result),
|
|
488
|
+
"skipped": False,
|
|
489
|
+
"output": str(provider_output),
|
|
490
|
+
}
|
|
491
|
+
if provider_result.returncode != 0:
|
|
492
|
+
provider_facets["degraded_to_local_only"] = args.history_mode != "provider-required"
|
|
493
|
+
provider_facets["reason"] = "provider_facets_failed"
|
|
494
|
+
provider_facets["output"] = ""
|
|
495
|
+
if provider_output.exists():
|
|
496
|
+
provider_output.unlink()
|
|
497
|
+
if args.history_mode == "provider-required":
|
|
498
|
+
progress.fail(2, "provider facets failed")
|
|
499
|
+
return write_failure(
|
|
500
|
+
"provider_facets",
|
|
501
|
+
provider_result,
|
|
502
|
+
args.pretty,
|
|
503
|
+
repo=repo,
|
|
504
|
+
memory=memory,
|
|
505
|
+
provider=provider,
|
|
506
|
+
steps={
|
|
507
|
+
"prepare": json_step(prepare),
|
|
508
|
+
"git_commits": git_commits_step,
|
|
509
|
+
},
|
|
510
|
+
outputs=outputs,
|
|
511
|
+
notices=[provider_failure_notice(provider, provider_result, continuing=False)],
|
|
512
|
+
effective_settings=args.effective_settings,
|
|
513
|
+
)
|
|
514
|
+
notices.append(provider_failure_notice(provider, provider_result, continuing=True))
|
|
515
|
+
else:
|
|
516
|
+
outputs["provider_facets"] = provider_facets["output"]
|
|
517
|
+
else:
|
|
518
|
+
if args.history_mode == "provider-required":
|
|
519
|
+
provider_result = subprocess.CompletedProcess(
|
|
520
|
+
args=[],
|
|
521
|
+
returncode=1,
|
|
522
|
+
stdout="",
|
|
523
|
+
stderr=f"provider evidence is required but provider_evidence_state={provider['evidence_state']}",
|
|
524
|
+
)
|
|
525
|
+
progress.fail(2, "provider facets unavailable")
|
|
526
|
+
return write_failure(
|
|
527
|
+
"provider_facets",
|
|
528
|
+
provider_result,
|
|
529
|
+
args.pretty,
|
|
530
|
+
repo=repo,
|
|
531
|
+
memory=memory,
|
|
532
|
+
provider=provider,
|
|
533
|
+
steps={
|
|
534
|
+
"prepare": json_step(prepare),
|
|
535
|
+
"git_commits": git_commits_step,
|
|
536
|
+
},
|
|
537
|
+
outputs=outputs,
|
|
538
|
+
notices=notices,
|
|
539
|
+
effective_settings=args.effective_settings,
|
|
540
|
+
)
|
|
541
|
+
provider_facets = {
|
|
542
|
+
"ok": True,
|
|
543
|
+
"skipped": True,
|
|
544
|
+
"reason": f"provider_evidence_state={provider['evidence_state']}",
|
|
545
|
+
"output": "",
|
|
546
|
+
}
|
|
547
|
+
progress.update(3, "provider facets")
|
|
548
|
+
progress.finish()
|
|
549
|
+
|
|
550
|
+
counts = {
|
|
551
|
+
"raw": {
|
|
552
|
+
"git_commits": raw_source_counts(git_commits_path),
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
if provider_facets.get("output"):
|
|
556
|
+
counts["raw"]["provider_facets"] = raw_source_counts(Path(str(provider_facets["output"])))
|
|
557
|
+
|
|
558
|
+
report = {
|
|
559
|
+
"ok": True,
|
|
560
|
+
"repo_path": str(repo),
|
|
561
|
+
"memory_path": str(memory),
|
|
562
|
+
"provider": provider,
|
|
563
|
+
"notices": notices,
|
|
564
|
+
"effective_settings": args.effective_settings,
|
|
565
|
+
"steps": {
|
|
566
|
+
"prepare": json_step(prepare),
|
|
567
|
+
"git_commits": git_commits_step,
|
|
568
|
+
"provider_facets": provider_facets,
|
|
569
|
+
},
|
|
570
|
+
"outputs": outputs,
|
|
571
|
+
"counts": counts,
|
|
572
|
+
"next_step": "Inspect raw evidence, then author PROFILE.md and resources/*.md.",
|
|
573
|
+
}
|
|
574
|
+
write_report(report, args.pretty)
|
|
575
|
+
return 0
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
if __name__ == "__main__":
|
|
579
|
+
raise SystemExit(main(sys.argv[1:]))
|