rl-workspaces 0.6.0 → 0.7.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 (49) hide show
  1. package/README.md +62 -53
  2. package/bin/rl +2 -54
  3. package/dist/agent-launch.js +1 -1
  4. package/dist/agent-resume.js +2 -2
  5. package/dist/bootstrap.js +74 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/command-context.js +1 -1
  8. package/dist/common.js +3 -2
  9. package/dist/dependencies.js +197 -0
  10. package/dist/doctor.js +1 -1
  11. package/dist/help.js +81 -84
  12. package/dist/hooks.js +1 -1
  13. package/dist/install.js +143 -0
  14. package/dist/mcp-setup.js +171 -0
  15. package/dist/mcp.js +13 -16
  16. package/dist/picker.js +24 -7
  17. package/dist/pr-workflow.js +7 -28
  18. package/dist/repos.js +5 -1
  19. package/dist/session-workflows.js +71 -1
  20. package/dist/session.js +1 -1
  21. package/dist/state.js +4 -14
  22. package/dist/storage.js +2 -2
  23. package/dist/store.js +2 -2
  24. package/dist/summary.js +1 -1
  25. package/dist/workflow.js +24 -38
  26. package/lib/entry.zsh +54 -0
  27. package/lib/rl.zsh +35 -6
  28. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  29. package/node_modules/smol-toml/LICENSE +24 -0
  30. package/node_modules/smol-toml/README.md +605 -0
  31. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  32. package/node_modules/smol-toml/dist/date.js +156 -0
  33. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  34. package/node_modules/smol-toml/dist/error.js +67 -0
  35. package/node_modules/smol-toml/dist/extract.js +293 -0
  36. package/node_modules/smol-toml/dist/index.cjs +771 -0
  37. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  38. package/node_modules/smol-toml/dist/index.js +34 -0
  39. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  40. package/node_modules/smol-toml/dist/parse.js +164 -0
  41. package/node_modules/smol-toml/dist/primitive.js +151 -0
  42. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  43. package/node_modules/smol-toml/dist/stringify.js +219 -0
  44. package/node_modules/smol-toml/dist/struct.js +156 -0
  45. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  46. package/node_modules/smol-toml/dist/util.js +57 -0
  47. package/node_modules/smol-toml/package.json +68 -0
  48. package/package.json +6 -3
  49. package/shell/rl.zsh +1 -1
package/dist/picker.js CHANGED
@@ -15,22 +15,37 @@ const stateColors = {
15
15
  none: 90,
16
16
  unknown: 90,
17
17
  };
