@homericintelligence/athena-opencode 0.5.0 → 0.5.2

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 (146) hide show
  1. package/README.md +30 -17
  2. package/package.json +2 -2
  3. package/plugin.js +20 -5
  4. package/skills/TECHNICAL_ENGLISH.md +78 -0
  5. package/skills/_cli.py +33 -22
  6. package/skills/_plugin.json +1 -0
  7. package/skills/_support/docs/dependency-resolution.md +203 -0
  8. package/skills/_support/docs/policies/development.md +131 -0
  9. package/skills/_support/docs/policies/evidence-integrity.md +40 -0
  10. package/skills/_support/docs/principles/README.md +565 -0
  11. package/skills/_support/docs/principles/details/p001-kiss.md +114 -0
  12. package/skills/_support/docs/principles/details/p002-yagni.md +109 -0
  13. package/skills/_support/docs/principles/details/p003-dry.md +126 -0
  14. package/skills/_support/docs/principles/details/p004-solid.md +165 -0
  15. package/skills/_support/docs/principles/details/p005-modularity.md +123 -0
  16. package/skills/_support/docs/principles/details/p006-principle-of-least-astonishment.md +115 -0
  17. package/skills/_support/docs/principles/details/p007-subtraction-over-addition.md +113 -0
  18. package/skills/_support/docs/principles/details/p008-understand-before-subtracting.md +115 -0
  19. package/skills/_support/docs/principles/details/p009-general-mechanisms-over-special-cases.md +108 -0
  20. package/skills/_support/docs/principles/details/p010-scope-fidelity.md +117 -0
  21. package/skills/_support/docs/principles/details/p011-minimal-coherent-change.md +128 -0
  22. package/skills/_support/docs/principles/details/p012-evidence-before-modification.md +115 -0
  23. package/skills/_support/docs/principles/details/p013-avoid-hasty-abstractions.md +112 -0
  24. package/skills/_support/docs/principles/details/p014-preserve-unrequested-behavior.md +110 -0
  25. package/skills/_support/docs/principles/details/p015-architecture-conformance.md +124 -0
  26. package/skills/_support/docs/principles/details/p016-separation-of-concerns.md +125 -0
  27. package/skills/_support/docs/principles/details/p017-high-cohesion-low-coupling.md +130 -0
  28. package/skills/_support/docs/principles/details/p018-information-hiding.md +128 -0
  29. package/skills/_support/docs/principles/details/p019-explicit-contracts.md +134 -0
  30. package/skills/_support/docs/principles/details/p020-executable-architecture.md +130 -0
  31. package/skills/_support/docs/principles/details/p021-evolutionary-and-reversible-design.md +126 -0
  32. package/skills/_support/docs/principles/details/p022-test-behavior-not-implementation.md +129 -0
  33. package/skills/_support/docs/principles/details/p023-parameterized-table-driven-testing.md +129 -0
  34. package/skills/_support/docs/principles/details/p024-boundary-value-testing.md +128 -0
  35. package/skills/_support/docs/principles/details/p025-property-based-testing-for-invariants.md +133 -0
  36. package/skills/_support/docs/principles/details/p026-regression-before-repair.md +134 -0
  37. package/skills/_support/docs/principles/details/p027-deterministic-and-hermetic-tests.md +128 -0
  38. package/skills/_support/docs/principles/details/p028-test-failure-paths.md +133 -0
  39. package/skills/_support/docs/principles/details/p029-generalize-error-policy-preserve-specific-cause.md +131 -0
  40. package/skills/_support/docs/principles/details/p030-nearest-responsible-error-boundary.md +133 -0
  41. package/skills/_support/docs/principles/details/p031-propagate-rather-than-swallow.md +131 -0
  42. package/skills/_support/docs/principles/details/p032-handle-once-preserve-causality.md +137 -0
  43. package/skills/_support/docs/principles/details/p033-state-safe-failure-semantics.md +140 -0
  44. package/skills/_support/docs/principles/details/p034-fail-fast.md +132 -0
  45. package/skills/_support/docs/principles/details/p035-fail-secure-fail-closed.md +137 -0
  46. package/skills/_support/docs/principles/details/p036-graceful-degradation.md +137 -0
  47. package/skills/_support/docs/principles/details/p037-idempotency-before-retry.md +139 -0
  48. package/skills/_support/docs/principles/details/p038-bounded-retry.md +145 -0
  49. package/skills/_support/docs/principles/details/p039-bounded-waiting.md +151 -0
  50. package/skills/_support/docs/principles/details/p040-bounded-resources.md +138 -0
  51. package/skills/_support/docs/principles/details/p041-backpressure-and-load-shedding.md +141 -0
  52. package/skills/_support/docs/principles/details/p042-fault-isolation-bulkheads.md +140 -0
  53. package/skills/_support/docs/principles/details/p043-circuit-breakers.md +148 -0
  54. package/skills/_support/docs/principles/details/p044-atomicity-where-possible.md +141 -0
  55. package/skills/_support/docs/principles/details/p045-compensation-where-atomicity-is-impossible.md +163 -0
  56. package/skills/_support/docs/principles/details/p046-resumability.md +138 -0
  57. package/skills/_support/docs/principles/details/p047-observability-is-part-of-correctness.md +134 -0
  58. package/skills/_support/docs/principles/details/p048-secure-by-design.md +132 -0
  59. package/skills/_support/docs/principles/details/p049-secure-by-default.md +126 -0
  60. package/skills/_support/docs/principles/details/p050-least-privilege.md +123 -0
  61. package/skills/_support/docs/principles/details/p051-complete-mediation.md +124 -0
  62. package/skills/_support/docs/principles/details/p052-separation-of-duties.md +129 -0
  63. package/skills/_support/docs/principles/details/p053-validate-at-trust-boundaries.md +126 -0
  64. package/skills/_support/docs/principles/details/p054-defense-in-depth.md +135 -0
  65. package/skills/_support/docs/principles/details/p055-minimize-attack-surface.md +133 -0
  66. package/skills/_support/docs/principles/details/p056-secrets-stay-out-of-code-and-context.md +137 -0
  67. package/skills/_support/docs/principles/details/p057-supply-chain-integrity.md +135 -0
  68. package/skills/_support/docs/principles/details/p058-bounded-agent-authority.md +137 -0
  69. package/skills/_support/docs/principles/details/p059-data-is-not-instruction.md +130 -0
  70. package/skills/_support/docs/principles/details/p060-constrain-sub-agents.md +136 -0
  71. package/skills/_support/docs/principles/details/p061-separate-decision-from-high-impact-execution.md +131 -0
  72. package/skills/_support/docs/principles/details/p062-human-approval-for-irreversible-or-high-risk-actions.md +129 -0
  73. package/skills/_support/docs/principles/details/p063-requirement-to-code-traceability.md +116 -0
  74. package/skills/_support/docs/principles/details/p064-requirement-to-test-traceability.md +116 -0
  75. package/skills/_support/docs/principles/details/p065-verify-before-claiming-completion.md +131 -0
  76. package/skills/_support/docs/principles/details/p066-preserve-existing-work.md +114 -0
  77. package/skills/_support/docs/principles/details/p067-no-test-cheating.md +126 -0
  78. package/skills/_support/docs/principles/details/p068-no-validation-bypass.md +119 -0
  79. package/skills/_support/docs/principles/details/p069-independent-review-for-high-risk-changes.md +129 -0
  80. package/skills/_support/docs/principles/details/p070-code-health-must-not-regress.md +116 -0
  81. package/skills/_support/docs/principles/details/p071-consistency-over-personal-preference.md +117 -0
  82. package/skills/_support/docs/principles/details/p072-technical-evidence-over-preference.md +122 -0
  83. package/skills/_support/docs/principles/details/p073-optimize-only-with-evidence.md +123 -0
  84. package/skills/_support/docs/principles/details/p074-prefer-existing-mechanisms.md +121 -0
  85. package/skills/_support/docs/principles/details/p075-make-invalid-states-hard-to-represent.md +128 -0
  86. package/skills/_support/docs/principles/details/p076-parse-then-validate-then-operate.md +133 -0
  87. package/skills/_support/docs/principles/details/p077-separate-policy-from-mechanism.md +112 -0
  88. package/skills/_support/docs/principles/details/p078-single-source-of-truth.md +131 -0
  89. package/skills/_support/docs/principles/details/p079-explicit-ownership-and-lifetimes.md +124 -0
  90. package/skills/_support/docs/principles/details/p080-make-concurrency-deliberate.md +122 -0
  91. package/skills/_support/docs/principles/details/p081-forward-progress-with-safety.md +125 -0
  92. package/skills/_support/docs/principles/details/p082-design-for-cancellation.md +133 -0
  93. package/skills/_support/docs/principles/details/p083-irreversible-actions-last.md +124 -0
  94. package/skills/_support/docs/principles/details/p084-prefer-local-reasoning.md +131 -0
  95. package/skills/_support/docs/principles/details/p085-explicit-is-better-than-implicit.md +123 -0
  96. package/skills/_support/docs/principles/details/p086-readability-counts.md +113 -0
  97. package/skills/_support/docs/principles/details/p087-comments-explain-why-code-explains-what.md +120 -0
  98. package/skills/_support/docs/principles/details/p088-delete-dead-code.md +125 -0
  99. package/skills/_support/docs/principles/details/p089-delete-obsolete-configuration-and-dependencies.md +125 -0
  100. package/skills/_support/docs/principles/details/p090-prefer-negative-code.md +134 -0
  101. package/skills/_support/docs/principles/details/p091-test-driven-development.md +149 -0
  102. package/skills/_support/docs/review/README.md +63 -0
  103. package/skills/_support/docs/review/behavior-first-testing.md +78 -0
  104. package/skills/_support/docs/review/common.md +400 -0
  105. package/skills/_support/docs/review/design-docs.md +74 -0
  106. package/skills/_support/docs/review/issue-planning.md +247 -0
  107. package/skills/_support/docs/review/language-routing.md +141 -0
  108. package/skills/_support/docs/review/repository-scorecard.md +170 -0
  109. package/skills/advise/SKILL.md +108 -74
  110. package/skills/advise/scripts/list_retrievable_skills.py +20 -8
  111. package/skills/advise/scripts/resolve_knowledge_checkout.py +533 -0
  112. package/skills/brainstorm/SKILL.md +163 -127
  113. package/skills/change-review/SKILL.md +91 -71
  114. package/skills/change-review/references/scope-resolution.md +69 -46
  115. package/skills/change-review/scripts/resolve_scope.py +144 -84
  116. package/skills/finalize-plan/SKILL.md +205 -139
  117. package/skills/git-worktrees/SKILL.md +100 -80
  118. package/skills/git-worktrees/scripts/prepare_worktree.py +34 -15
  119. package/skills/issue-review/SKILL.md +136 -79
  120. package/skills/learn/SKILL.md +359 -192
  121. package/skills/myrmidon-swarm/SKILL.md +126 -86
  122. package/skills/plan-issue/SKILL.md +122 -81
  123. package/skills/pr-review/SKILL.md +184 -116
  124. package/skills/pr-review/references/criteria.md +61 -37
  125. package/skills/pr-review/references/delivery.md +277 -101
  126. package/skills/pr-review/references/evidence.md +346 -149
  127. package/skills/pr-review/references/prevalidated.md +93 -61
  128. package/skills/pr-review/scripts/collect_evidence.py +312 -162
  129. package/skills/pr-review/scripts/deliver_go.py +701 -0
  130. package/skills/pr-review/scripts/diff_context.py +34 -15
  131. package/skills/pr-review/scripts/materialize_snapshot.py +141 -130
  132. package/skills/pr-review/scripts/pr_identity.py +27 -15
  133. package/skills/pr-review/scripts/resolve_pr.py +77 -41
  134. package/skills/realign/SKILL.md +516 -0
  135. package/skills/realign/references/aislop-integration.md +215 -0
  136. package/skills/realign/references/architecture-and-structure.md +271 -0
  137. package/skills/realign/references/control-flow-and-errors.md +344 -0
  138. package/skills/realign/references/tests-dependencies-and-security.md +261 -0
  139. package/skills/realign/scripts/resolve_assessment.py +1525 -0
  140. package/skills/repo-review/SKILL.md +202 -131
  141. package/skills/simplify/SKILL.md +174 -0
  142. package/skills/systematic-debugging/SKILL.md +212 -192
  143. package/skills/systematic-debugging/scripts/repository_evidence.py +26 -8
  144. package/skills/test-driven-development/SKILL.md +126 -96
  145. package/skills/tidy/SKILL.md +104 -59
  146. package/skills/tidy/scripts/run_tidy.py +54 -6
