@shardflux/cli 0.25.0 → 0.28.1

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 (93) hide show
  1. package/CHANGELOG.md +154 -1
  2. package/README.md +212 -36
  3. package/dist/account-commands.d.ts +2 -4
  4. package/dist/account-commands.js +2 -6
  5. package/dist/account-pool.d.ts +11 -1
  6. package/dist/account-pool.js +48 -0
  7. package/dist/agent-commands.js +4 -3
  8. package/dist/agent-git-commands.d.ts +1 -2
  9. package/dist/agent-git-commands.js +2 -21
  10. package/dist/agent-models.d.ts +31 -1
  11. package/dist/agent-models.js +27 -4
  12. package/dist/agent-setup.d.ts +4 -2
  13. package/dist/agent-setup.js +6 -4
  14. package/dist/agent-shell.d.ts +2 -0
  15. package/dist/agent-shell.js +5 -0
  16. package/dist/args.d.ts +1 -1
  17. package/dist/args.js +67 -12
  18. package/dist/board-args.js +8 -3
  19. package/dist/board-commands.d.ts +7 -3
  20. package/dist/board-commands.js +22 -12
  21. package/dist/brain-commands.d.ts +2 -0
  22. package/dist/brain-commands.js +78 -0
  23. package/dist/cli-proxy.d.ts +1 -13
  24. package/dist/cli-proxy.js +2 -27
  25. package/dist/client-settings.d.ts +2 -1
  26. package/dist/client-settings.js +14 -2
  27. package/dist/codex-cloud.d.ts +2 -0
  28. package/dist/codex-cloud.js +17 -12
  29. package/dist/commands.js +8 -0
  30. package/dist/conversation-commands.d.ts +5 -1
  31. package/dist/conversation-commands.js +58 -6
  32. package/dist/conversation-terminal.js +2 -2
  33. package/dist/desktop-linux.d.ts +12 -1
  34. package/dist/desktop-linux.js +47 -18
  35. package/dist/device-approvals.js +24 -2
  36. package/dist/device-bridge.d.ts +24 -4
  37. package/dist/device-bridge.js +181 -8
  38. package/dist/device-folders.d.ts +6 -1
  39. package/dist/device-folders.js +6 -2
  40. package/dist/device-helper.d.ts +8 -0
  41. package/dist/device-helper.js +71 -12
  42. package/dist/device-mode.d.ts +44 -0
  43. package/dist/device-mode.js +119 -0
  44. package/dist/drive-commands.d.ts +2 -2
  45. package/dist/drive-commands.js +1 -1
  46. package/dist/format.js +3 -0
  47. package/dist/forwarder-refresh.d.ts +15 -0
  48. package/dist/forwarder-refresh.js +32 -0
  49. package/dist/handler-loader.d.ts +3 -0
  50. package/dist/handler-loader.js +43 -0
  51. package/dist/handlers.js +4 -2
  52. package/dist/land.d.ts +10 -5
  53. package/dist/land.js +56 -9
  54. package/dist/linux-helper.js +1 -2
  55. package/dist/mac-app.d.ts +11 -0
  56. package/dist/mac-app.js +11 -0
  57. package/dist/machine-setup-guard.d.ts +9 -0
  58. package/dist/machine-setup-guard.js +22 -0
  59. package/dist/machine-setup.d.ts +1 -8
  60. package/dist/machine-setup.js +2 -22
  61. package/dist/main.js +19 -16
  62. package/dist/menubar.d.ts +18 -13
  63. package/dist/menubar.js +60 -50
  64. package/dist/orchestrator-args.d.ts +7 -0
  65. package/dist/orchestrator-args.js +64 -0
  66. package/dist/orchestrator-commands.d.ts +43 -0
  67. package/dist/orchestrator-commands.js +260 -0
  68. package/dist/person-scope.d.ts +20 -0
  69. package/dist/person-scope.js +61 -0
  70. package/dist/project-hub-seed.d.ts +20 -0
  71. package/dist/project-hub-seed.js +59 -0
  72. package/dist/registry.d.ts +1 -1
  73. package/dist/registry.js +2 -1
  74. package/dist/router-commands.d.ts +12 -21
  75. package/dist/router-commands.js +5 -59
  76. package/dist/startup-housekeeping.d.ts +5 -0
  77. package/dist/startup-housekeeping.js +30 -0
  78. package/dist/t3-launch.js +2 -2
  79. package/dist/t3-provider.js +4 -2
  80. package/dist/task-land-hub.py +16 -1
  81. package/dist/this-computer.d.ts +4 -2
  82. package/dist/this-computer.js +20 -2
  83. package/dist/thread-on-computer.js +2 -2
  84. package/dist/timing.js +3 -1
  85. package/dist/transcribe-commands.js +6 -2
  86. package/dist/update.js +4 -2
  87. package/dist/wait-status.d.ts +1 -1
  88. package/dist/wait-status.js +2 -0
  89. package/dist/worktree-twin-engine.d.ts +1 -1
  90. package/dist/worktree-twin-engine.js +10 -7
  91. package/dist/worktree-twin.d.ts +6 -0
  92. package/dist/worktree-twin.js +8 -1
  93. package/package.json +12 -6
