sideby 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.json CHANGED
@@ -1 +1 @@
1
- {"id":"20261015T030631-4ed31c"}
1
+ {"id":"20261015T120149-abb6bb"}
@@ -733,17 +733,23 @@ export async function cmdHandoff(a, io) {
733
733
  if (sub === 'enable' || sub === 'disable')
734
734
  return cmdHandoffSwitch(a, io, sub);
735
735
  if (sub !== 'ready')
736
- throw new UsageError('usage: sideby handoff status | enable [--same-family] [--order <family>=<acct,…>] [--threshold n] [--prepare-at n] [--yes] | disable [--yes] | ready [--brief <file>] | setup|teardown <family> [--yes]');
736
+ throw new UsageError('usage: sideby handoff status | enable [--same-family] [--order <family>=<acct,…>] [--threshold n] [--prepare-at n] [--yes] | disable [--yes] | ready [--to <account>] [--brief <file>] | setup|teardown <family> [--yes]');
737
737
  const flag = a.flags.get('brief');
738
738
  const brief = typeof flag === 'string' ? flag : undefined;
739
- const decision = await readyDecision(await createRuntime(), process.env);
739
+ const toFlag = a.flags.get('to');
740
+ if (toFlag === true)
741
+ throw new UsageError('usage: sideby handoff ready --to <account> [--brief <file>]');
742
+ const rt = await createRuntime();
743
+ // An Account or a short command; resolve() says what to do when there is no such Account.
744
+ const to = toFlag === undefined ? undefined : (await rt.resolve(toFlag)).ref;
745
+ const decision = await readyDecision(rt, process.env, to);
740
746
  if (decision && decision.kind !== 'pick')
741
- throw new UserError(`nothing can take over now: ${explain(decision)}; keep working in this account`, {
742
- code: 'no-pick',
743
- });
747
+ throw new UserError(to
748
+ ? `cannot hand over to ${to}: ${explain(decision)}; keep working in this account or pick another with --to`
749
+ : `nothing can take over now: ${explain(decision)}; keep working in this account`, { code: 'no-pick' });
744
750
  let request;
745
751
  try {
746
- request = await requestHandoff(process.env, brief);
752
+ request = await requestHandoff(process.env, brief, to);
747
753
  }
748
754
  catch (err) {
749
755
  throw new UserError(`cannot read the brief ${brief}: ${err.message}; check the path and retry`, {
@@ -752,10 +758,16 @@ export async function cmdHandoff(a, io) {
752
758
  }
753
759
  if (!request)
754
760
  throw new UserError('not inside a session sideby started with Auto Handoff; set `handoff.auto` in the config and start the account with sideby (`sideby run`, a short command), or use `sideby next <family>`', { code: 'not-in-handoff-run' });
761
+ // The account that will really take over; `sideby next` cannot tell, it does not know the chain.
762
+ const next = decision?.kind === 'pick' ? decision.ref : undefined;
755
763
  if (a.flags.has('json'))
756
- json(io, { requested: true, ...(request.brief ? { brief: request.brief } : {}) });
764
+ json(io, {
765
+ requested: true,
766
+ ...(next ? { next } : {}),
767
+ ...(request.brief ? { brief: request.brief } : {}),
768
+ });
757
769
  else
758
- io.out('The next account starts when this turn ends; finish the turn now.');
770
+ io.out(`${next ? `${next} takes over` : 'The next account starts'} when this turn ends; finish the turn now.`);
759
771
  return 0;
760
772
  }
761
773
  export async function cmdPlugins(a, io) {
@@ -351,7 +351,12 @@ export const OUTPUT_SCHEMAS = {
351
351
  nextSteps: Type.Array(Str),
352
352
  }, { title: `sideby handoff ${sub} --json` }),
353
353
  ])),
