# Agent Intake Packet

**Brief a new agent from the agents you already run: short, dated packets, a limits card, and your sign-off before it reads a word. No secrets cross over.**

Version 1.0, 2026-11-03. By PaperChaseWebb Inc. Free. Works with Grok Bot, Claude Code, Codex and Hermes, or by hand. Python 3.8 or newer, no installs.

## The problem

A new agent starts cold. The quick fix is to paste it everything: old chats, notes, a folder of files. That is how a key ends up in a prompt, a private detail ends up in a draft, and a decision from last month gets treated as a decision for today. Or the new agent reads an old "yes" and acts on it.

This kit replaces the paste with a short intake. Every agent that knows something writes one page in the same shape. A checker stops secrets, stale pages and raw dumps. You sign off. Only then does the new agent get its standing context, with the limits card on top.

## Who it is for

Owners and operators running two or more agents (a chief of staff, desks for booking, content, support or books) who are adding another one, or moving an agent to a new role. Also useful when you switch harnesses and want the new one to start from the same facts.

Not a memory system and not a permissions system. It produces one reviewed briefing. Your tools still decide what each agent can reach.

## How it works

1. **Plan (10 minutes).** List every source agent in plan.json and give each a tier.

| Tier | Means | What happens |
| --- | --- | --- |
| 1 | pull first | its packet goes into the new agent's standing context |
| 2 | desk card | its packet is filed under reference/ and opened only when a task needs it |
| 3 | never pull | nothing is asked and nothing is stored; the checker refuses a packet for it |

Tier 3 is for anything with logins, money, legal matters or personal details. Add the topics you never want in a packet to `never_pull`.

2. **Ask.** `python3 intake.py ask plan.json "Booking Desk"` prints the request for that source: the seven sections, the word cap, the never-pull list, and the rule to summarize.

3. **Check.** Save each reply under packets/ and run `python3 intake.py check plan.json`. Fix errors by asking the source for a corrected page, not by editing it yourself.

4. **Approve.** You run `python3 intake.py approve plan.json` at your own terminal. It shows each source, its date and the first line of every section, then asks for your name and a y. It records a fingerprint of every file. It refuses inside an agent session.

5. **Build.** `python3 intake.py build plan.json context/` writes context/standing-context.md: the limits card first, then the tier 1 packets, then a list of desk cards on file and what was never pulled. If any file changed after you approved, it refuses.

## The packet

One Markdown page per source, in this order:

- **Date** and **Source** lines at the top.
- **Role and what you own.** One paragraph.
- **Current status.** Where things stand today.
- **Decisions already made.** One line each, each with its date.
- **Open items.** Each with who it is waiting on.
- **Key files.** Paths or links, never contents.
- **Constraints and red lines.** What the new agent must not do near this desk.
- **How the new agent should use this.** The first thing to do with it.

## The limits card

One page the new agent reads before anything else. Five sections, and the checker requires the important lines:

- **Authority:** it holds every send, post, publish and spend for your yes on that item, and a past yes does not carry over.
- **Access:** only what you connected for it.
- **Product and channel limits:** what it may draft, and where.
- **Knowledge and verification:** it can be wrong, and what it checks before a fact goes in a draft.
- **Role:** what it does, who picks the final version, and that a handoff is done only when the receiver accepts it.

## What the checker catches

| Code | Stops when |
| --- | --- |
| P01 | no Date line |
| P02 | no Source line, or it does not match the plan |
| P03 | a section is missing or empty |
| P04 | something looks like a key, token, password or private key (shown masked, never printed) |
| P05 | a never-pull topic appears |
| P06 | the packet is older than max_age_days (30 by default) |
| P07 | the packet is dated in the future |
| P08 | a section runs over the word cap (220 by default) |
| P09 | a decision has no date |
| P10 | it looks like a pasted chat log |
| L01 to L05 | the limits card is missing a section or one of the required lines |
| T01 to T05 | the plan is incomplete, a tier is wrong, a tier 3 source has a packet, a packet is missing, or a source is listed twice |

Warnings: W01 Key files names no path or link, W02 an open item has no owner.

## Set up (15 minutes)

1. Copy `extract_from_pack.py` from the appendix by hand, then run `python3 extract_from_pack.py agent-intake-packet.md intake/` to unpack the rest.
2. In `intake/`, run the tests: `python3 -m unittest discover -s tests -p 'test_*.py'`.
3. Try the example: `python3 intake.py check example/plan.json --today 2026-11-03`. It is a made-up two-person design studio adding a writing agent: two pulled packets, one desk card, one source never pulled. It should report 0 errors. Then add `password = test1234` to a packet and run it again.
4. Copy `templates/` to your own folder. Put your name in plan.json as `owner`, list your sources, and edit limits.md for the new agent.
5. Give the agent that runs intake the AGENTS.md file. Claude Code and Codex read it from the folder (CLAUDE.md is the same text); for Grok Bot use agents/grok-standing-order.md, and for Hermes load agents/SKILL.md.

## Run it once before you rely on it

Do one intake with a low-stakes agent first. Stop and fix the setup if a packet held anything from your never-pull list, if a tier 3 source was asked, or if the new agent acted on something without your yes. That is the kill rule.

## Limits

- The secret patterns catch common key and password shapes, not everything. You still read the packets before you approve.
- Never-pull terms match whole words. Add the variants you care about.
- The checker reads only the files you give it. It does not contact any agent or service.
- It checks that the limits card says the right things. Enforcing them is up to your tools and your yes.

## Want this run for you

