Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions docs/portable-transcript.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ One JSON document, UTF-8, written to `~/.moshcode/handoffs/<id>.json` with mode
| `changes` | array | yes | files the source session wrote. May be empty. |
| `changes[].path` | string | yes | absolute path as the engine recorded it. |
| `changes[].action` | string | yes | `written`. More verbs may be added; a reader must tolerate one it does not know. |
| `redactions` | object | no (0.2) | `{ total, kinds }`: how many secrets were replaced with `[redacted:<kind>]`, by kind. Never the secrets. |
| `evidence` | object | no (0.2) | what git showed when the handoff ran: `branch`, `head`, `status` (porcelain lines), `untracked`, `diffstat`, `commits` since the session started. Absent outside a repository. |
| `checks` | array | no (0.2) | checks moshcode ran (`--verify`): `label`, `command`, `code` (null if it never started), `timed_out`, `ms`. |

## Rules

Expand Down Expand Up @@ -94,10 +97,34 @@ conversation without checking it.
with a line reading `… (clipped)`. Clipping one message does not change
`source.messages` or `source.dropped`.

**Secrets do not travel.** Every message, and the output of every check, is
redacted before it is written: private key blocks, JWTs, credentials in URLs,
bearer tokens, the key shapes of the common providers (Anthropic, OpenAI,
GitHub, Slack, AWS, Google, Stripe, npm, c0mpute), and `NAME=value` where the
name says it is a secret. There is no flag to turn this off.

**An empty `messages` array is invalid.** A conversation with nothing in it is
not a short conversation, it is a failed read, and a writer must refuse rather
than emit one.

## The brief

Beside every transcript is `<id>.md`, the brief, and that is what the target
engine is told to read first. It is built without a model, from the redacted
transcript, the `evidence` and the `checks`, inside `--budget` tokens (4000 by
default, estimated as characters / 4). Every fact carries where it came from:

| tag | source |
|---|---|
| `[user]` | the operator's own words: the session's first request (the goal) and the most recent ones |
| `[agent-reported]` | the previous engine's last message, which is a claim and may be wrong |
| `[observed]` | git state and check exit codes, which moshcode saw itself |
| `[inferred]` | moshcode's reading of the above, such as "failing checks come first" |

The full transcript stays the record. The brief is the cheap first read, which
is where a handoff saves tokens: the next engine reads about 4000 tokens, not
200 messages, and opens the transcript only if it needs to.

## What is fed to the target engine