@@ -0,0 +1,119 @@
1
+ /**
2
+ * How this computer answers its own agents (0.27.0+): the person chooses once, per device, whether their agents run
3
+ * things here without asking each time. Kept in `<config>/device-settings.json`; changed in the app's Settings or with
4
+ * `shard device approvals auto|read|ask`. Requests that ran without asking are kept in `<config>/device-activity.jsonl`
5
+ * (what ran, for which conversation, and how it ended), shown by the app and `shard device approvals`.
6
+ *
7
+ * - `ask`: every request asks (Allow once, or Always where a grant applies).
8
+ * - `read`: listing a folder and copying a file to a conversation run without asking; anything that changes this
9
+ * computer (commands, files saved here, ports, linking a folder) asks. Commands are never classified by their text.
10
+ * - `auto`: every request from this person's own agents runs without asking.
11
+ *
12
+ * Only this person's own conversations run without asking (`ownConversation`): a teammate's request always asks.
13
+ */
14
+ import { appendFileSync, lstatSync, readFileSync, statSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { writePrivateFile, ensurePrivateDir } from "./credentials.js";
17
+ import { UsageError } from "./errors.js";
18
+ import { thisComputer } from "./this-computer.js";
19
+ export const DEVICE_APPROVAL_MODES = ['auto', 'read', 'ask'];
20
+ /**
21
+ * The mode before the person chooses one. `undefined`: nothing is decided for them; the first request asks which they
22
+ * want (the app's prompt, or one line from `shard`), and the app's onboarding asks too. 'auto' or 'ask' here chooses
23
+ * for everyone who has not.
24
+ */
25
+ export const DEFAULT_DEVICE_APPROVALS = undefined;
26
+ /** The kinds that only read this computer: a folder listing, and a file copied to the conversation. */
27
+ export const READ_KINDS = new Set(['dir_list', 'files_pull']);
28
+ export const MODE_LABELS = {
29
+ auto: 'Allow everything from my agents',
30
+ read: 'Allow reading, ask before changes',
31
+ ask: 'Ask every time',
32
+ };
33
+ /** The one line a person without the app reads when a request needs the choice ("this Mac" on macOS). */
34
+ export const chooseLine = (platform = process.platform) => `Let your agents run things on ${thisComputer(platform)} without asking each time? Allow everything: shard device approvals auto · Ask each time: shard device approvals ask`;
35
+ const SETTINGS = 'device-settings.json', ACTIVITY = 'device-activity.jsonl';
36
+ export function readDeviceSettings(dir) {
37
+ const file = join(dir, SETTINGS);
38
+ try {
39
+ if (lstatSync(file).isSymbolicLink() || (statSync(file).mode & 0o077))
40
+ return {};
41
+ const value = JSON.parse(readFileSync(file, 'utf8'));
42
+ if (!value || typeof value !== 'object' || Array.isArray(value))
43
+ return {};
44
+ return DEVICE_APPROVAL_MODES.includes(value.approvals) ? value : { ...value, approvals: undefined };
45
+ }
46
+ catch {
47
+ return {};
48
+ }
49
+ }
50
+ /** This person's mode on this computer; undefined until they choose one (and DEFAULT_DEVICE_APPROVALS is unset). */
51
+ export function deviceApprovalMode(dir, userId) {
52
+ const s = readDeviceSettings(dir);
53
+ if (s.approvals && !(s.user_id && userId && s.user_id !== userId))
54
+ return s.approvals;
55
+ return DEFAULT_DEVICE_APPROVALS;
56
+ }
57
+ export function setDeviceApprovalMode(dir, mode, userId) {
58
+ if (!DEVICE_APPROVAL_MODES.includes(mode))
59
+ throw new UsageError(`Unknown approval mode "${mode}": auto, read or ask.`);
60
+ const next = { ...readDeviceSettings(dir), approvals: mode, chosen_at: new Date().toISOString() };
61
+ if (userId)
62
+ next.user_id = userId;
63
+ else
64
+ delete next.user_id;
65
+ writePrivateFile(dir, join(dir, SETTINGS), JSON.stringify(next, null, 2) + '\n');
66
+ return next;
67
+ }
68
+ /** Whether a request of this kind runs without asking under this mode (for the person's own agents). */
69
+ export function runsWithoutAsking(mode, kind) {
70
+ return mode === 'auto' || (mode === 'read' && READ_KINDS.has(kind));
71
+ }
72
+ const ACTIVITY_MAX_BYTES = 512 << 10, ACTIVITY_KEEP = 400;
73
+ export function recordDeviceActivity(dir, entry) {
74
+ const file = join(dir, ACTIVITY);
75
+ try {
76
+ ensurePrivateDir(dir);
77
+ let size = 0;
78
+ try {
79
+ size = statSync(file).size;
80
+ }
81
+ catch { /* first entry */ }
82
+ if (size > ACTIVITY_MAX_BYTES) {
83
+ const keep = readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-ACTIVITY_KEEP);
84
+ writePrivateFile(dir, file, keep.join('\n') + '\n');
85
+ }
86
+ appendFileSync(file, JSON.stringify(entry) + '\n', { mode: 0o600 });
87
+ }
88
+ catch { /* the request's own result still reaches its conversation */ }
89
+ }
90
+ /** What ran without asking, newest first: one entry per request with its latest state. */
91
+ export function deviceActivity(dir, limit = 20) {
92
+ let text;
93
+ try {
94
+ if (lstatSync(join(dir, ACTIVITY)).isSymbolicLink())
95
+ return [];
96
+ text = readFileSync(join(dir, ACTIVITY), 'utf8');
97
+ }
98
+ catch {
99
+ return [];
100
+ }
101
+ const byId = new Map();
102
+ for (const line of text.split('\n')) {
103
+ if (!line)
104
+ continue;
105
+ let e;
106
+ try {
107
+ e = JSON.parse(line);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (!e || typeof e.request_id !== 'string')
113
+ continue;
114
+ const had = byId.get(e.request_id);
115
+ byId.delete(e.request_id);
116
+ byId.set(e.request_id, had ? { ...had, ...e, at: had.at, ...(e.state !== 'running' ? { finished_at: e.at } : {}) } : e);
117
+ }
118
+ return [...byId.values()].reverse().slice(0, limit);
119
+ }
@@ -14,8 +14,8 @@ export interface ProjectScope {
14
14
  * `needId` false leaves the key's project unnamed (the API defaults to it), saving a lookup.
15
15
  */
16
16
  export declare function projectScope(ctx: Ctx, values: Values, needId?: boolean): Promise<ProjectScope>;
17
- /** `drive ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
18
- export declare function driveTable(rows: readonly DriveFile[]): string;
17
+ /** `drive ls` and `brain ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
18
+ export declare function driveTable(rows: readonly Pick<DriveFile, 'size' | 'created_at' | 'content_type' | 'path'>[]): string;
19
19
  /**
20
20
  * The model template as people read it: the primary provider and why, the connected accounts, then one row per role
21
21
  * with what it runs and, for a role whose provider is out of usage, until when it waits and what the other provider
@@ -51,7 +51,7 @@ function drivePath(path, what = 'drive path') {
51
51
  }
52
52
  return path;
53
53
  }
54
- /** `drive ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
54
+ /** `drive ls` and `brain ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
55
55
  export function driveTable(rows) {
56
56
  return table(['SIZE', 'SAVED', 'TYPE', 'PATH'], rows.map((f) => [humanBytes(f.size), time(f.created_at), f.content_type, f.path]));
57
57
  }
package/dist/format.js CHANGED
@@ -83,6 +83,8 @@ export function workspaceState(w) {
83
83
  tags.push('session');
84
84
  if (w.deleted_at)
85
85
  tags.push(w.ended_reason ? `ended: ${w.ended_reason}` : 'deleted');
86
+ if (w.storage_recovery)
87
+ tags.push('files stored');
86
88
  return tags.length ? `${w.observed_state} (${tags.join(', ')})` : w.observed_state;
87
89
  }
88
90
  export function workspaceRow(w) {
@@ -230,6 +232,7 @@ export function workspaceDetail(w) {
230
232
  ['id', w.id],
231
233
  ['key', w.workspace_key],
232
234
  ['state', `${w.observed_state} (desired ${w.desired_state})`],
235
+ ...(w.storage_recovery ? [['storage', w.storage_recovery.message]] : []),
233
236
  ['ready', isReady(w) ? 'yes' : 'no'],
234
237
  ['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
235
238
  // Immutable paths (0.8.0): the template version whose immutable paths the workspace has mounted, when it has one.
@@ -0,0 +1,15 @@
1
+ import type { ProgramRunner } from './cli-proxy.js';
2
+ export declare const FORWARD_LABEL = "dev.shardflux.cli-proxy-forward";
3
+ export declare function forwarderPlistPath(home: string): string;
4
+ /** The shard version the launchd forwarder runs (refreshForwarderAgent compares it with this one). */
5
+ export declare function recordForwarder(dir: string): void;
6
+ /**
7
+ * After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
8
+ * (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
9
+ * one. Returns whether it was.
10
+ */
11
+ export declare function refreshForwarderAgent(env: Record<string, string | undefined>, dir: string, deps?: {
12
+ run?: ProgramRunner;
13
+ uid?: number;
14
+ platform?: NodeJS.Platform;
15
+ }): Promise<boolean>;
@@ -0,0 +1,32 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir, userInfo } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { AgentStateStore } from "./agent-state.js";
5
+ import { CLI_VERSION } from "./args.js";
6
+ export const FORWARD_LABEL = 'dev.shardflux.cli-proxy-forward';
7
+ export function forwarderPlistPath(home) {
8
+ return join(home, 'Library', 'LaunchAgents', `${FORWARD_LABEL}.plist`);
9
+ }
10
+ /** The shard version the launchd forwarder runs (refreshForwarderAgent compares it with this one). */
11
+ export function recordForwarder(dir) {
12
+ new AgentStateStore(dir).update((doc) => { doc.cli_proxy_forwarder = { version: CLI_VERSION }; });
13
+ }
14
+ /**
15
+ * After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
16
+ * (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
17
+ * one. Returns whether it was.
18
+ */
19
+ export async function refreshForwarderAgent(env, dir, deps = {}) {
20
+ if ((deps.platform ?? process.platform) !== 'darwin')
21
+ return false;
22
+ if (!existsSync(forwarderPlistPath(env.HOME || homedir())))
23
+ return false;
24
+ const store = new AgentStateStore(dir);
25
+ const record = store.read().cli_proxy_forwarder;
26
+ if (record?.version === CLI_VERSION)
27
+ return false;
28
+ // Recorded first: a failed restart is not retried on every command (the next import installs the agent again).
29
+ recordForwarder(dir);
30
+ await (deps.run ?? (await import("./cli-proxy.js")).cliProxyTools.run)('launchctl', ['kickstart', '-k', `gui/${deps.uid ?? userInfo().uid}/${FORWARD_LABEL}`]);
31
+ return true;
32
+ }
@@ -0,0 +1,3 @@
1
+ import type { Handler } from './commands.js';
2
+ export declare const HANDLER_PATHS: string[];
3
+ export declare function handlerFor(path: string): Promise<Handler | undefined>;
@@ -0,0 +1,43 @@
1
+ // Exact command ownership, in the same override order as HANDLERS. The parity test catches additions
2
+ // or moves without loading every command module during an ordinary command.
3
+ const groups = [
4
+ [["projects sync list", "projects sync add", "projects sync pause", "projects sync remove", "projects sync run"], async () => (await import("./project-preparation.js")).PROJECT_PREPARATION_HANDLERS],
5
+ [["projects link"], async () => (await import("./device-folders.js")).PROJECT_LINK_HANDLERS],
6
+ [["move"], async () => (await import("./move.js")).MOVE_HANDLERS],
7
+ [["device approvals", "device requests", "device approve", "device deny"], async () => (await import("./device-approvals.js")).DEVICE_HANDLERS],
8
+ [["agents codex status", "agents codex enable", "agents codex disable"], async () => (await import("./codex-cloud.js")).CODEX_CLOUD_HANDLERS],
9
+ [["cli-proxy", "cli-proxy import", "cli-proxy export", "cli-proxy use", "cli-proxy forward"], async () => (await import("./cli-proxy.js")).CLI_PROXY_HANDLERS],
10
+ [["agents computers"], async () => (await import("./thread-on-computer.js")).THREAD_HANDLERS],
11
+ [["conversations resume", "conversations reader", "conversations list", "conversations feed", "conversations get", "conversations permissions", "conversations answer", "conversations watch", "conversations send", "conversations start", "conversations wait", "conversations land", "files pull", "conversations move", "conversations archive", "conversations unarchive", "conversations seen", "conversations delete"], async () => (await import("./conversation-commands.js")).CONVERSATION_HANDLERS],
12
+ [["orchestrator say", "orchestrator list", "orchestrator retry", "orchestrator dismiss", "orchestrator restore"], async () => (await import("./orchestrator-commands.js")).ORCHESTRATOR_HANDLERS],
13
+ [["board", "task add", "task show", "task edit", "task rm", "task steer", "task land", "task clear-done", "needs-you", "needs-you answer", "approvals", "approvals set", "approvals allow", "approvals remove"], async () => (await import("./board-commands.js")).BOARD_HANDLERS],
14
+ [["conversations transcript", "conversations events"], async () => (await import("./conversation-transcript.js")).TRANSCRIPT_HANDLERS],
15
+ [["conversations try"], async () => (await import("./conversation-local.js")).CONVERSATION_HANDLERS],
16
+ [["conversations landing"], async () => (await import("./worktree-twin-engine.js")).TWIN_HANDLERS],
17
+ [["secrets log", "credentials list", "credentials enable", "credentials approve", "credentials deny", "credentials forget"], async () => (await import("./credential-prompts.js")).CREDENTIAL_HANDLERS],
18
+ [["agents credentials login", "agents credentials sync", "agents credentials status", "agents credentials forget"], async () => (await import("./agent-credentials.js")).AGENT_CREDENTIAL_HANDLERS],
19
+ [["agents accounts", "agents accounts list", "agents accounts usage", "agents accounts add", "agents accounts remove", "agents accounts use", "accounts", "accounts list", "accounts usage", "accounts add", "accounts remove", "accounts use"], async () => (await import("./account-pool.js")).ACCOUNT_POOL_HANDLERS],
20
+ [["agents models"], async () => (await import("./account-pool.js")).AGENT_MODELS_HANDLERS],
21
+ [["agents list", "login", "whoami", "version", "feedback", "usage", "workspaces labels", "workspaces hint", "workspaces idle", "workspaces exec-start", "workspaces tunnel", "workspaces exec-read", "workspaces exec-cancel", "workspaces exec-input", "workspaces retention", "projects retention", "workspaces open", "workspaces list", "workspaces get", "workspaces inputs", "workspaces exec", "files read-many", "files read", "files write", "files delete", "files ls", "files stat", "files search", "files patch", "workspaces suspend", "workspaces resume", "workspaces delete", "workspaces close", "workspaces upgrade", "workspaces reset", "workspaces save-as-template", "workspaces changes", "templates list", "templates get", "templates init", "templates build", "templates export", "templates test", "templates builds list", "templates builds get", "templates builds log", "templates builds cancel", "templates languages", "templates packages search", "templates files", "templates diff", "templates draft open", "templates draft status", "templates draft state", "templates draft test", "templates draft publish", "templates draft discard", "workspaces fork", "workspaces resize", "workspaces sessions", "workspaces port expose", "workspaces port ls", "workspaces port close", "workspaces port token", "workspaces port link", "workspaces port callback", "workspaces computer status", "workspaces computer screenshot", "workspaces computer act", "workspaces computer click", "workspaces computer type", "workspaces computer paste-text", "workspaces computer key", "workspaces computer scroll", "workspaces computer move", "workspaces computer drag", "workspaces computer mouse-down", "workspaces computer mouse-up", "workspaces computer hold-key", "workspaces computer wait", "workspaces computer zoom", "workspaces computer cursor", "workspaces computer start", "workspaces computer stop", "workspaces computer stream", "workspaces computer enable", "workspaces computer disable", "workspaces computer inherit", "templates computer-use", "workspaces secrets", "secrets import", "secrets create", "secrets list", "secrets get", "secrets update", "secrets rotate", "secrets versions", "secrets delete", "secrets access-log", "operations list", "operations get", "operations progress", "operations cancel", "operations wait", "executions get"], async () => (await import("./commands.js")).KEY_HANDLERS],
22
+ [["auth register", "auth verify-email", "auth resend-verification", "auth login", "auth mfa", "auth status", "auth logout", "auth sessions", "auth step-up", "auth password change", "auth password reset", "auth password reset-confirm", "auth email change", "auth email confirm", "auth totp enroll", "auth totp confirm", "auth totp disable", "auth totp recovery-codes", "setup", "signup", "verify", "env", "orgs list", "orgs create", "orgs get", "orgs use", "orgs export", "orgs workspaces", "orgs deletion", "orgs delete", "projects list", "projects create", "projects get", "projects use", "api-keys list", "api-keys create", "api-keys revoke", "members list", "members set-role", "members remove", "invitations list", "invitations create", "invitations revoke", "invitations accept", "billing plans", "billing status", "billing upgrade", "billing portal", "billing invoices", "billing alerts", "billing overage", "audit list", "audit export", "account status", "account export", "account delete", "account cancel-deletion", "egress get", "egress set", "egress history", "egress clear", "volumes list", "status", "volumes open", "volumes create", "volumes get", "volumes delete", "volumes attachments", "volumes attach", "volumes detach", "templates publish", "templates archive"], async () => (await import("./account-commands.js")).ACCOUNT_HANDLERS],
23
+ [["agents open"], async () => (await import("./agent-commands.js")).AGENT_HANDLERS],
24
+ [["agents new", "agents fork", "agents park", "agents logs"], async () => (await import("./command-center-actions.js")).CENTER_HANDLERS],
25
+ [["agents targets", "agents terminal-run", "agents open claude-desktop", "agents open codex-app", "agents open t3", "agents open web", "ssh-proxy"], async () => (await import("./agent-target-commands.js")).TARGET_HANDLERS],
26
+ [["agents merge", "agents keep", "agents discard", "agents worktree", "agents restore"], async () => (await import("./agent-git-commands.js")).AGENT_GIT_HANDLERS],
27
+ [["menubar start", "menubar stop", "menubar status", "menubar account"], async () => (await import("./menubar.js")).MENUBAR_HANDLERS],
28
+ [["machine", "machine open", "machine save", "machine rollback", "template", "template save", "template rollback", "template delete"], async () => (await import("./filesystem-commands.js")).FILESYSTEM_HANDLERS],
29
+ [["machine setup"], async () => (await import("./machine-setup.js")).MACHINE_SETUP_HANDLERS],
30
+ [["settings"], async () => (await import("./client-settings.js")).SETTINGS_HANDLERS],
31
+ [["update"], async () => (await import("./update.js")).UPDATE_HANDLERS],
32
+ [["agents config-sync"], async () => (await import("./agent-config-sync.js")).AGENT_CONFIG_HANDLERS],
33
+ [["route", "ask"], async () => (await import("./router-commands.js")).ROUTER_HANDLERS],
34
+ [["transcribe"], async () => (await import("./transcribe-commands.js")).TRANSCRIBE_HANDLERS],
35
+ [["drive ls", "drive get", "drive put", "drive rm", "models", "models primary"], async () => (await import("./drive-commands.js")).DRIVE_HANDLERS],
36
+ [["brain ls", "brain get"], async () => (await import("./brain-commands.js")).BRAIN_HANDLERS],
37
+ ];
38
+ const owners = new Map(groups.flatMap(([paths, load]) => paths.map(path => [path, load])));
39
+ export const HANDLER_PATHS = [...owners.keys()];
40
+ export async function handlerFor(path) {
41
+ const load = owners.get(path);
42
+ return load ? (await load())[path] : undefined;
43
+ }
package/dist/handlers.js CHANGED
@@ -5,6 +5,7 @@ import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
5
5
  import { BOARD_HANDLERS } from "./board-commands.js";
6
6
  import { TRANSCRIPT_HANDLERS } from "./conversation-transcript.js";
7
7
  import { ROUTER_HANDLERS } from "./router-commands.js";
8
+ import { ORCHESTRATOR_HANDLERS } from "./orchestrator-commands.js";
8
9
  import { TRANSCRIBE_HANDLERS } from "./transcribe-commands.js";
9
10
  /**
10
11
  * Every command's handler, keyed by its path ("workspaces open", "auth login"): the project-key commands of
@@ -25,11 +26,12 @@ import { TWIN_HANDLERS } from "./worktree-twin-engine.js";
25
26
  import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
26
27
  import { THREAD_HANDLERS } from "./thread-on-computer.js";
27
28
  import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
28
- import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
29
+ import { ACCOUNT_POOL_HANDLERS, AGENT_MODELS_HANDLERS } from "./account-pool.js";
29
30
  import { MOVE_HANDLERS } from "./move.js";
30
31
  import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
31
32
  import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
32
33
  import { SETTINGS_HANDLERS } from "./client-settings.js";
33
34
  import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
34
35
  import { DRIVE_HANDLERS } from "./drive-commands.js";
35
- export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS };
36
+ import { BRAIN_HANDLERS } from "./brain-commands.js";
37
+ export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...ORCHESTRATOR_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...AGENT_MODELS_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS, ...BRAIN_HANDLERS };
package/dist/land.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { TwinCell } from './worktree-twin.js';
4
4
  /** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
5
5
  export declare function hubScript(): string;
6
6
  export interface HubLanding {
7
- state: 'landed' | 'conflict' | 'missing' | 'raced' | 'error';
7
+ state: 'landed' | 'conflict' | 'unrelated' | 'missing' | 'raced' | 'error';
8
8
  into: string;
9
9
  before?: string | null;
10
10
  after?: string;
@@ -61,8 +61,13 @@ export declare function folderBranch(folder: string, fallback: string): Promise<
61
61
  * edits stay, and the landed commit is checked to be on the branch.
62
62
  */
63
63
  export declare function landInFolder(cell: TwinCell, f: FolderInput): Promise<FolderLanding>;
64
- /** The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started. */
65
- export declare function linkedFolder(ctx: Pick<Ctx, 'store' | 'apiUrl'>, projectId: string, conversationId?: string): string | null;
64
+ /** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
65
+ export declare function folderRepository(folder: string): string | null;
66
+ /**
67
+ * The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
68
+ * With `repository` (a project holds every folder its people linked), only a folder of that repository.
69
+ */
70
+ export declare function linkedFolder(ctx: Pick<Ctx, 'store' | 'apiUrl'>, projectId: string, conversationId?: string, repository?: string): string | null;
66
71
  export declare const displayPath: (path: string) => string;
67
72
  export interface LandOutcome {
68
73
  landed: true;
@@ -92,11 +97,11 @@ export interface LandDeps {
92
97
  tasks: Pick<ReturnType<typeof conversationClient>['tasks'], 'get' | 'recordLanding' | 'steer'>;
93
98
  conversations: Pick<ReturnType<typeof conversationClient>['conversations'], 'get' | 'land'>;
94
99
  };
95
- hubCell(projectId: string): Promise<{
100
+ hubCell(projectId: string, workspaceIds?: readonly string[]): Promise<{
96
101
  cell: TwinCell;
97
102
  close(): void;
98
103
  }>;
99
- folder(projectId: string, conversationId?: string): string | null;
104
+ folder(projectId: string, conversationId?: string, repository?: string): string | null;
100
105
  device: string;
101
106
  stateDir: string;
102
107
  hub?: string;
package/dist/land.js CHANGED
@@ -21,6 +21,7 @@ import { conversationClient } from "./conversation-commands.js";
21
21
  import { localConversations } from "./conversations.js";
22
22
  import { CodedError } from "./errors.js";
23
23
  import { selectedProjects } from "./project-preparation.js";
24
+ import { projectIdentity } from "./project-identity.js";
24
25
  import { HUB_PATH, backupRoot, deviceId, git, gitText, inspectWorktree, landingRound, listWorktrees } from "./worktree-twin.js";
25
26
  import { projectHubCell } from "./worktree-twin-engine.js";
26
27
  let script;
@@ -84,9 +85,22 @@ export async function landInFolder(cell, f) {
84
85
  }
85
86
  }
86
87
  }
87
- /** The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started. */
88
- export function linkedFolder(ctx, projectId, conversationId) {
89
- const repo = (folder) => folder && existsSync(join(folder, '.git')) ? folder : null;
88
+ /** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
89
+ export function folderRepository(folder) {
90
+ try {
91
+ const id = projectIdentity(folder);
92
+ return id.remote ?? `file://${id.folder}`;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
100
+ * With `repository` (a project holds every folder its people linked), only a folder of that repository.
101
+ */
102
+ export function linkedFolder(ctx, projectId, conversationId, repository) {
103
+ const repo = (folder) => folder && existsSync(join(folder, '.git')) && (!repository || folderRepository(folder) === repository) ? folder : null;
90
104
  const records = localConversations(ctx);
91
105
  const own = conversationId ? repo(records[conversationId]?.folder) : null;
92
106
  if (own)
@@ -113,11 +127,11 @@ function message(o) {
113
127
  return hub;
114
128
  }
115
129
  export function landDeps(ctx) {
116
- return { client: conversationClient(ctx), hubCell: p => projectHubCell(ctx, p, ctx.signal, 'shard-land'), folder: (p, c) => linkedFolder(ctx, p, c),
130
+ return { client: conversationClient(ctx), hubCell: (p, ids) => projectHubCell(ctx, p, ctx.signal, 'shard-land', ids), folder: (p, c, r) => linkedFolder(ctx, p, c, r),
117
131
  device: deviceId(ctx.store.dir), stateDir: ctx.store.dir, signal: ctx.signal };
118
132
  }
119
133
  async function land(d, l, onConflict) {
120
- const vm = await d.hubCell(l.projectId);
134
+ const vm = await d.hubCell(l.projectId, l.workspaceIds);
121
135
  try {
122
136
  const input = { result: l.result, sources: l.sources.map(s => s.branch), title: l.title, ...(d.hub ? { hub: d.hub } : {}) };
123
137
  let hub = await landOnHub(vm.cell, input, d.signal);
@@ -127,6 +141,19 @@ async function land(d, l, onConflict) {
127
141
  await onConflict(hub);
128
142
  throw new CodedError('land_conflict', `Not landed: ${list(hub.conflicts ?? [])} changed on both sides (${hub.stage === 'integrate' ? `${hub.source} and ${l.result}` : `${l.result} and ${hub.into}`}).`, 'its master merges them and asks to land again', { stage: hub.stage, into: hub.into, conflicts: hub.conflicts ?? [] });
129
143
  }
144
+ // The hub's default branch is another repository's (a project holds every folder its people linked): a result
145
+ // whose repository has its folder here lands in that folder, and the hub's default branch stays as it is.
146
+ // Awaited here: the computer that reads the hub closes in `finally` (staging 2026-10-17: returned unawaited, the
147
+ // landing was cut off mid-way, "Interrupted.").
148
+ if (hub.state === 'unrelated' && hub.stage === 'default' && l.repository && l.folder && hub.result)
149
+ return await ownFolder(d, vm.cell, l, hub);
150
+ // No history in common (started from another repository than the branch it lands in): it never clears by trying
151
+ // again, so it says what to do instead.
152
+ if (hub.state === 'unrelated') {
153
+ throw new CodedError('land_unrelated', hub.stage === 'integrate'
154
+ ? `Not landed: ${hub.source} and ${l.result} have no history in common; they were started from different repositories.`
155
+ : `Not landed: ${l.result} has no history in common with ${hub.into} on the project hub; it was started from another repository than the one ${hub.into} holds.`, 'land it from the folder of the repository it was started from, or start the task again from this project’s own folder', { stage: hub.stage, into: hub.into });
156
+ }
130
157
  if (hub.state === 'missing')
131
158
  throw new CodedError('land_nothing', `Nothing to land: the project hub has no ${l.result} yet.`, 'land after the work is done', { branch: l.result });
132
159
  if (hub.state !== 'landed')
@@ -149,6 +176,23 @@ async function land(d, l, onConflict) {
149
176
  vm.close();
150
177
  }
151
178
  }
179
+ /** A result into the folder of its own repository only (the hub's default branch holds another repository). */
180
+ async function ownFolder(d, cell, l, hub) {
181
+ const folder = l.folder, into = await folderBranch(folder, 'main');
182
+ const contained = l.sources.filter(s => hub.contained?.includes(s.branch)).map(s => s.conversation_id);
183
+ if (!into)
184
+ return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into: '', sha: hub.result, method: 'folder' }, contained,
185
+ folder: { path: folder, display: displayPath(folder), into: '', state: 'skipped', reason: 'detached' } };
186
+ const f = await landInFolder(cell, { folder, into, name: l.name, title: l.title, branch: l.result, base: l.base ?? hub.result, device: d.device, stateDir: d.stateDir,
187
+ ...(d.hub ? { hub: d.hub } : {}), ...(d.quietMs !== undefined ? { quietMs: d.quietMs } : {}), ...(d.signal ? { signal: d.signal } : {}) });
188
+ if (f.state !== 'landed' && f.state !== 'already') {
189
+ // Nothing else holds it: a folder that could not take it yet is not a landing.
190
+ throw new CodedError('land_failed', f.state === 'conflict'
191
+ ? `Not landed yet: ${list(f.conflicts ?? [])} changed in ${displayPath(folder)} too since ${l.result} started.`
192
+ : `Not landed yet: ${displayPath(folder)} is busy (${f.reason ?? 'busy'}).`, f.state === 'conflict' ? 'commit or put aside those changes, and it lands' : 'it lands once the folder is quiet', { into, folder: displayPath(folder) });
193
+ }
194
+ return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into, sha: f.sha ?? hub.result, method: 'folder' }, folder: { ...f, display: displayPath(f.path) }, contained };
195
+ }
152
196
  /** What a landing did, for people: the line, then what was not recorded. */
153
197
  export function landedText(o) {
154
198
  return `${o.message}\n${o.recorded ? '' : `Not recorded on the board: ${o.record_error ?? 'unknown'}.\n`}`;
@@ -180,9 +224,11 @@ export async function landTask(d, taskId, opts = {}) {
180
224
  if (w.join_mode !== 'reviewer' && (w.changes?.commits ?? 0) > 0 && w.landed?.how !== 'discarded')
181
225
  workers.push(w);
182
226
  }
183
- const folder = opts.path ?? d.folder(task.project_id);
227
+ // The repository the result is of: its workers' (or its master's), so it lands only in a folder of that repository.
228
+ const repository = workers.find(w => w.repo_remote)?.repo_remote ?? (task.master_id ? (await client.conversations.get(task.master_id).catch(() => null))?.repo_remote : undefined) ?? null;
229
+ const folder = opts.path ?? d.folder(task.project_id, undefined, repository ?? undefined);
184
230
  const done = await land(d, { projectId: task.project_id, title, name: taskId, result: resultBranch(taskId), sources: workers.map(w => ({ branch: w.branch, conversation_id: w.id })),
185
- base: workers.find(w => w.base_commit)?.base_commit ?? null, folder }, async (hub) => {
231
+ base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository, workspaceIds: workers.map(w => w.workspace_id).filter(Boolean) }, async (hub) => {
186
232
  const what = hub.stage === 'integrate' ? `${hub.source} into ${resultBranch(taskId)}` : `${resultBranch(taskId)} into ${hub.into}`;
187
233
  await client.tasks.steer(taskId, `Land could not merge ${what}: ${list(hub.conflicts ?? [])} changed on both sides. Merge ${hub.into} from the project hub and every worker branch into ${resultBranch(taskId)}, resolve the conflicts so both changes work (ask the person only if the choice is theirs), run the tests, push ${resultBranch(taskId)} to the hub and seal done again.`, signal);
188
234
  });
@@ -221,9 +267,10 @@ export async function landConversation(d, id, opts = {}) {
221
267
  const c = await client.conversations.get(id);
222
268
  if (c.changes?.uncommitted)
223
269
  throw new CodedError('uncommitted_changes', 'The conversation has uncommitted changes.', 'ask its agent to commit them, then land');
224
- const folder = opts.path ?? d.folder(c.project_id, c.id);
270
+ const repository = c.repo_remote ?? null;
271
+ const folder = opts.path ?? d.folder(c.project_id, c.id, repository ?? undefined);
225
272
  const done = await land(d, { projectId: c.project_id, title: c.title || c.branch, name: c.name ?? c.branch.replace(/^shard\//, ''), result: c.branch, sources: [],
226
- base: c.base_commit ?? null, folder }, () => Promise.resolve());
273
+ base: c.base_commit ?? null, folder, repository, workspaceIds: [c.workspace_id] }, () => Promise.resolve());
227
274
  let recorded = false, record_error;
228
275
  try {
229
276
  await client.conversations.land(id, 'merge', { into: done.hub.into, ...(d.signal ? { signal: d.signal } : {}) });
@@ -4,7 +4,6 @@ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { CLI_VERSION } from "./args.js";
6
6
  import { writePrivateFile } from "./credentials.js";
7
- import { runDeviceHelper } from "./device-helper.js";
8
7
  export const HELPER_UNIT = 'shardflux-helper.service';
9
8
  export const helperUnitPath = (home) => join(home, '.config/systemd/user', HELPER_UNIT);
10
9
  const quote = (s) => '"' + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('\n', '\\n').replaceAll('\r', '\\r') + '"';
@@ -83,6 +82,6 @@ export function startLinuxHelper(ctx) {
83
82
  writePrivateFile(ctx.store.dir, notice, '1\n');
84
83
  }
85
84
  const abort = new AbortController();
86
- const work = runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true }).catch(() => undefined);
85
+ const work = import("./device-helper.js").then(({ runDeviceHelper }) => runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true })).catch(() => undefined);
87
86
  return async () => { abort.abort(); await work; };