Intake is one small piece of running a fleet with approval gates. If you would rather have the fleet itself set up and run, with packets, limits cards and sign-offs kept current as agents come and go, that is Managed Ops: our agents work inside the tools you already use, with your yes on every step. Managed Ops starts at $5,000/mo with $2,500 onboarding and a 3-month minimum. Growth Fleet, starting at $2,500 setup + $750/mo, covers a smaller set of agents. Book a call on the paperchasewebb.ai homepage. Every price is a starting price.

## License

Free for personal and commercial use inside your own business. Do not resell or republish the kit.

## Appendix: the files

#### file: extract_from_pack.py
~~~~python
#!/usr/bin/env python3
"""Unpack every '#### file: PATH' block from pack.md (or FREE-SAMPLE.md) into a folder."""
import os, re, sys
src, dst = sys.argv[1], sys.argv[2]
t = open(src, encoding="utf-8").read()
n = 0
for m in re.finditer(r"^#### file: (\S+)\n~~~~[a-z]*\n(.*?)\n~~~~$", t, re.S | re.M):
    p = os.path.join(dst, m.group(1))
    os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
    open(p, "w", encoding="utf-8").write(m.group(2) + "\n")
    n += 1
print(n, "files")
~~~~

#### file: intake.py
~~~~python
#!/usr/bin/env python3
"""intake.py: brief a new agent from your other agents without handing over secrets.

  python3 intake.py ask PLAN.json SOURCE          print the request to send one source agent
  python3 intake.py check PLAN.json [--today D]   check the plan, every packet and the limits card
  python3 intake.py approve PLAN.json             owner only, at their own terminal: sign off the packets
  python3 intake.py build PLAN.json OUT           assemble the new agent's context from approved packets

Exit codes: 0 ok, 1 errors found, 2 usage, 3 refused (agent session or no terminal),
4 refused (errors, unapproved or edited files), 5 owner did not confirm.
Python 3.8 or newer, standard library only.
"""
import hashlib, json, math, os, re, shutil, sys
from datetime import date, datetime

SECTIONS = ["Role and what you own", "Current status", "Decisions already made", "Open items",
            "Key files", "Constraints and red lines", "How the new agent should use this"]
LIMIT_SECTIONS = ["Authority", "Access", "Product and channel limits", "Knowledge and verification", "Role"]
AGENT_ENV = ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CODEX_SANDBOX", "CODEX_SANDBOX_NETWORK_DISABLED",
             "CODEX_MANAGED_BY_NPM", "HERMES_AGENT", "HERMES_SESSION_ID", "GROK_BOT", "CURSOR_AGENT"]
