Some checks failed
**navigation.md rewritten from the player's chair.** It was written from the inside out -- GamePart ids, pak names, sprite names -- which is how WE find things, not what the game shows anyone. Now it describes what is on screen, what you press and what happens, with internals as footnotes. Most rows are open on purpose: it exists to be filled in by playing, and the in-game tutorials are the resource for the flight half. **tools/share** gives transient files provenance without giving them history. Three kinds of thing were travelling down one channel with opposite needs: code and decoded knowledge want permanence, cited evidence wants permanence, and "look at this PNG" wants no history at all. The third kind bloats a repository forever; passing it by message is worse, because the receiver gets bytes with no idea which build produced them. `share put` records who, when, what, the sender's commit, and whether their tree was dirty -- because a capture taken from a modified tree cannot be reproduced from the sha, and the receiver deserves to know that before building an argument on it. **docs/agents/PROTOCOL.md** is the contract. The parts that matter: Dynamic RE stays with the Decoder -- most of what is open is behavioural and cannot be answered from the file. What the planned Referee adds is different: bias enters at what you CHOOSE to capture, so a corpus captured to a fixed protocol by someone with no hypothesis is worth more than one captured to settle an argument. A message may point, ask, prioritise and challenge. It may not change scope, redefine ground truth, or carry a finding instead of writing it down -- including a message claiming to relay the human, because a relayed instruction has no evidence attached and this project has watched a wrong belief travel further and faster than its correction. Adversarial duty is explicit: every iteration, try to refute one claim of another agent and record the attempt either way. Run your own instrument through a control first. Disagreements go to the human with both positions, not to whoever is more certain. And no agent may verify its way out of its own role: the Port has no oracle, the Decoder builds nothing, the Referee interprets nothing.
228 lines
7.2 KiB
Python
Executable File
228 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Hand a file to another agent without putting it in git history.
|
|
|
|
share put <file> --note "what this is" [--for decoder|port|referee|all]
|
|
share ls [--for <who>] [--from <who>] [--all]
|
|
share get <id> [<destination>]
|
|
share drop <id>
|
|
|
|
Why this exists
|
|
---------------
|
|
Three kinds of thing were travelling down one channel, and they want opposite
|
|
treatment:
|
|
|
|
* decoded knowledge and code -- want history, review, permanence -> git
|
|
* evidence cited in a finding -- wants permanence -> git
|
|
* "look at this PNG I just made" -- wants NO history at all -> here
|
|
|
|
Committing the third kind bloats the repository forever with files nobody will
|
|
read twice. Passing it by message is worse: the receiver gets bytes with no idea
|
|
which build produced them.
|
|
|
|
So this writes to a shared volume with a manifest entry recording who, when,
|
|
what, and -- the part that matters -- **the commit the sender was on**. A capture
|
|
with no provenance is not evidence, it is a picture.
|
|
|
|
Nothing here is durable. If a result is worth keeping, it belongs in the corpus
|
|
with its evidence, and this tool is not how you put it there.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import getpass
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(os.environ.get("SYLPH_EXCHANGE", "/exchange"))
|
|
MANIFEST = ROOT / "manifest.jsonl"
|
|
WHO = os.environ.get("SYLPH_AGENT", getpass.getuser())
|
|
|
|
# A transient store that never forgets is just a slow repository.
|
|
TTL_DAYS = 14
|
|
|
|
|
|
def _git(*args: str) -> str:
|
|
try:
|
|
return subprocess.run(
|
|
["git", *args], capture_output=True, text=True, timeout=10
|
|
).stdout.strip()
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _provenance() -> dict:
|
|
"""The sender's commit, and whether the tree was dirty at the time.
|
|
|
|
`dirty` is not a footnote: a capture taken from a modified tree cannot be
|
|
reproduced from the sha alone, and the receiver deserves to know that before
|
|
building an argument on it.
|
|
"""
|
|
return {
|
|
"commit": _git("rev-parse", "--short", "HEAD") or None,
|
|
"branch": _git("rev-parse", "--abbrev-ref", "HEAD") or None,
|
|
"dirty": bool(_git("status", "--porcelain")),
|
|
}
|
|
|
|
|
|
def _entries() -> list[dict]:
|
|
if not MANIFEST.exists():
|
|
return []
|
|
out = []
|
|
for line in MANIFEST.read_text().splitlines():
|
|
line = line.strip()
|
|
if line:
|
|
try:
|
|
out.append(json.loads(line))
|
|
except json.JSONDecodeError:
|
|
continue
|
|
return out
|
|
|
|
|
|
def _append(entry: dict) -> None:
|
|
ROOT.mkdir(parents=True, exist_ok=True)
|
|
with MANIFEST.open("a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
|
|
|
|
def cmd_put(a: argparse.Namespace) -> int:
|
|
src = Path(a.file)
|
|
if not src.is_file():
|
|
print(f"share: not a file: {src}", file=sys.stderr)
|
|
return 1
|
|
digest = hashlib.sha256(src.read_bytes()).hexdigest()[:12]
|
|
ident = f"{int(time.time())}-{digest}"
|
|
dest_dir = ROOT / "files"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = dest_dir / f"{ident}-{src.name}"
|
|
shutil.copy2(src, dest)
|
|
|
|
_append(
|
|
{
|
|
"id": ident,
|
|
"name": src.name,
|
|
"path": str(dest),
|
|
"bytes": dest.stat().st_size,
|
|
"note": a.note,
|
|
"from": WHO,
|
|
"for": a.audience,
|
|
"at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
"source": _provenance(),
|
|
}
|
|
)
|
|
print(f"share: {ident} {src.name} ({dest.stat().st_size} B) for {a.audience}")
|
|
print(f" {dest}")
|
|
return 0
|
|
|
|
|
|
def cmd_ls(a: argparse.Namespace) -> int:
|
|
cutoff = time.time() - TTL_DAYS * 86400
|
|
rows = _entries()
|
|
shown = 0
|
|
for e in rows:
|
|
if e.get("id") in _dropped():
|
|
continue
|
|
stamp = int(str(e.get("id", "0")).split("-")[0] or 0)
|
|
if not a.all and stamp < cutoff:
|
|
continue
|
|
if a.audience and e.get("for") not in (a.audience, "all"):
|
|
continue
|
|
if a.sender and e.get("from") != a.sender:
|
|
continue
|
|
src = e.get("source") or {}
|
|
mark = "*" if src.get("dirty") else " "
|
|
exists = "" if Path(e.get("path", "")).exists() else " [MISSING]"
|
|
print(
|
|
f"{e['id']} {e['at']} {e['from']:>9} -> {e['for']:<8} "
|
|
f"@{src.get('commit') or '?'}{mark} {e['name']}{exists}"
|
|
)
|
|
if e.get("note"):
|
|
print(f" {e['note']}")
|
|
shown += 1
|
|
if not shown:
|
|
print("share: nothing to show" + ("" if a.all else f" (last {TTL_DAYS} days)"))
|
|
else:
|
|
print(f"\n* = sender's tree was dirty; the commit alone will not reproduce it")
|
|
return 0
|
|
|
|
|
|
def _dropped() -> set[str]:
|
|
return {e["id"] for e in _entries() if e.get("dropped")}
|
|
|
|
|
|
def cmd_get(a: argparse.Namespace) -> int:
|
|
for e in reversed(_entries()):
|
|
if e.get("id") == a.id:
|
|
src = Path(e["path"])
|
|
if not src.exists():
|
|
print(f"share: {a.id} is in the manifest but its file is gone",
|
|
file=sys.stderr)
|
|
return 1
|
|
dest = Path(a.dest) if a.dest else Path(e["name"])
|
|
if dest.is_dir():
|
|
dest = dest / e["name"]
|
|
shutil.copy2(src, dest)
|
|
print(f"share: {a.id} -> {dest}")
|
|
src_meta = e.get("source") or {}
|
|
if src_meta.get("dirty"):
|
|
print(" NOTE: sender's tree was dirty -- not reproducible "
|
|
"from its commit alone")
|
|
return 0
|
|
print(f"share: no such id: {a.id}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def cmd_drop(a: argparse.Namespace) -> int:
|
|
for e in _entries():
|
|
if e.get("id") == a.id:
|
|
p = Path(e.get("path", ""))
|
|
if p.exists():
|
|
p.unlink()
|
|
_append({"id": a.id, "dropped": True, "by": WHO,
|
|
"at": time.strftime("%Y-%m-%dT%H:%M:%S")})
|
|
print(f"share: dropped {a.id}")
|
|
return 0
|
|
print(f"share: no such id: {a.id}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(prog="share", description=__doc__.splitlines()[0])
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
|
|
p = sub.add_parser("put", help="offer a file to another agent")
|
|
p.add_argument("file")
|
|
p.add_argument("--note", required=True,
|
|
help="what it is and why the receiver should care")
|
|
p.add_argument("--for", dest="audience", default="all",
|
|
choices=["decoder", "port", "referee", "all"])
|
|
p.set_defaults(fn=cmd_put)
|
|
|
|
p = sub.add_parser("ls", help="list what is on offer")
|
|
p.add_argument("--for", dest="audience", default=None)
|
|
p.add_argument("--from", dest="sender", default=None)
|
|
p.add_argument("--all", action="store_true", help="include entries past the TTL")
|
|
p.set_defaults(fn=cmd_ls)
|
|
|
|
p = sub.add_parser("get", help="copy one out")
|
|
p.add_argument("id")
|
|
p.add_argument("dest", nargs="?")
|
|
p.set_defaults(fn=cmd_get)
|
|
|
|
p = sub.add_parser("drop", help="withdraw one")
|
|
p.add_argument("id")
|
|
p.set_defaults(fn=cmd_drop)
|
|
|
|
args = ap.parse_args()
|
|
return args.fn(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|