88
87
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
3
+ * @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
4
+ * optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
5
+ * in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
6
+ */
7
+ export declare const ONE_APP_ON_MAC = true;
8
+ /** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
9
+ export declare const desktopDarwinPackage: (arch?: string) => string;
10
+ /** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
11
+ export declare const macAppPackage: (arch?: string) => string;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
3
+ * @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
4
+ * optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
5
+ * in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
6
+ */
7
+ export const ONE_APP_ON_MAC = true;
8
+ /** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
9
+ export const desktopDarwinPackage = (arch = process.arch) => `@shardflux/desktop-darwin-${arch === 'x64' ? 'x64' : 'arm64'}`;
10
+ /** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
11
+ export const macAppPackage = (arch = process.arch) => ONE_APP_ON_MAC ? desktopDarwinPackage(arch) : '@shardflux/menubar-darwin';
@@ -0,0 +1,9 @@
1
+ import type { Values } from './args.js';
2
+ type Env = Record<string, string | undefined>;
3
+ export declare const SETUP_VM_ENV = "SHARDFLUX_MACHINE_SETUP_VM";
4
+ /**
5
+ * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
6
+ * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
7
+ */
8
+ export declare function setupGuard(env: Env, command: string, values: Values, positionals: string[]): string | undefined;
9
+ export {};
@@ -0,0 +1,22 @@
1
+ export const SETUP_VM_ENV = 'SHARDFLUX_MACHINE_SETUP_VM';
2
+ /**
3
+ * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
4
+ * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
5
+ */
6
+ export function setupGuard(env, command, values, positionals) {
7
+ const vm = env[SETUP_VM_ENV]?.trim();
8
+ if (!vm)
9
+ return undefined;
10
+ const allowed = ['workspaces exec', 'files write', 'files read', 'files ls', 'files stat', 'version'];
11
+ if (!allowed.includes(command))
12
+ return `machine setup runs only shard ws exec, files write and files read on ${vm}`;
13
+ if (command === 'version')
14
+ return undefined;
15
+ if (positionals[0] !== vm)
16
+ return `machine setup runs commands only on ${vm}`;
17
+ if (command === 'files write' && values.from !== undefined && values.from !== '-')
18
+ return 'machine setup writes files only from standard input (--from -)';
19
+ if (command === 'files read' && values.out !== undefined)
20
+ return 'machine setup reads files only to standard output';
21
+ return undefined;
22
+ }
@@ -4,8 +4,7 @@ import { AgentStateStore } from './agent-state.js';
4
4
  import { type Values } from './args.js';
5
5
  export type Runner = 'claude' | 'codex' | 'cloud';
6
6
  type Env = Record<string, string | undefined>;
7
- /** Set for the local agent's processes: the `shard` it runs only reaches this machine VM (see `setupGuard`). */
8
- export declare const SETUP_VM_ENV = "SHARDFLUX_MACHINE_SETUP_VM";
7
+ export { SETUP_VM_ENV, setupGuard } from './machine-setup-guard.js';
9
8
  export declare const DEFAULT_AGENT_TIMEOUT_MS: number;
10
9
  export interface ChildResult {
11
10
  code: number | null;
@@ -71,11 +70,6 @@ export declare const SECRET_PATHS: readonly ["~/.ssh/**", "~/.aws/**", "~/.azure
71
70
  export declare function claudeArgs(key: string, help: string): string[];
72
71
  /** `codex exec` argv: writes only in `workDir` (and temp folders), network on for `shard`, the task on standard input. */
73
72
  export declare function codexArgs(workDir: string, help: string): string[];
74
- /**
75
- * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
76
- * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
77
- */
78
- export declare function setupGuard(env: Env, command: string, values: Values, positionals: string[]): string | undefined;
79
73
  export interface Inventory {
80
74
  /** The home whose files were inventoried; a local agent's login shell can use a different home. */
81
75
  source_home?: string;
@@ -187,4 +181,3 @@ export declare function machineSetup(ctx: Ctx, values: Values, deps?: SetupDeps)
187
181
  */
188
182
  export declare function startMachineSetupInBackground(ctx: Ctx): boolean;
189
183
  export declare const MACHINE_SETUP_HANDLERS: Record<string, Handler>;
190
- export {};
@@ -31,7 +31,8 @@ import { progress as stderrProgress } from "./progress.js";
31
31
  import { writeProgress } from "./seed-phases.js";
32
32
  import { parseDuration } from "./args.js";
33
33
  /** Set for the local agent's processes: the `shard` it runs only reaches this machine VM (see `setupGuard`). */
34
- export const SETUP_VM_ENV = 'SHARDFLUX_MACHINE_SETUP_VM';
34
+ import { SETUP_VM_ENV } from "./machine-setup-guard.js";
35
+ export { SETUP_VM_ENV, setupGuard } from "./machine-setup-guard.js";
35
36
  export const DEFAULT_AGENT_TIMEOUT_MS = 10 * 60_000;
36
37
  const SAVE_TIMEOUT_MS = 10 * 60_000;
37
38
  /**
@@ -228,27 +229,6 @@ export function codexArgs(workDir, help) {
228
229
  '-',
229
230
  ];
230
231
  }
231
- /**
232
- * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
233
- * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
234
- */
235
- export function setupGuard(env, command, values, positionals) {
236
- const vm = env[SETUP_VM_ENV]?.trim();
237
- if (!vm)
238
- return undefined;
239
- const allowed = ['workspaces exec', 'files write', 'files read', 'files ls', 'files stat', 'version'];
240
- if (!allowed.includes(command))
241
- return `machine setup runs only shard ws exec, files write and files read on ${vm}`;
242
- if (command === 'version')
243
- return undefined;
244
- if (positionals[0] !== vm)
245
- return `machine setup runs commands only on ${vm}`;
246
- if (command === 'files write' && values.from !== undefined && values.from !== '-')
247
- return 'machine setup writes files only from standard input (--from -)';
248
- if (command === 'files read' && values.out !== undefined)
249
- return 'machine setup reads files only to standard output';
250
- return undefined;
251
- }
252
232
  function setupCapacity(ws) {
253
233
  const disk = ws.grants?.disk_gib;
254
234
  if (typeof disk !== 'number' || !Number.isFinite(disk) || disk <= 0)