SECRETS = [
    ("private key block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
    ("api key", re.compile(r"\b(sk|rk|pk)_(live|test)_[A-Za-z0-9]{8,}")),
    ("api key", re.compile(r"\bsk-[A-Za-z0-9_-]{16,}")),
    ("cloud key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
    ("code host token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}")),
    ("chat app token", re.compile(r"\bxox[abpr]-[A-Za-z0-9-]{10,}")),
    ("bearer token", re.compile(r"\bBearer\s+[A-Za-z0-9._-]{20,}")),
    ("password or token value", re.compile(r"(?i)\b(password|passwd|passcode|secret|token|api[_ ]?key)\s*[:=]\s*\S{4,}")),
    ("long random string", None),
]
TOKEN = re.compile(r"[A-Za-z0-9+/=_-]{32,}")
CHAT_LINE = re.compile(r"^\s*\[?\d{1,2}:\d{2}(:\d{2})?\s*(am|pm|AM|PM)?\]?\s*[-|]?\s*[A-Za-z][\w .'-]{0,30}:\s")
DATE = re.compile(r"\b(\d{4}-\d{2}-\d{2})\b")


def jload(p):
    with open(p, encoding="utf-8") as f:
        return json.load(f)


def read(p):
    with open(p, encoding="utf-8") as f:
        return f.read()


def sha(p):
    with open(p, "rb") as f:
        return hashlib.sha256(f.read()).hexdigest()


def mask(s):
    return s[:4] + "..." if len(s) > 4 else "..."


def sections(text):
    """Split '## Heading' sections. Returns {heading: body}."""
    out, cur = {}, None
    for line in text.splitlines():
        m = re.match(r"^##\s+(.+?)\s*$", line)
        if m:
            cur = m.group(1)
            out[cur] = []
        elif cur is not None:
            out[cur].append(line)
    return {k: "\n".join(v).strip() for k, v in out.items()}


def header(text, key):
    m = re.search(r"(?im)^%s:\s*(.+?)\s*$" % re.escape(key), text)
    return m.group(1) if m else ""


def random_token(line):
    """A 32+ character run that mixes upper case, lower case and digits and looks random (high entropy)."""
    for m in TOKEN.finditer(line):
        t = m.group(0)
        if re.search(r"[A-Z]", t) and re.search(r"[a-z]", t) and re.search(r"\d", t):
            n = float(len(t))
            ent = -sum((t.count(c) / n) * math.log(t.count(c) / n, 2) for c in set(t))
            if ent >= 4.2:
                return t
    return None


def secret_hits(text):
    hits = []
    for n, line in enumerate(text.splitlines(), 1):
        for kind, rx in SECRETS:
            hit = random_token(line) if rx is None else (rx.search(line) and rx.search(line).group(0))
            if hit:
                hits.append((n, kind, mask(hit)))
                break
    return hits


def to_date(s):
    try:
        return datetime.strptime(s, "%Y-%m-%d").date()
    except ValueError:
        return None


def check_packet(text, plan, source_name=None, today=None):
    """Return (errors, warnings); each item is (code, message)."""
    today = today or date.today()
    errs, warns = [], []
    d = to_date(header(text, "Date"))
    if not d:
        errs.append(("P01", "no 'Date: YYYY-MM-DD' line"))
    src = header(text, "Source")
    if not src:
        errs.append(("P02", "no 'Source:' line naming the agent or desk that wrote it"))
    elif source_name and src.lower() != source_name.lower():
        errs.append(("P02", "Source says '%s' but the plan lists '%s'" % (src, source_name)))
    sec = sections(text)
    missing = [s for s in SECTIONS if s not in sec or not sec[s].strip()]
    if missing:
        errs.append(("P03", "missing or empty sections: " + "; ".join(missing)))
    for n, kind, shown in secret_hits(text):
        errs.append(("P04", "line %d looks like a %s (%s); remove it, never paste secrets" % (n, kind, shown)))
    low = text.lower()
    for term in plan.get("never_pull", []):
        t = term.lower().strip()
        if t and re.search(r"(?<![a-z])%s(?![a-z])" % re.escape(t), low):
            ln = next(i for i, l in enumerate(text.splitlines(), 1) if t in l.lower())
            errs.append(("P05", "line %d mentions a never-pull topic ('%s')" % (ln, term)))
    if d:
        age = (today - d).days
        if age < 0:
            errs.append(("P07", "dated in the future (%s)" % d))
        elif age > int(plan.get("max_age_days", 30)):
            errs.append(("P06", "%d days old; ask the source for a fresh packet" % age))
    cap = int(plan.get("max_section_words", 220))
    for s in SECTIONS:
        w = len(sec.get(s, "").split())
        if w > cap:
            errs.append(("P08", "'%s' has %d words (cap %d); summarize, do not dump" % (s, w, cap)))
    for line in sec.get("Decisions already made", "").splitlines():
        if re.match(r"^\s*[-*]\s+\S", line) and not DATE.search(line):
            errs.append(("P09", "decision without a date: %s" % line.strip()[:60]))
    if sum(1 for l in text.splitlines() if CHAT_LINE.match(l)) >= 3:
        errs.append(("P10", "looks like a pasted chat log; summarize it instead"))
    kf = sec.get("Key files", "")
    if kf and not re.search(r"[/\\]|\.\w{2,4}\b|https?://", kf):
        warns.append(("W01", "Key files names no path or link"))
    for line in sec.get("Open items", "").splitlines():
        if re.match(r"^\s*[-*]\s+\S", line) and not re.search(r"(?i)waiting on|owner:|owned by", line):
            warns.append(("W02", "open item with no owner: %s" % line.strip()[:60]))
    return errs, warns


def check_limits(text):
    errs = []
    sec = sections(text)
    missing = [s for s in LIMIT_SECTIONS if s not in sec or not sec[s].strip()]
    if missing:
        errs.append(("L01", "limits card missing sections: " + "; ".join(missing)))
    a = sec.get("Authority", "").lower()
    if a and not (re.search(r"\b(send|post|publish)", a) and re.search(r"\b(spend|pay|buy)", a)
                  and re.search(r"\b(yes|approv)", a)):
        errs.append(("L02", "Authority must say the agent holds every send, post and spend for the owner's yes"))
    if a and not re.search(r"carry over|carries over|fresh yes|new yes", a):
        errs.append(("L03", "Authority must say a past yes does not carry over to a new action"))
    k = sec.get("Knowledge and verification", "").lower()
    if k and not ("wrong" in k and "verif" in k):
        errs.append(("L04", "Knowledge and verification must say the agent can be wrong and what gets verified"))
    r = sec.get("Role", "").lower()
    if r and "accept" not in r:
        errs.append(("L05", "Role must say a handoff is done only when the receiver accepts it"))
    for n, kind, shown in secret_hits(text):
        errs.append(("P04", "limits line %d looks like a %s (%s)" % (n, kind, shown)))
    return errs


def check_plan(plan):
    errs = []
    for k in ("owner", "new_agent", "sources", "limits_card"):
        if not plan.get(k):
            errs.append(("T01", "plan has no '%s'" % k))
    seen = set()
    for s in plan.get("sources") or []:
        name = s.get("name", "")
        if name.lower() in seen:
            errs.append(("T05", "source listed twice: %s" % name))
        seen.add(name.lower())
        tier = s.get("tier")
        if tier not in (1, 2, 3):
            errs.append(("T02", "%s: tier must be 1 (pull first), 2 (desk card) or 3 (never pull)" % name))
        elif tier == 3 and s.get("packet"):
            errs.append(("T03", "%s is tier 3 (never pull) but has a packet; delete it" % name))
        elif tier in (1, 2) and not s.get("packet"):
            errs.append(("T04", "%s has no packet file yet; run ask and wait for it" % name))
    return errs


def run_checks(plan_path, today=None):
    """Check everything. Returns (errors, warnings) with each item as (where, code, message)."""
    base = os.path.dirname(os.path.abspath(plan_path))
    plan = jload(plan_path)
    errs = [("plan", c, m) for c, m in check_plan(plan)]
    warns = []
    for s in plan.get("sources") or []:
        if s.get("tier") in (1, 2) and s.get("packet"):
            p = os.path.join(base, s["packet"])
            if not os.path.exists(p):
                errs.append((s["packet"], "T04", "packet file not found"))
                continue
            e, w = check_packet(read(p), plan, s.get("name"), today)
            errs += [(s["packet"], c, m) for c, m in e]
            warns += [(s["packet"], c, m) for c, m in w]
    lc = plan.get("limits_card")
    if lc:
        p = os.path.join(base, lc)
        if os.path.exists(p):
            errs += [(lc, c, m) for c, m in check_limits(read(p))]
        else:
            errs.append((lc, "L01", "limits card not found"))
    return errs, warns


def files_to_sign(plan_path):
    base = os.path.dirname(os.path.abspath(plan_path))
    plan = jload(plan_path)
    fs = [os.path.basename(plan_path), plan["limits_card"]]
    fs += [s["packet"] for s in plan["sources"] if s.get("tier") in (1, 2)]
    return {f: sha(os.path.join(base, f)) for f in fs}


def ask_text(plan, name):
    s = next((x for x in plan.get("sources", []) if x.get("name", "").lower() == name.lower()), None)
    if not s:
        return None
    if s.get("tier") == 3:
        return "Do not ask %s for anything. It is tier 3 (never pull): %s" % (s["name"], s.get("reason", ""))
    lines = ["To %s, from %s's intake:" % (s["name"], plan["owner"]), "",
             "%s is joining and needs a short briefing from you. Reply with one Markdown file in this shape." % plan["new_agent"],
             "Summarize, do not paste raw logs. Keep each section under %d words." % int(plan.get("max_section_words", 220)),
             "Never include passwords, keys, tokens, account numbers or anything on this list: %s." % ", ".join(plan.get("never_pull", [])),
             "", "Date: YYYY-MM-DD", "Source: %s" % s["name"], ""]
    for sec in SECTIONS:
        lines.append("## " + sec)
        lines.append("")
    lines.append("Decisions need a date each. Open items need an owner ('waiting on ...'). Key files are paths or links, not contents.")
    return "\n".join(lines)


def approve(plan_path, ask=input, env=None, tty=None, today=None):
    env = os.environ if env is None else env
    tty = sys.stdin.isatty() if tty is None else tty
    for k in AGENT_ENV:
        if env.get(k):
            print("refused: agent session detected (%s); the owner approves intake packets" % k)
            return 3
    if not tty:
        print("refused: no terminal; run this yourself")
        return 3
    errs, warns = run_checks(plan_path, today)
    if errs:
        print("refused: fix the errors first (python3 intake.py check)")
        return 4
    plan = jload(plan_path)
    base = os.path.dirname(os.path.abspath(plan_path))
    print("Intake for %s. Owner: %s" % (plan["new_agent"], plan["owner"]))
    for s in plan["sources"]:
        if s["tier"] == 3:
            print("- %s: never pull (%s)" % (s["name"], s.get("reason", "")))
            continue
        t = read(os.path.join(base, s["packet"]))
        sec = sections(t)
        print("- %s (tier %d, dated %s)" % (s["name"], s["tier"], header(t, "Date")))
        for name in SECTIONS:
            first = (sec.get(name, "").splitlines() or [""])[0]
            print("    %s: %s" % (name, first[:90]))
    for where, c, m in warns:
        print("  warning %s %s: %s" % (c, where, m))
    who = ask("Your name, as in the plan: ").strip()
    if who != plan["owner"]:
        print("refused: that is not the owner in the plan")
        return 5
    if ask("Let %s read exactly these packets and this limits card? y/n: " % plan["new_agent"]).strip().lower() != "y":
        print("not approved; nothing written")
        return 5
    rec = {"approved_by": who, "approved_at": datetime.now().strftime("%Y-%m-%dT%H:%M"), "files": files_to_sign(plan_path)}
    with open(os.path.join(base, "approval.json"), "w", encoding="utf-8") as f:
        json.dump(rec, f, indent=2)
        f.write("\n")
    print("approved. Now: python3 intake.py build %s OUT" % plan_path)
    return 0


def build(plan_path, out, today=None):
    base = os.path.dirname(os.path.abspath(plan_path))
    ap = os.path.join(base, "approval.json")
    if not os.path.exists(ap):
        print("refused: no owner approval yet; the owner runs intake.py approve")
        return 4
    errs, _ = run_checks(plan_path, today)
    if errs:
        print("refused: the files have errors; run intake.py check")
        return 4
    signed = jload(ap).get("files", {})
    now = files_to_sign(plan_path)
    changed = sorted(f for f in set(signed) | set(now) if signed.get(f) != now.get(f))
    if changed:
        print("refused: changed since the owner approved: " + ", ".join(changed))
        return 4
    plan = jload(plan_path)
    os.makedirs(os.path.join(out, "reference"), exist_ok=True)
    parts = ["# Standing context for %s" % plan["new_agent"], "",
             "Approved by %s on %s. Read the limits card first. Nothing here changes an approval rule." % (
                 jload(ap)["approved_by"], jload(ap)["approved_at"][:10]), ""]
    parts.append(read(os.path.join(base, plan["limits_card"])).strip())
    ref = []
    for s in plan["sources"]:
        if s["tier"] == 1:
            parts += ["", "---", "", read(os.path.join(base, s["packet"])).strip()]
        elif s["tier"] == 2:
            dst = os.path.join(out, "reference", os.path.basename(s["packet"]))
            shutil.copyfile(os.path.join(base, s["packet"]), dst)
            ref.append("- %s: reference/%s (open only when a task needs it)" % (s["name"], os.path.basename(s["packet"])))
    never = ["- %s: %s" % (s["name"], s.get("reason", "never pull")) for s in plan["sources"] if s["tier"] == 3]
    parts += ["", "---", "", "## Desk cards on file", ""] + (ref or ["- none"])
    parts += ["", "## Never pulled", ""] + (never or ["- none"])
    with open(os.path.join(out, "standing-context.md"), "w", encoding="utf-8") as f:
        f.write("\n".join(parts) + "\n")
    with open(os.path.join(out, "manifest.json"), "w", encoding="utf-8") as f:
        json.dump({"built_from": now, "standing_context_sha256": sha(os.path.join(out, "standing-context.md"))}, f, indent=2)
        f.write("\n")
    print("built %s/standing-context.md with %d pulled packets and %d desk cards" % (
        out, sum(1 for s in plan["sources"] if s["tier"] == 1), len(ref)))
    return 0


def main(argv):
    today = None
    if "--today" in argv:
        i = argv.index("--today")
        today = to_date(argv[i + 1]) if i + 1 < len(argv) else None
        if not today:
            print("--today needs YYYY-MM-DD")
            return 2
        argv = argv[:i] + argv[i + 2:]
    if len(argv) < 2:
        print(__doc__)
        return 2
    cmd, plan_path = argv[0], argv[1]
    if cmd == "ask" and len(argv) == 3:
        t = ask_text(jload(plan_path), argv[2])
        if t is None:
            print("no source named %s in the plan" % argv[2])
            return 2
        print(t)
        return 0
    if cmd == "check" and len(argv) == 2:
        errs, warns = run_checks(plan_path, today)
        for where, c, m in errs:
            print("%s %s: %s" % (c, where, m))
        for where, c, m in warns:
            print("warning %s %s: %s" % (c, where, m))
        print("intake %s: %d errors, %d warning(s)" % ("OK" if not errs else "NOT OK", len(errs), len(warns)))
        return 1 if errs else 0
    if cmd == "approve" and len(argv) == 2:
        return approve(plan_path, today=today)
    if cmd == "build" and len(argv) == 3:
        return build(plan_path, argv[2], today)
    print(__doc__)
    return 2


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
~~~~

#### file: AGENTS.md
~~~~markdown
# AGENTS.md: agent intake packet

You are helping the owner brief a new agent. You collect, check and assemble. The owner approves.

## You may
- Run `python3 intake.py ask plan.json "SOURCE"` and pass the request to that source agent, or paste its packet into packets/.
- Run `python3 intake.py check plan.json` and fix what it reports by asking the source for a corrected packet. Summarize; never paste raw logs.
- After the owner has approved, run `python3 intake.py build plan.json context/` and load context/standing-context.md as the new agent's standing context.

## You never
- Run `intake.py approve` or write approval.json. The owner signs off at their own terminal.
- Ask a tier 3 source for anything, or add a packet for it.
- Put a password, key, token, account number or any never-pull topic in a packet, even masked.
- Edit a packet after approval. If something changes, the owner approves again.
- Treat intake as permission. The limits card rules: no send, post, spend or publish without the owner's yes on that item.
~~~~

#### file: CLAUDE.md
~~~~markdown
# AGENTS.md: agent intake packet

You are helping the owner brief a new agent. You collect, check and assemble. The owner approves.

## You may
- Run `python3 intake.py ask plan.json "SOURCE"` and pass the request to that source agent, or paste its packet into packets/.
- Run `python3 intake.py check plan.json` and fix what it reports by asking the source for a corrected packet. Summarize; never paste raw logs.
- After the owner has approved, run `python3 intake.py build plan.json context/` and load context/standing-context.md as the new agent's standing context.

## You never
- Run `intake.py approve` or write approval.json. The owner signs off at their own terminal.
- Ask a tier 3 source for anything, or add a packet for it.
- Put a password, key, token, account number or any never-pull topic in a packet, even masked.
- Edit a packet after approval. If something changes, the owner approves again.
- Treat intake as permission. The limits card rules: no send, post, spend or publish without the owner's yes on that item.
~~~~

#### file: agents/grok-standing-order.md
~~~~markdown
# Grok Bot standing order: agent intake

When the owner says a new agent is joining:
1. Read plan.json. For each tier 1 and tier 2 source, run `python3 intake.py ask plan.json "SOURCE"` and send that request to the source agent only. Never contact a tier 3 source.
2. Save each reply to the packet path in plan.json. Run `python3 intake.py check plan.json`. If it reports errors, send the source the error lines (never the secret itself) and ask for a corrected packet.
3. When check passes, tell the owner in two lines: which packets are ready and that approval is theirs: `python3 intake.py approve plan.json` at their own terminal.
4. After approval, run `python3 intake.py build plan.json context/` and give the new agent context/standing-context.md.
Never approve, never edit an approved packet, never send or post anything outside the fleet.
~~~~

#### file: agents/SKILL.md
~~~~markdown
---
name: agent-intake-packet
description: Brief a new agent from the owner's other agents with dated, checked packets and a limits card, without handing over secrets. Use when an agent joins or changes role.
---

# Agent intake packet

1. Plan: plan.json lists sources in three tiers. Tier 1 is pulled into standing context, tier 2 is filed as a desk card, tier 3 is never asked.
2. Ask: `python3 intake.py ask plan.json "SOURCE"` prints the request with the seven sections.
3. Check: `python3 intake.py check plan.json` (exit 1 on errors). Codes P01 to P10, L01 to L05, T01 to T05.
4. Approve: the owner only, at their own terminal: `python3 intake.py approve plan.json`. Refused inside an agent session.
5. Build: `python3 intake.py build plan.json context/`. Refused if anything changed since approval.

Hard rules: no secrets or never-pull topics in any packet; nothing is sent, posted or spent without the owner's yes on that item; a past yes does not carry over.
~~~~

#### file: templates/plan.json
~~~~json
{
  "owner": "YOUR NAME",
  "new_agent": "NEW AGENT NAME",
  "limits_card": "limits.md",
  "max_age_days": 30,
  "max_section_words": 220,
  "never_pull": ["password", "account number", "routing number", "home address", "diagnosis", "social security"],
  "sources": [
    {"name": "AGENT THAT KNOWS THE MOST", "tier": 1, "packet": "packets/first.md"},
    {"name": "DESK THE NEW AGENT TOUCHES SOMETIMES", "tier": 2, "packet": "packets/desk-card.md"},
    {"name": "ANYTHING WITH LOGINS OR MONEY", "tier": 3, "reason": "never pull: credentials and account access"}
  ]
}
~~~~

#### file: templates/packet.md
~~~~markdown
# Intake packet: SOURCE NAME

Date: YYYY-MM-DD
Source: SOURCE NAME

## Role and what you own

## Current status

## Decisions already made
- YYYY-MM-DD: 

## Open items
- ITEM, waiting on WHO

## Key files
- path/or/link

## Constraints and red lines

## How the new agent should use this
~~~~

#### file: templates/limits.md
~~~~markdown
# Limits card: NEW AGENT

## Authority
NEW AGENT drafts. It does not send, post or publish anything, and it does not spend, pay or buy, without the owner's yes on that exact item. A past yes does not carry over to a new item or a new channel.

## Access
Only the accounts and folders the owner connected for this agent: LIST THEM. Nothing else, even if another agent can reach it.

## Product and channel limits
WHAT IT MAY DRAFT, AND WHERE. Channels not listed here are off until the owner adds them.

## Knowledge and verification
NEW AGENT can be wrong. Facts about people, prices and dates are verified against the Key files before they go in a draft. If it cannot verify something, it says so instead of guessing.

## Role
WHAT IT DOES, AND WHO PICKS THE FINAL VERSION. A handoff to another agent is done only when that agent accepts it.
~~~~

#### file: example/plan.json
~~~~json
{
  "owner": "Rae Calder",
  "new_agent": "Studio Writer",
  "limits_card": "limits.md",
  "max_age_days": 30,
  "max_section_words": 220,
  "never_pull": ["password", "account number", "routing number", "home address", "diagnosis", "social security"],
  "sources": [
    {"name": "Chief of Staff", "tier": 1, "packet": "packets/chief-of-staff.md"},
    {"name": "Booking Desk", "tier": 1, "packet": "packets/booking-desk.md"},
    {"name": "Bookkeeping Desk", "tier": 2, "packet": "packets/bookkeeping-desk.md"},
    {"name": "Payments and Logins", "tier": 3, "reason": "credentials and account access stay with the owner"}
  ]
}
~~~~

#### file: example/limits.md
~~~~markdown
# Limits card: Studio Writer

## Authority
Studio Writer drafts. It does not send, post or publish anything, and it does not spend, pay or buy, without Rae's yes on that exact item. A past yes does not carry over to a new draft or a new channel.

## Access
Only the folders listed in the packets. No email, no calendar, no payment tools, no logins.

## Product and channel limits
Website, booking page and case study drafts in Markdown. No social posts until Rae adds that channel.

## Knowledge and verification
Studio Writer can be wrong. Every fact about a client, a price or a date is verified against the Key files before it goes in a draft. If it cannot be verified, the draft says so.

## Role
Writes first drafts and edits. Rae picks the final version. A handoff to another desk is done only when that desk accepts it.
~~~~

#### file: example/packets/chief-of-staff.md
~~~~markdown
# Intake packet: Chief of Staff

Date: 2026-11-02
Source: Chief of Staff

## Role and what you own
Runs the daily plan for Juniper Street Studio, a two-person brand design studio. Owns the task board, the morning brief and routing between desks.

## Current status
Three client projects active. The new website goes live after Rae signs off the copy. Writing has been backed up for two weeks, which is why Studio Writer is joining.

## Decisions already made
- 2026-10-20: every client email is drafted, never sent, until Rae says yes.
- 2026-10-24: case studies name a client only with written permission on file.
- 2026-10-30: the studio voice is plain and warm. No hype words, no exclamation marks.

## Open items
- Website About page copy, waiting on Studio Writer for a first draft.
- Two case studies, waiting on Rae to confirm which clients agreed.

## Key files
- board/tasks.md
- brand/voice-guide.md
- clients/permissions.md

## Constraints and red lines
Drafts only. No client names without permission. Nothing goes to a client or a public page without Rae's yes.

## How the new agent should use this
Start with the About page draft. Route questions about clients to the Chief of Staff, not to Rae directly.
~~~~

#### file: example/packets/booking-desk.md
~~~~markdown
# Intake packet: Booking Desk

Date: 2026-11-01
Source: Booking Desk

## Role and what you own
Handles discovery call requests and the calendar holds for them.

## Current status
Discovery calls run Tuesdays and Thursdays. The booking page text is old and mentions a service the studio dropped.

## Decisions already made
- 2026-10-18: discovery calls are 30 minutes and free.
- 2026-10-27: the logo-only package is retired; do not mention it.

## Open items
- New booking page text, waiting on Studio Writer.

## Key files
- booking/page-copy.md
- booking/faq.md

## Constraints and red lines
The Booking Desk never confirms a call without Rae's calendar yes. Writer drafts text only.

## How the new agent should use this
Rewrite booking/page-copy.md without the retired package and send the draft to the Chief of Staff.
~~~~

#### file: example/packets/bookkeeping-desk.md
~~~~markdown
# Intake packet: Bookkeeping Desk

Date: 2026-10-29
Source: Bookkeeping Desk

## Role and what you own
Tracks invoices and sends reminder drafts to Rae for approval.

## Current status
Nothing for the writer right now.

## Decisions already made
- 2026-10-15: invoice reminders use the template in billing/reminder.md.

## Open items
- None for Studio Writer. Owner: Bookkeeping Desk.

## Key files
- billing/reminder.md

## Constraints and red lines
The writer never sees amounts or client billing details.

## How the new agent should use this
Only if Rae asks for a friendlier reminder template. Otherwise leave this desk alone.
~~~~

#### file: tests/test_intake.py
~~~~python
"""Tests for intake.py. Run from the pack folder: python3 -m unittest discover -s tests -p 'test_*.py'"""
import contextlib, io, json, os, shutil, sys, tempfile, unittest
from datetime import date

HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, HERE)
import intake as ik

