Files
Sylpheed/tools/share
MechaCat02 a8815f2826
Some checks failed
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
agents: the team protocol, the share tool, and a player's-eye navigation doc
**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.
2026-08-29 11:42:29 +02:00

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())