354
- 'handoff-ready': Type.Object({ schemaVersion: Version, requested: Type.Literal(true), brief: Type.Optional(Str) }, { title: 'sideby handoff ready --json' }),
354
+ 'handoff-ready': Type.Object({
355
+ schemaVersion: Version,
356
+ requested: Type.Literal(true),
357
+ next: Type.Optional(Str),
358
+ brief: Type.Optional(Str),
359
+ }, { title: 'sideby handoff ready --json' }),
355
360
  'handoff-setup': Type.Object({ schemaVersion: Version, family: Str, applied: Type.Boolean(), plan: QuotaSetupPlan }, { title: 'sideby handoff setup --json' }),
356
361
  'handoff-teardown': Type.Object({ schemaVersion: Version, family: Str, ok: Type.Boolean(), message: Str, diff: Type.Optional(Str) }, { title: 'sideby handoff teardown --json' }),
357
362
  'quota-teardown': Type.Object({ schemaVersion: Version, family: Str, ok: Type.Boolean(), message: Str, diff: Type.Optional(Str) }, { title: 'sideby quota teardown --json' }),
@@ -39,7 +39,8 @@ Usage:
39
39
  --order claude=codex001,cc002, --threshold n, --prepare-at n
40
40
  sideby handoff disable [--yes] turn it off, keeping the other settings
41
41
  sideby handoff ready [--brief <f>] inside a session sideby started with Auto Handoff: hand over to