@@ -0,0 +1,533 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve a trusted Mnemosyne checkout for read-only or write workflows."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib.util
8
+ import json
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from collections.abc import Callable
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any, Protocol, cast
18
+ from urllib.parse import urlparse
19
+
20
+
21
+ class _CliModule(Protocol):
22
+ """Describe the shared helpers loaded from the installed skill corpus."""
23
+
24
+ argument_parser: Callable[..., argparse.ArgumentParser]
25
+ git_read_arguments: Callable[[], tuple[str, ...]]
26
+ git_read_environment: Callable[[], dict[str, str]]
27
+ run_command: Callable[..., subprocess.CompletedProcess[str]]
28
+
29
+
30
+ def _load_installed_cli_module() -> _CliModule:
31
+ """Load the sibling shared helper from an installed skill corpus."""
32
+ cli_path = Path(__file__).resolve().parents[2] / "_cli.py"
33
+ spec = importlib.util.spec_from_file_location("athena_installed_cli", cli_path)
34
+ if spec is None or spec.loader is None:
35
+ raise RuntimeError(
36
+ f"The installed Athena CLI helper is unavailable: '{cli_path}'."
37
+ )
38
+ module = importlib.util.module_from_spec(spec)
39
+ spec.loader.exec_module(module)
40
+ return cast(_CliModule, module)
41
+
42
+
43
+ if __package__ in {None, ""}:
44
+ _cli = _load_installed_cli_module()
45
+ argument_parser = _cli.argument_parser
46
+ git_read_arguments = _cli.git_read_arguments
47
+ git_read_environment = _cli.git_read_environment
48
+ run_command = _cli.run_command
49
+ else:
50
+ from skills._cli import (
51
+ argument_parser,
52
+ git_read_arguments,
53
+ git_read_environment,
54
+ run_command,
55
+ )
56
+
57
+ DEFAULT_KNOWLEDGE_ROOT = Path.home() / ".agent_brain" / "knowledge"
58
+ DEFAULT_ORGANIZATION_OWNER = "HomericIntelligence"
59
+ REPOSITORY_NAME = "Mnemosyne"
60
+ BEST_EFFORT_REMOTE_TIMEOUT_SECONDS = 2.0
61
+ OWNER_PATTERN = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$")
62
+ FULL_SHA_PATTERN = re.compile(r"^[0-9a-f]{40}$")
63
+ SAFE_LOCAL_CONFIG_KEYS = frozenset(
64
+ {
65
+ "core.bare",
66
+ "core.filemode",
67
+ "core.ignorecase",
68
+ "core.logallrefupdates",
69
+ "core.precomposeunicode",
70
+ "core.repositoryformatversion",
71
+ "remote.origin.fetch",
72
+ "remote.origin.url",
73
+ }
74
+ )
75
+ SAFE_BRANCH_CONFIG_PATTERN = re.compile(r"^branch\..+\.(?:merge|remote)$")
76
+ SAFE_LOCAL_GIT_OVERRIDES = (
77
+ "-c",
78
+ f"core.hooksPath={os.devnull}",
79
+ "-c",
80
+ "core.fsmonitor=false",
81
+ )
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class LocalCheckout:
86
+ """Validated local checkout metadata."""
87
+
88
+ root: Path
89
+ repository: str
90
+ origin: str
91
+ branch: str | None
92
+ revision: str
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class RefreshOutcome:
97
+ """Best-effort refresh result."""
98
+
99
+ revision: str
100
+ refresh_state: str
101
+ freshness_limit: str
102
+ limitations: list[str]
103
+
104
+
105
+ def validate_owner(owner: str) -> str:
106
+ """Validate a GitHub owner name."""
107
+ if not OWNER_PATTERN.fullmatch(owner):
108
+ raise RuntimeError(f"The knowledge owner is not valid: '{owner}'.")
109
+ return owner
110
+
111
+
112
+ def expected_repository() -> str:
113
+ """Return the expected Mnemosyne repository for the current environment."""
114
+ owner = os.environ.get("HOMERIC_INTELLIGENCE_MNEMOSYNE_OWNER")
115
+ if owner is None or not owner:
116
+ owner = DEFAULT_ORGANIZATION_OWNER
117
+ return f"{validate_owner(owner)}/{REPOSITORY_NAME}"
118
+
119
+
120
+ def repository_from_origin(origin: str) -> str:
121
+ """Return the repository identity encoded in a Git origin URL or path."""
122
+ owner: str | None = None
123
+ repository: str | None = None
124
+ if origin.startswith("git@") and ":" in origin and "://" not in origin:
125
+ host, candidate = origin.split(":", maxsplit=1)
126
+ if host != "git@github.com":
127
+ raise RuntimeError(
128
+ f"The origin is not a trusted GitHub repository URL: '{origin}'."
129
+ )
130
+ segments = [
131
+ segment for segment in candidate.replace("\\", "/").split("/") if segment
132
+ ]
133
+ if len(segments) != 2:
134
+ raise RuntimeError(
135
+ f"The origin does not identify an owner and repository: '{origin}'."
136
+ )
137
+ owner, repository = segments
138
+ else:
139
+ parsed = urlparse(origin)
140
+ if parsed.scheme not in {"https", "ssh"}:
141
+ raise RuntimeError(
142
+ f"The origin is not a trusted GitHub repository URL: '{origin}'."
143
+ )
144
+ if parsed.hostname != "github.com":
145
+ raise RuntimeError(
146
+ f"The origin is not a trusted GitHub repository URL: '{origin}'."
147
+ )
148
+ segments = [
149
+ segment for segment in parsed.path.replace("\\", "/").split("/") if segment
150
+ ]
151
+ if len(segments) != 2:
152
+ raise RuntimeError(
153
+ f"The origin does not identify an owner and repository: '{origin}'."
154
+ )
155
+ owner, repository = segments
156
+ assert owner is not None
157
+ assert repository is not None
158
+ repository = repository.removesuffix(".git")
159
+ if not OWNER_PATTERN.fullmatch(owner):
160
+ raise RuntimeError(
161
+ f"The origin does not identify a valid repository owner: '{origin}'."
162
+ )
163
+ if repository.casefold() != REPOSITORY_NAME.casefold():
164
+ raise RuntimeError(
165
+ f"The origin does not identify the Mnemosyne repository: '{origin}'."
166
+ )
167
+ return f"{owner}/{repository}"
168
+
169
+
170
+ def run_git(
171
+ cwd: Path, *arguments: str, timeout: float | None = None
172
+ ) -> subprocess.CompletedProcess[str]:
173
+ """Run Git with the immutable read boundary."""
174
+ try:
175
+ return run_command(
176
+ [
177
+ "git",
178
+ *git_read_arguments(),
179
+ *SAFE_LOCAL_GIT_OVERRIDES,
180
+ *arguments,
181
+ ],
182
+ capture_output=True,
183
+ cwd=cwd,
184
+ env=git_read_environment(),
185
+ text=True,
186
+ check=False,
187
+ timeout=timeout,
188
+ )
189
+ except subprocess.TimeoutExpired as error:
190
+ raise RuntimeError(
191
+ f"The git {' '.join(arguments)} command timed out after "
192
+ f"{timeout:.1f} seconds."
193
+ ) from error
194
+
195
+
196
+ def require_safe_local_git_configuration(cwd: Path) -> None:
197
+ """Reject local Git settings that can redirect or execute refresh work."""
198
+ result = run_git(
199
+ cwd,
200
+ "config",
201
+ "--local",
202
+ "--name-only",
203
+ "--null",
204
+ "--list",
205
+ "--includes",
206
+ )
207
+ if result.returncode != 0:
208
+ message = result.stderr.strip() or "The local Git configuration cannot be read."
209
+ raise RuntimeError(message)
210
+ keys = [value.casefold() for value in result.stdout.split("\0") if value]
211
+ unsafe = sorted(
212
+ key
213
+ for key in keys
214
+ if key not in SAFE_LOCAL_CONFIG_KEYS
215
+ and SAFE_BRANCH_CONFIG_PATTERN.fullmatch(key) is None
216
+ )
217
+ if unsafe:
218
+ raise RuntimeError(
219
+ "The knowledge checkout has unsafe local Git configuration: "
220
+ + ", ".join(unsafe)
221
+ )
222
+
223
+
224
+ def git_text(cwd: Path, *arguments: str) -> str:
225
+ """Return the trimmed stdout for a successful immutable Git command."""
226
+ result = run_git(cwd, *arguments)
227
+ if result.returncode != 0:
228
+ message = result.stderr.strip() or (
229
+ f"The git {' '.join(arguments)} command failed."
230
+ )
231
+ raise RuntimeError(message)
232
+ return result.stdout.strip()
233
+
234
+
235
+ def validate_local_checkout(knowledge_root: Path) -> LocalCheckout:
236
+ """Validate the local checkout identity and cleanliness."""
237
+ if not knowledge_root.is_dir():
238
+ raise RuntimeError(
239
+ f"The knowledge checkout is not available: '{knowledge_root}'."
240
+ )
241
+ top_level = Path(git_text(knowledge_root, "rev-parse", "--show-toplevel"))
242
+ if top_level.resolve() != knowledge_root.resolve():
243
+ raise RuntimeError(
244
+ f"The knowledge checkout root does not match the requested path: "
245
+ f"'{knowledge_root}'."
246
+ )
247
+ origin = git_text(knowledge_root, "config", "--get", "remote.origin.url")
248
+ repository = repository_from_origin(origin)
249
+ revision = git_text(knowledge_root, "rev-parse", "HEAD")
250
+ if not FULL_SHA_PATTERN.fullmatch(revision):
251
+ raise RuntimeError(
252
+ f"The knowledge checkout revision is not a full commit SHA: '{revision}'."
253
+ )
254
+ status = git_text(
255
+ knowledge_root,
256
+ "status",
257
+ "--porcelain=v1",
258
+ "--untracked-files=all",
259
+ )
260
+ if status:
261
+ raise RuntimeError("The knowledge checkout is dirty:\n" + status)
262
+ branch_result = run_git(knowledge_root, "branch", "--show-current")
263
+ if branch_result.returncode != 0:
264
+ message = (
265
+ branch_result.stderr.strip() or "The current branch could not be read."
266
+ )
267
+ raise RuntimeError(message)
268
+ branch = branch_result.stdout.strip() or None
269
+ return LocalCheckout(
270
+ root=knowledge_root,
271
+ repository=repository,
272
+ origin=origin,
273
+ branch=branch,
274
+ revision=revision,
275
+ )
276
+
277
+
278
+ def gh_command(*arguments: str) -> subprocess.CompletedProcess[str]:
279
+ """Run `gh` and return the completed process."""
280
+ try:
281
+ return run_command(
282
+ ["gh", *arguments],
283
+ capture_output=True,
284
+ text=True,
285
+ check=False,
286
+ timeout=BEST_EFFORT_REMOTE_TIMEOUT_SECONDS,
287
+ )
288
+ except subprocess.TimeoutExpired as error:
289
+ raise RuntimeError(
290
+ f"The gh {' '.join(arguments)} command timed out after "
291
+ f"{BEST_EFFORT_REMOTE_TIMEOUT_SECONDS:.1f} seconds."
292
+ ) from error
293
+
294
+
295
+ def parse_repo_view(output: str, expected: str) -> str:
296
+ """Return the default branch reported by GitHub."""
297
+ value = json.loads(output)
298
+ if not isinstance(value, dict):
299
+ raise TypeError("GitHub returned repository metadata that is not valid.")
300
+ name_with_owner = value.get("nameWithOwner")
301
+ if (
302
+ isinstance(name_with_owner, str)
303
+ and name_with_owner.casefold() != expected.casefold()
304
+ ):
305
+ raise RuntimeError(
306
+ f"GitHub returned a different repository than expected: '{name_with_owner}'."
307
+ )
308
+ default_branch_ref = value.get("defaultBranchRef")
309
+ if not isinstance(default_branch_ref, dict):
310
+ raise TypeError(
311
+ "GitHub did not return a default branch for the knowledge repository."
312
+ )
313
+ default_branch = default_branch_ref.get("name")
314
+ if not isinstance(default_branch, str) or not default_branch:
315
+ raise TypeError(
316
+ "GitHub did not return a valid default branch for the knowledge repository."
317
+ )
318
+ return default_branch
319
+
320
+
321
+ def unavailable_refresh(
322
+ checkout: LocalCheckout,
323
+ mode: str,
324
+ limitations: list[str],
325
+ reason: str,
326
+ *,
327
+ cause: BaseException | None = None,
328
+ ) -> RefreshOutcome:
329
+ """Apply the read-only fallback or the write-mode failure policy."""
330
+ if mode == "write":
331
+ raise RuntimeError(reason) from cause
332
+ limitations.append(reason)
333
+ return RefreshOutcome(
334
+ revision=checkout.revision,
335
+ refresh_state="unavailable",
336
+ freshness_limit="freshness could not be verified or updated",
337
+ limitations=limitations,
338
+ )
339
+
340
+
341
+ def refresh_local_checkout(
342
+ checkout: LocalCheckout, expected: str, mode: str
343
+ ) -> RefreshOutcome:
344
+ """Attempt a best-effort refresh and report the result."""
345
+ limitations: list[str] = []
346
+ if shutil.which("gh") is None:
347
+ return unavailable_refresh(
348
+ checkout,
349
+ mode,
350
+ limitations,
351
+ "The required command is not available: 'gh'.",
352
+ )
353
+ try:
354
+ auth_result = gh_command("auth", "status", "--hostname", "github.com")
355
+ except RuntimeError as error:
356
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
357
+ if auth_result.returncode != 0:
358
+ reason = (
359
+ auth_result.stderr.strip()
360
+ or auth_result.stdout.strip()
361
+ or "GitHub authentication is unavailable."
362
+ )
363
+ return unavailable_refresh(checkout, mode, limitations, reason)
364
+ try:
365
+ repo_result = gh_command(
366
+ "repo",
367
+ "view",
368
+ expected,
369
+ "--json",
370
+ "nameWithOwner,defaultBranchRef",
371
+ )
372
+ except RuntimeError as error:
373
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
374
+ if repo_result.returncode != 0:
375
+ reason = (
376
+ repo_result.stderr.strip()
377
+ or repo_result.stdout.strip()
378
+ or "GitHub repository discovery failed."
379
+ )
380
+ return unavailable_refresh(checkout, mode, limitations, reason)
381
+ try:
382
+ default_branch = parse_repo_view(repo_result.stdout, expected)
383
+ except (json.JSONDecodeError, TypeError) as error:
384
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
385
+ if checkout.branch is None:
386
+ return unavailable_refresh(
387
+ checkout,
388
+ mode,
389
+ limitations,
390
+ "The knowledge checkout is detached and cannot be refreshed.",
391
+ )
392
+ if checkout.branch != default_branch:
393
+ reason = (
394
+ "The knowledge checkout branch does not match the remote default "
395
+ f"branch: '{checkout.branch}' != '{default_branch}'."
396
+ )
397
+ return unavailable_refresh(checkout, mode, limitations, reason)
398
+ try:
399
+ require_safe_local_git_configuration(checkout.root)
400
+ current_revision = git_text(checkout.root, "rev-parse", "HEAD")
401
+ except RuntimeError as error:
402
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
403
+ if current_revision != checkout.revision:
404
+ return unavailable_refresh(
405
+ checkout,
406
+ mode,
407
+ limitations,
408
+ "The knowledge checkout revision changed before refresh.",
409
+ )
410
+ trusted_url = f"https://github.com/{expected}.git"
411
+ try:
412
+ fetch_result = run_git(
413
+ checkout.root,
414
+ "fetch",
415
+ "--no-tags",
416
+ trusted_url,
417
+ default_branch,
418
+ timeout=BEST_EFFORT_REMOTE_TIMEOUT_SECONDS,
419
+ )
420
+ except RuntimeError as error:
421
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
422
+ if fetch_result.returncode != 0:
423
+ reason = (
424
+ fetch_result.stderr.strip()
425
+ or fetch_result.stdout.strip()
426
+ or f"The knowledge checkout could not fetch origin/{default_branch}."
427
+ )
428
+ return unavailable_refresh(checkout, mode, limitations, reason)
429
+ try:
430
+ require_safe_local_git_configuration(checkout.root)
431
+ fetched_revision = git_text(checkout.root, "rev-parse", "FETCH_HEAD")
432
+ except RuntimeError as error:
433
+ return unavailable_refresh(checkout, mode, limitations, str(error), cause=error)
434
+ if not FULL_SHA_PATTERN.fullmatch(fetched_revision):
435
+ reason = (
436
+ f"The upstream revision is not a full commit SHA: '{fetched_revision}'."
437
+ )
438
+ return unavailable_refresh(checkout, mode, limitations, reason)
439
+ ancestor_result = run_git(
440
+ checkout.root,
441
+ "merge-base",
442
+ "--is-ancestor",
443
+ checkout.revision,
444
+ fetched_revision,
445
+ )
446
+ if ancestor_result.returncode != 0:
447
+ reason = (
448
+ "The local knowledge revision is not an ancestor of the upstream revision."
449
+ if ancestor_result.returncode == 1
450
+ else ancestor_result.stderr.strip()
451
+ or "The knowledge checkout ancestry could not be verified."
452
+ )
453
+ return unavailable_refresh(checkout, mode, limitations, reason)
454
+ merge_result = run_git(checkout.root, "merge", "--ff-only", "FETCH_HEAD")
455
+ if merge_result.returncode != 0:
456
+ reason = (
457
+ merge_result.stderr.strip()
458
+ or merge_result.stdout.strip()
459
+ or "The knowledge checkout could not fast-forward."
460
+ )
461
+ return unavailable_refresh(checkout, mode, limitations, reason)
462
+ revision = git_text(checkout.root, "rev-parse", "HEAD")
463
+ if revision != fetched_revision:
464
+ raise RuntimeError(
465
+ "The knowledge checkout does not match the verified upstream revision."
466
+ )
467
+ return RefreshOutcome(
468
+ revision=revision,
469
+ refresh_state="updated",
470
+ freshness_limit="freshness verified by upstream refresh",
471
+ limitations=limitations,
472
+ )
473
+
474
+
475
+ def resolve_knowledge_checkout(knowledge_root: Path, mode: str) -> dict[str, Any]:
476
+ """Resolve the local checkout and report its revision and limits."""
477
+ expected = expected_repository()
478
+ checkout = validate_local_checkout(knowledge_root)
479
+ if checkout.repository.casefold() != expected.casefold():
480
+ raise RuntimeError(
481
+ f"The knowledge checkout origin does not match '{expected}': "
482
+ f"'{checkout.origin}'."
483
+ )
484
+ refresh = refresh_local_checkout(checkout, expected, mode)
485
+ return {
486
+ "branch": checkout.branch,
487
+ "checkout": str(checkout.root),
488
+ "checkout_state": "clean",
489
+ "freshness_limit": refresh.freshness_limit,
490
+ "limitations": refresh.limitations,
491
+ "local_revision": checkout.revision,
492
+ "mode": mode,
493
+ "refresh_state": refresh.refresh_state,
494
+ "repository": checkout.repository,
495
+ "revision": refresh.revision,
496
+ "trust_basis": "validated local checkout",
497
+ }
498
+
499
+
500
+ def main(argv: list[str] | None = None) -> int:
501
+ """Resolve the checkout and print a machine-readable result."""
502
+ parser = argument_parser(description=__doc__)
503
+ parser.add_argument(
504
+ "--mode",
505
+ choices=("read-only", "write"),
506
+ required=True,
507
+ help="Select the read-only or mutation boundary.",
508
+ )
509
+ parser.add_argument(
510
+ "--knowledge-root",
511
+ type=Path,
512
+ default=DEFAULT_KNOWLEDGE_ROOT,
513
+ help="Use the resolved Mnemosyne checkout path.",
514
+ )
515
+ parser.add_argument(
516
+ "--json",
517
+ action="store_true",
518
+ help="Emit JSON for downstream machine parsing.",
519
+ )
520
+ arguments = parser.parse_args(argv)
521
+ if not arguments.json:
522
+ parser.error("Specify --json.")
523
+ try:
524
+ result = resolve_knowledge_checkout(arguments.knowledge_root, arguments.mode)
525
+ except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
526
+ print(error, file=sys.stderr)
527
+ return 1
528
+ print(json.dumps(result, sort_keys=True))
529
+ return 0
530
+
531
+
532
+ if __name__ == "__main__":
533
+ raise SystemExit(main())