Add manual OpenCode cycle control and reusable prompt template
This commit is contained in:
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Manually control the durable NestQuest development cycle from OpenCode.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "usage: nq-cycle start|stop|status" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
unit=nestquest-controller.service
|
||||
controller=/home/overseer/.local/share/nestquest-controller/scripts/nq-controller
|
||||
|
||||
show_status() {
|
||||
systemctl --user show "$unit" -p ActiveState -p SubState -p UnitFileState
|
||||
if [[ -f /home/overseer/.local/state/nestquest-controller/last-run.json ]]; then
|
||||
python3 -c 'import json,pathlib; p=pathlib.Path.home()/".local/state/nestquest-controller/last-run.json"; d=json.loads(p.read_text()); print("LastTickState="+str(d.get("state","unknown"))); print("LastTickTime="+str(d.get("time","unknown")))'
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
systemctl --user start "$unit"
|
||||
show_status
|
||||
if session_status=$("$controller" --check-interactive); then
|
||||
echo "$session_status"
|
||||
if [[ "$session_status" == "interactive NestQuest session open" ]]; then
|
||||
echo "Close the interactive NestQuest OpenCode session to let the controller run its first tick."
|
||||
fi
|
||||
else
|
||||
echo "The controller will wait for OpenCode session detection to recover."
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
systemctl --user stop "$unit"
|
||||
show_status
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
*)
|
||||
echo "usage: nq-cycle start|stop|status" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a project-specific controller prompt from the reusable template."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
TEMPLATE = Path(__file__).resolve().parent.parent / "templates/ORCHESTRATOR-PROMPT.template.md"
|
||||
PLACEHOLDER = re.compile(r"\{\{([A-Z][A-Z0-9_]*)\}\}")
|
||||
|
||||
|
||||
def render(template: str, profile: dict[str, str]) -> str:
|
||||
keys = set(PLACEHOLDER.findall(template))
|
||||
missing = sorted(keys - profile.keys())
|
||||
if missing:
|
||||
raise ValueError("missing profile values: " + ", ".join(missing))
|
||||
for key in keys:
|
||||
if not isinstance(profile[key], str) or not profile[key].strip():
|
||||
raise ValueError(f"{key} must be a nonempty string")
|
||||
result = PLACEHOLDER.sub(lambda match: profile[match.group(1)], template)
|
||||
if PLACEHOLDER.search(result):
|
||||
raise ValueError("profile values must not contain template placeholders")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("profile", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
profile = json.loads(args.profile.read_text())
|
||||
if not isinstance(profile, dict):
|
||||
parser.error("profile must be a JSON object")
|
||||
content = render(TEMPLATE.read_text(), profile)
|
||||
if args.output:
|
||||
args.output.write_text(content)
|
||||
print(args.output)
|
||||
else:
|
||||
sys.stdout.write(content)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user