18
- export function candidates(repo, base) {
19
- return worktrees(repo)
18
+ export function candidates(repo, base, store) {
19
+ const adopted = new Map(store
20
+ ?.records()
21
+ .filter(([, data]) => data.status === "active")
22
+ .map(([, data]) => [data.worktree, data.branch]));
23
+ const records = worktrees(repo)
20
24
  .slice(1)
21
25
  .flatMap((record) => {
22
26
  const root = canonical(record.worktree);
23
- if (root === repo || !inside(root, base))
27
+ const managed = inside(root, base);
28
+ const branch = (record.branch ?? "").replace(/^refs\/heads\//, "");
29
+ if (root === repo || (!managed && adopted.get(root) !== branch))
24
30
  return [];
25
31
  return [
26
32
  {
27
- name: path.relative(base, root),
33
+ name: managed ? path.relative(base, root) : path.basename(root),
28
34
  path: root,
29
- branch: (record.branch ?? "").replace(/^refs\/heads\//, ""),
35
+ branch,
30
36
  locked: "locked" in record,
31
37
  },
32
38
  ];
33
39
  });
40
+ // Preserve existing managed names. Outside checkouts use their directory
41
+ // name unless it collides, in which case their absolute path is unambiguous.
42
+ const used = new Set(records.filter((item) => inside(item.path, base)).map((item) => item.name));
43
+ for (const item of records.filter((item) => !inside(item.path, base))) {
44
+ if (used.has(item.name))
45
+ item.name = item.path;
46
+ used.add(item.name);
47
+ }
48
+ return records;
34
49
  }
35
50
  export function prState(data) {
36
51
  if (!data?.pr)
@@ -120,7 +135,9 @@ export function pickerRows(store, items, defaultBase, prs = new Map()) {
120
135
  }
121
136
  export async function select(store, items, defaultBase, multiple) {
122
137
  if (!items.length) {
123
- console.error("No managed worktrees to " + (multiple ? "delete." : "open."));
138
+ console.error(multiple
139
+ ? "No managed worktrees to delete."
140
+ : "No RL workspaces yet. Create one with rl switch <name> --create.");
124
141
  return [];
125
142
  }
126
143
  const prs = await discoverPrs(store, items.map((item) => item.branch));
@@ -175,7 +192,7 @@ export async function main(argv) {
175
192
  ]);
176
193
  arity(positionals, 0);
177
194
  const store = new Store(required(values, "repo"), required(values, "state-dir"));
178
- const selected = await select(store, candidates(store.repo, canonical(required(values, "base-dir"))), required(values, "default-base"), false);
195
+ const selected = await select(store, candidates(store.repo, canonical(required(values, "base-dir")), store), required(values, "default-base"), false);
179
196
  if (selected.length)
180
197
  process.stdout.write(selected[0].path + "\n");
181
198
  return 0;
@@ -2,12 +2,11 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { args, arity, canonical, execute, git, inside, locked, } from "./common.js";
5
- import { commandContext, contextOptions, resolveWorkspace, } from "./command-context.js";
5
+ import { commandContext, contextOptions } from "./command-context.js";
6
6
  import { GIT_WORKFLOW_DIRECTORY, GIT_WORKFLOW_LOCK } from "./git-safety.js";
7
7
  import { isJsonObject, parseJsonObject } from "./json.js";
8
8
  const GH_TIMEOUT_MS = 20_000;
9
9
  const PR_FIELDS = "number,url,headRefName,headRefOid,baseRefName,isCrossRepository,headRepository,headRepositoryOwner";
10
- const CHECK_FIELDS = "name,state,bucket,link,workflow";
11
10
  const FETCH_PREFIX = "refs/rl/pr-fetch/";
12
11
  /** Bound requests and ignore a caller's unrelated GH_REPO override. */
13
12
  export async function github(repo, argv) {
@@ -53,33 +52,13 @@ export async function repositoryIdentity(repo) {
53
52
  return { name: raw.nameWithOwner, url: raw.url };
54
53
  }
55
54
  export async function main(argv) {
56
- const { values, positionals } = args(argv, [...contextOptions, "name"], ["json"]);
57
- const context = commandContext(values);
55
+ const { values, positionals } = args(argv, [...contextOptions, "name"], []);
58
56
  const [command, action, ...rest] = positionals;
59
- if (command !== "pr" || !["checks", "checkout"].includes(action))
60
- throw new Error("Usage: rl pr checks|checkout");
61
- const identity = await repositoryIdentity(context.repo);
62
- if (action === "checks") {
63
- arity(rest, 0);
64
- if (values.name)
65
- throw new Error("checks does not accept --name");
66
- const workspace = resolveWorkspace(context);
67
- const result = await github(workspace.path, [
68
- "pr",
69
- "checks",
70
- workspace.branch,
71
- "--repo",
72
- identity.url,
73
- ...(values.json ? ["--json", CHECK_FIELDS] : []),
74
- ]);
75
- process.stdout.write(result.stdout);
76
- if (result.stderr)
77
- process.stderr.write(result.stderr);
78
- return result.code;
79
- }
57
+ if (command !== "pr" || action !== "checkout")
58
+ throw new Error("Usage: rl pr checkout <number> [--name <branch>]");
80
59
  arity(rest, 1);
81
- if (values.json)
82
- throw new Error("checkout does not accept --json");
60
+ const context = commandContext(values);
61
+ const identity = await repositoryIdentity(context.repo);
83
62
  const [number] = rest;
84
63
  if (!/^[1-9]\d*$/.test(number))
85
64
  throw new Error("Expected a positive PR number");
@@ -149,7 +128,7 @@ export async function main(argv) {
149
128
  }
150
129
  catch (error) {
151
130
  // Preserve the valid checkout on adoption failure so hooks/files can be inspected.
152
- throw new Error(`Created ${destination}, but RL adoption failed: ${error instanceof Error ? error.message : "unknown error"}. Run rl adopt inside it`);
131
+ throw new Error(`Created ${destination}, but RL adoption failed: ${error instanceof Error ? error.message : "unknown error"}. Run rl setup adopt inside it`);
153
132
  }
154
133
  console.log(destination);
155
134
  return 0;
package/dist/repos.js CHANGED
@@ -183,7 +183,11 @@ class Registry {
183
183
  if (rest.length === 3 && values.alias !== undefined)
184
184
  throw new Error("Choose either --alias <name> or the legacy <alias> <path> form");
185
185
  const target = rest.length === 3 ? rest[2] : (rest[1] ?? ".");
186
- const root = canonical(worktrees(expandHome(target))[0].worktree);
186
+ const directoryPath = path.resolve(expandHome(target));
187
+ if (!fs.existsSync(directoryPath) ||
188
+ !fs.statSync(directoryPath).isDirectory())
189
+ throw new Error(`Repository directory does not exist: ${directoryPath}. Use 'rl repo add .' for the current repository, with --alias <name> to name it.`);
190
+ const root = canonical(worktrees(directoryPath)[0].worktree);
187
191
  name = String(values.alias ?? (rest.length === 3 ? name : repositoryName(root)));
188
192
  if (!validName(name))
189
193
  throw new Error("Repository names must contain letters, numbers, dots, underscores, or hyphens; supply --alias <name>");
@@ -223,7 +223,7 @@ export function automationContext(root) {
223
223
  .digest("hex");
224
224
  return [
225
225
  `RL native agent configuration detected (${fingerprint}; source: ${hasAutomation(root) ? "workspace configuration" : "built-in defaults"}; default workflow: ${defaultWorkflow(config)}). You are the coordinator of this session's own native subagents.`,
226
- "Discover profiles, workflow dependencies, trigger rules and policies with rl_workflow_context (MCP), or 'rl workflow context --json'. At planning milestones report the configured event with rl_workflow_event. Tool and before-finish hooks also queue matching requests. Read pending requests with rl_workflow_pending.",
226
+ "Use RL from this existing session; no special agent launcher or user-pasted workflow prompt is required. Run 'rl workflow context --json' to discover the active profiles, workflows, policies and the complete protocol.tools CLI commands and input schemas. Use the CLI through your normal shell tools; an MCP connection is optional. If MCP tools are connected, the same operation names and schemas apply. Follow the active configuration automatically for applicable tasks. At planning milestones report the configured event with rl_workflow_event. Tool and before-finish hooks also queue matching requests. Read pending requests with rl_workflow_pending.",
227
227
  "For coding tasks, report task_planned before implementation. A collaborative workflow starts all writer, reviewer and DRY native peers in parallel: claim all three roles, spawn them using your native tools, bind their handles with rl_workflow_bind, then relay messages or let peers use their own inbox. The writer submits checkpoints, the reviewer returns policy-based ratings, and DRY answers both peers. Keep all three peers available; do not replace them with sequential one-shot passes. Use collaboration tools for handoffs before final stage reports. Semantic new-function and duplication obligations require explicit declarations and consultation responses; RL cannot read private reasoning.",
228
228
  "For each request: rl_workflow_start creates a run tied to YOUR parent session; rl_workflow_claim atomically returns a ready step and its instructions. Spawn that step with YOUR native subagent tool, then submit its evidence with rl_workflow_report. Run command steps yourself and report actual output. RL never starts Codex, Claude, or check processes for a workflow. Do not substitute a separate CLI or API agent. Respect current permissions, workspace boundaries, explicit user instructions and native delegation availability. If delegation is unavailable, dismiss the request with the concrete reason and tell the user; do not claim it ran.",
229
229
  "Supply agent (codex or claude) and this parent conversation's full sessionId to workflow tools. UUIDs and observed Codex thr_ IDs are supported for workflow coordination; only verified UUIDs enter resumable history. CLI equivalents: 'rl workflow pending', 'rl workflow run --request <requestId>', 'rl workflow claim <run> --step <step>', 'rl workflow report <run> --file <report.json>', with --agent and --session. Profiles describe roles; their provider field is a legacy preference, never permission to start another provider. Unsupported native model/reasoning preferences must be reported, not silently substituted. Policies own scoring; return evidence without assigning yourself a score.",
@@ -320,6 +320,75 @@ export const WORKFLOW_TOOLS = [
320
320
  },
321
321
  ...COLLABORATION_TOOLS,
322
322
  ];
323
+ export function workflowToolSchema(tool) {
324
+ return {
325
+ type: "object",
326
+ properties: {
327
+ agent: { type: "string", enum: ["codex", "claude"] },
328
+ sessionId: {
329
+ type: "string",
330
+ description: "Current parent conversation ID. Claude requires a UUID; Codex also accepts observed thr_ IDs and may inherit it.",
331
+ },
332
+ ...tool.properties,
333
+ },
334
+ required: tool.required,
335
+ additionalProperties: false,
336
+ };
337
+ }
338
+ function workflowProtocol() {
339
+ const identity = { agent: "--agent", sessionId: "--session" };
340
+ return {
341
+ version: 1,
342
+ coordinator: "current-session",
343
+ requiresMcp: false,
344
+ requiresRlLauncher: false,
345
+ requiresNativeSubagents: true,
346
+ discovery: "rl workflow context --json",
347
+ instruction: "Use your current session's native subagent and messaging tools. RL supplies roles and coordination state. CLI operations below use the same inputSchema as MCP; tools with stdin: json accept the entire input object via --file -. These are coordination commands, never instructions to start a second provider process.",
348
+ tools: WORKFLOW_TOOLS.map((tool) => {
349
+ const action = tool.name.slice("rl_workflow_".length);
350
+ const file = !["context", "pending", "start", "claim", "inbox"].includes(action);
351
+ const flags = action === "start"
352
+ ? {
353
+ ...identity,
354
+ base: "--base",
355
+ prompt: "--prompt",
356
+ requestId: "--request",
357
+ }
358
+ : {
359
+ ...identity,
360
+ ...(action === "claim" || action === "inbox"
361
+ ? { step: "--step" }
362
+ : {}),
363
+ ...(action === "inbox" ? { claim: "--claim" } : {}),
364
+ };
365
+ return {
366
+ name: tool.name,
367
+ description: tool.description,
368
+ inputSchema: workflowToolSchema(tool),
369
+ cli: {
370
+ command: [
371
+ "rl",
372
+ "workflow",
373
+ action === "start" ? "run" : action,
374
+ ...(file ? ["--file", "-"] : []),
375
+ "--json",
376
+ ],
377
+ stdin: file ? "json" : "none",
378
+ positionals: file
379
+ ? []
380
+ : action === "start"
381
+ ? ["workflow"]
382
+ : action === "claim" || action === "inbox"
383
+ ? ["run"]
384
+ : [],
385
+ flags: file ? {} : flags,
386
+ output: "json",
387
+ },
388
+ };
389
+ }),
390
+ };
391
+ }
323
392
  export async function callWorkflowTool(store, name, input) {
324
393
  const definition = WORKFLOW_TOOLS.find((tool) => tool.name === name);
325
394
  if (!definition)
@@ -337,6 +406,7 @@ export async function callWorkflowTool(store, name, input) {
337
406
  source: hasAutomation(data.worktree) ? "workspace" : "built-in",
338
407
  ...readAutomation(data.worktree),
339
408
  instructions: automationContext(data.worktree),
409
+ protocol: workflowProtocol(),
340
410
  };
341
411
  }
342
412
  const parent = owner(input);
package/dist/session.js CHANGED
@@ -7,6 +7,6 @@ export function currentSessionId(agent, explicit) {
7
7
  // Resume the current conversation, including a fork, rather than its tree root.
8
8
  const id = process.env.CODEX_THREAD_ID ?? process.env.CODEX_SESSION_ID;
9
9
  if (!id)
10
- throw new Error("No current Codex session ID. Run 'rl session save codex' inside Codex, or supply the full UUID: rl session save codex <UUID>");
10
+ throw new Error("No current Codex session ID. Run 'rl agent save codex' inside Codex, or supply the full UUID: rl agent save codex <UUID>");
11
11
  return sessionId(id);
12
12
  }
package/dist/state.js CHANGED
@@ -145,7 +145,7 @@ export async function main(argv) {
145
145
  arity(rest, 0, rest[0] === "open" ? 1 : 0);
146
146
  return openPr(store, !!values.json);
147
147
  }
148
- if (command === "status" || command === "instances")
148
+ if (command === "status")
149
149
  arity(rest, 0);
150
150
  if (command === "session" && rest[0] === "list") {
151
151
  arity(rest, 1, 2);
@@ -154,10 +154,8 @@ export async function main(argv) {
154
154
  }
155
155
  if (command === "status" && values.fetch)
156
156
  git(store.repo, "fetch", "--all", "--prune");
157
- if (command === "status" ||
158
- command === "instances" ||
159
- (command === "session" && rest[0] === "list"))
160
- await syncCodexSessions(store, command === "instances" ? undefined : process.cwd());
157
+ if (command === "status" || (command === "session" && rest[0] === "list"))
158
+ await syncCodexSessions(store, process.cwd());
161
159
  let replacement = "";
162
160
  if (["context", "progress"].includes(command) &&
163
161
  ["set", "append"].includes(rest[0])) {
@@ -165,14 +163,6 @@ export async function main(argv) {
165
163
  replacement = fs.readFileSync(rest[1] === "-" ? 0 : rest[1], "utf8");
166
164
  }
167
165
  return store.locked(() => {
168
- if (command === "instances") {
169
- arity(rest, 0);
170
- json(store.records().map(([, data]) => ({
171
- ...data,
172
- gitStatus: branchStatus(store.repo, data),
173
- })));
174
- return 0;
175
- }
176
166
  if (command === "archive") {
177
167
  arity(rest, 0);
178
168
  const root = canonical(required(values, "worktree"));
@@ -219,7 +209,7 @@ export async function main(argv) {
219
209
  json(rest[1] ? data.sessions[agent(rest[1])] : data.sessions);
220
210
  }
221
211
  else
222
- throw new Error("Usage: rl session list [agent] | save [agent] [UUID] | add <agent> <UUID>");
212
+ throw new Error("Usage: rl agent sessions [agent] | save [agent] [UUID]");
223
213
  }
224
214
  else if (command === "context" || command === "progress") {
225
215
  const target = path.join(path.dirname(file), command + ".md"), action = rest[0];
package/dist/storage.js CHANGED
@@ -239,7 +239,7 @@ function remove(workspace, targets, recheck) {
239
239
  function suggestions(data, usage, estimatedBytes, days) {
240
240
  const result = [];
241
241
  if (estimatedBytes)
242
- result.push(`Run rl clean --dry-run to preview ${size(estimatedBytes)} of configured generated files.`);
242
+ result.push(`Run rl storage clean --dry-run to preview ${size(estimatedBytes)} of configured generated files.`);
243
243
  else if (usage.dependenciesBytes || usage.buildsBytes)
244
244
  result.push("Add generated directory paths to the clean list in this workspace's .rl/workflows.json.");
245
245
  const accessed = lastAccessed(data);
@@ -256,7 +256,7 @@ export async function main(argv) {
256
256
  if (!["storage", "clean"].includes(command) ||
257
257
  (command === "storage" && values["dry-run"]) ||
258
258
  (command === "clean" && values["older-than"] !== undefined))
259
- throw new Error("Usage: rl storage [--json] [--older-than 14d] | rl clean [--dry-run] [--json] (current workspace only)");
259
+ throw new Error("Usage: rl storage [--json] [--older-than 14d] | rl storage clean [--dry-run] [--json] (current workspace only)");
260
260
  const context = commandContext(values);
261
261
  // Resolve from the caller's cwd before any scan, lock creation, or mutation.
262
262
  const [file, data] = context.store.current();
package/dist/store.js CHANGED
@@ -183,7 +183,7 @@ export class Store {
183
183
  throw new Error(`Instance owns branch ${data.branch}; current branch is ${branch}`);
184
184
  return [file, data];
185
185
  }
186
- throw new Error("No RL instance for this worktree. Run 'rl adopt' first");
186
+ throw new Error("No RL instance for this worktree. Run 'rl setup adopt' first");
187
187
  }
188
188
  save(file, data) {
189
189
  data.updatedAt = now();
@@ -359,7 +359,7 @@ function installHooks(store, file, data) {
359
359
  export function contextText(file, data) {
360
360
  const sections = [
361
361
  `RL feature workspace: ${data.id}\nBranch: ${data.branch}`,
362
- "The following RL-owned documents are shared living feature documentation, not just conversation memory. Read them before starting work, and read the full files when excerpts are truncated. At meaningful milestones and before finishing your task, document what changed, what remains, implementation details (file/symbol references, flows, APIs, data structures), decisions and rationale, checks actually run, and blockers. Keep completed and planned work distinct; do not mark unverified work as tested or done. Respect your assigned scope and permissions; read-only agents should return proposed documentation updates to their parent. Use 'rl context append <file>' for durable domain or design notes and 'rl progress append <file>' for milestone/handoff updates ('-' reads stdin). Append is locked so concurrent agents do not overwrite one another. Use 'rl context set <file>' or 'rl progress set <file>' only for a deliberate replacement after reading the latest document and coordinating with other writers. Re-read the documents before planning subsequent work.",
362
+ "The following RL-owned documents are shared living feature documentation, not just conversation memory. Read them before starting work, and read the full files when excerpts are truncated. At meaningful milestones and before finishing your task, document what changed, what remains, implementation details (file/symbol references, flows, APIs, data structures), decisions and rationale, checks actually run, and blockers. Keep completed and planned work distinct; do not mark unverified work as tested or done. Respect your assigned scope and permissions; read-only agents should return proposed documentation updates to their parent. Use 'rl notes append context <file>' for durable domain or design notes and 'rl notes append progress <file>' for milestone/handoff updates ('-' reads stdin). Append is locked so concurrent agents do not overwrite one another. Use 'rl notes set context <file>' or 'rl notes set progress <file>' only for a deliberate replacement after reading the latest document and coordinating with other writers. Re-read the documents before planning subsequent work.",
363
363
  ];
364
364
  sections.push("When the user asks to pause or save this task for later, save a task handoff with rl_task_pause through MCP, or 'rl pause --summary <text> --changes <text> --validation <text> --next <text> [--blockers <text>]'. Include the current session UUID (Codex shell commands can infer it), concrete changes, checks actually run and their results, remaining work, and blockers. Use 'Not run' for unperformed checks. A pause saves task state; it does not terminate the agent. The user can return with 'rl continue'.");
365
365
  const automation = automationContext(data.worktree);
package/dist/summary.js CHANGED
@@ -86,7 +86,7 @@ function render(store, data, context, progress, defaultBase, requested) {
86
86
  parts.push("### Committed file changes", changes ? fence(changes) : "No net committed file changes.");
87
87
  }
88
88
  else
89
- parts.push(`The configured comparison base ${inline(comparison)} could not be resolved. Committed work was omitted; run \`rl summary --base <branch-or-commit>\` to include it.`);
89
+ parts.push(`The configured comparison base ${inline(comparison)} could not be resolved. Committed work was omitted; run \`rl notes summary --base <branch-or-commit>\` to include it.`);
90
90
  parts.push("## Uncommitted work", `Git status entries: ${status.staged} staged, ${status.unstaged} unstaged, ${status.untracked} untracked, ${status.conflicts} conflicts. An untracked directory counts as one status entry.`);
91
91
  for (const [title, flags] of [
92
92
  ["Staged changes", ["--cached"]],
package/dist/workflow.js CHANGED
@@ -16,8 +16,6 @@ const COMMAND_OPTIONS = {
16
16
  exec: ["workspace"],
17
17
  setup: ["workspace", "dry-run"],
18
18
  switch: ["create", "base", "no-fetch", "setup", "agent", "output-path"],
19
- new: ["base", "no-fetch", "setup", "output-path"],
20
- open: ["branch", "base", "no-fetch", "setup", "output-path"],
21
19
  };
22
20
  async function createWorkspace(context, name, base, noFetch, options = {}) {
23
21
  if (name.startsWith("-") ||
@@ -29,8 +27,7 @@ async function createWorkspace(context, name, base, noFetch, options = {}) {
29
27
  throw new Error(`Invalid workspace name: ${name}`);
30
28
  git(context.repo, "check-ref-format", "--branch", name);
31
29
  return locked(path.join(context.store.root, CREATE_LOCK), LOCK_FILE, async () => {
32
- const directoryName = options.directoryName ?? name;
33
- const destination = path.resolve(context.managedDir, directoryName);
30
+ const destination = path.resolve(context.managedDir, name);
34
31
  if (!inside(destination, context.managedDir) ||
35
32
  canonical(destination) !== destination)
36
33
  throw new Error("Workspace path must stay inside the managed directory without symlinks");
@@ -41,7 +38,7 @@ async function createWorkspace(context, name, base, noFetch, options = {}) {
41
38
  const [root, branch] = context.store.worktree(registered.worktree);
42
39
  if (branch !== name)
43
40
  throw new Error("Workspace branch changed during selection; retry");
44
- return { path: root, name: directoryName, branch, created: false };
41
+ return { path: root, name, branch, created: false };
45
42
  }
46
43
  if (fs.existsSync(destination))
47
44
  throw new Error(`Workspace path already exists: ${destination}`);
@@ -83,7 +80,7 @@ async function createWorkspace(context, name, base, noFetch, options = {}) {
83
80
  git(context.repo, "worktree", "add", ...creationArgs);
84
81
  return {
85
82
  path: destination,
86
- name: directoryName,
83
+ name,
87
84
  branch: name,
88
85
  created: true,
89
86
  };
@@ -92,7 +89,7 @@ async function createWorkspace(context, name, base, noFetch, options = {}) {
92
89
  export async function main(argv) {
93
90
  const separator = argv.indexOf("--");
94
91
  const forwarded = separator < 0 ? [] : argv.slice(separator + 1);
95
- const { values, positionals } = args(separator < 0 ? argv : argv.slice(0, separator), [...contextOptions, "base", "branch", "workspace", "agent", "output-path"], ["create", "no-fetch", "setup", "dry-run"], { create: "c", base: "b" });
92
+ const { values, positionals } = args(separator < 0 ? argv : argv.slice(0, separator), [...contextOptions, "base", "workspace", "agent", "output-path"], ["create", "no-fetch", "setup", "dry-run"], { create: "c", base: "b" });
96
93
  const context = commandContext(values);
97
94
  const [command, ...names] = positionals;
98
95
  const allowedOptions = COMMAND_OPTIONS[command];
@@ -101,10 +98,8 @@ export async function main(argv) {
101
98
  for (const option of Object.keys(values))
102
99
  if (!contextOptions.includes(option) && !allowedOptions.includes(option))
103
100
  throw new Error(`--${option} is not supported by rl ${command}`);
104
- if (command === "switch" &&
105
- !values.create &&
106
- (values.base || values["no-fetch"]))
107
- throw new Error("--base and --no-fetch require --create with rl switch");
101
+ if (command === "switch" && !values.create && values.base)
102
+ throw new Error("--base requires --create with rl switch");
108
103
  const selector = typeof values.workspace === "string" ? values.workspace : undefined;
109
104
  if (forwarded.length &&
110
105
  command !== "exec" &&
@@ -132,7 +127,7 @@ export async function main(argv) {
132
127
  console.log(file);
133
128
  }
134
129
  else
135
- throw new Error("Usage: rl config show|init");
130
+ throw new Error("Usage: rl setup show|init");
136
131
  return 0;
137
132
  }
138
133
  if (command === "exec") {
@@ -159,35 +154,26 @@ export async function main(argv) {
159
154
  });
160
155
  });
161
156
  }
162
- if (!["switch", "new", "open"].includes(command))
163
- throw new Error("Unknown workspace workflow command");
164
- arity(names, command === "open" ? 0 : 1);
165
- const selectedBranch = values.branch ?? (command === "open" ? values.base : undefined);
166
- if (command === "open" && typeof selectedBranch !== "string")
167
- throw new Error("Usage: rl open --branch <branch>");
157
+ arity(names, 1);
168
158
  const agent = typeof values.agent === "string" ? values.agent : undefined;
169
159
  if (agent && !SUPPORTED_AGENTS.has(agent))
170
160
  throw new Error("Agent must be codex or claude");
171
- const base = command !== "open" && typeof values.base === "string"
172
- ? values.base
173
- : context.defaultBase;
174
- const creating = !!values.create || command === "new" || command === "open";
175
- const branch = typeof selectedBranch === "string"
176
- ? selectedBranch.replace(/^origin\//, "")
177
- : names[0];
178
- const target = creating
179
- ? await createWorkspace(context, branch, base, !!values["no-fetch"], command === "switch"
180
- ? {}
181
- : {
182
- reuseExisting: true,
183
- ...(command === "open"
184
- ? {
185
- directoryName: branch.replaceAll("/", "-"),
186
- requireBranch: true,
187
- }
188
- : {}),
189
- })
190
- : resolveWorkspace(context, names[0]);
161
+ const base = typeof values.base === "string" ? values.base : context.defaultBase;
162
+ let target;
163
+ if (values.create) {
164
+ target = await createWorkspace(context, names[0], base, !!values["no-fetch"]);
165
+ }
166
+ else {
167
+ try {
168
+ target = resolveWorkspace(context, names[0]);
169
+ }
170
+ catch (error) {
171
+ if (!(error instanceof Error) ||
172
+ !error.message.startsWith("Workspace not found:"))
173
+ throw error;
174
+ target = await createWorkspace(context, names[0].replace(/^origin\//, ""), base, !!values["no-fetch"], { reuseExisting: true, requireBranch: true });
175
+ }
176
+ }
191
177
  await context.store.adopt(target.path, base);
192
178
  if (values.setup) {
193
179
  const code = await locked(path.join(context.store.root, SETUP_LOCK), LOCK_FILE, () => {
package/lib/entry.zsh ADDED
@@ -0,0 +1,54 @@
1
+ #!/bin/zsh -f
2
+ emulate -L zsh
3
+ readonly rl_home="${${(%):-%x}:A:h:h}"
4
+ if [[ "$1" == __hook ]]; then
5
+ # Hooks resolve the installed CLI on PATH, not a version-specific package path.
6
+ # Their explicit repository/state arguments must not be replaced by user config.
7
+ shift
8
+ exec node "$rl_home/dist/cli.js" state "$@"
9
+ fi
10
+ if [[ "$1" == --version || "$1" == -V ]]; then
11
+ exec node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).version' "$rl_home/package.json"
12
+ fi
13
+ if [[ "$1" == setup && "$2" == shell ]]; then
14
+ if (( $# == 2 )); then
15
+ exec node "$rl_home/dist/help.js" setup shell
16
+ fi
17
+ for argument in "$@"; do
18
+ [[ "$argument" == -- ]] && break
19
+ if [[ "$argument" == --help || "$argument" == -h ]]; then
20
+ exec node "$rl_home/dist/help.js" "$@"
21
+ fi
22
+ done
23
+ if [[ "$3" != zsh && "$3" != bash || $# != 3 ]]; then
24
+ print -u2 'Usage: rl setup shell <zsh|bash>'
25
+ exit 1
26
+ fi
27
+ if [[ "$3" == zsh ]]; then
28
+ print -r -- "source ${(q)rl_home}/shell/rl.zsh"
29
+ else
30
+ print -r -- "_rl_executable=${(qq)rl_home}/bin/rl"
31
+ cat <<'BASH'
32
+ rl() {
33
+ local destination_file destination result previous="$PWD"
34
+ local -a arguments=("$@")
35
+ if [[ "$1" == switch && "${2:-}" == - ]]; then
36
+ [[ -n "${RL_PREVIOUS_WORKSPACE:-}" ]] || { printf '%s\n' 'No previous workspace in this shell' >&2; return 1; }
37
+ arguments[1]="$RL_PREVIOUS_WORKSPACE"
38
+ fi
39
+ destination_file=$(mktemp "${TMPDIR:-/tmp}/rl-shell.XXXXXXXX") || return 1
40
+ if RL_SHELL_OUTPUT="$destination_file" command "$_rl_executable" "${arguments[@]}"; then result=0; else result=$?; fi
41
+ destination=$(cat -- "$destination_file")
42
+ rm -f -- "$destination_file"
43
+ if [[ -n "$destination" ]]; then
44
+ builtin cd -- "$destination" || return 1
45
+ RL_PREVIOUS_WORKSPACE="$previous"
46
+ fi
47
+ return "$result"
48
+ }
49
+ BASH
50
+ fi
51
+ exit 0
52
+ fi
53
+ source "$rl_home/lib/rl.zsh" || exit 1
54
+ _rl_main "$@"
package/lib/rl.zsh CHANGED
@@ -24,6 +24,12 @@ _rl_change_directory() {
24
24
  print -rn -- "$PWD" > "$RL_SHELL_OUTPUT"
25
25
  else
26
26
  print -r -- "$PWD"
27
+ if [[ -t 1 && -t 2 ]]; then
28
+ case "${SHELL:t}" in
29
+ zsh|bash) print -u2 -r -- "Enable directory switching: eval \"\$(rl setup shell ${SHELL:t})\"" ;;
30
+ *) print -u2 'Enable directory switching with shell integration: rl help setup shell' ;;
31
+ esac
32
+ fi
27
33
  fi
28
34
  }
29
35
 
@@ -57,12 +63,12 @@ _rl_main() {
57
63
  return
58
64
  fi
59
65
  done
60
- if (( ${#help_args} == 1 )) && [[ "$help_args[1]" == (agent|workflow|policy|repo|git|notes|session|config) ]]; then
66
+ if (( ${#help_args} == 1 )) && [[ "$help_args[1]" == (agent|workflow|policy|repo|git|notes) ]]; then
61
67
  _rl_help "${help_args[@]}"
62
68
  return
63
69
  fi
64
70
 
65
- # Canonical command families; older spellings remain compatibility aliases.
71
+ # Translate public command families to their internal handlers.
66
72
  # Preserve a leading repository selector while normalizing the public command.
67
73
  local -a routing_prefix=()
68
74
  if [[ "$1" == --repo ]]; then
@@ -72,7 +78,23 @@ _rl_main() {
72
78
  routing_prefix=("$1")
73
79
  shift
74
80
  fi
81
+ case "$1" in
82
+ new|open|instances|merge|clean|summary|handoff|mcp|session|context|progress|config|adopt|init)
83
+ print -u2 -r -- "Unknown command: $1"
84
+ print -u2 'Run rl help for available commands.'
85
+ return 1 ;;
86
+ status)
87
+ if (( ${argv[(Ie)--all]} || ${argv[(Ie)--list]} )); then
88
+ print -u2 'Use rl list for workspace overviews.'
89
+ return 1
90
+ fi ;;
91
+ esac
75
92
  case "$1:$2" in
93
+ setup:dependencies|setup:install)
94
+ local setup_module=dependencies
95
+ [[ "$2" == install ]] && setup_module=install
96
+ command node "${${functions_source[_rl_main]}:A:h:h}/dist/$setup_module.js" "${@:3}"
97
+ return ;;
76
98
  git:merge) argv=("$2" "${@:3}") ;;
77
99
  agent:sessions) argv=(session list "${@:3}") ;;
78
100
  agent:save) argv=(session save "${@:3}") ;;
@@ -81,9 +103,10 @@ _rl_main() {
81
103
  storage:clean) argv=(clean "${@:3}") ;;
82
104
  setup:init|setup:show) argv=(config "$2" "${@:3}") ;;
83
105
  setup:adopt) argv=(adopt "${@:3}") ;;
106
+ setup:mcp) argv=(mcp-setup "${@:3}") ;;
84
107
  setup:run) argv=(setup "${@:3}") ;;
85
108
  setup:shell)
86
- command "${${functions_source[_rl_main]}:A:h:h}/bin/rl" init "${@:3}"
109
+ command "${${functions_source[_rl_main]}:A:h:h}/bin/rl" setup shell "${@:3}"
87
110
  return ;;
88
111
  notes:summary) argv=(summary "${@:3}") ;;
89
112
  notes:show|notes:path|notes:set|notes:append)
@@ -209,13 +232,19 @@ _rl_main() {
209
232
  --default-base "$default_base" "$@"
210
233
  return
211
234
  fi
212
- if [[ "$command" == pr && ( "$2" == checks || "$2" == checkout ) ]]; then
235
+ if [[ "$command" == pr && "$2" == checkout ]]; then
213
236
  command node "${${functions_source[_rl_main]}:A:h:h}/dist/cli.js" pr \
214
237
  --repo "$repo_root" --state-dir "$state_dir" --base-dir "$base_dir/$repo_name" \
215
238
  --default-base "$default_base" "$@"
216
239
  return
217
240
  fi
218
241
  case "$command" in
242
+ mcp-setup)
243
+ shift
244
+ command node "${${functions_source[_rl_main]}:A:h:h}/dist/cli.js" mcp-setup \
245
+ --repo "$repo_root" --state-dir "$state_dir" --base-dir "$base_dir/$repo_name" \
246
+ --default-base "$default_base" "$@"
247
+ return ;;
219
248
  storage|clean)
220
249
  command node "${${functions_source[_rl_main]}:A:h:h}/dist/cli.js" storage \
221
250
  --repo "$repo_root" --state-dir "$state_dir" --base-dir "$base_dir/$repo_name" \
@@ -260,13 +289,13 @@ _rl_main() {
260
289
  command node "${${functions_source[_rl_main]}:A:h:h}/dist/cli.js" summary \
261
290
  --repo "$repo_root" --state-dir "$state_dir" --default-base "$default_base" "$@"
262
291
  return ;;
263
- adopt|doctor|status|instances|resume|session|context|progress|pr)
292
+ adopt|doctor|status|resume|session|context|progress|pr)
264
293
  _rl_state "$@"
265
294
  return ;;
266
295
  esac
267
296
 
268
297
  case "$command" in
269
- switch|new|open|continue)
298
+ switch|continue)
270
299
  if [[ "$command" == switch && "$2" == - ]]; then
271
300
  [[ -n "${RL_PREVIOUS_WORKSPACE:-}" ]] || { print -u2 'No previous workspace in this shell'; return 1; }
272
301
  argv[2]="$RL_PREVIOUS_WORKSPACE"