The file is not pasted into the engine. Every engine takes its first prompt as
Expand All @@ -116,7 +143,14 @@ where the transcript stops.
```

That keeps one code path for every target engine, and it means the transcript
can be as long as the disk allows.
can be as long as the disk allows. Since 0.2 the seed names the brief first and
the transcript second.

The exception is `c0mpute`, moshcode's own agent on the c0mpute network. It runs
inside moshcode, so it has no argv limit and is given the brief itself as its
first turn. A small model on a CPU node takes a minute per tool call, and
reading a file it could have been handed is one call too many. Its prompts are
read by the network peers that run them, which is why redaction is not optional.

## Which engines can read and write it

Expand All @@ -137,14 +171,16 @@ is short and honest.
| mimocode | no | no, `run` prints one answer and exits |
| aider | no, its history is rendered prose with no turn markers | no, `--message` runs one exchange and exits |
| openagents | no, it launches engines and holds no conversation | no, same reason |
| c0mpute | no, it keeps no session log yet | yes, in process, the brief as its first turn |

A refusal here is always by name and always with the reason. An engine that
cannot be read is not an engine with nothing to say, and an engine that answers
one prompt headlessly is not an engine that can receive a conversation.

## Versioning

`0.1` is the first version. A minor bump adds optional fields and a reader that
`0.1` is the first version. `0.2` adds the optional `redactions`, `evidence`
and `checks` fields. A minor bump adds optional fields and a reader that
does not know them must ignore them. A major bump may change or remove a
required field, and a reader that does not know the major must refuse the file
rather than read what it recognises.
13 changes: 13 additions & 0 deletions src/c0agent-repl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export function parseArgs(argv) {
const opts = { print: null, model: null, agent: null, mode: null, cwd: null, words: [], manage: null, help: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
// Everything after `--` is the prompt, even words that look like flags.
if (a === "--") { opts.words.push(...argv.slice(i + 1)); break; }
if (a === "-h" || a === "--help") opts.help = true;
else if (a === "-p" || a === "--print") { opts.print = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : ""; }
else if (a === "-m" || a === "--model") opts.model = argv[++i];
Expand Down Expand Up @@ -596,6 +598,17 @@ async function repl({ cfg, opts, write }) {
if (saved) log(ash(` the lead agent can now delegate to it; or run it yourself: @${saved.name} <task>`));
}

// A prompt given on the command line is the first turn, the way
// `claude "<prompt>"` starts: interactive, with that already submitted.
// `moshcode handoff <engine> c0mpute` opens the session this way.
const initial = opts.words.join(" ").trim();
if (initial) {
const firstLine = initial.split("\n")[0];
log(`${acid(">")} ${firstLine.length > 100 ? `${firstLine.slice(0, 100)}…` : firstLine}${initial.includes("\n") ? ash(` (+${initial.split("\n").length - 1} lines)`) : ""}`);
try { await runTurn(lead, initial); } catch (e) { spinner.stop(); log(err(String(e?.stack || e))); }
log("");
}

// The loop.
prompt();
for await (const raw of rl) {
Expand Down
22 changes: 15 additions & 7 deletions src/cli-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,21 +312,29 @@ export const CORE_CLI_COMMANDS = [
flags: [
["--session <id>", "which source session, by id or a unique prefix", "the newest for this directory"],
["--cwd <dir>", "the directory whose session to read", "."],
["--max <n>", "how many messages to carry, newest kept", "200"],
["--dry-run", "do everything but launch: the transcript, the fleet record, the argv", ""],
["--json", "machine-readable: the transcript path, the argv, the fleet edge", ""],
["--max <n>", "how many messages to carry in the full transcript, newest kept", "200"],
["--budget <tokens>", "size of the brief the next engine reads first", "4000"],
["--verify [auto|none]", "run the project's own typecheck/lint/test (no shell) and put the exit codes in the brief", "none"],
["--verify=<command>", "run this command instead, split on spaces, no shell; repeatable", ""],
["--verify-timeout <s>", "limit per check", "600"],
["--dry-run", "do everything but launch: the brief, the transcript, the fleet record, the argv", ""],
["--json", "machine-readable: the brief and transcript paths, checks, the argv, the fleet edge", ""],
],
examples: [
["moshcode handoff claude codex", "continue in Codex what Claude Code was doing here"],
["moshcode handoff claude codex --verify", "the same, with the test results in the brief"],
["moshcode handoff claude c0mpute", "out of Claude credits: keep going on the c0mpute agent"],
["moshcode handoff codex omp --session 01a01818", "a particular session, by the start of its id"],
["moshcode handoff opencode claude --dry-run", "see what would be carried, and where"],
],
seeAlso: ["cost", "fleet", "agents", "start"],
note: "reads from claude, codex, opencode and privacycode, the four engines whose session logs hold the conversation and not just "
+ "token counts. launches claude, codex, opencode, privacycode, qwen, gemini or omp, the engines that take a first prompt and stay "
+ "interactive. anything else is refused by name with the reason. the conversation and a list of the files that changed travel; "
+ "the edits do not, because the working tree is already the real state. a transcript that parses to nothing is a refusal, never "
+ "an empty conversation. the rendered file is ~/.moshcode/handoffs/<id>.json in the format documented at "
+ "token counts. launches claude, codex, opencode, privacycode, qwen, gemini, omp, or c0mpute (moshcode's own agent on the c0mpute "
+ "network, whose prompts are read by network peers). anything else is refused by name with the reason. the next engine reads a "
+ "brief first, ~/.moshcode/handoffs/<id>.md: the goal and recent requests [user], the last report [agent-reported], git state and "
+ "check exit codes [observed], and next steps [inferred], built without a model inside --budget. secrets are redacted from "
+ "everything carried, always. the edits do not travel, because the working tree is already the real state. a transcript that "
+ "parses to nothing is a refusal, never an empty conversation. the full transcript is ~/.moshcode/handoffs/<id>.json in the format documented at "
+ "docs/portable-transcript.md, and each handoff writes an OpenFleet record linking the new session to the old one so "
+ "`moshcode fleet tree` shows the edge.",
},
Expand Down
Loading
Loading