42
- the next account when this turn ends (config \`handoff\`)
42
+ the next account when this turn ends (config \`handoff\`);
43
+ --to <account> names it, also one this chain already used
43
44
  sideby handoff setup grok [--yes] add sideby's hook file so Grok can hand over at its limit
44
45
  sideby handoff teardown grok remove it
45
46
  sideby ui [--port n] [--no-open] open the local panel
@@ -180,7 +181,7 @@ const COMMANDS = {
180
181
  model: { flags: ['json', 'yes'], valued: ['effort'] },
181
182
  handoff: {
182
183
  flags: ['json', 'yes', 'same-family', 'no-same-family'],
183
- valued: ['brief', 'order', 'prepare-at', 'threshold', 'wait-minutes', 'countdown'],
184
+ valued: ['brief', 'to', 'order', 'prepare-at', 'threshold', 'wait-minutes', 'countdown'],
184
185
  },
185
186
  ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
186
187
  app: { flags: ['json'], valued: ['url'] },
@@ -35,6 +35,8 @@ export interface ReadyFile {
35
35
  /** The Brief file, when the session or the user wrote one. */
36
36
  brief?: string;
37
37
  briefSource?: 'agent' | 'user';
38
+ /** `sideby handoff ready --to`: the Account ref to hand over to. */
39
+ to?: string;
38
40
  sessionId?: string;
39
41
  transcriptPath?: string;
40
42
  }
@@ -68,7 +70,8 @@ export declare function readJsonFile<T>(path: string): Promise<T | null>;
68
70
  export declare function writeJsonFile(path: string, value: unknown): Promise<void>;
69
71
  /**
70
72
  * `sideby handoff ready` (spec §3.17): asks the parent to hand over after this turn. Works only inside a run sideby
71
- * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600).
73
+ * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600); `to` is the
74
+ * Account ref to hand over to.
72
75
  * Returns the request, or null outside such a run.
73
76
  */
74
- export declare function requestHandoff(env: Readonly<Record<string, string | undefined>>, briefFile?: string): Promise<ReadyFile | null>;
77
+ export declare function requestHandoff(env: Readonly<Record<string, string | undefined>>, briefFile?: string, to?: string): Promise<ReadyFile | null>;
@@ -50,10 +50,11 @@ export async function writeJsonFile(path, value) {
50
50
  }
51
51
  /**
52
52
  * `sideby handoff ready` (spec §3.17): asks the parent to hand over after this turn. Works only inside a run sideby
53
- * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600).
53
+ * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600); `to` is the
54
+ * Account ref to hand over to.
54
55
  * Returns the request, or null outside such a run.
55
56
  */
56
- export async function requestHandoff(env, briefFile) {
57
+ export async function requestHandoff(env, briefFile, to) {
57
58
  const runId = env[HANDOFF_ENV.run];
58
59
  const dir = env[HANDOFF_ENV.dir];
59
60
  if (!runId || !dir)
@@ -68,6 +69,7 @@ export async function requestHandoff(env, briefFile) {
68
69
  at: new Date().toISOString(),
69
70
  trigger: 'manual',
70
71
  ...(brief ? { brief, briefSource: 'user' } : {}),
72
+ ...(to ? { to } : {}),
71
73
  };
72
74
  await writeJsonFile(join(dir, FILES.request), request);
73
75
  return request;
@@ -28,6 +28,6 @@ opts?: {
28
28
  }): Promise<number | null>;
29
29
  /**
30
30
  * For `sideby handoff ready`: where a Handoff from the session running under `env` would go now, using the chain's
31
- * Accounts so far; null when `env` is not a run sideby started with Auto Handoff.
31
+ * Accounts so far (or to `to` only); null when `env` is not a run sideby started with Auto Handoff.
32
32
  */
33
- export declare function readyDecision(rt: Runtime, env: Env): Promise<HandoffDecision | null>;
33
+ export declare function readyDecision(rt: Runtime, env: Env, to?: string): Promise<HandoffDecision | null>;
@@ -131,9 +131,9 @@ export function hookEntryWorks(env) {
131
131
  });
132
132
  }
133
133
  /** The decision, or `none` with the reason when sideby cannot tell (an account removed mid-chain, say). */
134
- async function decide(rt, account, used) {
134
+ async function decide(rt, account, used, to) {
135
135
  try {
136
- return await rt.handoffDecision(account, used);
136
+ return await rt.handoffDecision(account, used, to !== undefined ? { to } : {});
137
137
  }
138
138
  catch (err) {
139
139
  return { kind: 'none', reason: err.message };
@@ -258,7 +258,7 @@ opts = {}) {
258
258
  return outcome.code;
259
259
  }
260
260
  // Nothing to move on to (or better to wait): leave the session alone and say why once it ends.
261
- const decision = await decide(rt, account, used);
261
+ const decision = await decide(rt, account, used, outcome.ready.to);
262
262
  if (decision.kind !== 'pick') {
263
263
  await rm(join(dir, FILES.ready), { force: true });
264
264
  const code = await host.exited;
@@ -319,7 +319,7 @@ opts = {}) {
319
319
  hop = {
320
320
  ref: next.ref,
321
321
  args: next.family === account.family ? h.continueArgs(hop.args) : [],
322
- prompt: firstPrompt(account.ref, done.path),
322
+ prompt: firstPrompt(account.ref, done.path, ready.trigger),
323
323
  };
324
324
  n++;
325
325
  }
@@ -340,14 +340,14 @@ async function noteUnhandedHighQuota(rt, account, used, threshold, io) {
340
340
  }
341
341
  /**
342
342
  * For `sideby handoff ready`: where a Handoff from the session running under `env` would go now, using the chain's
343
- * Accounts so far; null when `env` is not a run sideby started with Auto Handoff.
343
+ * Accounts so far (or to `to` only); null when `env` is not a run sideby started with Auto Handoff.
344
344
  */
345
- export async function readyDecision(rt, env) {
345
+ export async function readyDecision(rt, env, to) {
346
346
  const dir = env[HANDOFF_ENV.dir];
347
347
  const ref = env.SIDEBY_ACCOUNT;
348
348
  if (!env[HANDOFF_ENV.run] || !dir || !ref)
349
349
  return null;
350
350
  const account = await rt.resolve(ref);
351
351
  const chain = await readJsonFile(join(dir, FILES.chain));
352
- return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref]);
352
+ return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref], to);
353
353
  }
@@ -50,4 +50,9 @@ export declare function selectNext(input: {
50
50
  now: Date;
51
51
  /** Decide now what happens when the origin's fullest window reaches the threshold (see `waitUntil`). */
52
52
  ahead?: boolean;
53
+ /**
54
+ * `sideby handoff ready --to`: the only candidate, which may be an Account this chain already used (handing back to
55
+ * an Account whose Quota came back); every other rule still applies.
56
+ */
57
+ to?: string;
53
58
  }): HandoffDecision;
@@ -52,13 +52,19 @@ function stateOf(c, s, now) {
52
52
  }
53
53
  export function selectNext(input) {
54
54
  const { origin, settings: s, now } = input;
55
+ const refOf = (name) => input.aliases[name] ?? name;
56
+ const byRef = new Map(input.candidates.map((c) => [c.ref, c]));
57
+ if (input.to !== undefined) {
58
+ const target = byRef.get(refOf(input.to));
59
+ if (!target)
60
+ return { kind: 'none', reason: `${input.to} is not an account sideby can find` };
61
+ return toTarget(origin, target, s, new Set(s.crossOrganization.map(refOf)), now);
62
+ }
55
63
  const until = waitUntil(origin, s, now, input.ahead === true);
56
64
  if (until)
57
65
  return { kind: 'wait', until };
58
- const refOf = (name) => input.aliases[name] ?? name;
59
66
  const allowed = new Set(s.crossOrganization.map(refOf));
60
67
  const used = new Set([origin.ref, ...input.used]);
61
- const byRef = new Map(input.candidates.map((c) => [c.ref, c]));
62
68
  const family = s.families[origin.family];
63
69
  let list;
64
70
  if (family?.policy === 'order') {
@@ -100,3 +106,32 @@ export function selectNext(input) {
100
106
  ...withSkipped,
101
107
  };
102
108
  }
109
+ /** Why `target` may not take over from `origin`, or null when it may; the same rules as `selectNext` but `used`. */
110
+ function refusal(origin, target, s, allowed) {
111
+ if (target.ref === origin.ref)
112
+ return 'it is the account this session runs in';
113
+ if (origin.sameLogin?.includes(target.ref))
114
+ return `it is the same login as ${origin.ref}, so it shares the same quota`;
115
+ if (!target.installed)
116
+ return 'its host is not installed';
117
+ if (!target.receive)
118
+ return 'it cannot take over a task';
119
+ if (!s.sameFamily && target.family === origin.family && target.kind !== 'api')
120
+ return 'handing over within one host is off; turn on `handoff.sameFamily`';
121
+ if (!allowed.has(target.ref) && !sameOrganization(origin.identity, target.identity))
122
+ return `it is signed in to another organization; add it to \`handoff.crossOrganization\` to allow it`;
123
+ return null;
124
+ }
125
+ function toTarget(origin, target, s, allowed, now) {
126
+ const why = refusal(origin, target, s, allowed);
127
+ if (why)
128
+ return { kind: 'none', reason: `${target.ref} may not take over: ${why}` };
129
+ const e = stateOf(target, s, now);
130
+ if (e.state === 'ready' || e.state === 'unknown' || e.state === 'api')
131
+ return { kind: 'pick', ref: target.ref };
132
+ return {
133
+ kind: 'none',
134
+ reason: `${target.ref} cannot take over now: ${e.state === 'full' ? 'its quota is full' : 'it is signed out'}`,
135
+ ...(e.state === 'full' && e.resetsAt ? { earliestReset: { ref: target.ref, at: e.resetsAt } } : {}),
136
+ };
137
+ }
@@ -6,12 +6,14 @@ import { editConfigAccounts } from './config.js';
6
6
  import { UserError } from './errors.js';
