@shardflux/cli 0.15.2 → 0.17.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +144 -14
  2. package/README.md +92 -7
  3. package/dist/acp-models.js +5 -1
  4. package/dist/agent-commands.d.ts +15 -1
  5. package/dist/agent-commands.js +84 -23
  6. package/dist/agent-models.d.ts +61 -0
  7. package/dist/agent-models.js +79 -0
  8. package/dist/agent-state.d.ts +10 -0
  9. package/dist/agent-state.js +13 -0
  10. package/dist/agent-target-commands.d.ts +13 -3
  11. package/dist/agent-target-commands.js +129 -23
  12. package/dist/agent-targets.d.ts +5 -0
  13. package/dist/agent-targets.js +14 -5
  14. package/dist/agent-terminal.d.ts +8 -0
  15. package/dist/agent-terminal.js +35 -2
  16. package/dist/args.d.ts +5 -1
  17. package/dist/args.js +74 -11
  18. package/dist/cli-proxy-forward.d.ts +45 -0
  19. package/dist/cli-proxy-forward.js +432 -0
  20. package/dist/cli-proxy-hub.d.ts +273 -0
  21. package/dist/cli-proxy-hub.js +749 -0
  22. package/dist/cli-proxy.d.ts +325 -0
  23. package/dist/cli-proxy.js +1016 -0
  24. package/dist/codex-cloud.d.ts +3 -0
  25. package/dist/codex-cloud.js +17 -5
  26. package/dist/command-center-actions.js +25 -1
  27. package/dist/commands.d.ts +22 -0
  28. package/dist/commands.js +54 -14
  29. package/dist/conversation-commands.js +6 -0
  30. package/dist/conversation-local.js +2 -2
  31. package/dist/conversation-stash.js +4 -3
  32. package/dist/conversation-terminal.d.ts +5 -0
  33. package/dist/conversation-terminal.js +54 -6
  34. package/dist/conversations.d.ts +31 -3
  35. package/dist/conversations.js +53 -10
  36. package/dist/device-approvals.d.ts +12 -0
  37. package/dist/device-approvals.js +73 -0
  38. package/dist/device-helper.d.ts +3 -1
  39. package/dist/device-helper.js +14 -4
  40. package/dist/errors.d.ts +7 -0
  41. package/dist/errors.js +12 -2
  42. package/dist/files-mirror.d.ts +164 -0
  43. package/dist/files-mirror.js +761 -0
  44. package/dist/handlers.js +5 -1
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.js +3 -0
  47. package/dist/linux-helper.d.ts +12 -0
  48. package/dist/linux-helper.js +88 -0
  49. package/dist/linux-terminal.d.ts +11 -0
  50. package/dist/linux-terminal.js +42 -0
  51. package/dist/main.js +38 -8
  52. package/dist/onboarding.d.ts +2 -0
  53. package/dist/onboarding.js +53 -5
  54. package/dist/registry.d.ts +2 -42
  55. package/dist/registry.js +6 -90
  56. package/dist/t3-launch.d.ts +37 -0
  57. package/dist/t3-launch.js +133 -0
  58. package/dist/t3-mode.d.ts +121 -0
  59. package/dist/t3-mode.js +654 -0
  60. package/dist/t3-phone.d.ts +17 -0
  61. package/dist/t3-phone.js +15 -0
  62. package/dist/t3-thread-script.d.ts +18 -0
  63. package/dist/t3-thread-script.js +245 -0
  64. package/dist/terminal-onboarding.d.ts +11 -0
  65. package/dist/terminal-onboarding.js +75 -0
  66. package/dist/thread-on-computer.d.ts +35 -0
  67. package/dist/thread-on-computer.js +206 -0
  68. package/dist/update.d.ts +1 -1
  69. package/dist/update.js +12 -1
  70. package/dist/wait-status.d.ts +22 -0
  71. package/dist/wait-status.js +59 -0
  72. package/dist/worktree-twin-engine.d.ts +125 -0
  73. package/dist/worktree-twin-engine.js +615 -0
  74. package/dist/worktree-twin-land.py +331 -0
  75. package/dist/worktree-twin.d.ts +313 -0
  76. package/dist/worktree-twin.js +843 -0
  77. package/package.json +3 -3
@@ -1,11 +1,14 @@
1
1
  import type { CodexCloudTask } from '@shardflux/sdk';
2
2
  import type { Ctx, Handler } from './commands.js';
3
3
  import type { Values } from './args.js';
4
+ import type { ModelChoice } from './agent-models.js';
4
5
  import type { Conversation } from './conversations.js';
5
6
  import type { InitialImage } from './initial-prompt.js';
6
7
  /** Only provider-returned task ids enter the registered Codex deep-link route. */
7
8
  export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env']): Promise<void>;
8
9
  export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
10
+ /** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
11
+ export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
9
12
  /** How long the account's Codex remote device may take to come up before it counts as stuck. */
10
13
  export declare const CODEX_STARTING_LIMIT_MS: number;
11
14
  export type CodexConnectorView = {
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
2
2
  import { CodedError, UsageError } from "./errors.js";
3
3
  import { json } from "./format.js";
4
4
  import { syncAgentCredentials } from "./agent-credentials.js";
5
- import { createConversation, resumeConversation } from "./conversations.js";
5
+ import { createConversation, getConversation, recordedChoice, resumeConversation } from "./conversations.js";
6
+ import { CHOICE_PENDING, choiceReport, hasChoice, modelChoice } from "./agent-models.js";
6
7
  import { AgentStateStore } from "./agent-state.js";
7
8
  import { ensureProjectGit } from "./project-git.js";
8
9
  import { gitRunner } from "./agent-git.js";
@@ -32,15 +33,25 @@ export async function submitCloudInitialPrompt(ctx, c, prompt, workspaceKey) {
32
33
  throw new CodedError('initial_prompt_failed', `Created ${workspaceKey}, but the initial prompt could not be confirmed. Open that conversation before starting another.`, undefined, { workspace_key: workspaceKey, conversation_id: c.id, submitted: null });
33
34
  }
34
35
  }
35
- async function output(ctx, c, task, submitted = false) {
36
+ /** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
37
+ export async function printCloudThread(ctx, c, task, submitted = false, choice) { return output(ctx, c, task, submitted, choice); }
38
+ async function output(ctx, c, task, submitted = false, choice) {
36
39
  const url = threadLink(task);
40
+ // model_choice (0.17.0+): an admitted thread's record keeps the model and effort only when Cloud started it with them.
41
+ const applied = hasChoice(choice) ? await cloudChoiceApplied(ctx, c, choice) : true;
37
42
  if (ctx.json) {
38
- ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}) }));
43
+ ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}), ...(hasChoice(choice) ? { model_choice: choiceReport(choice, applied) } : {}) }));
39
44
  return;
40
45
  }
46
+ if (!applied)
47
+ ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
41
48
  ctx.io.stdout.write(`${c.key}\n${task.desktop_url}\n`);
42
49
  await openCodexThread(task, ctx.io.env);
43
50
  }
51
+ async function cloudChoiceApplied(ctx, c, choice) {
52
+ const kept = recordedChoice(await getConversation(ctx, c.id));
53
+ return kept.model === choice.model && kept.effort === choice.effort;
54
+ }
44
55
  /** How long the account's Codex remote device may take to come up before it counts as stuck. */
45
56
  export const CODEX_STARTING_LIMIT_MS = 5 * 60_000;
46
57
  /** When a connector generation started: its UUIDv7 time. */
@@ -112,11 +123,12 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
112
123
  throw new UsageError('New Codex conversations open in Codex Cloud. Omit --in or use --in codex-app.');
113
124
  if (values.like !== undefined)
114
125
  throw new UsageError('Choose the project directory with --path; new Codex conversations use its latest machine template.');
126
+ const choice = modelChoice(values, 'codex');
115
127
  const project = projectIdentity(path);
116
128
  await ensureProjectGit(ctx, project.folder);
117
129
  await gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal)(['rev-parse', 'HEAD']);
118
130
  await ensureCodexCloud(ctx);
119
- const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined);
131
+ const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined, choice);
120
132
  const c = created.conversation;
121
133
  // --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
122
134
  if (values.computer === true)
@@ -130,7 +142,7 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
130
142
  const submitted = typeof values.initialPrompt === 'string';
131
143
  if (submitted)
132
144
  await submitCloudInitialPrompt(ctx, c, values.initialPrompt, created.workspace.key);
133
- await output(ctx, c, task, submitted);
145
+ await output(ctx, c, task, submitted, choice);
134
146
  return 0;
135
147
  }
136
148
  export async function openCodexConversation(ctx, c) {
@@ -11,7 +11,9 @@ import { agentName as gitAgentName } from "./agent-git.js";
11
11
  import { randomUUID } from 'node:crypto';
12
12
  import { prepareAgentMachine } from "./agent-machine.js";
13
13
  import { OperationFailedError } from '@shardflux/sdk';
14
- import { launchConversationProject } from "./conversation-terminal.js";
14
+ import { launchConversationProject, launchProjectCommand } from "./conversation-terminal.js";
15
+ import { startThreadOn } from "./thread-on-computer.js";
16
+ import { launchT3 } from "./t3-launch.js";
15
17
  /** L9 can register target-specific handlers after detecting the installed applications. */
16
18
  export async function availableTargets(ctx) {
17
19
  const { HANDLERS } = await import("./handlers.js");
@@ -34,6 +36,28 @@ export async function agentAction(ctx, action, values, args) {
34
36
  }
35
37
  export const CENTER_HANDLERS = {
36
38
  async 'agents new'(ctx, values, [ref]) {
39
+ const fresh = !ref && values.name === undefined && values.like === undefined;
40
+ // T3 Code mode (0.17.0+): --in t3 starts the first message as a T3 thread (--on: on that computer's T3 server);
41
+ // --in auto opens where open_in says.
42
+ if (fresh && values.in === 't3')
43
+ return launchT3(ctx, values, str(values.path) ?? './', str(values.app));
44
+ if (fresh && values.in === 'auto') {
45
+ const next = { ...values };
46
+ delete next.in;
47
+ return launchProjectCommand(ctx, next, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
48
+ }
49
+ if (values.in === 'auto') {
50
+ values = { ...values };
51
+ delete values.in;
52
+ }
53
+ // A thread on an existing conversation's computer (0.16.0+).
54
+ if (values.on !== undefined) {
55
+ if (ref || values.like !== undefined)
56
+ throw new UsageError('--on starts a thread on that conversation’s computer; omit the agent argument and --like.');
57
+ return startThreadOn(ctx, values, str(values.path) ?? './');
58
+ }
59
+ if (values.review === true)
60
+ throw new UsageError('--review needs --on <conversation>.');
37
61
  if ((values.prompt !== undefined || values['prompt-file'] !== undefined || values.image !== undefined) && (ref || values.name !== undefined || values.like !== undefined || values.in !== undefined)) {
38
62
  throw new UsageError('An initial prompt needs a new conversation: use agents new --path <folder> --app claude|codex.');
39
63
  }
@@ -31,6 +31,11 @@ export interface Ctx {
31
31
  wake: boolean;
32
32
  /** Bound on lifecycle waits per cell call, wakes included (--wake-timeout / SHARDFLUX_WAKE_TIMEOUT_MS). */
33
33
  wakeTimeoutMs: number;
34
+ /**
35
+ * The wake bound was given (--wake-timeout or SHARDFLUX_WAKE_TIMEOUT_MS; 0.16.0+): it is kept as given. Otherwise a
36
+ * resume queued for capacity is waited for until its deadline, its place shown on stderr.
37
+ */
38
+ wakeTimeoutExplicit?: boolean;
34
39
  signal: AbortSignal;
35
40
  /** --timing: print the SDK's timing of the command's open, wait or wake (stderr; a `timing` field with --json). */
36
41
  timing: boolean;
@@ -127,4 +132,21 @@ export declare function ttlSeconds(values: Values, b: {
127
132
  max: number;
128
133
  range: string;
129
134
  }): number | undefined;
135
+ type ThreadRow = import('@shardflux/sdk').Agent & {
136
+ conversation_id?: string | null;
137
+ conversation_key?: string;
138
+ };
139
+ /**
140
+ * Each session's own conversation: the one whose harness session it is, else the computer's only live conversation.
141
+ * Rows that cannot be told apart keep no conversation rather than another thread's.
142
+ */
143
+ export declare function withConversations(rows: import('@shardflux/sdk').Agent[], conversations: {
144
+ id: string;
145
+ key: string;
146
+ workspace_id: string;
147
+ harness_session_id: string | null;
148
+ state: string;
149
+ ended_at?: string | null;
150
+ }[] | null): ThreadRow[];
130
151
  export declare const KEY_HANDLERS: Record<string, Handler>;
152
+ export {};
package/dist/commands.js CHANGED
@@ -12,7 +12,7 @@ import { agentConfigLikeSecrets, syncAgentConfig } from "./agent-config-sync.js"
12
12
  import { randomUUID } from 'node:crypto';
13
13
  import { access, readFile, writeFile } from 'node:fs/promises';
14
14
  import { basename, dirname, resolve as resolvePath } from 'node:path';
15
- import { EXECUTION_ID, ExecStartError, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, ShardfluxProtocolError, newExecutionId, exitCodePosix } from '@shardflux/sdk';
15
+ import { EXECUTION_ID, ExecStartError, capacityOf, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, ShardfluxProtocolError, newExecutionId, exitCodePosix } from '@shardflux/sdk';
16
16
  import { parse as parseYaml } from 'yaml';
17
17
  import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
18
18
  import { ExecSessionRunningError, ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
@@ -32,7 +32,15 @@ export const str = (v) => (typeof v === 'string' ? v : undefined);
32
32
  * The workspace's cell client as this invocation configures it: the CLI's agent label, wake on use unless turned off,
33
33
  * and the wake timeout (an OperationTimeoutError past it exits 5).
34
34
  */
35
- const cellOf = (ctx, ws) => ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs, ...(ctx.wake ? {} : { wake: null }) });
35
+ const cellOf = (ctx, ws) => ws.cell({
36
+ agentLabel: ctx.agentLabel,
37
+ transitionTimeoutMs: ctx.wakeTimeoutMs,
38
+ ...(!ctx.wake
39
+ ? { wake: null }
40
+ : ctx.wakeTimeoutExplicit
41
+ ? { wake: (timeoutMs, signal) => ws.wake({ timeoutMs, ...(signal ? { signal } : {}), agentLabel: ctx.agentLabel, untilCapacityDeadline: false }) }
42
+ : {}),
43
+ });
36
44
  export const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
37
45
  const MAX_SECRET_VALUE_BYTES = 65_536;
38
46
  /** `files patch --content-file`: a patch request is at most 7 MiB; larger files go through `files write`. */
@@ -357,10 +365,17 @@ async function writeNew(path, text, force) {
357
365
  throw new UsageError(`${path} exists; pass --force to overwrite it`);
358
366
  await writeFile(path, text);
359
367
  }
368
+ /**
369
+ * An explicit `--timeout` is kept as given (0.16.0+): without one, a wait whose start is queued for capacity keeps waiting
370
+ * until the start's deadline (the SDK's `untilCapacityDeadline`), printing where it stands on stderr.
371
+ */
372
+ function capacityWait(values) {
373
+ return values !== undefined && str(values.timeout) !== undefined ? { untilCapacityDeadline: false } : {};
374
+ }
360
375
  /** Waits with the SDK's backoff; Ctrl-C reports the operation that keeps running. */
361
- async function waitOperation(ctx, operationId, timeoutMs) {
376
+ async function waitOperation(ctx, operationId, timeoutMs, values) {
362
377
  try {
363
- return await ctx.cloud().workspaces.waitForOperation(operationId, { timeoutMs, signal: ctx.signal });
378
+ return await ctx.cloud().workspaces.waitForOperation(operationId, { timeoutMs, signal: ctx.signal, ...capacityWait(values) });
364
379
  }
365
380
  catch (err) {
366
381
  if (ctx.signal.aborted)
@@ -782,7 +797,28 @@ function reportExecution(ctx, ref, r, opts) {
782
797
  }
783
798
  function lifecycleText(verb, w, op) {
784
799
  const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
785
- return `${verb} ${w.key !== undefined ? `${w.key} (${w.id})` : w.id}: operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
800
+ // A start queued for capacity (0.16.0+): its place in the queue.
801
+ const position = capacityOf(op)?.queuePosition;
802
+ const why = [op.state_reason, position ? `position ${position} in the queue` : null].filter(Boolean).join('; ');
803
+ return `${verb} ${w.key !== undefined ? `${w.key} (${w.id})` : w.id}: operation ${op.id} ${op.kind} ${op.state}${why ? ` (${why})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
804
+ }
805
+ /**
806
+ * Each session's own conversation: the one whose harness session it is, else the computer's only live conversation.
807
+ * Rows that cannot be told apart keep no conversation rather than another thread's.
808
+ */
809
+ export function withConversations(rows, conversations) {
810
+ if (!conversations)
811
+ return rows;
812
+ const live = conversations.filter(c => c.state !== 'done' && c.state !== 'archived' && !c.ended_at);
813
+ return rows.map(a => {
814
+ const session = a.id.slice(a.workspace_id.length + 1);
815
+ const bound = live.filter(c => c.workspace_id === a.workspace_id);
816
+ // An API that names the session's conversation (0.16.0+) decides; older ones are matched here by the same rule.
817
+ const named = a.conversation_id;
818
+ const c = (named ? conversations.find(c => c.id === named) : undefined)
819
+ ?? conversations.find(c => c.workspace_id === a.workspace_id && c.harness_session_id === session) ?? (bound.length === 1 ? bound[0] : undefined);
820
+ return c ? { ...a, conversation_id: c.id, conversation_key: c.key } : a;
821
+ });
786
822
  }
787
823
  /** The project-key commands; handlers.ts adds the account plane's. */
788
824
  function agentGrace(text) {
@@ -807,6 +843,9 @@ export const KEY_HANDLERS = {
807
843
  ...(str(values.limit) ? { limit: parsePositiveInt(str(values.limit), 'limit', 200) } : {}),
808
844
  };
809
845
  const rows = [];
846
+ // Which conversation each session belongs to (0.16.0+): several threads can share one computer. Read alongside
847
+ // the sessions; an API without conversations leaves the rows as they are.
848
+ const threads = import("./conversations.js").then(m => m.listConversations(ctx)).catch(() => null);
810
849
  let cursor = str(values.cursor);
811
850
  let next;
812
851
  do {
@@ -820,7 +859,8 @@ export const KEY_HANDLERS = {
820
859
  rows.splice(0, rows.length, ...await withLiveAgents(ctx, rows, { ...(state ? { state } : {}), labels }));
821
860
  // LAST MESSAGE (0.13.4+): the session's latest message, from an API that reports it.
822
861
  const message = (a) => !a.last_message ? '-' : a.last_message.length > 60 ? `${a.last_message.slice(0, 59)}…` : a.last_message;
823
- out(ctx, rows.length ? table(['WORKSPACE', 'APP', 'STATE', 'TITLE', 'LAST MESSAGE'], rows.map(a => [a.workspace_key, a.app, a.state, a.title, message(a)])) : 'No agent sessions.\n', { data: rows, next_cursor: next });
862
+ const enriched = withConversations(rows, await threads);
863
+ out(ctx, rows.length ? table(['CONVERSATION', 'COMPUTER', 'APP', 'STATE', 'TITLE', 'LAST MESSAGE'], enriched.map(a => [a.conversation_key?.split('/').at(-1) ?? '-', a.workspace_key, a.app, a.state, a.title, message(a)])) : 'No agent sessions.\n', { data: enriched, next_cursor: next });
824
864
  if (!ctx.json && next)
825
865
  ctx.io.stderr.write(`more: shard agents list --cursor ${next}\n`);
826
866
  return 0;
@@ -1090,7 +1130,7 @@ export const KEY_HANDLERS = {
1090
1130
  ...(mode ? { mode } : {}),
1091
1131
  ...(computerUse !== undefined ? { computerUse } : {}),
1092
1132
  agentLabel: ctx.agentLabel,
1093
- wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
1133
+ wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal, ...capacityWait(values) },
1094
1134
  }));
1095
1135
  const op = ws.activeOperation;
1096
1136
  if (ws.ready && ws.mode === 'processful' && ws.labels.shard_project)
@@ -1653,7 +1693,7 @@ export const KEY_HANDLERS = {
1653
1693
  });
1654
1694
  const op = ws.activeOperation;
1655
1695
  if (values.wait !== false && !ws.ready && op) {
1656
- await waitOperation(ctx, op.id, timeoutMs);
1696
+ await waitOperation(ctx, op.id, timeoutMs, values);
1657
1697
  ws = await ctx.cloud().workspaces.get(ws.id);
1658
1698
  }
1659
1699
  const human = ws.ready
@@ -1849,7 +1889,7 @@ export const KEY_HANDLERS = {
1849
1889
  let ws = opened.workspace;
1850
1890
  const op = opened.operation;
1851
1891
  if (values.wait !== false && !ws.ready && op) {
1852
- await waitOperation(ctx, op.id, timeoutMs);
1892
+ await waitOperation(ctx, op.id, timeoutMs, values);
1853
1893
  ws = await cloud.workspaces.get(ws.id);
1854
1894
  }
1855
1895
  const human = ws.ready
@@ -1875,7 +1915,7 @@ export const KEY_HANDLERS = {
1875
1915
  const label = str(values.label);
1876
1916
  let op = await draft.captureState(label === undefined ? {} : { label });
1877
1917
  if (values.wait === true)
1878
- op = await waitOperation(ctx, op.id, waitTimeout(values));
1918
+ op = await waitOperation(ctx, op.id, waitTimeout(values), values);
1879
1919
  const checkpoint = typeof op.result?.checkpoint_id === 'string' ? `state ${op.result.checkpoint_id}\n` : '';
1880
1920
  out(ctx, `Capturing a state of the ${slug} draft: operation ${op.id} ${op.kind} ${op.state}\n${checkpoint}${op.state === 'succeeded' ? '' : `wait with: shard operations wait ${op.id}\n`}`, op);
1881
1921
  return 0;
@@ -1902,7 +1942,7 @@ export const KEY_HANDLERS = {
1902
1942
  let ws = await draft.openTestInstance({ ...(state ? { stateId: state } : {}), ...(instanceKey ? { key: instanceKey } : {}), ...(c ? { caps: c } : {}), agentLabel: ctx.agentLabel, wait: false });
1903
1943
  const op = ws.activeOperation;
1904
1944
  if (values.wait !== false && !ws.ready && op) {
1905
- await waitOperation(ctx, op.id, timeoutMs);
1945
+ await waitOperation(ctx, op.id, timeoutMs, values);
1906
1946
  ws = await ctx.cloud().workspaces.get(ws.id);
1907
1947
  }
1908
1948
  const human = ws.ready
@@ -1931,7 +1971,7 @@ export const KEY_HANDLERS = {
1931
1971
  throw new UsageError('refusing to discard without --yes (the draft is deleted and its live test instances end)');
1932
1972
  let op = await ctx.cloud().templates.draft(slug).discard();
1933
1973
  if (values.wait === true)
1934
- op = await waitOperation(ctx, op.id, waitTimeout(values));
1974
+ op = await waitOperation(ctx, op.id, waitTimeout(values), values);
1935
1975
  out(ctx, `Discarding the ${slug} draft: operation ${op.id} ${op.kind} ${op.state}\n`, op);
1936
1976
  return 0;
1937
1977
  },
@@ -2344,7 +2384,7 @@ export const KEY_HANDLERS = {
2344
2384
  const timeoutMs = waitTimeout(values);
2345
2385
  const op = values.durable === true
2346
2386
  ? await waited(ctx, `operation ${id} continues server side.`, () => ctx.cloud().workspaces.waitForDurable(id, { timeoutMs, signal: ctx.signal }))
2347
- : await waitOperation(ctx, id, timeoutMs);
2387
+ : await waitOperation(ctx, id, timeoutMs, values);
2348
2388
  out(ctx, operationDetail(op), op);
2349
2389
  return 0;
2350
2390
  },
@@ -2488,7 +2528,7 @@ async function resolveSecret(ctx, values, ref) {
2488
2528
  }
2489
2529
  /** `--wait`: the SDK's own wait (the call resolves once the operation finished; one trace covers request and wait). */
2490
2530
  function lifecycleOptions(ctx, values, timeoutMs) {
2491
- return values.wait === true ? { wait: { timeoutMs, signal: ctx.signal } } : {};
2531
+ return values.wait === true ? { wait: { timeoutMs, signal: ctx.signal, ...capacityWait(values) } } : {};
2492
2532
  }
2493
2533
  async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
2494
2534
  const timeoutMs = waitTimeout(values);
@@ -1,5 +1,6 @@
1
1
  import { out, str } from "./commands.js";
2
2
  import { UsageError } from "./errors.js";
3
+ import { FilesMirror, humanBytes, listMirrorConversations, workspaceHub } from "./files-mirror.js";
3
4
  export function conversationClient(ctx) {
4
5
  const key = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
5
6
  const session = Boolean(ctx.io.env.SHARDFLUX_SESSION_TOKEN?.trim()) || ctx.store.profile(ctx.apiUrl).session !== undefined;
@@ -32,5 +33,10 @@ export const CONVERSATION_HANDLERS = {
32
33
  out(ctx, `${r.title}: ${r.landed.how}\n`, r);
33
34
  return 0;
34
35
  },
36
+ async 'files pull'(ctx, _values, [conversation, path]) {
37
+ const r = await new FilesMirror(ctx, workspaceHub(ctx)).pull(conversation, path, () => listMirrorConversations(ctx));
38
+ out(ctx, r.already ? `${r.local_path} is already downloaded.\n` : `Downloaded ${r.path} (${humanBytes(r.bytes)}) to ${r.local_path}\n`, r);
39
+ return 0;
40
+ },
35
41
  async 'conversations move'(ctx, _values, [id, to]) { const r = await conversationClient(ctx).conversations.move(id, to, { signal: ctx.signal }); out(ctx, `Move scheduled for the end of the turn.\n`, r); return 0; },
36
42
  };
@@ -1,5 +1,5 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
- import { getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
2
+ import { conversationEnded, getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
3
3
  import { ensureConversationWorktree, devicePort } from "./device-bridge.js";
4
4
  import { startProjectSync } from "./project-sync.js";
5
5
  import { projectIdentity } from "./project-identity.js";
@@ -48,7 +48,7 @@ export const CONVERSATION_HANDLERS = {
48
48
  try {
49
49
  while (!ctx.signal.aborted) {
50
50
  const next = await getConversation(ctx, c.id);
51
- if (next.state === 'done' || next.state === 'archived' || next.workspace_id !== c.workspace_id)
51
+ if (conversationEnded(next) || next.workspace_id !== c.workspace_id)
52
52
  break;
53
53
  await delay(1000, undefined, { signal: ctx.signal }).catch(() => undefined);
54
54
  }
@@ -182,7 +182,7 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
182
182
  const c = all.find(c => c.id === record.conversation.id);
183
183
  if (!c || record.handed_off || !c.harness_session_id)
184
184
  continue;
185
- const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary]);
185
+ const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary, c.ended_at]);
186
186
  if (state.mirrored.get(c.id) === revision)
187
187
  continue;
188
188
  let retry = state.retries.get(c.id);
@@ -220,8 +220,9 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
220
220
  await mirrorConversation(ctx, { ...record, conversation: c }, source, skipped);
221
221
  retry.failures = 0;
222
222
  retry.retryAt = 0;
223
- // An ended record may precede its VM tombstone; retry until the handoff receipt is written.
224
- if (c.state !== 'done' && c.state !== 'archived')
223
+ // An ended record may precede its VM tombstone; retry until the handoff receipt is written. A finished turn is
224
+ // `done` while its VM lives: mirrored once per revision, and again when it ends.
225
+ if (c.state !== 'archived' && !(c.state === 'done' && c.ended_at))
225
226
  state.mirrored.set(c.id, revision);
226
227
  }
227
228
  catch (e) {
@@ -4,5 +4,10 @@ import type { Values } from './args.js';
4
4
  import type { Conversation } from './conversations.js';
5
5
  /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
6
6
  export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values): Promise<number>;
7
+ /**
8
+ * `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
9
+ * Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
10
+ */
11
+ export declare const launchProjectCommand: Handler;
7
12
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
8
13
  export declare const launchConversationProject: Handler;
@@ -1,7 +1,9 @@
1
+ import { linuxTerminalSession } from "./linux-terminal.js";
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { AgentStateStore } from "./agent-state.js";
3
4
  import { projectIdentity, projectLabels } from "./project-identity.js";
4
- import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
5
+ import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, recordedChoice, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
6
+ import { modelChoice } from "./agent-models.js";
5
7
  import { terminal } from "./agent-commands.js";
6
8
  import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
7
9
  import { syncAgentCredentials } from "./agent-credentials.js";
@@ -15,15 +17,47 @@ import { localResumeTerminal } from "./conversation-local-resume.js";
15
17
  import { agentReachability } from "./agent-reachability.js";
16
18
  import { promptWithImages, readInitialImages, readInitialPrompt, uploadInitialImages } from "./initial-prompt.js";
17
19
  import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
20
+ import { startThreadOn } from "./thread-on-computer.js";
21
+ import { openInChoices } from "./t3-mode.js";
22
+ import { launchT3 } from "./t3-launch.js";
23
+ import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
24
+ import { openUrl } from "./browser.js";
25
+ import { out } from "./commands.js";
18
26
  /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
19
27
  export async function conversationTerminal(ctx, ws, c, project, values) {
20
28
  if (c.route === 'codex-cloud')
21
29
  return openCodexConversation(ctx, c);
22
30
  await restoreConversationBinding(ctx, c, ws);
23
- return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id });
31
+ // A thread on another conversation's computer has its own PTY there, never the VM's own (0.16.0+).
32
+ const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
33
+ return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c) });
24
34
  }
35
+ /**
36
+ * `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
37
+ * Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
38
+ */
39
+ export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
40
+ const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
41
+ if (values.resume !== true && values.review !== true) {
42
+ const project = projectIdentity(path);
43
+ const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
44
+ const where = process.platform === 'linux' && ctx.io.stdin.isTTY ? 'terminal' : openInChoices(ctx, project.slug)[app];
45
+ if (where === 't3')
46
+ return launchT3(ctx, values, path, app);
47
+ if (values.on === undefined && app === 'claude' && (where === 'claude-desktop' || where === 'web'))
48
+ return startConversation(ctx, values, [path, ...(explicit ? [explicit] : [])], where);
49
+ }
50
+ return launchConversationProject(ctx, values, [path, ...(explicit ? [explicit] : [])]);
51
+ };
25
52
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
26
- export const launchConversationProject = async (ctx, values, [path = './', explicit]) => {
53
+ export const launchConversationProject = (ctx, values, args) => startConversation(ctx, values, args);
54
+ async function startConversation(ctx, values, [path = './', explicit], openIn) {
55
+ if (values.on !== undefined) {
56
+ const app = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
57
+ return startThreadOn(ctx, values, path, app);
58
+ }
59
+ if (values.review === true)
60
+ throw new UsageError('--review needs --on <conversation>.');
27
61
  const text = await readInitialPrompt(ctx, values);
28
62
  // Images are read and validated before allocation, held in memory, and written only into the new VM.
29
63
  const images = await readInitialImages(ctx, values, text);
@@ -33,6 +67,8 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
33
67
  const app = explicit === 'claude' || explicit === 'codex' ? explicit : store.app(project.remote ?? project.folder);
34
68
  if (explicit && explicit !== app)
35
69
  throw new UsageError('Choose claude or codex.');
70
+ // Checked before a VM is allocated; the new conversation's record carries it to its guest (0.17.0+).
71
+ const choice = modelChoice(values, app);
36
72
  if (values.resume === true) {
37
73
  // The native pickers are VM-local. C-K6's project picker must select the binding first.
38
74
  const records = localConversations(ctx);
@@ -53,7 +89,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
53
89
  const ws = await conversationWorkspace(ctx, next);
54
90
  return conversationTerminal(ctx, ws, next, project.remote ?? project.folder, values);
55
91
  }
56
- if (app === 'codex')
92
+ if (app === 'codex' && !linuxTerminalSession(ctx, values))
57
93
  return launchCodexCloud(ctx, values, path, images);
58
94
  await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
59
95
  const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
@@ -61,7 +97,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
61
97
  requireClaudeTerminal(ctx);
62
98
  let created;
63
99
  try {
64
- created = await createConversation(ctx, project.folder, app, 'terminal');
100
+ created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice);
65
101
  }
66
102
  catch (e) {
67
103
  if (!conversationsUnavailable(e))
@@ -93,5 +129,17 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
93
129
  if (!(await savedMachine(ctx))?.template)
94
130
  await syncAgentConfig(ctx, ws);
95
131
  await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
132
+ if (openIn) {
133
+ // The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
134
+ const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
135
+ await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true });
136
+ if (openIn === 'claude-desktop')
137
+ return openClaudeDesktopSession(ctx, ws, c);
138
+ const url = await remoteControlUrl(ctx, ws, c);
139
+ if (!ctx.json && !openUrl(url, ctx.io.env))
140
+ throw new Error(`could not open ${url}`);
141
+ out(ctx, `${url}\n`, { target: 'web', workspace: ws.data, conversation_id: c.id, url });
142
+ return 0;
143
+ }
96
144
  return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values);
97
- };
145
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Workspace, WorkspaceView } from '@shardflux/sdk';
2
2
  import type { Ctx } from './commands.js';
3
+ import type { ModelChoice } from './agent-models.js';
3
4
  export type Harness = 'claude' | 'codex';
4
5
  export interface Conversation {
5
6
  id: string;
@@ -17,6 +18,11 @@ export interface Conversation {
17
18
  state: 'working' | 'needs_you' | 'unresolved' | 'done' | 'archived';
18
19
  created_at: string;
19
20
  ended_at: string | null;
21
+ landed?: {
22
+ how: string;
23
+ at: string;
24
+ ref: string;
25
+ } | null;
20
26
  last_activity_at?: string;
21
27
  durable?: {
22
28
  branch_sha: string;
@@ -26,6 +32,14 @@ export interface Conversation {
26
32
  } | null;
27
33
  ports?: number[];
28
34
  restore?: Record<string, unknown> | null;
35
+ /** The repository and the branch it started from, from APIs that return them. */
36
+ repo_remote?: string;
37
+ base_branch?: string | null;
38
+ role?: string;
39
+ join_mode?: string | null;
40
+ /** The model and effort its harness starts with; null is the agent's own default (0.17.0+, APIs that keep them). */
41
+ model?: string | null;
42
+ effort?: string | null;
29
43
  }
30
44
  export interface LocalConversation {
31
45
  api_url: string;
@@ -33,21 +47,35 @@ export interface LocalConversation {
33
47
  project_id: string;
34
48
  conversation: Conversation;
35
49
  handed_off?: boolean;
50
+ /** The branch the conversation started from (0.16.0+), for APIs that do not return base_branch yet. */
51
+ base_branch?: string;
36
52
  }
53
+ /**
54
+ * Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
55
+ * while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
56
+ */
57
+ export declare const conversationEnded: (c: Pick<Conversation, "state" | "ended_at" | "route" | "landed">) => boolean;
37
58
  export declare const conversationName: (c: Conversation) => string;
38
59
  export declare function validateConversation(c: Conversation): Conversation;
39
60
  export declare function localConversations(ctx: Pick<Ctx, 'store' | 'apiUrl'>): Record<string, LocalConversation>;
40
- export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>, c: Conversation, folder: string, handedOff?: boolean): void;
61
+ export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>, c: Conversation, folder: string, handedOff?: boolean, baseBranch?: string): void;
41
62
  export declare function conversationsUnavailable(e: unknown): boolean;
42
63
  export declare function getConversation(ctx: Ctx, id: string): Promise<Conversation>;
43
64
  export declare function listConversations(ctx: Ctx, projectId?: string): Promise<Conversation[]>;
44
65
  export declare function resolveConversation(ctx: Ctx, ref: string, folder?: string): Promise<Conversation>;
45
66
  /** Re-resolve the binding on every attach: a move changes the VM, never the conversation identity. */
46
67
  export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promise<Workspace>;
68
+ /**
69
+ * A thread on an existing computer (a join) is prepared by that computer's guest: its worktree, then its harness. Waits
70
+ * until the guest confirms it (the conversation no longer carries restore instructions), within the wake timeout.
71
+ */
72
+ export declare function waitThreadReady(ctx: Ctx, c: Conversation, pollMs?: number): Promise<Conversation>;
47
73
  export declare function restoreConversationBinding(ctx: Ctx, c: Conversation, ws: Workspace): Promise<void>;
48
- export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string): Promise<Conversation>;
49
- export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string): Promise<{
74
+ export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice): Promise<Conversation>;
75
+ export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice): Promise<{
50
76
  conversation: Conversation;
51
77
  workspace: Workspace;
52
78
  }>;
53
79
  export declare function resumeConversation(ctx: Ctx, c: Conversation): Promise<Conversation>;
80
+ /** The model and effort a conversation's harness starts with, as its record keeps them. */
81
+ export declare const recordedChoice: (c: Pick<Conversation, "model" | "effort">) => ModelChoice;