Skip to content

feat(100x-continuity): a factory that builds a team's session handoff - #10

Open
tisu-ai wants to merge 10 commits into
mainfrom
feat/100x-continuity
Open

feat(100x-continuity): a factory that builds a team's session handoff#10
tisu-ai wants to merge 10 commits into
mainfrom
feat/100x-continuity

Conversation

@tisu-ai

@tisu-ai tisu-ai commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

What this is

100x-continuity is a factory. It is not a handoff plugin — it is the thing that writes
one.

An Operator installs it in Claude Code, answers questions about their team and their
storage, and it emits a Kit: one tailored plugin, with the store baked in, written
into their plugin repo with its marketplace row. Their Teammates install the Kit and
use it in Cowork. They never see this.

That one fact explains the layout. skills/ holds set-up-handoff, verify and
store-service — none of them hand anything over. The skills a person actually talks to,
hand-off and pick-up, live under templates/kit/ and exist only inside emitted Kits.
CONTEXT.md is the glossary: Factory, Kit, Emit, Plan, Operator, Teammate, Handle,
Bundle, Store, Registered name, Route, Unreachable.

How setting one up goes

flowchart TD
    A["Operator: build my team a handoff plugin"] --> B[set-up-handoff]
    B --> C{Is a Kit already in this repo?}
    C -->|Yes| D[Read kit.json. Use it for the defaults.]
    C -->|No| E[Read the repo. Find the synced folders.]
    D --> F[Ask: the name, the repo, who it is for]
    E --> F
    F --> G{Where do handoffs go?}
    G -->|A folder the drive syncs| H[Ask for the folder and the group]
    G -->|A store service| I["Ask the name it will be registered under,
    how Teammates reach it, and where its source will live"]
    H --> K[Write continuity-plan.md]
    I --> K
    K --> L{Does the Operator approve the Plan?}
    L -->|No| F
    L -->|Yes| M["Write the Kit, its marketplace row,
    and the Operator's notes into the repo's CLAUDE.md"]
    M --> N{Did the Plan choose a service?}
    N -->|No| V[verify]
    N -->|Yes| S[store-service]
    S --> S1["Copy the server template out,
    to a directory outside the plugin repo"]
    S1 --> S2[Ask which storage vendor. Get credentials.]
    S2 --> S3[Run it from the Operator's own .env and check it answers]
    S3 --> V
    V --> O[Run the contract test. Hand a session over. Open it again.]
    O --> P["Report: proven / proven against a stand-in / not proven yet"]
Loading

One entry point, and Plan and Emit are phases rather than skills. A chain split across
skills has a step nobody starts, and the step that gets skipped is always the last one —
docs/adr/0001-one-setup-skill.md records that decision. verify stays separately
invokable because "a Teammate says pick-up is broken" must not re-interview anyone.

Four decisions worth reviewing

The registered name is chosen at Plan time. The Operator picks it before anything is
written, and builds the server to answer to it afterwards. Asking for the name once a
server exists is how a Kit and a server end up never meeting — a failure nothing reports,
which surfaces as tools that are simply absent.

The store server lives outside the plugin repo. A plugin marketplace is a git clone:
installing one plugin copies the whole repository to every Teammate's machine. Server
source inside it is server source on every laptop, and one bad .gitignore day is a
credential on every laptop. kit.json never records where the server lives — it ships to
every Teammate. The Operator's own CLAUDE.md records it instead.

Emitting is a script, not a copy-and-substitute done by hand. A skill filling twenty
files does it slightly differently every run, and the difference surfaces in a Teammate's
session weeks later. scripts/emit.py owns five rules: placeholders are all-or-nothing, a
Kit describes one store, the marketplace source is repo-root relative, a non-empty
directory with no kit.json is refused, and only the text between its own markers in
CLAUDE.md is rewritten. Those markers carry the Kit's name, because one repo can ship
two Kits.

A Kit checks its store is reachable before it packages anything. No matching tool, or
a folder root that is not on this machine, means nothing was sent — and the skill says
that and stops rather than filing the work where nobody is looking.

Where handoffs go

Store What it is Who can read a handoff
Folder a directory a sync client already watches anyone who can open the folder reads every handoff in it
Service object storage behind an MCP server the Operator runs only the people the sender shared it with

There is deliberately no s3 kind. This engine addresses a store it can list, read back
and verify; a presigned PUT can do none of those, so config.check_store_kind rejects
s3 and minio by name and points at the service store.

A publication is one immutable bundle plus a marker written last. The manifest describes
content and nothing else — no timestamp, no source path, no store — so the same
conversation and files pack to identical bytes and an unchanged republish is recognised
rather than filed twice.

What travels

Travels: the conversation, a readable Digest of it, and the artifacts named at
handoff.

Removed on the way: credential-shaped values. Artifacts are scanned and refused, never
rewritten — they are files a person composed, so a credential-shaped value inside one
stops the publish by name.

Never read: the host's audit log and its session metadata. A handoff is a record of
the conversation, not of the machine it ran on.

Redaction matches shapes. It does not catch a secret written out in prose. Nothing here
describes a handoff as safe or clean.

Two test layers, and only one gates anything

templates/kit/tests/contract_test.py ships inside every Kit: deterministic, no model, no
money, driven from a synthetic session in a throwaway HOME. It branches on kit.json and
skips the half its store does not have. CI emits a Kit of each store kind and route and
runs it.

The Kit's evals/ are claude plugin eval cases about what the model does with the two
skills. They cost money, gate nothing, and exist for the failures a contract test
structurally cannot see — a skill that fires on the wrong words, or one that improvises
when the store cannot be reached.

Checking it locally

cd plugins/100x-continuity
PYTHONPATH=scripts python3 -m unittest discover -s tests -p 'test_*.py'   # 360 tests
python3 plugins/100xeval/skills/100xeval/scripts/run.py eval --static-only  # all plugins 1.00
python3 scripts/check_docs.py

Python 3.11 is the floor and macOS ships 3.9, so uv run --python 3.11 --no-project python
runs the suites on the real floor.

Review note

These files need author ≠ reviewer:

  • plugins/100x-continuity/scripts/engine/redact.py — the only thing between a full
    session transcript and a folder that syncs to somebody's cloud account.
  • plugins/100x-continuity/scripts/engine/bundle.py — what refuses a hostile archive
    before it is unpacked onto a reader's disk, and what refuses to publish a staged file
    holding a credential.
  • plugins/100x-continuity/templates/store-service/server.py — the template that decides
    who can read whose session, on infrastructure the Operator runs. principal() fails
    closed without a verified identity.
  • .github/workflows/ci.yml — gains a matrix row for the second suite and a step that
    emits a Kit and runs its contract test.

No guard is relaxed anywhere in this branch. The CI change is added coverage only.

redact.py's patterns look like 100xeval's SECRET_PATTERNS and must not be merged with
them: the linter optimises for precision, because a false positive costs a plugin its
security sub-score; the redactor optimises for recall, because redacting a placeholder
costs nothing and missing a credential costs everything.

Rebased onto the squashed main

The previous branch is gone, so this one starts from init: 100xtools. It carries only the
continuity work — plugins/100xeval/ is taken from main verbatim, including the
--comment scorecard this branch predates.

tisu-ai and others added 10 commits August 25, 2026 16:06
An Operator installs this in Claude Code, answers questions about their team
and their storage, and it emits a Kit — a tailored plugin, with the store baked
in — into their own plugin repo with its marketplace row. Their Teammates
install the Kit and use `hand-off` / `pick-up` in Cowork; they never see this.

Three skills, one entry point. `set-up-handoff` runs interview → plan →
approval → write → verify in a single turn, because a chain split across skills
has a step nobody starts. `verify` stays separately invokable so a broken
pick-up does not re-interview anyone; `store-service` stands up the MCP server
a service store needs.

Emitting is a script rather than a copy-and-substitute done by hand: one
unfilled placeholder aborts the write, a Kit describes one store, and only the
text between its own markers in the destination `CLAUDE.md` is rewritten. That
marked section is the handover — the factory runs once, so what is still the
Operator's to do lives in their repository rather than in the conversation.

A Kit checks its store is reachable before it packages anything, and says
plainly that nothing was sent when it is not. Every Kit carries a deterministic
contract test; CI emits a Kit of each store kind and route and runs it.

Trust boundary: `scripts/engine/redact.py`, `scripts/engine/bundle.py` and
`templates/store-service/server.py` need a second reader. `.github/workflows/ci.yml`
gains a matrix row and an emitted-Kit step; no guard is relaxed anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…als need

Three things a Kit could not tell you about itself.

A Kit's README now opens with a diagram of how a handoff actually travels —
`hand-off` to the store to `pick-up` — spliced from a fragment, so a folder Kit
draws a cloud drive and a service Kit draws its own server minting one-time
addresses. Neither draws the other; a diagram of a server this team never ran
is a diagram of somebody else's system. Labels use `<br/>` rather than wrapped
quoted strings, which do not parse.

`claude plugin eval` is an early-access command and does not run bare, so the
Kit's evals/README.md is generated with the whole line rather than the bare
command: the two environment variables that admit you to it, `remote_cowork`
because that is the surface a Teammate is actually on, and — for a service Kit
only — `--allow-tools 'mcp__*'`, without which every case fails as though the
skill were broken rather than unequipped. `verify` carries the same note.

A CLAUDE.md created from nothing is now about the handoff plugin rather than
titled after the repository. Naming a fresh file after the repo claims to
describe the whole of it, and this knows about exactly one plugin in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five of nine graders in an emitted Kit could not pass no matter how the
skills behaved, and a sixth passed no matter how they behaved. A $7.55 run
against a real Kit measured almost nothing.

Four were ours and are fixed. `max: 0` with no `min` is not "never" — both
harnesses default `min` to 1, so the range was `1..0`; the zero has to be said
on both ends. Inline `(?i)` is Python and the Kit's cases run on a JavaScript
harness, where the pattern throws before it is applied; `flags: "i"` is the
field that works. Both confirmed against the real loader.

The sixth is worse and is why the eval line now grants tools. `Bash` and
`Write` are gated: without `--allow-tools` the skills are refused the tool they
need to reach their own engine, and "permission denied" reads almost exactly
like "that code didn't open". `pick-up-explains-an-unknown-code` scored 1.00
three times over while `open --handle` was never called once.

`credential-file-stops-and-asks` is now
`hand-off-never-claims-what-it-did-not-do`, graded on the invariant that holds
whatever stops a run — a handoff that stopped is reported as stopped — rather
than on one cause. Its pop-up grader is gone: `AskUserQuestion` is not among
the tools a headless run is offered, so that rule is real, written into the
skill, and simply not observable from here. The Kit's evals/README.md now says
what the harness cannot reach instead of leaving it to be discovered.

Engine discovery gains a third rung. `$CLAUDE_PLUGIN_ROOT` was empty and the
`~/mnt` fallback absent, so a run burned three `find` calls and hit max_turns.
A skill sits at `<plugin>/skills/<name>/`, so `$SKILL_BASE_DIR/../../scripts/`
resolves with nothing set at all — and both skills are now told to stop rather
than hunt when all three miss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The emitted skills said "Everything about where things are kept is already
decided — don't ask, and don't look", and then never said where. The path was
in kit.json and nowhere in the prose, so the only way to act on it was the
thing the skill forbids.

A folder Kit's skills now carry the root and the group, and the sync client is
recognised from the root at emit time, so the passage names it and carries the
one sharp edge that client actually has: iCloud, Google Drive and Dropbox all
drop file contents and leave the name behind, so a short read means wait;
OneDrive under one tenant has a stable path after the home directory. Both
skills also say the drive arrives under `~/mnt/` in Cowork, which is the other
reason a path looks absent when it is not.

Two regressions from the previous commit, both found by linting an emitted Kit
rather than the factory:

SEC3 fired on the emitted `pick-up`. The third discovery rung was
`$SKILL_BASE_DIR/../../scripts/run.py`, and the check looks for a read verb
beside a `../` — the variable is called `OPEN`. It is now
`${SKILL_BASE_DIR%/skills/*}`, which has no traversal to squint at and does not
care how deep the skill sits. Shortest match, not longest: a path with
`/skills/` twice would otherwise strip back too far.

The three rungs were three near-identical `if [ ! -f ]` guards. Now one ordered
list, which reads as "try these in order" and stops repeating itself.

CI now lints every emitted Kit and fails on findings. Gated on findings rather
than score because two engine-path literals are necessarily identical across
the two skills, which costs a little token_efficiency permanently and is not a
defect.

Trust boundary: `.github/workflows/ci.yml` gains that lint step. Verified both
directions — clean Kit exits 0, a Kit with its README removed exits 1. No guard
is relaxed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a bad token

botocore falls back to SigV2 when presigning against a custom endpoint, and
Cloudflare R2 refuses SigV2 outright: `401 Unauthorized: SigV2 authorization is
not supported`. SigV2 is deprecated on AWS S3 and B2 as well, so the default
was wrong everywhere and only R2 was loud about it.

The 401 is the expensive part. It reads exactly like a bad credential, so an
Operator debugging it re-rolls their R2 token, gets the same 401, and concludes
the store service does not work. Found against real R2, not theorised, and the
template now matches the copy that was verified there byte for byte.

Also documented: R2 accepts `ChecksumSHA256` in a presigned PUT, so it joins
AWS and MinIO in the compatibility note, and the credentials reference warns
about the misleading 401 next to the R2 token instructions.

A text-level guard comes with it. `server.py` needs boto3 and fastmcp, which
this repo does not depend on, so nothing imports it and nothing would have
caught a revert. `tests/test_store_service_template.py` asserts the pin, the
import, and that every boto3 client carries the config, which is what a second
client added later would miss. Verified it fails when the fix is removed.

Trust boundary: `templates/store-service/server.py` is on the author-not-reviewer
list. This tightens it. SigV4 replaces a weaker deprecated scheme, no guard is
relaxed, and `principal()` is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most of `set-up-handoff` runs unattended, and what it left undone used to
survive only as a checklist frozen at emit time. Now the whole run goes into
the Operator's repo as tasks at plan time, every one still todo — the
factory's steps and the ones that stay theirs — so they approve a plan they
can see rather than a paragraph. Each is marked off as it lands, with the
evidence that settled it and whether it was proven here or against a
stand-in.

`scripts/board.py` writes and updates it; `templates/status/board.html` is
the page, copied in beside `tasks.json` and reading nothing off the network.
They are one contract split across two files, so the suite asserts the page
reads no field the script never writes.

Four rules the script owns:

- The Operator's half of the board is `emit.operator_items()`, the same list
  their CLAUDE.md checklist renders from. Two hand-written lists of what is
  outstanding is two lists that stop agreeing. The test checks the board
  against the rendered checklist, not against the shared function, because
  comparing a function with itself passes whatever anybody does to it.
- Everything written goes through `redact.py` first. Evidence lines are
  composed from what just happened on the Operator's machine, and the file
  lands in a repo teammates clone. This tightens what can reach that repo.
- `blockedBy` means cannot be started, never comes after. Sequencing the
  factory's own steps through it opened every board with most cards in
  Blocked, which is the opposite of showing somebody their plan.
- No board is ever copied into a Kit. It is the record of one run and it
  holds the Operator's machine; teammates install the Kit.

`operator_items()` gained the leftovers the old checklist was missing: a
verified `principal()`, a bucket retention policy, re-running verify against
the registered server, and somebody other than the author picking a handoff
up. Both surfaces get them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board cannot be seeded until the store is chosen, so the first thing an
Operator saw was a question. `board.py outline` prints the shape of the run
with nothing answered and nothing written — rendered from the same task list
the board is, because a run described in two places is a run described
differently in two places. The server steps are marked as the conditional
ones rather than being hidden or promised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writing `continuity-plan.md` and then retyping the whole thing into the chat
is the same document twice, and the second copy is the one nobody reads. The
skill now says to display the file. The board gets the same treatment: it is
a file and a page, and `board.py show` is for reading the state back, not
for pasting.

Reverts the printed step-1 outline added in 0a5eda2 — it was one more
rendering of a list that already has two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…part

Manifest and marketplace row moved together, and the sample kit.json in
kit-layout.md with them — the factory stamps its own version into every Kit
it writes, so a stale number in the reference is a wrong one. Everything
here goes out at 1.0.0 on the actual roll-out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A real setup run against Cloudflare R2 on 27 Aug found ten things. All of
them are fixed here, and each was verified against source rather than taken
from the run's own account.

**A path on the S3 endpoint silently misfiled everything.** boto3 treats it
as a prefix, so an endpoint ending `/my-bucket` writes every object under a
phantom `my-bucket/` and `put_object` returns 200 either way. A day's
handoffs were stranded before anyone noticed. `_check_endpoint` refuses at
startup rather than stripping the path: silently correcting config somebody
wrote on purpose is the same class of thing as the bug. Every endpoint in
our own `.env.example` passes it, and the test runs the real function by
compiling it out of the template.

**A failed pick-up blamed the sender.** `_PLAIN_FALLBACK` said "nothing was
sent" — true while publishing, a lie while receiving, and by design the
branch every unrecognised failure landed in. Replaced with an `error` object
carrying `code`, `op`, `origin`, `fix_by`, `remedy` and a quarantined
`hint`; the Kit skills compose their own sentence from it. Three things hold
that shape together: `ERROR_CODES` is closed and `UnknownCode` raises, the
transfer path classifies off `wire.TransferError.code` rather than prose,
and no remedy may assert which half failed — `op` says that, and `_BY_SIDE`
splits the one fault with two owners. `emit.error_codes()` renders the table
into the Operator's notes from the registry.

**The eval that should have caught it could not.** Its prompt is a folder
handle, so on a service Kit it never reaches the store. The case belongs in
the contract test, which stubs the transport; the evals README now records
that as a third structural blind spot beside the two already there.

Also: `hand-off` hands back the sentence the receiver pastes rather than a
bare code, named by a new `label` config key; both skill descriptions name
the team, which is the only thing telling two Kits apart in one workspace;
the redaction count is split into credential-shaped values and
sensitive-looking keys, with the patterns deliberately untouched because
that breadth is recall working; `resolve_publication` HEADs the object so an
abandoned publish is refused precisely; `store-service` checks the port
before starting and stops telling people to run a bare `python3` against a
3.11 floor; and the eval suite, a missing `git init` and the local server
left running all reach the board and the notes from one generated list.

Glossary gains **Emptied** — the store answered, the code resolved, the
bytes are gone — and a note that its terms are prose, never field names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant