sideby 0.9.1 → 0.9.3

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":"20261015T091733-83e791"}
1
+ {"id":"20261015T124638-3ce884"}
@@ -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'] },
@@ -18,6 +18,8 @@ export interface PlanFile {
18
18
  threshold: number;
19
19
  /** False when the session cannot write its Brief: no preparing step, and the threshold only ends the turn. */
20
20
  agentBrief?: boolean;
21
+ /** The parent reads `to` from a manual request; a parent from before 0.9.3 does not write it and ignores `to`. */
22
+ acceptsTo?: true;
21
23
  }
22
24
  /** What the hook already did in one run. */
23
25
  export interface RunState {
@@ -35,6 +37,8 @@ export interface ReadyFile {
35
37
  /** The Brief file, when the session or the user wrote one. */
36
38
  brief?: string;
37
39
  briefSource?: 'agent' | 'user';
40
+ /** `sideby handoff ready --to`: the Account ref to hand over to. */
41
+ to?: string;
38
42
  sessionId?: string;
39
43
  transcriptPath?: string;
40
44
  }
@@ -68,7 +72,8 @@ export declare function readJsonFile<T>(path: string): Promise<T | null>;
68
72
  export declare function writeJsonFile(path: string, value: unknown): Promise<void>;
69
73
  /**
70
74
  * `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).
75
+ * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600); `to` is the
76
+ * Account ref to hand over to.
72
77
  * Returns the request, or null outside such a run.
73
78
  */
74
- export declare function requestHandoff(env: Readonly<Record<string, string | undefined>>, briefFile?: string): Promise<ReadyFile | null>;
79
+ 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>;
@@ -8,6 +8,7 @@ import { join } from 'node:path';
8
8
  import { HOOK_FAMILIES } from '../handoff/hook.js';
9
9
  import { firstPrompt } from '../handoff/messages.js';
10
10
  import { aliasArgs } from './config.js';
11
+ import { UserError } from './errors.js';
11
12
  import { finishBrief } from './handoff-brief.js';
12
13
  import { briefPath, chainDir, ensureChainDir, FILES, HANDOFF_ENV, newId, readJsonFile, writeJsonFile, } from './handoff-chain.js';
13
14
  import { runHost, sidebyBinProblem, spawnHost } from './launch.js';
@@ -131,9 +132,9 @@ export function hookEntryWorks(env) {
131
132
  });
132
133
  }
133
134
  /** The decision, or `none` with the reason when sideby cannot tell (an account removed mid-chain, say). */
134
- async function decide(rt, account, used) {
135
+ async function decide(rt, account, used, to) {
135
136
  try {
136
- return await rt.handoffDecision(account, used);
137
+ return await rt.handoffDecision(account, used, to !== undefined ? { to } : {});
137
138
  }
138
139
  catch (err) {
139
140
  return { kind: 'none', reason: err.message };
@@ -232,6 +233,7 @@ opts = {}) {
232
233
  prepareAt: settings.prepareAt,
233
234
  threshold: settings.threshold,
234
235
  ...(elig.agentBrief === false ? { agentBrief: false } : {}),
236
+ acceptsTo: true,
235
237
  };
236
238
  await writeJsonFile(join(dir, FILES.plan), plan);
237
239
  }
@@ -258,7 +260,7 @@ opts = {}) {
258
260
  return outcome.code;
259
261
  }
260
262
  // 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);
263
+ const decision = await decide(rt, account, used, outcome.ready.to);
262
264
  if (decision.kind !== 'pick') {
263
265
  await rm(join(dir, FILES.ready), { force: true });
264
266
  const code = await host.exited;
@@ -319,7 +321,7 @@ opts = {}) {
319
321
  hop = {
320
322
  ref: next.ref,
321
323
  args: next.family === account.family ? h.continueArgs(hop.args) : [],
322
- prompt: firstPrompt(account.ref, done.path),
324
+ prompt: firstPrompt(account.ref, done.path, ready.trigger),
323
325
  };
324
326
  n++;
325
327
  }
@@ -340,14 +342,19 @@ async function noteUnhandedHighQuota(rt, account, used, threshold, io) {
340
342
  }
341
343
  /**
342
344
  * 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.
345
+ * Accounts so far (or to `to` only); null when `env` is not a run sideby started with Auto Handoff.
344
346
  */
345
- export async function readyDecision(rt, env) {
347
+ export async function readyDecision(rt, env, to) {
346
348
  const dir = env[HANDOFF_ENV.dir];
347
349
  const ref = env.SIDEBY_ACCOUNT;
348
350
  if (!env[HANDOFF_ENV.run] || !dir || !ref)
349
351
  return null;
352
+ if (to !== undefined) {
353
+ const plan = await readJsonFile(join(dir, FILES.plan));
354
+ if (plan && plan.runId === env[HANDOFF_ENV.run] && !plan.acceptsTo)
355
+ throw new UserError(`this session was started by an older sideby, which cannot hand over to a named account; exit it and start ${to} yourself (\`sideby run ${to}\`), or run \`sideby handoff ready\` without --to`, { code: 'parent-too-old' });
356
+ }
350
357
  const account = await rt.resolve(ref);
351
358
  const chain = await readJsonFile(join(dir, FILES.chain));
352
- return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref]);
359
+ return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref], to);
353
360
  }
@@ -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
+ }
@@ -85,6 +85,7 @@ export const claudeHandoff = {
85
85
  ];
86
86
  const settings = {
87
87
  hooks: {
88
+ SessionStart: hook('SessionStart'),
88
89
  PostToolUse: hook('PostToolUse'),
89
90
  Stop: hook('Stop'),
90
91
  StopFailure: [
@@ -7,7 +7,7 @@ import { FILES, HANDOFF_ENV, readJsonFile, writeJsonFile, } from '../core/handof
7
7
  import { claudeHookReader } from '../families/claude/handoff-hook.js';
8
8
  import { codexHookReader } from '../families/codex/handoff-hook.js';
9
9
  import { grokHookReader } from '../families/grok/handoff-hook.js';
10
- import { endTurnMessage, prepareMessage, stopMessage, thresholdMessage } from './messages.js';
10
+ import { endTurnMessage, prepareMessage, sessionStartMessage, stopMessage, thresholdMessage, } from './messages.js';
11
11
  import { field } from './reader.js';
12
12
  export const HOOK_COMMAND = 'sideby handoff-hook';
13
13
  const READERS = {
@@ -17,7 +17,7 @@ const READERS = {
17
17
  };
18
18
  /** Families whose hooks this entry understands; only they can start a Handoff. */
19
19
  export const HOOK_FAMILIES = Object.keys(READERS);
20
- const EVENTS = new Set(['PostToolUse', 'Stop', 'StopFailure']);
20
+ const EVENTS = new Set(['SessionStart', 'PostToolUse', 'Stop', 'StopFailure']);
21
21
  async function readAll(stream) {
22
22
  const chunks = [];
23
23
  try {
@@ -29,8 +29,8 @@ async function readAll(stream) {
29
29
  }
30
30
  return Buffer.concat(chunks).toString('utf8');
31
31
  }
32
- const contextOutput = (text) => ({
33
- hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: text },
32
+ const contextOutput = (text, hookEventName = 'PostToolUse') => ({
33
+ hookSpecificOutput: { hookEventName, additionalContext: text },
34
34
  });
35
35
  const blockOutput = (reason) => ({ decision: 'block', reason });
36
36
  async function mtimeMs(path) {
@@ -67,6 +67,14 @@ export async function decide(opts) {
67
67
  return;
68
68
  await writeJsonFile(readyFile, { runId, at: now.toISOString(), ...session, ...r });
69
69
  };
70
+ // The session learns it may move the task on when the user asks; without this it tells the user to start the
71
+ // other account by hand.
72
+ if (event === 'SessionStart') {
73
+ const ref = env.SIDEBY_ACCOUNT;
74
+ return ref && usable
75
+ ? { output: contextOutput(sessionStartMessage(ref, usable.brief), 'SessionStart') }
76
+ : {};
77
+ }
70
78
  const threshold = usable?.threshold ?? 95;
71
79
  const pressure = reader.pressure ? await reader.pressure(input, env, now) : null;
72
80
  if (event === 'StopFailure') {
@@ -3,5 +3,13 @@ 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
+ /** At session start (Claude): this session may hand its task to another account when the user asks. */
7
+ export declare function sessionStartMessage(ref: string, brief: string): string;
8
+ declare const WHY: {
9
+ readonly threshold: 'its quota ran low';
10
+ readonly limit: 'its quota ran out';
11
+ readonly manual: 'the user asked to hand over';
12
+ };
6
13
  /** 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;
14
+ export declare function firstPrompt(fromRef: string, brief: string, trigger: keyof typeof WHY): string;
15
+ export {};
@@ -21,7 +21,16 @@ 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
+ /** At session start (Claude): this session may hand its task to another account when the user asks. */
25
+ export function sessionStartMessage(ref, brief) {
26
+ return `sideby: this session runs in ${ref} under sideby with Auto Handoff. If the user wants this task to go on in another account ("continue in cc008", "hand it back to cc008"), do not ask them to start that account: write a handoff brief to ${brief} (${TEMPLATE}; ${RULES}), run \`sideby handoff ready --to <account>\` (an account or short command), tell the user the account it prints, and end your turn. sideby then ends this session and starts that account in this terminal.`;
27
+ }
28
+ const WHY = {
29
+ threshold: 'its quota ran low',
30
+ limit: 'its quota ran out',
31
+ manual: 'the user asked to hand over',
32
+ };
24
33
  /** 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.`;
34
+ export function firstPrompt(fromRef, brief, trigger) {
35
+ 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
36
  }
@@ -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,
@@ -112,7 +112,7 @@ export interface FamilyDef {
112
112
  handoff?: FamilyHandoff;
113
113
  }
114
114
  /** A Host hook event sideby's handoff hook listens to. */
115
- export type HandoffHookEvent = 'PostToolUse' | 'Stop' | 'StopFailure';
115
+ export type HandoffHookEvent = 'SessionStart' | 'PostToolUse' | 'Stop' | 'StopFailure';
116
116
  /** Whether one run may start a Handoff, or start as the next Account of one, and why not. */
117
117
  export interface HandoffEligibility {
118
118
  start: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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
  }
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: sideby
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.
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, moving the current task to another account ("用 cc008 继续", "交回 cc008"), 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 (inside an Auto Handoff session, `sideby handoff ready --to` moves the task itself).
4
4
  metadata:
5
- version: "0.9.1"
5
+ version: "0.9.3"
6
6
  requires:
7
7
  bins: [sideby, node]
8
8
  cliHelp: sideby --help
@@ -20,7 +20,7 @@ Do every step you can yourself. Only browser sign-in, secrets and interactive se
20
20
  - **Never sign in for the user.** Give them `sideby login <ref>` as one line to run in their own terminal (Claude Code: `! sideby login <ref>`). OAuth needs their browser and their decision.
21
21
  - **Never put a secret on the command line or in chat.** For API accounts, the user edits `proxy.env` themselves.
22
22
  - **Never start a Host session from your shell.** `sideby run`, `sideby next` (without `--dry-run`) and `sideby login` open an interactive session; give the user the command instead.
23
- - **Never switch, rotate or proxy accounts yourself.** Do not work around sideby by editing config, copying session files or moving credentials. `sideby next` only recommends. Moving a running task on its own is Auto Handoff, which the user turns on: use `sideby handoff enable` (exit 10, then their yes), never hand-edit `config.json`. Inside such a session you may run `sideby handoff ready` when the user asks to hand over.
23
+ - **Never switch, rotate or proxy accounts yourself.** Do not work around sideby by editing config, copying session files or moving credentials. `sideby next` only recommends. Moving a running task on its own is Auto Handoff, which the user turns on: use `sideby handoff enable` (exit 10, then their yes), never hand-edit `config.json`. Inside such a session (the environment has `SIDEBY_HANDOFF_RUN`), when the user wants the task to go on in another account ("用 cc008 继续", "交回 cc008"), do not hand them a command to start it: update the brief, run `sideby handoff ready --to <account>`, tell them the account it prints, and end your turn; sideby starts that account in the same terminal.
24
24
  - **Say what you change.** Read-only commands first. What the user asked for (the account they asked to create, its short command, the shell line that makes it work) you do, saying what you do. Changes they did not ask for (`doctor --fix` on other accounts, `quota setup`, `app install`) wait until they have seen the change and agreed.
25
25
  - Do not edit Host config files (`settings.json`, `config.toml`, symlinks in account directories) by hand when a `sideby` command does the job.
26
26
 
@@ -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