@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.
- package/README.md +30 -17
- package/package.json +2 -2
- package/plugin.js +20 -5
- package/skills/TECHNICAL_ENGLISH.md +78 -0
- package/skills/_cli.py +33 -22
- package/skills/_plugin.json +1 -0
- package/skills/_support/docs/dependency-resolution.md +203 -0
- package/skills/_support/docs/policies/development.md +131 -0
- package/skills/_support/docs/policies/evidence-integrity.md +40 -0
- package/skills/_support/docs/principles/README.md +565 -0
- package/skills/_support/docs/principles/details/p001-kiss.md +114 -0
- package/skills/_support/docs/principles/details/p002-yagni.md +109 -0
- package/skills/_support/docs/principles/details/p003-dry.md +126 -0
- package/skills/_support/docs/principles/details/p004-solid.md +165 -0
- package/skills/_support/docs/principles/details/p005-modularity.md +123 -0
- package/skills/_support/docs/principles/details/p006-principle-of-least-astonishment.md +115 -0
- package/skills/_support/docs/principles/details/p007-subtraction-over-addition.md +113 -0
- package/skills/_support/docs/principles/details/p008-understand-before-subtracting.md +115 -0
- package/skills/_support/docs/principles/details/p009-general-mechanisms-over-special-cases.md +108 -0
- package/skills/_support/docs/principles/details/p010-scope-fidelity.md +117 -0
- package/skills/_support/docs/principles/details/p011-minimal-coherent-change.md +128 -0
- package/skills/_support/docs/principles/details/p012-evidence-before-modification.md +115 -0
- package/skills/_support/docs/principles/details/p013-avoid-hasty-abstractions.md +112 -0
- package/skills/_support/docs/principles/details/p014-preserve-unrequested-behavior.md +110 -0
- package/skills/_support/docs/principles/details/p015-architecture-conformance.md +124 -0
- package/skills/_support/docs/principles/details/p016-separation-of-concerns.md +125 -0
- package/skills/_support/docs/principles/details/p017-high-cohesion-low-coupling.md +130 -0
- package/skills/_support/docs/principles/details/p018-information-hiding.md +128 -0
- package/skills/_support/docs/principles/details/p019-explicit-contracts.md +134 -0
- package/skills/_support/docs/principles/details/p020-executable-architecture.md +130 -0
- package/skills/_support/docs/principles/details/p021-evolutionary-and-reversible-design.md +126 -0
- package/skills/_support/docs/principles/details/p022-test-behavior-not-implementation.md +129 -0
- package/skills/_support/docs/principles/details/p023-parameterized-table-driven-testing.md +129 -0
- package/skills/_support/docs/principles/details/p024-boundary-value-testing.md +128 -0
- package/skills/_support/docs/principles/details/p025-property-based-testing-for-invariants.md +133 -0
- package/skills/_support/docs/principles/details/p026-regression-before-repair.md +134 -0
- package/skills/_support/docs/principles/details/p027-deterministic-and-hermetic-tests.md +128 -0
- package/skills/_support/docs/principles/details/p028-test-failure-paths.md +133 -0
- package/skills/_support/docs/principles/details/p029-generalize-error-policy-preserve-specific-cause.md +131 -0
- package/skills/_support/docs/principles/details/p030-nearest-responsible-error-boundary.md +133 -0
- package/skills/_support/docs/principles/details/p031-propagate-rather-than-swallow.md +131 -0
- package/skills/_support/docs/principles/details/p032-handle-once-preserve-causality.md +137 -0
- package/skills/_support/docs/principles/details/p033-state-safe-failure-semantics.md +140 -0
- package/skills/_support/docs/principles/details/p034-fail-fast.md +132 -0
- package/skills/_support/docs/principles/details/p035-fail-secure-fail-closed.md +137 -0
- package/skills/_support/docs/principles/details/p036-graceful-degradation.md +137 -0
- package/skills/_support/docs/principles/details/p037-idempotency-before-retry.md +139 -0
- package/skills/_support/docs/principles/details/p038-bounded-retry.md +145 -0
- package/skills/_support/docs/principles/details/p039-bounded-waiting.md +151 -0
- package/skills/_support/docs/principles/details/p040-bounded-resources.md +138 -0
- package/skills/_support/docs/principles/details/p041-backpressure-and-load-shedding.md +141 -0
- package/skills/_support/docs/principles/details/p042-fault-isolation-bulkheads.md +140 -0
- package/skills/_support/docs/principles/details/p043-circuit-breakers.md +148 -0
- package/skills/_support/docs/principles/details/p044-atomicity-where-possible.md +141 -0
- package/skills/_support/docs/principles/details/p045-compensation-where-atomicity-is-impossible.md +163 -0
- package/skills/_support/docs/principles/details/p046-resumability.md +138 -0
- package/skills/_support/docs/principles/details/p047-observability-is-part-of-correctness.md +134 -0
- package/skills/_support/docs/principles/details/p048-secure-by-design.md +132 -0
- package/skills/_support/docs/principles/details/p049-secure-by-default.md +126 -0
- package/skills/_support/docs/principles/details/p050-least-privilege.md +123 -0
- package/skills/_support/docs/principles/details/p051-complete-mediation.md +124 -0
- package/skills/_support/docs/principles/details/p052-separation-of-duties.md +129 -0
- package/skills/_support/docs/principles/details/p053-validate-at-trust-boundaries.md +126 -0
- package/skills/_support/docs/principles/details/p054-defense-in-depth.md +135 -0
- package/skills/_support/docs/principles/details/p055-minimize-attack-surface.md +133 -0
- package/skills/_support/docs/principles/details/p056-secrets-stay-out-of-code-and-context.md +137 -0
- package/skills/_support/docs/principles/details/p057-supply-chain-integrity.md +135 -0
- package/skills/_support/docs/principles/details/p058-bounded-agent-authority.md +137 -0
- package/skills/_support/docs/principles/details/p059-data-is-not-instruction.md +130 -0
- package/skills/_support/docs/principles/details/p060-constrain-sub-agents.md +136 -0
- package/skills/_support/docs/principles/details/p061-separate-decision-from-high-impact-execution.md +131 -0
- package/skills/_support/docs/principles/details/p062-human-approval-for-irreversible-or-high-risk-actions.md +129 -0
- package/skills/_support/docs/principles/details/p063-requirement-to-code-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p064-requirement-to-test-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p065-verify-before-claiming-completion.md +131 -0
- package/skills/_support/docs/principles/details/p066-preserve-existing-work.md +114 -0
- package/skills/_support/docs/principles/details/p067-no-test-cheating.md +126 -0
- package/skills/_support/docs/principles/details/p068-no-validation-bypass.md +119 -0
- package/skills/_support/docs/principles/details/p069-independent-review-for-high-risk-changes.md +129 -0
- package/skills/_support/docs/principles/details/p070-code-health-must-not-regress.md +116 -0
- package/skills/_support/docs/principles/details/p071-consistency-over-personal-preference.md +117 -0
- package/skills/_support/docs/principles/details/p072-technical-evidence-over-preference.md +122 -0
- package/skills/_support/docs/principles/details/p073-optimize-only-with-evidence.md +123 -0
- package/skills/_support/docs/principles/details/p074-prefer-existing-mechanisms.md +121 -0
- package/skills/_support/docs/principles/details/p075-make-invalid-states-hard-to-represent.md +128 -0
- package/skills/_support/docs/principles/details/p076-parse-then-validate-then-operate.md +133 -0
- package/skills/_support/docs/principles/details/p077-separate-policy-from-mechanism.md +112 -0
- package/skills/_support/docs/principles/details/p078-single-source-of-truth.md +131 -0
- package/skills/_support/docs/principles/details/p079-explicit-ownership-and-lifetimes.md +124 -0
- package/skills/_support/docs/principles/details/p080-make-concurrency-deliberate.md +122 -0
- package/skills/_support/docs/principles/details/p081-forward-progress-with-safety.md +125 -0
- package/skills/_support/docs/principles/details/p082-design-for-cancellation.md +133 -0
- package/skills/_support/docs/principles/details/p083-irreversible-actions-last.md +124 -0
- package/skills/_support/docs/principles/details/p084-prefer-local-reasoning.md +131 -0
- package/skills/_support/docs/principles/details/p085-explicit-is-better-than-implicit.md +123 -0
- package/skills/_support/docs/principles/details/p086-readability-counts.md +113 -0
- package/skills/_support/docs/principles/details/p087-comments-explain-why-code-explains-what.md +120 -0
- package/skills/_support/docs/principles/details/p088-delete-dead-code.md +125 -0
- package/skills/_support/docs/principles/details/p089-delete-obsolete-configuration-and-dependencies.md +125 -0
- package/skills/_support/docs/principles/details/p090-prefer-negative-code.md +134 -0
- package/skills/_support/docs/principles/details/p091-test-driven-development.md +149 -0
- package/skills/_support/docs/review/README.md +63 -0
- package/skills/_support/docs/review/behavior-first-testing.md +78 -0
- package/skills/_support/docs/review/common.md +400 -0
- package/skills/_support/docs/review/design-docs.md +74 -0
- package/skills/_support/docs/review/issue-planning.md +247 -0
- package/skills/_support/docs/review/language-routing.md +141 -0
- package/skills/_support/docs/review/repository-scorecard.md +170 -0
- package/skills/advise/SKILL.md +108 -74
- package/skills/advise/scripts/list_retrievable_skills.py +20 -8
- package/skills/advise/scripts/resolve_knowledge_checkout.py +533 -0
- package/skills/brainstorm/SKILL.md +163 -127
- package/skills/change-review/SKILL.md +91 -71
- package/skills/change-review/references/scope-resolution.md +69 -46
- package/skills/change-review/scripts/resolve_scope.py +144 -84
- package/skills/finalize-plan/SKILL.md +205 -139
- package/skills/git-worktrees/SKILL.md +100 -80
- package/skills/git-worktrees/scripts/prepare_worktree.py +34 -15
- package/skills/issue-review/SKILL.md +136 -79
- package/skills/learn/SKILL.md +359 -192
- package/skills/myrmidon-swarm/SKILL.md +126 -86
- package/skills/plan-issue/SKILL.md +122 -81
- package/skills/pr-review/SKILL.md +184 -116
- package/skills/pr-review/references/criteria.md +61 -37
- package/skills/pr-review/references/delivery.md +277 -101
- package/skills/pr-review/references/evidence.md +346 -149
- package/skills/pr-review/references/prevalidated.md +93 -61
- package/skills/pr-review/scripts/collect_evidence.py +312 -162
- package/skills/pr-review/scripts/deliver_go.py +701 -0
- package/skills/pr-review/scripts/diff_context.py +34 -15
- package/skills/pr-review/scripts/materialize_snapshot.py +141 -130
- package/skills/pr-review/scripts/pr_identity.py +27 -15
- package/skills/pr-review/scripts/resolve_pr.py +77 -41
- package/skills/realign/SKILL.md +516 -0
- package/skills/realign/references/aislop-integration.md +215 -0
- package/skills/realign/references/architecture-and-structure.md +271 -0
- package/skills/realign/references/control-flow-and-errors.md +344 -0
- package/skills/realign/references/tests-dependencies-and-security.md +261 -0
- package/skills/realign/scripts/resolve_assessment.py +1525 -0
- package/skills/repo-review/SKILL.md +202 -131
- package/skills/simplify/SKILL.md +174 -0
- package/skills/systematic-debugging/SKILL.md +212 -192
- package/skills/systematic-debugging/scripts/repository_evidence.py +26 -8
- package/skills/test-driven-development/SKILL.md +126 -96
- package/skills/tidy/SKILL.md +104 -59
- 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())
|