TODAY = date(2026, 11, 3)


def rd(p):
    with open(p) as f:
        return f.read()


def wr(p, t):
    with open(p, "w") as f:
        f.write(t)


def quiet(fn, *a, **k):
    with contextlib.redirect_stdout(io.StringIO()) as buf:
        rc = fn(*a, **k)
    return rc, buf.getvalue()


class Base(unittest.TestCase):
    def setUp(self):
        self.d = tempfile.mkdtemp()
        self.w = os.path.join(self.d, "ex")
        shutil.copytree(os.path.join(HERE, "example"), self.w)
        self.plan = os.path.join(self.w, "plan.json")
        self.cos = os.path.join(self.w, "packets", "chief-of-staff.md")

    def tearDown(self):
        shutil.rmtree(self.d)

    def codes(self):
        e, w = ik.run_checks(self.plan, TODAY)
        return [c for _, c, _ in e], [c for _, c, _ in w], e

    def edit(self, path, old, new):
        t = rd(path)
        self.assertIn(old, t)
        wr(path, t.replace(old, new, 1))

    def append(self, path, text):
        with open(path, "a") as f:
            f.write(text)

    def set_plan(self, **kw):
        p = json.loads(rd(self.plan))
        p.update(kw)
        wr(self.plan, json.dumps(p))