7
7
  import { lineDiff } from './handoff-status.js';
8
8
  function pick(key, sideby, host, last) {
9
+ // What the Account really ran last beats the Host's default: subscription Accounts often share one settings.json,
10
+ // so its value would show the same for all of them.
9
11
  if (sideby[key])
10
12
  return { value: sideby[key], from: 'sideby' };
11
- if (host[key])
12
- return { value: host[key], from: 'host' };
13
13
  if (last[key])
14
14
  return { value: last[key], from: 'last' };
15
+ if (host[key])
16
+ return { value: host[key], from: 'host' };
15
17
  return undefined;
16
18
  }
17
19
  /** `last` is the model and effort of the newest session, from the usage reader when the caller already has it. */
@@ -3,5 +3,11 @@ export declare function thresholdMessage(brief: string, pressure: number): strin
3
3
  /** At the threshold when the session cannot write a Brief: sideby puts one together from its records. */
4
4
  export declare function endTurnMessage(pressure: number): string;
5
5
  export declare function stopMessage(brief: string): string;
6
+ declare const WHY: {
7
+ readonly threshold: 'its quota ran low';
8
+ readonly limit: 'its quota ran out';
9
+ readonly manual: 'the user asked to hand over';
10
+ };
6
11
  /** The next session's first prompt: the Brief's path only, never its text (it would show in a process list). */
