feat(100x-continuity): a factory that builds a team's session handoff - #10
Open
tisu-ai wants to merge 10 commits into
Open
feat(100x-continuity): a factory that builds a team's session handoff#10tisu-ai wants to merge 10 commits into
tisu-ai wants to merge 10 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/holdsset-up-handoff,verifyandstore-service— none of them hand anything over. The skills a person actually talks to,hand-offandpick-up, live undertemplates/kit/and exist only inside emitted Kits.CONTEXT.mdis 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"]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.mdrecords that decision.verifystays separatelyinvokable 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
.gitignoreday is acredential on every laptop.
kit.jsonnever records where the server lives — it ships toevery Teammate. The Operator's own
CLAUDE.mdrecords 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.pyowns five rules: placeholders are all-or-nothing, aKit describes one store, the marketplace
sourceis repo-root relative, a non-emptydirectory with no
kit.jsonis refused, and only the text between its own markers inCLAUDE.mdis rewritten. Those markers carry the Kit's name, because one repo can shiptwo 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
There is deliberately no
s3kind. This engine addresses a store it can list, read backand verify; a presigned PUT can do none of those, so
config.check_store_kindrejectss3andminioby 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.pyships inside every Kit: deterministic, no model, nomoney, driven from a synthetic session in a throwaway
HOME. It branches onkit.jsonandskips the half its store does not have. CI emits a Kit of each store kind and route and
runs it.
The Kit's
evals/areclaude plugin evalcases about what the model does with the twoskills. 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
Python 3.11 is the floor and macOS ships 3.9, so
uv run --python 3.11 --no-project pythonruns 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 fullsession transcript and a folder that syncs to somebody's cloud account.
plugins/100x-continuity/scripts/engine/bundle.py— what refuses a hostile archivebefore 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 decideswho can read whose session, on infrastructure the Operator runs.
principal()failsclosed without a verified identity.
.github/workflows/ci.yml— gains a matrix row for the second suite and a step thatemits 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 like100xeval'sSECRET_PATTERNSand must not be merged withthem: 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 thecontinuity work —
plugins/100xeval/is taken frommainverbatim, including the--commentscorecard this branch predates.