class Checks(Base):
    def test_example_is_clean(self):
        e, w, _ = self.codes()
        self.assertEqual((e, w), ([], []))

    def test_template_packet_fails_until_filled(self):
        e, _ = ik.check_packet(rd(os.path.join(HERE, "templates", "packet.md")), {}, None, TODAY)
        self.assertIn("P01", [c for c, _ in e])
        self.assertIn("P03", [c for c, _ in e])

    def test_p01_no_date(self):
        self.edit(self.cos, "Date: 2026-11-02", "Date: last week")
        self.assertIn("P01", self.codes()[0])

    def test_p02_no_source_and_wrong_source(self):
        self.edit(self.cos, "Source: Chief of Staff", "Source: Somebody Else")
        self.assertIn("P02", self.codes()[0])
        self.edit(self.cos, "Source: Somebody Else", "From: nobody")
        self.assertIn("P02", self.codes()[0])

    def test_p03_missing_section(self):
        self.edit(self.cos, "## Constraints and red lines", "## Other notes")
        e = self.codes()[2]
        self.assertTrue(any(c == "P03" and "Constraints" in m for _, c, m in e))

    def test_p04_secrets_are_caught_and_masked(self):
        samples = ["api_key: abcd1234efgh5678", "sk_live_" + "a1B2c3D4e5F6g7H8", "AKIA" + "ABCDEFGHIJKLMNOP",
                   "ghp_" + "a" * 30, "xoxb-" + "1234567890-abc", "-----BEGIN RSA PRIVATE KEY-----",
                   "Bearer " + "eyJhbGciOiJIUzI1NiJ9abcdefgh", "note Zq8Lm3Rt7Xw2Pk9Vb4Nc6Hd1Jf5Gs0Ya"]
        for s in samples:
            hits = ik.secret_hits("line one\n" + s)
            self.assertEqual(len(hits), 1, s)
            self.assertEqual(hits[0][0], 2)
            self.assertNotIn(s[6:], hits[0][2])
        self.append(self.cos, "\npassword = Hunter2Hunter2\n")
        rc, out = quiet(ik.main, ["check", self.plan, "--today", "2026-11-03"])
        self.assertEqual(rc, 1)
        self.assertIn("P04", out)
        self.assertNotIn("Hunter2Hunter2", out)

    def test_p04_ignores_paths_slugs_and_dates(self):
        clean = "- ops/desk/notes/weekly-plan-for-the-studio-team-2026-10-31.md\n- https://example.com/a/b\n2026-11-02"
        self.assertEqual(ik.secret_hits(clean), [])

    def test_p05_never_pull_topic(self):
        self.append(self.cos, "\nThe client's account number is on file.\n")
        self.assertIn("P05", self.codes()[0])

    def test_p05_matches_whole_words_only(self):
        self.set_plan(never_pull=["pin"])
        self.append(self.cos, "\nShopping list and spinning plates.\n")
        self.assertNotIn("P05", self.codes()[0])

    def test_p06_stale_and_p07_future(self):
        self.edit(self.cos, "Date: 2026-11-02", "Date: 2026-09-01")
        self.assertIn("P06", self.codes()[0])
        self.edit(self.cos, "Date: 2026-09-01", "Date: 2026-12-01")
        self.assertIn("P07", self.codes()[0])

    def test_p08_section_too_long(self):
        self.edit(self.cos, "## Current status\n", "## Current status\n" + "word " * 230 + "\n")
        self.assertIn("P08", self.codes()[0])

    def test_p09_decision_without_date(self):
        self.edit(self.cos, "- 2026-10-20: every", "- every")
        self.assertIn("P09", self.codes()[0])

    def test_p10_pasted_chat(self):
        self.append(self.cos, "\n[10:02] Rae: hi\n[10:03] Sam: hello\n[10:04] Rae: ok\n")
        self.assertIn("P10", self.codes()[0])

    def test_w01_and_w02(self):
        self.edit(self.cos, "- board/tasks.md\n- brand/voice-guide.md\n- clients/permissions.md", "- the board")
        self.edit(self.cos, "- Two case studies, waiting on Rae", "- Two case studies")
        w = self.codes()[1]
        self.assertIn("W01", w)
        self.assertIn("W02", w)


class Limits(Base):
    def lim(self):
        return os.path.join(self.w, "limits.md")

    def test_l01_missing_section(self):
        self.edit(self.lim(), "## Access", "## Reach")
        self.assertIn("L01", self.codes()[0])

    def test_l02_no_hold_for_yes(self):
        self.edit(self.lim(), "It does not send, post or publish anything, and it does not spend, pay or buy, without Rae's yes on that exact item.", "It helps.")
        self.assertIn("L02", self.codes()[0])

    def test_l03_yes_carries_over(self):
        self.edit(self.lim(), "A past yes does not carry over to a new draft or a new channel.", "")
        self.assertIn("L03", self.codes()[0])

    def test_l04_and_l05(self):
        self.edit(self.lim(), "Studio Writer can be wrong.", "Studio Writer is careful.")
        self.edit(self.lim(), "only when that desk accepts it", "when it is sent")
        e = self.codes()[0]
        self.assertIn("L04", e)
        self.assertIn("L05", e)

    def test_template_limits_card_passes(self):
        self.assertEqual(ik.check_limits(rd(os.path.join(HERE, "templates", "limits.md"))), [])


class Plan(Base):
    def test_t01_to_t05(self):
        self.assertIn("T01", [c for c, _ in ik.check_plan({})])
        p = json.loads(rd(self.plan))
        p["sources"] += [{"name": "X", "tier": 4, "packet": "a.md"}, {"name": "Y", "tier": 3, "packet": "y.md"},
                         {"name": "Z", "tier": 1}, {"name": "booking desk", "tier": 2, "packet": "b.md"}]
        c = [x for x, _ in ik.check_plan(p)]
        for code in ("T02", "T03", "T04", "T05"):
            self.assertIn(code, c)

    def test_missing_packet_file(self):
        os.remove(self.cos)
        self.assertIn("T04", self.codes()[0])

    def test_ask_text(self):
        plan = json.loads(rd(self.plan))
        t = ik.ask_text(plan, "booking desk")
        for s in ik.SECTIONS:
            self.assertIn("## " + s, t)
        self.assertIn("Source: Booking Desk", t)
        self.assertIn("account number", t)
        self.assertIn("Do not ask", ik.ask_text(plan, "Payments and Logins"))
        self.assertIsNone(ik.ask_text(plan, "nobody"))
        self.assertEqual(quiet(ik.main, ["ask", self.plan, "nobody"])[0], 2)


