Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the canonical goal extension through its isolated project-local installation.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
unset MOSAIC_LAUNCH_INCARNATION
|
||||
scripts/sync-dev-extensions.sh
|
||||
exec pi --no-extensions --extension "$PWD/.pi/extensions/goal/index.ts" \
|
||||
--session-dir "$PWD/.pi/state/sessions" "$@"
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install canonical extension source into the project-local native Pi directory.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
MODE="sync"
|
||||
case "${1:-}" in
|
||||
"") ;;
|
||||
--check) MODE="check" ;;
|
||||
*) echo "usage: scripts/sync-dev-extensions.sh [--check]" >&2; exit 4 ;;
|
||||
esac
|
||||
|
||||
SOURCE_ROOT="$PWD/extensions"
|
||||
PI_ROOT="$PWD/.pi"
|
||||
DEST="$PI_ROOT/extensions"
|
||||
RECORD="$PI_ROOT/extensions.installed.sha256"
|
||||
LOCK="$PI_ROOT/.extensions-sync.lock"
|
||||
TRANSACTION="$PI_ROOT/.extensions-transaction"
|
||||
BACKUP="$PI_ROOT/.extensions-backup"
|
||||
MANAGED=(goal mosaic-core)
|
||||
|
||||
mkdir -p "$PI_ROOT"
|
||||
command -v flock >/dev/null || { echo "sync-dev-extensions: flock is required" >&2; exit 2; }
|
||||
exec 9>"$LOCK"
|
||||
if ! flock -n 9; then
|
||||
echo "sync-dev-extensions: another sync is active" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
manifest_tree() {
|
||||
local root="$1" output="$2"
|
||||
if find "$root" -type l -print -quit | grep -q .; then
|
||||
echo "sync-dev-extensions: symlink found in $root" >&2
|
||||
return 2
|
||||
fi
|
||||
(
|
||||
cd "$root"
|
||||
while IFS= read -r -d '' path; do
|
||||
path="${path#./}"
|
||||
if [ -d "$path" ]; then
|
||||
printf 'd %q\n' "$path"
|
||||
elif [ -f "$path" ]; then
|
||||
printf 'f %s %q\n' "$(sha256sum "$path" | cut -d ' ' -f 1)" "$path"
|
||||
else
|
||||
echo "sync-dev-extensions: unsupported entry $path" >&2
|
||||
exit 2
|
||||
fi
|
||||
done < <(find . -mindepth 1 -print0 | LC_ALL=C sort -z)
|
||||
) > "$output"
|
||||
}
|
||||
|
||||
# Recover a process killed after transaction intent was recorded. A matching
|
||||
# destination and installed manifest commits; every other state rolls back.
|
||||
recover_transaction() {
|
||||
[ -f "$TRANSACTION" ] || return 0
|
||||
local current="$PI_ROOT/.recovery-manifest.$$"
|
||||
local destination_matches=false
|
||||
if [ -d "$DEST" ] && [ -f "$RECORD" ] && manifest_tree "$DEST" "$current" && cmp -s "$RECORD" "$current"; then
|
||||
destination_matches=true
|
||||
fi
|
||||
rm -f "$current"
|
||||
if [ -d "$BACKUP" ]; then
|
||||
if [ "$destination_matches" = "true" ]; then
|
||||
rm -rf "$BACKUP"
|
||||
else
|
||||
rm -rf "$DEST"
|
||||
mv "$BACKUP" "$DEST"
|
||||
fi
|
||||
elif [ "$destination_matches" = "false" ]; then
|
||||
rm -rf "$DEST"
|
||||
fi
|
||||
rm -f "$TRANSACTION"
|
||||
}
|
||||
|
||||
recover_transaction
|
||||
find "$PI_ROOT" -mindepth 1 -maxdepth 1 -type d -name '.extensions-sync.*' -exec rm -rf {} +
|
||||
WORK="$(mktemp -d "$PI_ROOT/.extensions-sync.XXXXXX")"
|
||||
COMMITTED=false
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
if [ "$COMMITTED" = "false" ] && [ -f "$TRANSACTION" ]; then recover_transaction || true; fi
|
||||
rm -rf "$WORK"
|
||||
exit "$rc"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for name in "${MANAGED[@]}"; do
|
||||
[ -d "$SOURCE_ROOT/$name" ] || { echo "sync-dev-extensions: missing source $SOURCE_ROOT/$name" >&2; exit 2; }
|
||||
done
|
||||
if find "$SOURCE_ROOT" -type l -print -quit | grep -q .; then
|
||||
echo "sync-dev-extensions: source symlinks are forbidden" >&2
|
||||
exit 2
|
||||
fi
|
||||
mapfile -t ENTRYPOINTS < <(find "$SOURCE_ROOT" -name index.ts -type f -printf '%P\n' | LC_ALL=C sort)
|
||||
if [ "${#ENTRYPOINTS[@]}" -ne 1 ] || [ "${ENTRYPOINTS[0]}" != "goal/index.ts" ]; then
|
||||
echo "sync-dev-extensions: expected exactly one entrypoint, goal/index.ts" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
EXPECTED="$WORK/expected"
|
||||
mkdir "$EXPECTED"
|
||||
for name in "${MANAGED[@]}"; do cp -a "$SOURCE_ROOT/$name" "$EXPECTED/$name"; done
|
||||
EXPECTED_MANIFEST="$WORK/expected.sha256"
|
||||
manifest_tree "$EXPECTED" "$EXPECTED_MANIFEST"
|
||||
|
||||
if [ "$MODE" = "check" ]; then
|
||||
[ -d "$DEST" ] || { echo "sync-dev-extensions: development installation is missing" >&2; exit 1; }
|
||||
CURRENT="$WORK/current.sha256"
|
||||
manifest_tree "$DEST" "$CURRENT" || exit 1
|
||||
if ! cmp -s "$EXPECTED_MANIFEST" "$CURRENT"; then
|
||||
echo "sync-dev-extensions: development installation differs from canonical source" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "sync-dev-extensions: canonical source matches .pi installation"
|
||||
COMMITTED=true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -d "$DEST" ]; then
|
||||
CURRENT="$WORK/current.sha256"
|
||||
manifest_tree "$DEST" "$CURRENT" || exit 1
|
||||
if [ -f "$RECORD" ]; then
|
||||
cmp -s "$RECORD" "$CURRENT" || { echo "sync-dev-extensions: refusing to overwrite local changes under .pi/extensions" >&2; exit 1; }
|
||||
else
|
||||
cmp -s "$EXPECTED_MANIFEST" "$CURRENT" || { echo "sync-dev-extensions: unrecorded .pi/extensions differs from canonical source" >&2; exit 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
[ ! -e "$BACKUP" ] || { echo "sync-dev-extensions: unreconciled backup remains" >&2; exit 2; }
|
||||
printf 'prepared\n' > "$TRANSACTION"
|
||||
if [ -e "$DEST" ]; then mv "$DEST" "$BACKUP"; fi
|
||||
mv "$EXPECTED" "$DEST"
|
||||
if [ "${MOSAIC_SYNC_TEST_FAULT:-}" = "after-destination" ]; then
|
||||
kill -KILL "$$"
|
||||
fi
|
||||
RECORD_TMP="$WORK/installed.sha256"
|
||||
cp "$EXPECTED_MANIFEST" "$RECORD_TMP"
|
||||
mv "$RECORD_TMP" "$RECORD"
|
||||
COMMITTED=true
|
||||
rm -rf "$BACKUP"
|
||||
rm -f "$TRANSACTION"
|
||||
echo "sync-dev-extensions: installed canonical goal extension into .pi/extensions"
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify canonical extension source and fail-closed native development packaging.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SANDBOX="$(mktemp -d)"
|
||||
trap 'rm -rf "$SANDBOX"' EXIT
|
||||
mkdir -p "$SANDBOX/repo/scripts" "$SANDBOX/repo/extensions"
|
||||
cp scripts/sync-dev-extensions.sh "$SANDBOX/repo/scripts/"
|
||||
cp -a extensions/goal extensions/mosaic-core "$SANDBOX/repo/extensions/"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
check_rc() {
|
||||
local name="$1" expected="$2"
|
||||
shift 2
|
||||
"$@" >/dev/null 2>&1
|
||||
local rc=$?
|
||||
if [ "$rc" -eq "$expected" ]; then PASS=$((PASS+1)); printf 'OK %s\n' "$name"
|
||||
else FAIL=$((FAIL+1)); printf 'FAIL %s (exit %s, expected %s)\n' "$name" "$rc" "$expected"
|
||||
fi
|
||||
}
|
||||
|
||||
SYNC="$SANDBOX/repo/scripts/sync-dev-extensions.sh"
|
||||
check_rc "initial ordinary-file install" 0 bash "$SYNC"
|
||||
check_rc "installed tree matches canonical source" 0 bash "$SYNC" --check
|
||||
if [ -z "$(find "$SANDBOX/repo/.pi/extensions" -type l -print -quit)" ]; then
|
||||
PASS=$((PASS+1)); echo "OK installed tree has no symlinks"
|
||||
else FAIL=$((FAIL+1)); echo "FAIL installed tree contains symlinks"
|
||||
fi
|
||||
printf '\nlocal edit\n' >> "$SANDBOX/repo/.pi/extensions/goal/README.md"
|
||||
check_rc "check detects installation drift" 1 bash "$SYNC" --check
|
||||
check_rc "sync refuses to overwrite installation drift" 1 bash "$SYNC"
|
||||
cp "$SANDBOX/repo/extensions/goal/README.md" "$SANDBOX/repo/.pi/extensions/goal/README.md"
|
||||
printf '\nextra\n' > "$SANDBOX/repo/.pi/extensions/extra-file"
|
||||
check_rc "check detects an extra destination file" 1 bash "$SYNC" --check
|
||||
rm "$SANDBOX/repo/.pi/extensions/extra-file"
|
||||
mkdir "$SANDBOX/repo/.pi/extensions/extra-directory"
|
||||
check_rc "check detects an extra destination directory" 1 bash "$SYNC" --check
|
||||
rmdir "$SANDBOX/repo/.pi/extensions/extra-directory"
|
||||
ln -s goal "$SANDBOX/repo/.pi/extensions/extra-link"
|
||||
check_rc "check rejects a destination symlink" 1 bash "$SYNC" --check
|
||||
rm "$SANDBOX/repo/.pi/extensions/extra-link"
|
||||
printf '\ncanonical edit\n' >> "$SANDBOX/repo/extensions/goal/README.md"
|
||||
check_rc "sync accepts a canonical source update" 0 bash "$SYNC"
|
||||
check_rc "updated installation matches canonical source" 0 bash "$SYNC" --check
|
||||
printf '\ninterrupted edit\n' >> "$SANDBOX/repo/extensions/goal/README.md"
|
||||
check_rc "forced interruption kills the replacing process" 137 env MOSAIC_SYNC_TEST_FAULT=after-destination bash "$SYNC"
|
||||
check_rc "next invocation recovers old consistent installation" 1 bash "$SYNC" --check
|
||||
if ! grep -q 'interrupted edit' "$SANDBOX/repo/.pi/extensions/goal/README.md"; then
|
||||
PASS=$((PASS+1)); echo "OK interrupted replacement rolled back"
|
||||
else FAIL=$((FAIL+1)); echo "FAIL interrupted replacement remained active"
|
||||
fi
|
||||
check_rc "sync succeeds after interruption recovery" 0 bash "$SYNC"
|
||||
touch "$SANDBOX/repo/.pi/.extensions-sync.lock"
|
||||
check_rc "unlocked stale lock file does not block" 0 bash "$SYNC" --check
|
||||
exec 8>"$SANDBOX/repo/.pi/.extensions-sync.lock"
|
||||
flock -n 8
|
||||
check_rc "active lock refuses a concurrent sync" 3 bash "$SYNC" --check
|
||||
flock -u 8
|
||||
ln -s README.md "$SANDBOX/repo/extensions/goal/forbidden-link"
|
||||
check_rc "source symlink fails closed" 2 bash "$SYNC"
|
||||
rm "$SANDBOX/repo/extensions/goal/forbidden-link"
|
||||
mkdir "$SANDBOX/repo/extensions/goal/nested"
|
||||
cp "$SANDBOX/repo/extensions/goal/index.ts" "$SANDBOX/repo/extensions/goal/nested/index.ts"
|
||||
check_rc "nested second entrypoint fails closed" 2 bash "$SYNC"
|
||||
|
||||
printf '\nextension package selftest: %s passed, %s failed\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""No-model native Pi smoke: project discovery, footer, /goal and Alt+G."""
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pty
|
||||
import re
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import termios
|
||||
import time
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ANSI = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))")
|
||||
TEXT = "native recall " + "full acceptance criteria " * 16 + "END-OF-GOAL"
|
||||
|
||||
# A fresh clone has no ignored development installation. Exercise the same
|
||||
# fail-closed sync used by the launcher before testing Pi discovery.
|
||||
subprocess.run([str(ROOT / "scripts/sync-dev-extensions.sh")], cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def receive(fd, pattern, timeout=12):
|
||||
data = b""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([fd], [], [], max(0, deadline - time.monotonic()))
|
||||
if not ready:
|
||||
break
|
||||
try:
|
||||
data += os.read(fd, 65536)
|
||||
except OSError:
|
||||
break
|
||||
clean = ANSI.sub("", data.decode(errors="replace"))
|
||||
if pattern in clean:
|
||||
return data
|
||||
raise AssertionError(f"missing {pattern!r}: {data[-4000:]!r}")
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="ng-native-pi-") as home:
|
||||
env = {**os.environ, "PI_CODING_AGENT_DIR": home, "PI_OFFLINE": "1", "TERM": "xterm-256color"}
|
||||
env.pop("MOSAIC_LAUNCH_INCARNATION", None)
|
||||
rpc = subprocess.Popen(["pi", "--approve", "--offline", "--mode", "rpc", "--no-session", "--no-tools"], cwd=ROOT, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
try:
|
||||
rpc.stdin.write('{"id":"commands","type":"get_commands"}\n')
|
||||
rpc.stdin.flush()
|
||||
deadline = time.monotonic() + 15
|
||||
while True:
|
||||
assert time.monotonic() < deadline, "RPC discovery timed out"
|
||||
response = json.loads(rpc.stdout.readline())
|
||||
if response.get("id") == "commands":
|
||||
break
|
||||
commands = [c for c in response["data"]["commands"] if c["name"] == "goal"]
|
||||
assert len(commands) == 1, commands
|
||||
assert str(ROOT / ".pi/extensions/goal/index.ts") in json.dumps(commands[0]), commands
|
||||
print("PASS native project discovery: exactly one local /goal", flush=True)
|
||||
finally:
|
||||
rpc.terminate()
|
||||
rpc.communicate(timeout=10)
|
||||
|
||||
for status, label, width, no_color in [("paused", "Paused", 120, False), ("blocked", "Blocked", 45, False), ("none", "Complete", 120, True), ("active", "Waiting", 45, False)]:
|
||||
incarnation = "ng-native-test-" + uuid.uuid4().hex
|
||||
statefile = ROOT / ".pi/state/goal" / f"goal-state.{incarnation}.json"
|
||||
statefile.parent.mkdir(parents=True, exist_ok=True)
|
||||
state = dict(version=1, text=TEXT, status=status, checks=0, maxChecks=25, noProgressReports=0, maxNoProgressReports=3, workEventSinceReport=False, setAt="2026-09-06T00:00:00.000Z")
|
||||
if status == "none":
|
||||
state.update(text="", lastOutcome=dict(text=TEXT, status="complete", evidence="native fixture", at="2026-09-06T00:00:00.000Z"))
|
||||
if status == "active":
|
||||
state.update(waitTimeoutSeconds=3600, waitWakeUsed=False, activeWait=dict(owner="native fixture", nextCheck="manual acceptance", deadlineAt=int(time.time() * 1000) + 3600000, wakeSent=False))
|
||||
statefile.write_text(json.dumps(state))
|
||||
tty_env = {**env, "MOSAIC_LAUNCH_INCARNATION": incarnation}
|
||||
tty_env.pop("NO_COLOR", None)
|
||||
if no_color:
|
||||
tty_env["NO_COLOR"] = "1"
|
||||
master, slave = pty.openpty()
|
||||
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 40, width, 0, 0))
|
||||
child = subprocess.Popen(["pi", "--approve", "--offline", "--no-session", "--no-tools", "--no-skills", "--no-context-files"], cwd=ROOT, env=tty_env, stdin=slave, stdout=slave, stderr=slave, start_new_session=True)
|
||||
os.close(slave)
|
||||
try:
|
||||
startup = receive(master, f"Goal: {label}")
|
||||
Path("/tmp/ng-goal-native-startup.log").write_bytes(startup)
|
||||
os.write(master, b"\x1b[1;1R\x1b[?1;2c\x1b[?0u")
|
||||
# Let terminal capability negotiation and editor initialization settle.
|
||||
while select.select([master], [], [], 0.5)[0]:
|
||||
os.read(master, 65536)
|
||||
os.write(master, b"\x1b[103;3u")
|
||||
receive(master, "END-OF-GOAL")
|
||||
os.write(master, b"\x1b[200~/goal --max 0\x1b[201~")
|
||||
receive(master, "/goal --max 0")
|
||||
os.write(master, b"\x1b[13u")
|
||||
receive(master, 'integer, got "0"')
|
||||
os.write(master, b"\x1b[200~/goal\x1b[201~")
|
||||
entered = receive(master, "/goal")
|
||||
Path("/tmp/ng-goal-native-entered.log").write_bytes(entered)
|
||||
os.write(master, b"\x1b[13u")
|
||||
receive(master, "END-OF-GOAL")
|
||||
print(f"PASS native {label}, width={width}, NO_COLOR={no_color}: footer + /goal + Alt+G", flush=True)
|
||||
finally:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
child.wait(timeout=10)
|
||||
os.close(master)
|
||||
statefile.unlink(missing_ok=True)
|
||||
Reference in New Issue
Block a user