@memorax/memorax-code 0.1.9 → 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.
Files changed (101) hide show
  1. package/README.md +10 -0
  2. package/bin/memorax-code-codebuddy.mjs +4 -0
  3. package/bin/memorax-code-setup.mjs +182 -65
  4. package/bin/memorax-code.mjs +74 -45
  5. package/docs/configuration.md +82 -22
  6. package/docs/troubleshooting.md +69 -5
  7. package/lib/automatic-update.mjs +233 -0
  8. package/lib/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  9. package/lib/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  10. package/lib/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  11. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  12. package/lib/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  13. package/lib/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  14. package/lib/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  15. package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
  16. package/lib/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  17. package/lib/memorax-code-backend/dist/app/backend-server.js +2 -2
  18. package/lib/memorax-code-backend/dist/app/memory-observability.js +2 -2
  19. package/lib/memorax-code-backend/dist/clients/codebuddy/jsonl-history.js +208 -0
  20. package/lib/memorax-code-backend/dist/clients/codebuddy/lifecycle.js +44 -0
  21. package/lib/memorax-code-backend/dist/clients/codebuddy/memory-hook-runtime.js +250 -0
  22. package/lib/memorax-code-backend/dist/clients/codebuddy/turn-id.js +16 -0
  23. package/lib/memorax-code-backend/dist/clients/codex/plugin-install.js +129 -7
  24. package/lib/memorax-code-backend/dist/config/memorax-code.js +14 -1
  25. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +43 -6
  26. package/lib/memorax-code-backend/dist/lifecycle/active-clients.js +3 -0
  27. package/lib/memorax-code-backend/dist/lifecycle/automatic-update-scheduler.js +133 -0
  28. package/lib/memorax-code-backend/dist/lifecycle/client-plugin-removal.js +14 -2
  29. package/lib/memorax-code-backend/dist/lifecycle/client-selection.js +6 -4
  30. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +48 -9
  31. package/lib/memorax-code-backend/dist/memory/cli.js +4 -2
  32. package/lib/memorax-code-backend/dist/memory/hook-command.js +31 -1
  33. package/lib/memorax-code-backend/dist/memory/reminder-trace-recorder.js +5 -1
  34. package/lib/memorax-code-backend/dist/memory/service.js +12 -0
  35. package/lib/memorax-code-backend/dist/shared/windows-cli-invocation.js +10 -1
  36. package/lib/memorax-code-backend/dist/trace/config.js +12 -0
  37. package/lib/memorax-code-backend/dist/trace/context.js +21 -1
  38. package/lib/memorax-code-backend/dist/trace/store.js +12 -1
  39. package/lib/memorax-code-backend/package.json +1 -1
  40. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  41. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  42. package/lib/memorax-code-claude-adapter/package.json +1 -1
  43. package/lib/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  44. package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  54. 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
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  59. package/lib/memorax-code-codebuddy-adapter/.codebuddy-plugin/plugin.json +7 -0
  60. package/lib/memorax-code-codebuddy-adapter/hooks/common-runtime.mjs +13 -0
  61. package/lib/memorax-code-codebuddy-adapter/hooks/hooks.json +38 -0
  62. package/lib/memorax-code-codebuddy-adapter/hooks/repo-memory-job.mjs +40 -0
  63. package/lib/memorax-code-codebuddy-adapter/hooks/runtime-hook.mjs +279 -0
  64. package/lib/memorax-code-codebuddy-adapter/package.json +9 -0
  65. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/SKILL.md +85 -0
  66. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/claude.yaml +10 -0
  67. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/openai.yaml +7 -0
  68. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/defaults.json +12 -0
  69. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-add.md +88 -0
  70. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-search.md +93 -0
  71. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-read.md +46 -0
  72. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-write.md +120 -0
  73. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-build.md +319 -0
  74. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-read.md +103 -0
  75. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-templates.md +390 -0
  76. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-update.md +127 -0
  77. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/collect_all.py +579 -0
  78. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/detect_updates.py +919 -0
  79. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/git_commit_facets.py +222 -0
  80. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/github_resource_facets.py +512 -0
  81. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/gitlab_resource_facets.py +517 -0
  82. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/prepare_repo_memory.py +411 -0
  83. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/user_profile_memory.py +528 -0
  84. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/validate_memory.py +248 -0
  85. package/lib/memorax-code-codebuddy-adapter/src/cli.mjs +58 -0
  86. package/lib/memorax-code-codebuddy-adapter/src/config.mjs +277 -0
  87. package/lib/memorax-code-codebuddy-adapter/src/hook-manifest.mjs +47 -0
  88. package/lib/memorax-code-codebuddy-adapter/src/runtime-observation.mjs +65 -0
  89. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  90. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  91. package/lib/memorax-code-codex-adapter/package.json +1 -1
  92. package/lib/memorax-code-codex-adapter/runtime-hooks/ensure-backend.mjs +4 -1
  93. package/lib/memorax-code-dsh-adapter/package.json +2 -1
  94. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +1 -0
  95. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  96. package/lib/npm-invocation.mjs +51 -8
  97. package/lib/resolve-codebuddy-command.mjs +112 -0
  98. package/lib/run-entrypoint.mjs +25 -1
  99. package/lib/windows-cli-invocation.mjs +11 -1
  100. package/lib/windows-user-path.mjs +218 -0
  101. 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:]))