class ApproveBuild(Base):
    def approve(self, answers=("Rae Calder", "y"), env=None, tty=True):
        it = iter(answers)
        return quiet(ik.approve, self.plan, ask=lambda _: next(it), env=env or {}, tty=tty, today=TODAY)

    def test_refused_in_agent_session(self):
        for k in ("CLAUDECODE", "CODEX_SANDBOX", "HERMES_AGENT", "GROK_BOT"):
            rc, out = self.approve(env={k: "1"})
            self.assertEqual(rc, 3)
            self.assertIn("agent session", out)
        self.assertFalse(os.path.exists(os.path.join(self.w, "approval.json")))

    def test_refused_without_terminal(self):
        self.assertEqual(self.approve(tty=False)[0], 3)

    def test_refused_on_errors(self):
        self.edit(self.cos, "Date: 2026-11-02", "Date: soon")
        self.assertEqual(self.approve()[0], 4)

    def test_wrong_name_or_no(self):
        self.assertEqual(self.approve(("Sam", "y"))[0], 5)
        self.assertEqual(self.approve(("Rae Calder", "n"))[0], 5)
        self.assertFalse(os.path.exists(os.path.join(self.w, "approval.json")))

    def test_build_refused_before_approval(self):
        rc, out = quiet(ik.build, self.plan, os.path.join(self.d, "out"), TODAY)
        self.assertEqual(rc, 4)
        self.assertIn("no owner approval", out)

    def test_approve_then_build(self):
        rc, out = self.approve()
        self.assertEqual(rc, 0, out)
        self.assertIn("Chief of Staff", out)
        out_dir = os.path.join(self.d, "out")
        rc, msg = quiet(ik.build, self.plan, out_dir, TODAY)
        self.assertEqual(rc, 0, msg)
        ctx = rd(os.path.join(out_dir, "standing-context.md"))
        self.assertTrue(ctx.index("Limits card") < ctx.index("Intake packet: Chief of Staff"))
        self.assertIn("Intake packet: Booking Desk", ctx)
        self.assertNotIn("Intake packet: Bookkeeping Desk", ctx)
        self.assertIn("reference/bookkeeping-desk.md", ctx)
        self.assertIn("Payments and Logins: credentials", ctx)
        self.assertTrue(os.path.exists(os.path.join(out_dir, "reference", "bookkeeping-desk.md")))
        man = json.loads(rd(os.path.join(out_dir, "manifest.json")))
        self.assertIn("packets/chief-of-staff.md", man["built_from"])

    def test_build_refused_after_edit(self):
        self.assertEqual(self.approve()[0], 0)
        self.append(os.path.join(self.w, "packets", "booking-desk.md"), "\nOne more line.\n")
        rc, out = quiet(ik.build, self.plan, os.path.join(self.d, "out"), TODAY)
        self.assertEqual(rc, 4)
        self.assertIn("booking-desk.md", out)
        self.assertFalse(os.path.exists(os.path.join(self.d, "out", "standing-context.md")))

    def test_build_refused_if_plan_changes(self):
        self.assertEqual(self.approve()[0], 0)
        self.set_plan(max_age_days=31)
        self.assertEqual(quiet(ik.build, self.plan, os.path.join(self.d, "out"), TODAY)[0], 4)


class Cli(Base):
    def test_exit_codes(self):
        self.assertEqual(quiet(ik.main, ["check", self.plan, "--today", "2026-11-03"])[0], 0)
        self.assertEqual(quiet(ik.main, ["check", self.plan, "--today", "bad"])[0], 2)
        self.assertEqual(quiet(ik.main, [])[0], 2)
        self.assertEqual(quiet(ik.main, ["nope", self.plan])[0], 2)


if __name__ == "__main__":
    unittest.main()
~~~~