7
- export declare function firstPrompt(fromRef: string, brief: string): string;
12
+ export declare function firstPrompt(fromRef: string, brief: string, trigger: keyof typeof WHY): string;
13
+ export {};
@@ -21,7 +21,12 @@ export function endTurnMessage(pressure) {
21
21
  export function stopMessage(brief) {
22
22
  return `sideby: before you stop, update the handoff brief at ${brief} (${TEMPLATE}; ${RULES}), then end your turn.`;
23
23
  }
24
+ const WHY = {
25
+ threshold: 'its quota ran low',
26
+ limit: 'its quota ran out',
27
+ manual: 'the user asked to hand over',
28
+ };
24
29
  /** The next session's first prompt: the Brief's path only, never its text (it would show in a process list). */
25
- export function firstPrompt(fromRef, brief) {
26
- return `This task was handed over from ${fromRef} by sideby because its quota ran low. Read ${brief} first, then continue the task it describes.`;
30
+ export function firstPrompt(fromRef, brief, trigger) {
31
+ return `This task was handed over from ${fromRef} by sideby because ${WHY[trigger]}. Read ${brief} first, then continue the task it describes. If the user asks to move the task to another account (back to ${fromRef}, say), run \`sideby handoff ready --to <account>\` and end your turn.`;
27
32
  }
@@ -256,6 +256,7 @@ declare const en: {
256
256
  'md.from.sideby': string;
257
257
  'md.from.host': string;
258
258
  'md.from.last': string;
259
+ 'md.newDefault': string;
259
260
  'md.none': string;
260
261
  'md.click': string;
261
262
  'md.envNote': string;
@@ -258,6 +258,7 @@ const en = {
258
258
  'md.from.sideby': 'set by sideby for this account',
259
259
  'md.from.host': 'from {host}’s settings',
260
260
  'md.from.last': 'used in the last session',
261
+ 'md.newDefault': 'new sessions start with {value} ({from})',
261
262
  'md.none': 'not known yet',
262
263
  'md.click': 'Click to change.',
263
264
  'md.envNote': 'ANTHROPIC_MODEL or CLAUDE_CODE_EFFORT_LEVEL in your shell, and settings from your organization, override this; sideby cannot see them.',
@@ -720,6 +721,7 @@ const zh = {
720
721
  'md.from.sideby': 'sideby 为这个账号设置',
721
722
  'md.from.host': '来自 {host} 的设置',
722
723
  'md.from.last': '最近一次会话用的',
724
+ 'md.newDefault': '新会话默认是 {value}({from})',
723
725
  'md.none': '暂时不知道',
724
726
  'md.click': '点击修改。',
725
727
  'md.envNote': 'shell 里的 ANTHROPIC_MODEL、CLAUDE_CODE_EFFORT_LEVEL 和组织下发的设置会覆盖这里,sideby 看不到它们。',
@@ -2079,10 +2079,21 @@ const MAIN = String.raw `
2079
2079
  function modelChip(a) {
2080
2080
  const mi = a.modelInfo
2081
2081
  if (!mi || (!mi.model && !mi.effort)) return null
2082
- const text = [mi.model && mi.model.value, mi.effort && mi.effort.value].filter(Boolean).join(' · ')
2082
+ // The row has little room; the hover text keeps the full model id.
2083
+ const text = [mi.model && mi.model.value.replace(/^claude-/, ''), mi.effort && mi.effort.value].filter(Boolean).join(' · ')
2084
+ // A value from the last session may differ from what a new session starts with; say both.
2085
+ const line = (label, v, key) => {
2086
+ if (!v) return null
2087
+ const out = label + ': ' + v.value + ' (' + modelFrom(a, v.from) + ')'
2088
+ const host = mi.host && mi.host[key]
2089
+ // 'opus' and 'claude-opus-5-5' name the same model: only a real difference is worth a line.
2090
+ return v.from === 'last' && host && !v.value.includes(host)
2091
+ ? out + '; ' + t('md.newDefault', { value: host, from: modelFrom(a, 'host') })
2092
+ : out
2093
+ }
2083
2094
  const title = [
2084
- mi.model ? t('md.model') + ': ' + mi.model.value + ' (' + modelFrom(a, mi.model.from) + ')' : null,
2085
- mi.effort ? t('md.effort') + ': ' + mi.effort.value + ' (' + modelFrom(a, mi.effort.from) + ')' : null,
2095
+ line(t('md.model'), mi.model, 'model'),
2096
+ line(t('md.effort'), mi.effort, 'effort'),
2086
2097
  mi.settable ? t('md.click') : null,
2087
2098
  ].filter(Boolean).join('\n')
2088
2099
  if (!mi.settable) return h('span', { class: 'mono', title }, text)
@@ -239,10 +239,11 @@ export interface Runtime {
239
239
  /**
240
240
  * Where an Auto Handoff from `origin` would go now (spec §3.17 "选号"): every Account of every Family is a
241
241
  * candidate; `used` lists the chain's Accounts so far. `ahead` decides early what happens once the origin's fullest
242
- * window reaches the threshold, as plan.json and the Panel need.
242
+ * window reaches the threshold, as plan.json and the Panel need. `to` names the only candidate (`handoff ready --to`).
243
243
  */
244
244
  handoffDecision(origin: Account, used: readonly string[], opts?: {
245
245
  ahead?: boolean;
246
+ to?: string;
246
247
  }): Promise<HandoffDecision>;
247
248
  readContext(): ReadContext;
248
249
  }
@@ -666,6 +666,7 @@ export async function createRuntime(opts = {}) {
666
666
  used,
667
667
  now: now(),
668
668
  ...(opts.ahead ? { ahead: true } : {}),
669
+ ...(opts.to !== undefined ? { to: opts.to } : {}),
669
670
  });
670
671
  },
671
672
  readContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
5
5
  "license": "MIT",
6
6
  "author": "Adonis0123 (https://github.com/Adonis0123)",
@@ -15,6 +15,9 @@
15
15
  "type": "boolean",
16
16
  "const": true
17
17
  },
18
+ "next": {
19
+ "type": "string"
20
+ },
18
21
  "brief": {
19
22
  "type": "string"
20
23
  }
@@ -2,7 +2,7 @@
2
2
  name: sideby
3
3
  description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
4
4
  metadata:
5
- version: "0.9.0"
5
+ version: "0.9.2"
6
6
  requires:
7
7
  bins: [sideby, node]
8
8
  cliHelp: sideby --help
@@ -41,7 +41,7 @@ With config `"handoff": { "auto": true }`, a session sideby started hands its ta
41
41
 
42
42
  - When sideby tells you the quota is at 80%, write the handoff brief at the path it names and keep it current (do not tell the user that another account will take over; sideby decides that and says so itself): the goal, what is done, the remaining steps, key decisions and why, the files that matter. Reference specs, ADRs, commits and diffs by path; list the skills the next agent should use; never include keys, passwords or personal data.
43
43
  - When it says the quota is at 95%, finish the current step, update the brief, and end your turn. sideby starts the next account from it.
44
- - When the user asks to hand over now, update the brief, run `sideby handoff ready` (add `--brief <file>` when the user's own handoff tool wrote one), and end your turn.
44
+ - When the user asks to hand over now, update the brief, run `sideby handoff ready` (add `--brief <file>` when the user's own handoff tool wrote one), and end your turn. Tell the user the account it prints, not what `sideby next` says: the chain never goes back to an account it already used. When the user names an account ("go back to cc008"), run `sideby handoff ready --to <account>`; it works for an account this chain already used.
45
45
 
46
46
  ### Turning it on for the user
47
47