Simplify Athena operator maintenance workflow

This commit is contained in:
Mikei386
2026-08-25 08:42:30 +02:00
parent fa9911eb3b
commit ac725416b8
11 changed files with 126 additions and 33 deletions
+10 -1
View File
@@ -136,14 +136,23 @@ class OperatorTests(unittest.TestCase):
return {"argv": argv, "exit_code": 0, "output": " compose.yaml | 2 +-"} return {"argv": argv, "exit_code": 0, "output": " compose.yaml | 2 +-"}
return {"argv": argv, "exit_code": 0, "output": "ok"} return {"argv": argv, "exit_code": 0, "output": "ok"}
self.module.run = fake_run self.module.run = fake_run
payload, preview = self.module.normalise_operation("git_publish", {"message": "Update Athena platform"}) payload, preview = self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml"]})
self.assertEqual(payload["reviewed_status"], "M compose.yaml") self.assertEqual(payload["reviewed_status"], "M compose.yaml")
self.assertEqual(payload["paths"], ["compose.yaml"])
self.assertIn("compose.yaml | 2 +-", preview) self.assertIn("compose.yaml | 2 +-", preview)
def test_git_publish_rejects_empty_repository(self): def test_git_publish_rejects_empty_repository(self):
self.module.run = lambda argv, **kwargs: {"argv": argv, "exit_code": 0, "output": ""} self.module.run = lambda argv, **kwargs: {"argv": argv, "exit_code": 0, "output": ""}
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml"]})
def test_git_publish_requires_explicit_safe_paths(self):
with self.assertRaises(ValueError):
self.module.normalise_operation("git_publish", {"message": "Update Athena platform"}) self.module.normalise_operation("git_publish", {"message": "Update Athena platform"})
with self.assertRaises(ValueError):
self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml", "compose.yaml"]})
with self.assertRaises((ValueError, PermissionError)):
self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": [".git/config"]})
def test_path_traversal_and_protected_paths_are_rejected(self): def test_path_traversal_and_protected_paths_are_rejected(self):
for path in ("../etc/passwd", ".git/config", "/etc/passwd", "secrets/key"): for path in ("../etc/passwd", ".git/config", "/etc/passwd", "secrets/key"):
+16
View File
@@ -89,6 +89,22 @@ Der Router übernimmt:
## Hermes Agent ## Hermes Agent
- Kontextkompression läuft frühzeitig bei 65 Prozent des jeweiligen
Profilfensters. Alte große Werkzeugausgaben werden ab 50.000 Token zunächst
ohne Modellaufruf bereinigt; `tail_mode: lean` hält nach der Kompression einen
kleinen, zusammenhängenden jüngsten Abschnitt. Dadurch sollen keine
mehrfachen minutenlangen Zusammenfassungen eines bereits weit überfüllten
Threads mehr nötig werden.
- Die Kompressionszusammenfassung nutzt weiterhin dasselbe aktive Modell. Ein
kleineres Fast-Modell wäre zwar schneller, besitzt aber nicht genug Kontext,
um die vollständige Mitte einer Medium-, Large- oder Ultra-Sitzung sicher zu
verarbeiten. `reasoning_effort: none` vermeidet unnötiges Nachdenken beim
reinen Zusammenfassen.
- `Summarizing thread` ist eine echte zusätzliche Modellanfrage. Bei sehr alten
Sitzungen kann die Desktop-Anzeige nach abgeschlossener Kompression außerdem
veraltet stehen bleiben. Maßgeblich sind dann Sitzungsfortschritt und
Backend-Log, nicht das Label allein.
- Container: `mike-ai-hermes` - Container: `mike-ai-hermes`
- Standardsprache: Deutsch (`display.language`, deutsches `SOUL.md`) - Standardsprache: Deutsch (`display.language`, deutsches `SOUL.md`)
- Spracheingabe: lokales Faster-Whisper, Modell `base`, Sprachhinweis `de` - Spracheingabe: lokales Faster-Whisper, Modell `base`, Sprachhinweis `de`
+25 -5
View File
@@ -79,16 +79,36 @@ Code, Compose, Profile, Installer, Netzwerke, Services, Git und Secrets können
## Git und Recovery ## Git und Recovery
Die kanonische Quelle ist der private Gitea-Stand Die kanonische Quelle ist der private Gitea-Stand
`git@192.168.1.2:michael/AI-Profile-Router.git`, Branch `main`. Das `ssh://git@192.168.1.2:33/michael/AI-Profile-Router.git`, Branch `main`, im
Working Tree `/data/mike-ai-operator/repository`. Das
Installationsverzeichnis `/opt/mike-ai/stack` ist eine ausgerollte Kopie und Installationsverzeichnis `/opt/mike-ai/stack` ist eine ausgerollte Kopie und
kein Git-Working-Tree; `.mike-ai-source-commit` benennt den ausgerollten kein Git-Working-Tree; `.mike-ai-source-commit` benennt den ausgerollten
Commit. Der offizielle GitHub-MCP ist read-only und kann dieses private Commit. Der offizielle GitHub-MCP ist read-only und kann dieses private
Gitea-Repository weder ändern noch pushen. Dafür ist ein getrennt Gitea-Repository weder ändern noch pushen. Dafür besitzt der Athena Operator
autorisiertes Git-Werkzeug beziehungsweise ein administrativer Git-Workflow den autorisierten, strukturierten Arbeitsweg. Ein Modell darf weder im eigenen
notwendig. Sandbox-Container einen weiteren Clone anlegen noch einen SSH-Schlüssel
anfordern oder kopieren.
Der verbindliche Ablauf für dauerhafte Änderungen lautet:
1. `athena_operator_prepare` und nach separater Benutzerfreigabe
`athena_operator_execute` mit `file_update`; dies schreibt dieselben
ausgewählten Dateien driftgeschützt in den kanonischen Working Tree und die
ausgerollte Kopie.
2. Prüfungen über die Operation `run_checks` ausführen.
3. Nur betroffene Dienste über `compose_deploy` ausrollen.
4. Ausschließlich die ausdrücklich angegebenen geänderten Pfade mit
`git_publish` committen und pushen. Fremde Dirty-Worktree-Dateien bleiben
unberührt.
5. Mit `recovery` einen neuen Recovery-Koffer erzeugen und prüfen.
Das allgemeine Terminal ist weder Ersatz für diesen Ablauf noch ein Weg zu
Git-Schlüsseln. `/data/mike-ai-operator/repository` muss aus der
Modellsandbox nicht direkt erreichbar sein; der rootseitige Executor besitzt
den notwendigen Zugriff.
Eine angewandte Dokumentationspflege ist erst vollständig abgeschlossen, wenn Eine angewandte Dokumentationspflege ist erst vollständig abgeschlossen, wenn
getrennte, dafür autorisierte Werkzeuge Folgendes bestätigt haben: die dafür vorgesehenen Athena-Operator-Operationen Folgendes bestätigt haben:
1. dieselbe Änderung ist im privaten Quellrepository geprüft, committed und 1. dieselbe Änderung ist im privaten Quellrepository geprüft, committed und
gepusht; gepusht;
+2 -1
View File
@@ -158,7 +158,8 @@ geändert. Die Abweichung wird benannt und zuerst geklärt.
## Wichtige Pfade ## Wichtige Pfade
```text ```text
/opt/mike-ai/stack installierte versionierte Plattformquelle /opt/mike-ai/stack ausgerollte Plattformkopie; kein Git-Working-Tree
/data/mike-ai-operator/repository kanonischer Git-Working-Tree; nur über Athena Operator ändern
/data/models produktive Modelle; für Inferenz read-only eingehängt /data/models produktive Modelle; für Inferenz read-only eingehängt
/etc/mike-ai root-only Secrets und Standortkonfiguration /etc/mike-ai root-only Secrets und Standortkonfiguration
/data persistente Daten- und Recovery-SSD /data persistente Daten- und Recovery-SSD
+11 -3
View File
@@ -97,10 +97,18 @@ Inferenzcontainer eingehängt. Dauerhafte
nur in einen laufenden Container. Die Hauptbestandteile sind: nur in einen laufenden Container. Die Hauptbestandteile sind:
Die kanonische Git-Quelle ist der private Gitea-Branch `main` unter Die kanonische Git-Quelle ist der private Gitea-Branch `main` unter
`git@192.168.1.2:michael/AI-Profile-Router.git`. `/opt/mike-ai/stack` ist kein `ssh://git@192.168.1.2:33/michael/AI-Profile-Router.git`; der vom Athena
Working Tree; `.mike-ai-source-commit` bezeichnet den ausgerollten Stand. Der Operator verwaltete Working Tree liegt unter
`/data/mike-ai-operator/repository`. `/opt/mike-ai/stack` ist kein Working
Tree; `.mike-ai-source-commit` bezeichnet den ausgerollten Stand. Der
offizielle GitHub-MCP ist strikt read-only und kann Gitea nicht pflegen. Für offizielle GitHub-MCP ist strikt read-only und kann Gitea nicht pflegen. Für
Commit und Push ist daher ein getrennt autorisierter Git-Arbeitsweg nötig. dauerhafte Änderungen ist ausschließlich der strukturierte Athena-Operator-
Arbeitsweg vorgesehen: `file_update` ändert driftgeschützt den kanonischen
Working Tree und die ausgerollte Kopie, `run_checks` prüft, `compose_deploy`
rollt nur benannte Dienste aus, `git_publish` veröffentlicht nur ausdrücklich
ausgewählte Pfade und `recovery` erneuert den Recovery-Koffer. Lege niemals
einen zweiten Clone in der Sandbox an und fordere oder kopiere keinen
SSH-Schlüssel; der Operator besitzt bereits den autorisierten Hostzugriff.
- Open WebUI als Benutzeroberfläche und Speicher für Arbeitsbereichsmodelle, - Open WebUI als Benutzeroberfläche und Speicher für Arbeitsbereichsmodelle,
Filter, Aktionen und Chats Filter, Aktionen und Chats
+22 -1
View File
@@ -39,7 +39,7 @@ web:
keyless_rescue: true keyless_rescue: true
agent: agent:
max_turns: 100 max_turns: 500
gateway_timeout: 3600 gateway_timeout: 3600
session_stall_timeout: 600 session_stall_timeout: 600
tool_loop_guardrails: tool_loop_guardrails:
@@ -57,6 +57,27 @@ agent:
max_web_searches: 20 max_web_searches: 20
max_subagents: 8 max_subagents: 8
# Compact before a tool-heavy session can grow beyond the selected model's
# usable window. Large old tool results are pruned without an LLM call first;
# lean tail retention avoids several expensive back-to-back summary passes.
compression:
enabled: true
progress_notices: true
threshold: 0.65
target_ratio: 0.15
tail_mode: "lean"
protect_last_n: 20
proactive_prune_tokens: 50000
proactive_prune_min_result_chars: 4000
proactive_prune_min_reclaim_tokens: 4096
context_total_ceiling_seconds: 600
auxiliary:
compression:
provider: "main"
model: ""
reasoning_effort: "none"
# German is the platform default. The display setting localizes the static # German is the platform default. The display setting localizes the static
# messages Hermes currently supports; agent replies are governed by SOUL.md. # messages Hermes currently supports; agent replies are governed by SOUL.md.
display: display:
@@ -65,6 +65,9 @@ repository and use live measurements only as evidence of current state.
5. **Change the source of truth.** Modify repository sources, not only a live 5. **Change the source of truth.** Modify repository sources, not only a live
container. Prefer `athena_operator_prepare`; show its full preview and stop container. Prefer `athena_operator_prepare`; show its full preview and stop
for the exact user confirmation before `athena_operator_execute`. for the exact user confirmation before `athena_operator_execute`.
Use `file_update` for the exact source paths. Never clone the repository in
the Hermes sandbox and never request or copy an SSH key; the Operator owns
the canonical worktree and its deploy credentials.
Completion: the approved ticket matches the intended content. Completion: the approved ticket matches the intended content.
6. **Deploy narrowly.** Change only named services. Never restart the entire 6. **Deploy narrowly.** Change only named services. Never restart the entire
stack merely to activate one component. Completion: unrelated containers stack merely to activate one component. Completion: unrelated containers
@@ -72,8 +75,9 @@ repository and use live measurements only as evidence of current state.
7. **Verify behavior.** Run syntax/config checks, focused tests, service health, 7. **Verify behavior.** Run syntax/config checks, focused tests, service health,
and one bounded functional test. A running container alone is not proof. and one bounded functional test. A running container alone is not proof.
Completion: expected behavior and rollback path are both verified. Completion: expected behavior and rollback path are both verified.
8. **Close the maintenance loop.** Update relevant docs, publish the Git 8. **Close the maintenance loop.** Update relevant docs, publish only the
change, create a newer recovery bundle, then check maintenance status. explicitly selected changed paths with `git_publish`, create a newer
recovery bundle, then check maintenance status.
Completion: source commit, deployed state, docs, and recovery agree. Completion: source commit, deployed state, docs, and recovery agree.
## Persistent Versus Temporary Work ## Persistent Versus Temporary Work
@@ -102,7 +106,8 @@ repository and use live measurements only as evidence of current state.
- A profile switch can terminate active generation and invalidate prompt cache. - A profile switch can terminate active generation and invalidate prompt cache.
- A healthy container can still expose the wrong model, route, or tool set. - A healthy container can still expose the wrong model, route, or tool set.
- `/opt/mike-ai/stack` is deployed source, not automatically the canonical Git - `/opt/mike-ai/stack` is deployed source, not automatically the canonical Git
worktree. Complete durable changes through the documented Git workflow. worktree. Complete durable changes through Operator operations `file_update`,
`run_checks`, `compose_deploy`, `git_publish`, and `recovery`.
- New skills are loaded at the next Hermes session; absence in the current - New skills are loaded at the next Hermes session; absence in the current
session is expected. session is expected.
+7 -3
View File
@@ -63,7 +63,9 @@ TOOLS = [
"and SSH to configured remote systems. Prefer a single focused command and cap noisy output " "and SSH to configured remote systems. Prefer a single focused command and cap noisy output "
"with the command itself. The server blocks power control and changes to Athena's SSH, LAN, " "with the command itself. The server blocks power control and changes to Athena's SSH, LAN, "
"WireGuard, firewall, boot, kernel, mounts and partitions so remote reachability cannot be " "WireGuard, firewall, boot, kernel, mounts and partitions so remote reachability cannot be "
"accidentally destroyed. Other commands execute immediately and must be verified afterwards." "accidentally destroyed. Other commands execute immediately and must be verified afterwards. "
"Do not clone the canonical repository, request/copy an SSH key, or use Terminal as a substitute "
"for the durable file_update, git_publish and recovery workflow."
), ),
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
@@ -84,12 +86,14 @@ TOOLS = [
"content-bound ticket, exact preview and confirmation phrase. Supported operations: " "content-bound ticket, exact preview and confirmation phrase. Supported operations: "
"file_update (write repository and deployed stack files), run_checks, compose_deploy, " "file_update (write repository and deployed stack files), run_checks, compose_deploy, "
"container_action, openwebui_sync, git_publish, model_download, benchmark, recovery. Show the complete " "container_action, openwebui_sync, git_publish, model_download, benchmark, recovery. Show the complete "
"preview to the user and stop. Never execute in the same autonomous tool sequence. " "preview to the user and stop. Never execute in the same autonomous tool sequence. This is the "
"supported durable source and Git path: never clone the repository inside a sandbox and never "
"request or copy an SSH key. "
"file_update payload: {files:[{path,content,expected_sha256?}]}; run_checks: " "file_update payload: {files:[{path,content,expected_sha256?}]}; run_checks: "
"{checks:[operator-tests,openwebui-filter-tests,platform-verify,compose-main,compose-mcp]}; " "{checks:[operator-tests,openwebui-filter-tests,platform-verify,compose-main,compose-mcp]}; "
"compose_deploy: {compose_file,services,build}; container_action: {action,containers}; " "compose_deploy: {compose_file,services,build}; container_action: {action,containers}; "
"openwebui_sync: {}; " "openwebui_sync: {}; "
"git_publish: {message}; model_download: {url,destination,sha256?}; benchmark: " "git_publish: {message,paths:[exact changed repository paths]}; model_download: {url,destination,sha256?}; benchmark: "
"{script,arguments}; recovery: {label}." "{script,arguments}; recovery: {label}."
), ),
"inputSchema": { "inputSchema": {
+1 -1
View File
@@ -191,7 +191,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.athena-operator dockerfile: Dockerfile.athena-operator
image: mike-ai/mcp-athena-operator:2.0.0 image: mike-ai/mcp-athena-operator:2.1.0
container_name: mike-ai-mcp-athena-operator container_name: mike-ai-mcp-athena-operator
environment: environment:
ATHENA_OPERATOR_SOCKET: /operator/operator.sock ATHENA_OPERATOR_SOCKET: /operator/operator.sock
+7 -5
View File
@@ -428,7 +428,7 @@ def read_source(arguments: dict[str, Any]) -> dict[str, Any]:
WORKFLOWS = { WORKFLOWS = {
"mcp": ["platform/mcp/compose.yaml", "platform/mcp/README.md", "compose.yaml", "docs/COMPONENTS.md", "docs/SECURITY.md", "docs/QWEN_OPERATOR_CONTEXT.md"], "mcp": ["platform/mcp/compose.yaml", "platform/mcp/README.md", "platform/mcp/athena_operator_mcp.py", "platform/hermes/skills/athena-operator/SKILL.md", "compose.yaml", "docs/COMPONENTS.md", "docs/SECURITY.md", "docs/PLATFORM_CONTEXT_MCP.md", "docs/QWEN_OPERATOR_CONTEXT.md"],
"model": ["config/install.env.example", "platform/models/manifest.example.yaml", "platform/profiles/", "docs/STANDARD_PROFILE_MATRIX.md", "docs/QWEN_OPERATOR_CONTEXT.md"], "model": ["config/install.env.example", "platform/models/manifest.example.yaml", "platform/profiles/", "docs/STANDARD_PROFILE_MATRIX.md", "docs/QWEN_OPERATOR_CONTEXT.md"],
"profile": ["platform/profiles/", "router/router_profiles.json", "platform/openwebui/install-models.sh", "docs/STANDARD_PROFILE_MATRIX.md"], "profile": ["platform/profiles/", "router/router_profiles.json", "platform/openwebui/install-models.sh", "docs/STANDARD_PROFILE_MATRIX.md"],
"tts": ["compose.yaml", "router/xtts_worker.py", "platform/scripts/rollback-tts-production.sh", "docs/XTTS_EVALUATION_2026-08-23.md"], "tts": ["compose.yaml", "router/xtts_worker.py", "platform/scripts/rollback-tts-production.sh", "docs/XTTS_EVALUATION_2026-08-23.md"],
@@ -453,15 +453,17 @@ def change_workflow(arguments: dict[str, Any]) -> dict[str, Any]:
"Capture current state with the narrowest specialist tool.", "Capture current state with the narrowest specialist tool.",
"Read relevant versioned sources and identify documentation drift.", "Read relevant versioned sources and identify documentation drift.",
"Define rollback and protect SSH, LAN, WireGuard and the active inference path.", "Define rollback and protect SSH, LAN, WireGuard and the active inference path.",
"Change source-of-truth files, not only a running container.", "Prepare and apply source changes with Athena Operator operation file_update. Never clone the repository inside the sandbox and never request or copy an SSH key.",
"Validate syntax/configuration and run a bounded synthetic test.", "Validate syntax/configuration with Athena Operator operation run_checks and run a bounded synthetic test.",
"Deploy only named services with Athena Operator operation compose_deploy.",
"Verify service health and remote reachability without reading chats or private payloads.", "Verify service health and remote reachability without reading chats or private payloads.",
"Update PLATFORM_OVERVIEW/CURRENT_REFERENCE/QWEN_OPERATOR_CONTEXT and the affected runbook.", "Update PLATFORM_OVERVIEW/CURRENT_REFERENCE/QWEN_OPERATOR_CONTEXT and the affected runbook.",
"Commit and push the private Git repository using a separate authorized Git tool.", "Commit and push only the explicitly selected changed paths with Athena Operator operation git_publish.",
"Create and verify a new encrypted recovery bundle and self-contained data-disk kit.", "Create and verify a new encrypted recovery bundle and self-contained data-disk kit with Athena Operator operation recovery.",
], ],
"hard_boundaries": [ "hard_boundaries": [
"This context MCP does not modify services, Docker, networking, models or secrets.", "This context MCP does not modify services, Docker, networking, models or secrets.",
"The sandbox needs neither a Git clone nor an SSH key; Athena Operator owns the canonical repository and deploy credentials.",
"No shutdown, reboot, kernel/driver, SSH, firewall or VPN change without exact user approval and rollback.", "No shutdown, reboot, kernel/driver, SSH, firewall or VPN change without exact user approval and rollback.",
"Never claim Git or recovery is current until separately verified.", "Never claim Git or recovery is current until separately verified.",
], ],
+17 -10
View File
@@ -310,12 +310,18 @@ def normalise_operation(operation: str, payload: dict[str, Any]) -> tuple[dict[s
message = str(payload.get("message", "")) message = str(payload.get("message", ""))
if not SAFE_COMMIT.fullmatch(message): if not SAFE_COMMIT.fullmatch(message):
raise ValueError("invalid commit message") raise ValueError("invalid commit message")
status = run(["git", "status", "--short"], cwd=REPOSITORY, check=True)["output"].strip() paths = payload.get("paths")
if not isinstance(paths, list) or not 1 <= len(paths) <= MAX_FILES:
raise ValueError("paths must contain 1..24 repository files")
selected = [str(safe_relative(str(path))) for path in paths]
if len(set(selected)) != len(selected):
raise ValueError("paths must be unique")
status = run(["git", "status", "--short", "--", *selected], cwd=REPOSITORY, check=True)["output"].strip()
if not status: if not status:
raise RuntimeError("repository has no changes to publish") raise RuntimeError("selected repository paths have no changes to publish")
diff = run(["git", "diff", "--stat"], cwd=REPOSITORY, check=True)["output"] diff = run(["git", "diff", "--stat", "--", *selected], cwd=REPOSITORY, check=True)["output"]
preview = f"Commit message: {message}\n\nChanged and untracked files:\n{status}\n\nDiff summary:\n{diff}" preview = f"Commit message: {message}\nSelected paths: {', '.join(selected)}\n\nSelected changes:\n{status}\n\nDiff summary:\n{diff}"
return {"message": message, "reviewed_status": status}, preview return {"message": message, "paths": selected, "reviewed_status": status}, preview
if operation == "model_download": if operation == "model_download":
url = str(payload.get("url", "")) url = str(payload.get("url", ""))
parsed = urllib.parse.urlparse(url) parsed = urllib.parse.urlparse(url)
@@ -414,12 +420,13 @@ def execute_operation(ticket: str, operation: str, payload: dict[str, Any]) -> d
raise FileNotFoundError("versioned Open WebUI synchronisation script is missing") raise FileNotFoundError("versioned Open WebUI synchronisation script is missing")
return {"sync": run(["bash", str(installer)], cwd=STACK, timeout=1800, check=True)} return {"sync": run(["bash", str(installer)], cwd=STACK, timeout=1800, check=True)}
if operation == "git_publish": if operation == "git_publish":
current_status = run(["git", "status", "--short"], cwd=REPOSITORY, check=True)["output"].strip() selected = payload["paths"]
current_status = run(["git", "status", "--short", "--", *selected], cwd=REPOSITORY, check=True)["output"].strip()
if current_status != payload["reviewed_status"]: if current_status != payload["reviewed_status"]:
raise RuntimeError("repository changed after the Git publish preview") raise RuntimeError("selected repository paths changed after the Git publish preview")
run(["git", "add", "--all"], cwd=REPOSITORY, check=True) run(["git", "add", "--", *selected], cwd=REPOSITORY, check=True)
run(["git", "diff", "--cached", "--check"], cwd=REPOSITORY, check=True) run(["git", "diff", "--cached", "--check", "--", *selected], cwd=REPOSITORY, check=True)
commit = run(["git", "commit", "-m", payload["message"]], cwd=REPOSITORY, check=True) commit = run(["git", "commit", "-m", payload["message"], "--", *selected], cwd=REPOSITORY, check=True)
pushed = run(["git", "push", "origin", "HEAD:main"], cwd=REPOSITORY, timeout=300, check=True) pushed = run(["git", "push", "origin", "HEAD:main"], cwd=REPOSITORY, timeout=300, check=True)
head = run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip() head = run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip()
(STACK / ".mike-ai-source-commit").write_text(head + "\n") (STACK / ".mike-ai-source-commit").write_text(head + "\n")