rl-workspaces 0.5.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 (55) hide show
  1. package/README.md +234 -193
  2. package/bin/rl +2 -54
  3. package/dist/agent-launch.js +1 -1
  4. package/dist/agent-presets.js +42 -17
  5. package/dist/agent-resume.js +2 -2
  6. package/dist/agent-runner.js +20 -0
  7. package/dist/agent-workflows.js +60 -16
  8. package/dist/automation-config.js +144 -5
  9. package/dist/bootstrap.js +74 -0
  10. package/dist/cli.js +2 -0
  11. package/dist/command-context.js +1 -1
  12. package/dist/common.js +3 -2
  13. package/dist/dependencies.js +197 -0
  14. package/dist/doctor.js +1 -1
  15. package/dist/git-workflow.js +8 -102
  16. package/dist/help.js +113 -116
  17. package/dist/hooks.js +1 -1
  18. package/dist/install.js +143 -0
  19. package/dist/mcp-setup.js +171 -0
  20. package/dist/mcp.js +13 -16
  21. package/dist/picker.js +24 -7
  22. package/dist/pr-workflow.js +7 -28
  23. package/dist/repos.js +5 -1
  24. package/dist/session-workflows.js +122 -18
  25. package/dist/session.js +1 -1
  26. package/dist/state.js +4 -14
  27. package/dist/storage.js +2 -2
  28. package/dist/store.js +2 -2
  29. package/dist/summary.js +1 -1
  30. package/dist/workflow-collaboration.js +531 -0
  31. package/dist/workflow.js +24 -38
  32. package/lib/entry.zsh +54 -0
  33. package/lib/rl.zsh +37 -9
  34. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  35. package/node_modules/smol-toml/LICENSE +24 -0
  36. package/node_modules/smol-toml/README.md +605 -0
  37. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  38. package/node_modules/smol-toml/dist/date.js +156 -0
  39. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  40. package/node_modules/smol-toml/dist/error.js +67 -0
  41. package/node_modules/smol-toml/dist/extract.js +293 -0
  42. package/node_modules/smol-toml/dist/index.cjs +771 -0
  43. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  44. package/node_modules/smol-toml/dist/index.js +34 -0
  45. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  46. package/node_modules/smol-toml/dist/parse.js +164 -0
  47. package/node_modules/smol-toml/dist/primitive.js +151 -0
  48. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  49. package/node_modules/smol-toml/dist/stringify.js +219 -0
  50. package/node_modules/smol-toml/dist/struct.js +156 -0
  51. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  52. package/node_modules/smol-toml/dist/util.js +57 -0
  53. package/node_modules/smol-toml/package.json +68 -0
  54. package/package.json +6 -3
  55. package/shell/rl.zsh +1 -1
@@ -53,7 +53,7 @@ export async function main(argv) {
53
53
  if (extra.length ||
54
54
  values.prompt !== undefined ||
55
55
  (values.stdout && values.output !== undefined))
56
- throw new Error("Usage: rl handoff [agent] [--stdout | --output path]");
56
+ throw new Error("Usage: rl agent handoff [agent] [--stdout | --output path]");
57
57
  const text = await handoff(context.store, name, target.path);
58
58
  if (typeof values.output === "string")
59
59
  fs.writeFileSync(values.output, text, { flag: "wx", mode: 0o600 });
@@ -1,45 +1,70 @@
1
1
  export const PRESET = {
2
+ defaultWorkflow: "writer-reviewer-dry",
2
3
  agents: {
3
- behavior: {
4
+ writer: {
4
5
  provider: "inherit",
5
6
  reasoning: "high",
6
- description: "Runtime correctness, user journeys, persistence and security",
7
- instructions: "Investigate supported end-to-end user journeys: input, validation, serialization, persistence, queries, subscriptions, caches, rendering and downstream consumers. Own runtime bugs, regressions, concurrency, error behavior, tenant isolation and demonstrable runtime performance failures. Check current code and intended behavior; history and old comments are leads, not proof. Distinguish current defects from fixed regressions, intentional boundaries and unverified hypotheses. Findings need prerequisites, reproducible steps, expected versus actual behavior, and evidence; label code-inferred reproductions. Do not review type hygiene without runtime impact, test design, debug cleanup, duplication or structural simplification. Attach regression advice to the corresponding bug rather than filing duplicate missing-test findings.",
7
+ access: "workspace-write",
8
+ description: "Implement scoped changes and submit review checkpoints",
9
+ instructions: "Own implementation in the assigned workspace and files. You are not alone: preserve other people's work and coordinate changes with the parent. Start alongside the reviewer and DRY peer; do not spawn replacement reviewers or searchers. Follow the workflow's configured consultation rules. By default, before adding any new function consult the existing DRY peer using event new_function and a precise subject such as src/file.ts#functionName, then wait for its answer and reuse existing behavior when appropriate. Submit bounded, stable file groups to the reviewer with all newFunctions declared. Freeze submitted files while a review is pending; you may continue on unrelated files. Address feedback and resubmit until the configured rating and policy requirements pass. Report actual checks, remaining work and limitations honestly. Do not close your stage until current changes have accepted reviews and your consultations are answered.",
8
10
  },
9
- reuse: {
11
+ reviewer: {
10
12
  provider: "inherit",
11
13
  reasoning: "high",
12
- description: "Reuse, duplicated behavior and avoidable complexity",
13
- instructions: "Search the repository for existing components, hooks, services, utilities, dependencies and alternate implementations before judging a new capability. Own duplicated business behavior, unnecessary abstractions/features, redundant state and misplaced responsibilities with concrete maintenance cost. Prefer reuse, extension, composition, then a small focused implementation. Cite both the reviewed implementation and existing counterpart, the behavioral overlap and a viable smaller solution. For pure simplification, name removable machinery and explain how requirements are preserved. Small duplication and different presentations can be justified. Do not investigate runtime/security bugs, type-only duplication, logging cleanup or test quality. Labels such as DRY, YAGNI and code smell are not evidence.",
14
+ access: "read-only",
15
+ description: "Review writer submissions and return evidence-backed ratings",
16
+ instructions: "Stay read-only and available alongside the writer and DRY peer. Review each writer submission against its saved checkpoint, surrounding source, intended behavior and the separate policy. Assess correctness, authorization, persistence, concurrency, type contracts and meaningful validation. Return a rating and rationale using the configured rating scale, with structured findings, coverage limitations and required fixes. Follow the workflow's configured consultation rules. By default, if you suspect duplication consult the existing DRY peer using event suspected_duplicate and a precise subject before returning the review. Declare every duplicateSuspicions subject. Incorporate its evidence rather than treating similarity as proof. Send actionable feedback to the writer, re-review resubmissions, and never equate missing evidence with a clean result. Do not edit code, silently replace peers, or finish before the writer and all submitted work are settled.",
14
17
  },
15
- quality: {
18
+ dry: {
16
19
  provider: "inherit",
17
20
  reasoning: "high",
18
- description: "Type contracts, static checks, debug leftovers and test quality",
19
- instructions: "Own unjustified any, casts and suppressions, overly broad contracts, failure to reuse applicable types, accidental debug leftovers, attributable static-check failures, and tests that cannot protect their claimed behavior. Distinguish safe unknown narrowing and justified dependency escape hatches from weakened checking. Preserve intentional operational logging. For test findings name a specific material contract and a useful assertion, not a coverage percentage. Do not review runtime correctness/security, architecture or duplicated business behavior. A test failure is not automatically a product bug. Request configured checks from the coordinator instead of running duplicate commands. Omit style preferences and broad cleanup of existing code.",
21
+ access: "read-only",
22
+ description: "Shared reuse and duplication specialist for writer and reviewer",
23
+ instructions: "Stay read-only and available as the single DRY peer for both writer and reviewer. You may map relevant existing utilities while waiting for requests. Answer new_function consultations from the writer before it adds behavior, and suspected_duplicate consultations from the reviewer before it returns a rating. Search current source for existing functions, components, services, types and dependencies. Cite concrete paths/symbols, behavioral overlap, relevant differences and a viable reuse approach; state explicitly when no suitable implementation was found or coverage is incomplete. Small or superficial similarity alone is not evidence of harmful duplication. Respond to the requesting peer through its consultation record; do not edit files, assign ratings, create more agents, or close while requests or either peer remain active.",
20
24
  },
21
25
  },
22
26
  policies: {
23
27
  review: {
24
- instructions: "Report only evidence-backed problems caused or materially worsened by the reviewed changes. Separate required fixes from optional quick wins with reasons. Critical means security exposure, destructive data loss or outage; high means broken core behavior or substantial structural cost; medium means a contained defect or worthwhile required quality fix; low means an optional quick win. Do not block on stylistic preference, tiny duplication, justified type escape hatches, intentional operational logging, speculative flexibility or generic demands for tests. Missing dependencies, unverified critical paths and unresolved material questions are coverage limitations. Do not invent findings or treat a score as a probability of correctness.",
28
+ instructions: "Report evidence-backed defects caused or materially worsened by the submitted work. Separate required fixes from optional improvements, and explain prerequisites, expected behavior and observed or inferred failures. Evaluate reuse against concrete alternatives. Avoid style-only blockers and invented findings. Missing checks, dependencies and unverified material paths are coverage limitations. Review ratings are judgments supported by evidence; RL's policy score is calculated separately and is not a probability of correctness.",
25
29
  weights: { critical: 50, high: 20, medium: 10, low: 2 },
26
30
  minimumScore: 0,
27
31
  blockRequired: true,
32
+ rating: {
33
+ min: 0,
34
+ max: 100,
35
+ minimum: 90,
36
+ instructions: "Rate each submission from 0 to 100. 90–100 means ready with verified material behavior and no required fixes; 70–89 needs contained corrections or validation; below 70 needs substantial corrections. Explain the rating and uncertainty. A high rating never overrides required fixes, failed checks or incomplete coverage.",
37
+ },
28
38
  },
29
39
  },
30
40
  workflows: {
31
- "triple-review": {
32
- description: "Three independent reviewers, owner follow-ups and a policy-based verdict",
41
+ "writer-reviewer-dry": {
42
+ description: "Parallel writer, reviewer and DRY peers with rated review checkpoints",
33
43
  policy: "review",
34
44
  concurrency: 3,
35
45
  retries: 1,
36
- timeoutSeconds: 600,
37
- triggers: [{ event: "before_finish", changes: true }],
46
+ timeoutSeconds: 3600,
47
+ triggers: [{ event: "task_planned" }, { event: "before_edit" }],
38
48
  steps: [
39
- { id: "behavior", agent: "behavior", after: [] },
40
- { id: "reuse", agent: "reuse", after: [] },
41
- { id: "quality", agent: "quality", after: [] },
49
+ { id: "writer", agent: "writer", after: [] },
50
+ { id: "reviewer", agent: "reviewer", after: [] },
51
+ { id: "dry", agent: "dry", after: [] },
42
52
  ],
53
+ collaboration: {
54
+ writer: "writer",
55
+ reviewer: "reviewer",
56
+ dry: "dry",
57
+ maxRounds: 5,
58
+ rules: [
59
+ { from: "writer", event: "new_function", to: "dry", required: true },
60
+ {
61
+ from: "reviewer",
62
+ event: "suspected_duplicate",
63
+ to: "dry",
64
+ required: true,
65
+ },
66
+ ],
67
+ },
43
68
  },
44
69
  },
45
70
  };
@@ -19,7 +19,7 @@ export async function resumeSessions(store, kind, options = {}) {
19
19
  : a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id);
20
20
  });
21
21
  if (!choices.length)
22
- throw new Error(`No ${kind ? `${kind} ` : ""}session recorded for ${data.id}. Use 'rl session save' to save a session first`);
22
+ throw new Error(`No ${kind ? `${kind} ` : ""}session recorded for ${data.id}. Use 'rl agent save' to save a session first`);
23
23
  let selected = choices[0];
24
24
  if (choices.length > 1 && !options.latest) {
25
25
  const visible = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
@@ -70,7 +70,7 @@ export async function resumeAgent(store, kind, options = {}) {
70
70
  ? sessions.find((item) => item.id === sessionId(options.sessionId))
71
71
  : sessions.reduce((a, b) => !a || timestamp(b.lastUsedAt) > timestamp(a.lastUsedAt) ? b : a, undefined);
72
72
  if (!item)
73
- throw new Error(`No ${kind} session recorded for ${data.id}. Use 'rl session save ${kind}${kind === "claude" ? " <UUID>" : ""}'`);
73
+ throw new Error(`No ${kind} session recorded for ${data.id}. Use 'rl agent save ${kind}${kind === "claude" ? " <UUID>" : ""}'`);
74
74
  return [file, data, sessionId(item.id)];
75
75
  });
76
76
  const stamp = now();
@@ -6,6 +6,7 @@ import { readJsonObject } from "./json.js";
6
6
  import { validateAutomation, } from "./automation-config.js";
7
7
  import { evaluate, validateResult, } from "./review-policy.js";
8
8
  import { agentAdapter } from "./agent-registry.js";
9
+ import { collaborationAssessment, initializeCollaboration, validateCollaborationState, } from "./workflow-collaboration.js";
9
10
  export function captureScope(cwd, base) {
10
11
  if (!base || base.startsWith("-") || /[\x00-\x20\x7f]/.test(base))
11
12
  throw new Error("Expected a literal base ref");
@@ -126,6 +127,8 @@ function load(directory) {
126
127
  if (!Object.hasOwn(raw.config.workflows, raw.workflow))
127
128
  throw new Error("Invalid saved workflow");
128
129
  const definition = raw.config.workflows[raw.workflow];
130
+ if (definition.collaboration)
131
+ validateCollaborationState(raw.collaboration);
129
132
  const seen = new Set();
130
133
  for (const stage of raw.stages) {
131
134
  if (!object(stage) ||
@@ -213,11 +216,14 @@ export function evaluation(run, policy = run.config.policies[run.config.workflow
213
216
  stage.result !== undefined &&
214
217
  currentScope(stage))
215
218
  .map((stage) => ({ owner: stage.id, result: stage.result }));
219
+ const collaboration = collaborationAssessment(run);
220
+ reports.push(...collaboration.reports);
216
221
  const limitations = run.stages
217
222
  .filter((stage) => stage.status !== "completed")
218
223
  .map((stage) => `${stage.id}: ${stage.error ?? stage.status}`);
219
224
  if (run.status !== "completed")
220
225
  limitations.push(`Run ${run.status}`);
226
+ limitations.push(...collaboration.limitations);
221
227
  for (const stage of run.stages.filter((stage) => stage.status === "completed" && !currentScope(stage))) {
222
228
  if (stage.agent &&
223
229
  run.config.agents[stage.agent].access === "workspace-write")
@@ -269,6 +275,19 @@ export function runMetrics(run) {
269
275
  outputTokens: total("outputTokens"),
270
276
  costUsd: total("costUsd"),
271
277
  usageCoverage: `${reported.length}/${run.stages.filter((step) => step.agent && step.attempts).length} agent stages`,
278
+ ...(run.collaboration
279
+ ? {
280
+ collaboration: {
281
+ consultations: run.collaboration.consultations.length,
282
+ awaitingAnswers: run.collaboration.consultations.filter((item) => !item.answer).length,
283
+ submissions: run.collaboration.submissions.length,
284
+ accepted: run.collaboration.submissions.filter((item) => !item.supersededBy && item.review?.accepted).length,
285
+ latestRating: [...run.collaboration.submissions]
286
+ .reverse()
287
+ .find((item) => item.review)?.review?.rating ?? null,
288
+ },
289
+ }
290
+ : {}),
272
291
  stages: run.stages.map(({ id, status, attempts, durationMs, usage }) => ({
273
292
  id,
274
293
  status,
@@ -326,6 +345,7 @@ export function createRunLocked(store, config, workflow, base, prompt) {
326
345
  durationMs: 0,
327
346
  })),
328
347
  };
348
+ initializeCollaboration(run);
329
349
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
330
350
  fs.writeFileSync(path.join(directory, "scope.json"), context, {
331
351
  mode: 0o600,
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import { stripVTControlCharacters } from "node:util";
4
4
  import { args, arity, json } from "./common.js";
5
5
  import { commandContext, contextOptions } from "./command-context.js";
6
- import { CONFIG_FILES, initAutomation, readAutomation, setAutomation, } from "./automation-config.js";
6
+ import { CONFIG_FILES, initAutomation, readAutomation, setAutomation, defaultWorkflow, } from "./automation-config.js";
7
7
  import { controlRun, planRun, readRun, rescoreRun, runMetrics, } from "./agent-runner.js";
8
8
  import { callWorkflowTool, resumeNativeRun } from "./session-workflows.js";
9
9
  import { parseJsonObject } from "./json.js";
@@ -17,6 +17,12 @@ function report(run, asJson) {
17
17
  console.log(`\n${visible(run.workflow)} · ${run.status}\nRun: ${run.id}\nWorkspace: ${visible(run.workspace)}\nScope: ${visible(run.scope.base)} → ${run.scope.head.slice(0, 12)} · ${run.scope.files.length} changed files\n`);
18
18
  for (const stage of run.stages)
19
19
  console.log(` ${stage.id.padEnd(22)} ${stage.status.padEnd(10)} ${stage.attempts} attempt(s) · ${(stage.durationMs / 1000).toFixed(1)}s${stage.error ? ` · ${visible(stage.error)}` : ""}`);
20
+ if (run.collaboration) {
21
+ const rating = run.config.policies[run.config.workflows[run.workflow].policy].rating;
22
+ for (const submission of run.collaboration.submissions.filter((item) => !item.supersededBy))
23
+ console.log(` Review ${submission.id.slice(0, 8)} · round ${submission.round} · ${submission.review ? `${submission.review.rating}/${rating.max} · ${submission.review.accepted ? "accepted" : "changes required"}` : "awaiting reviewer"}`);
24
+ console.log(` DRY: ${run.collaboration.consultations.filter((item) => !item.answer).length} unanswered consultation(s)`);
25
+ }
20
26
  if (run.evaluation) {
21
27
  const result = run.evaluation;
22
28
  console.log(`\n${result.score === null ? "Not scored" : `Score: ${result.score}/100`} · ${result.verdict}${result.verdict === "incomplete" && result.score !== null ? " (provisional)" : ""}`);
@@ -44,7 +50,8 @@ export async function main(argv) {
44
50
  "session",
45
51
  "step",
46
52
  "request",
47
- ], ["json", "dry-run"]);
53
+ "claim",
54
+ ], ["json", "dry-run", "default"]);
48
55
  const context = commandContext(values);
49
56
  const [namespace, action, ...rest] = positionals;
50
57
  arity(rest, 0, 1);
@@ -52,6 +59,9 @@ export async function main(argv) {
52
59
  const asJson = !!values.json;
53
60
  if (values.file && values.instructions)
54
61
  throw new Error("Choose --file or --instructions, not both");
62
+ if (values.default &&
63
+ !(namespace === "workflow" && ["add", "set"].includes(action)))
64
+ throw new Error("--default requires workflow add/set");
55
65
  if ((values.description || values.model || values.reasoning) &&
56
66
  !(namespace === "agent" &&
57
67
  ["add", "set"].includes(action) &&
@@ -89,16 +99,27 @@ export async function main(argv) {
89
99
  if ((namespace === "agent" || namespace === "workflow") &&
90
100
  ["add", "set"].includes(action)) {
91
101
  arity(rest, 1);
92
- const value = namespace === "agent" && values.instructions !== undefined
93
- ? {
94
- provider: "inherit",
95
- description: String(values.description ?? rest[0]),
96
- instructions: fs.readFileSync(values.instructions === "-" ? 0 : String(values.instructions), "utf8"),
97
- ...(values.model ? { model: values.model } : {}),
98
- ...(values.reasoning ? { reasoning: values.reasoning } : {}),
99
- }
100
- : loadInput();
101
- await context.store.locked(() => setAutomation(data.worktree, namespace === "agent" ? "agents" : "workflows", rest[0], value, action === "add"));
102
+ const instructions = namespace === "agent" && values.instructions !== undefined
103
+ ? fs.readFileSync(values.instructions === "-" ? 0 : String(values.instructions), "utf8")
104
+ : undefined;
105
+ const input = instructions === undefined ? loadInput() : undefined;
106
+ const value = await context.store.locked(() => {
107
+ const previous = namespace === "agent" && action === "set"
108
+ ? readAutomation(data.worktree).agents[rest[0]]
109
+ : undefined;
110
+ const profile = instructions === undefined
111
+ ? input
112
+ : {
113
+ provider: "inherit",
114
+ ...previous,
115
+ description: String(values.description ?? previous?.description ?? rest[0]),
116
+ instructions,
117
+ ...(values.model ? { model: values.model } : {}),
118
+ ...(values.reasoning ? { reasoning: values.reasoning } : {}),
119
+ };
120
+ setAutomation(data.worktree, namespace === "agent" ? "agents" : "workflows", rest[0], profile, action === "add", !!values.default);
121
+ return profile;
122
+ });
102
123
  await context.store.adopt(data.worktree);
103
124
  if (asJson)
104
125
  json(value);
@@ -111,8 +132,25 @@ export async function main(argv) {
111
132
  ...(values.session ? { sessionId: values.session } : {}),
112
133
  };
113
134
  if (namespace === "workflow" &&
114
- ["context", "pending", "event", "claim", "report", "dismiss"].includes(action)) {
115
- const input = action === "report" || action === "dismiss" || action === "event"
135
+ [
136
+ "context",
137
+ "pending",
138
+ "event",
139
+ "claim",
140
+ "report",
141
+ "dismiss",
142
+ "bind",
143
+ "inbox",
144
+ "consult",
145
+ "answer",
146
+ "submit",
147
+ "review",
148
+ ].includes(action)) {
149
+ const input = action === "report" ||
150
+ action === "dismiss" ||
151
+ action === "event" ||
152
+ ["bind", "consult", "answer", "submit", "review"].includes(action) ||
153
+ values.file
116
154
  ? loadInput()
117
155
  : {};
118
156
  const result = await callWorkflowTool(context.store, `rl_workflow_${action}`, {
@@ -120,6 +158,7 @@ export async function main(argv) {
120
158
  ...parent,
121
159
  ...(rest[0] ? { run: rest[0] } : {}),
122
160
  ...(values.step ? { step: values.step } : {}),
161
+ ...(values.claim ? { claim: values.claim } : {}),
123
162
  });
124
163
  if (asJson || action !== "context")
125
164
  json(result);
@@ -166,7 +205,7 @@ export async function main(argv) {
166
205
  const name = rest[0];
167
206
  if (action === "run") {
168
207
  const config = configuration();
169
- const workflow = name ?? "triple-review", base = String(values.base ?? context.defaultBase);
208
+ const workflow = name ?? defaultWorkflow(config), base = String(values.base ?? context.defaultBase);
170
209
  if (values["dry-run"]) {
171
210
  const plan = planRun(context.store, config, workflow, base);
172
211
  if (asJson)
@@ -215,7 +254,12 @@ export async function main(argv) {
215
254
  else {
216
255
  console.log(`Metrics: ${run.id}`);
217
256
  for (const [key, value] of Object.entries(metrics))
218
- if (key !== "stages")
257
+ if (key === "collaboration" && metrics.collaboration) {
258
+ const team = metrics.collaboration;
259
+ console.log(`Reviews: ${team.accepted} accepted · ${team.submissions} submissions · latest rating ${team.latestRating ?? "not reported"}`);
260
+ console.log(`Consultations: ${team.consultations} total · ${team.awaitingAnswers} awaiting answers`);
261
+ }
262
+ else if (key !== "stages")
219
263
  console.log(`${key}: ${value ?? "not reported"}`);
220
264
  }
221
265
  return 0;
@@ -35,7 +35,7 @@ function integer(value, min, max) {
35
35
  throw new Error(`Expected an integer from ${min} to ${max}`);
36
36
  }
37
37
  export function validateAutomation(value) {
38
- fields(value, ["agents", "policies", "workflows"]);
38
+ fields(value, ["agents", "policies", "workflows", "defaultWorkflow"], ["agents", "policies", "workflows"]);
39
39
  for (const kind of ["agents", "policies", "workflows"]) {
40
40
  const entries = value[kind];
41
41
  if (!object(entries) ||
@@ -79,7 +79,8 @@ export function validateAutomation(value) {
79
79
  "weights",
80
80
  "minimumScore",
81
81
  "blockRequired",
82
- ]);
82
+ "rating",
83
+ ], ["instructions", "weights", "minimumScore", "blockRequired"]);
83
84
  nonempty(item.instructions);
84
85
  fields(item.weights, [...SEVERITIES]);
85
86
  for (const weight of Object.values(item.weights))
@@ -87,6 +88,16 @@ export function validateAutomation(value) {
87
88
  integer(item.minimumScore, 0, 100);
88
89
  if (typeof item.blockRequired !== "boolean")
89
90
  throw new Error("blockRequired must be boolean");
91
+ if (item.rating !== undefined) {
92
+ fields(item.rating, ["min", "max", "minimum", "instructions"]);
93
+ for (const key of ["min", "max", "minimum"])
94
+ integer(item.rating[key], 0, 1000);
95
+ if (Number(item.rating.max) <= Number(item.rating.min) ||
96
+ Number(item.rating.minimum) < Number(item.rating.min) ||
97
+ Number(item.rating.minimum) > Number(item.rating.max))
98
+ throw new Error("Invalid reviewer rating range");
99
+ nonempty(item.rating.instructions);
100
+ }
90
101
  }
91
102
  else {
92
103
  fields(item, [
@@ -97,6 +108,7 @@ export function validateAutomation(value) {
97
108
  "timeoutSeconds",
98
109
  "steps",
99
110
  "triggers",
111
+ "collaboration",
100
112
  ], [
101
113
  "description",
102
114
  "policy",
@@ -176,9 +188,77 @@ export function validateAutomation(value) {
176
188
  done.add(id);
177
189
  }
178
190
  }
191
+ if (item.collaboration !== undefined) {
192
+ const collaboration = item.collaboration;
193
+ fields(collaboration, [
194
+ "writer",
195
+ "reviewer",
196
+ "dry",
197
+ "maxRounds",
198
+ "rules",
199
+ ]);
200
+ const roles = [
201
+ collaboration.writer,
202
+ collaboration.reviewer,
203
+ collaboration.dry,
204
+ ];
205
+ if (new Set(roles).size !== 3 || Number(item.concurrency) < 3)
206
+ throw new Error("Collaborative roles require three distinct steps and concurrency of at least three");
207
+ for (const role of roles) {
208
+ identifier(role);
209
+ const step = item.steps.find((step) => step.id === role);
210
+ if (!step?.agent || step.after.length)
211
+ throw new Error("Collaborative roles must be agent steps ready together at startup");
212
+ const profile = object(value.agents)
213
+ ? value.agents[step.agent]
214
+ : undefined;
215
+ if (role === collaboration.writer &&
216
+ object(profile) &&
217
+ profile.access !== "workspace-write")
218
+ throw new Error("Collaborative writer must have workspace-write access");
219
+ if (role !== collaboration.writer &&
220
+ object(profile) &&
221
+ profile.access === "workspace-write")
222
+ throw new Error("Reviewer and DRY roles must be read-only");
223
+ }
224
+ integer(collaboration.maxRounds, 1, 20);
225
+ const policy = object(value.policies)
226
+ ? value.policies[item.policy]
227
+ : undefined;
228
+ if (!object(policy) || !object(policy.rating))
229
+ throw new Error("Collaborative workflows require a reviewer rating policy");
230
+ if (!Array.isArray(collaboration.rules) ||
231
+ collaboration.rules.length > 32)
232
+ throw new Error("Expected up to 32 collaboration rules");
233
+ const routes = new Set();
234
+ for (const rule of collaboration.rules) {
235
+ fields(rule, ["from", "event", "to", "required"]);
236
+ if (!roles.includes(rule.from) ||
237
+ !roles.includes(rule.to) ||
238
+ rule.from === rule.to ||
239
+ typeof rule.required !== "boolean")
240
+ throw new Error("Invalid collaboration route");
241
+ identifier(rule.event);
242
+ if (rule.required &&
243
+ !((rule.from === collaboration.writer &&
244
+ rule.event === "new_function") ||
245
+ (rule.from === collaboration.reviewer &&
246
+ rule.event === "suspected_duplicate")))
247
+ throw new Error("Required consultations support writer new_function and reviewer suspected_duplicate; use required: false for additional message routes");
248
+ const key = `${rule.from}:${rule.event}:${rule.to}`;
249
+ if (routes.has(key))
250
+ throw new Error("Duplicate collaboration rule");
251
+ routes.add(key);
252
+ }
253
+ }
179
254
  }
180
255
  }
181
256
  }
257
+ if (value.defaultWorkflow !== undefined &&
258
+ (!object(value.workflows) ||
259
+ typeof value.defaultWorkflow !== "string" ||
260
+ !Object.hasOwn(value.workflows, value.defaultWorkflow)))
261
+ throw new Error("Unknown default workflow");
182
262
  }
183
263
  function configPath(root, relative) {
184
264
  const directory = path.join(root, ".rl"), file = path.join(root, relative);
@@ -191,16 +271,68 @@ export function readAutomation(root) {
191
271
  const result = {};
192
272
  for (const [kind, relative] of Object.entries(CONFIG_FILES)) {
193
273
  const file = configPath(root, relative);
194
- if (!fs.existsSync(file))
274
+ if (!fs.existsSync(file)) {
195
275
  result[kind] = structuredClone(PRESET[kind]);
276
+ if (kind === "workflows")
277
+ result.defaultWorkflow = PRESET.defaultWorkflow;
278
+ }
196
279
  else {
197
280
  const data = readJsonObject(file);
198
- fields(data, ["version", kind]);
281
+ fields(data, kind === "workflows" ? ["version", kind, "default"] : ["version", kind], ["version", kind]);
199
282
  if (data.version !== 1)
200
283
  throw new Error("Unsupported agent configuration version");
201
284
  result[kind] = data[kind];
285
+ if (kind === "workflows")
286
+ result.defaultWorkflow =
287
+ data.default === undefined
288
+ ? object(data.workflows)
289
+ ? Object.keys(data.workflows)[0]
290
+ : undefined
291
+ : data.default;
202
292
  }
203
293
  }
294
+ // Older add/set commands could write just one configuration file. Supply
295
+ // the roles/rating needed by the inherited workflow without replacing any
296
+ // explicit profile or changing the user's files.
297
+ if (!fs.existsSync(configPath(root, CONFIG_FILES.workflows))) {
298
+ if (object(result.agents))
299
+ result.agents = { ...structuredClone(PRESET.agents), ...result.agents };
300
+ if (object(result.policies) &&
301
+ object(result.policies.review) &&
302
+ result.policies.review.rating === undefined)
303
+ result.policies.review = {
304
+ ...result.policies.review,
305
+ rating: structuredClone(PRESET.policies.review.rating),
306
+ };
307
+ }
308
+ else if (!fs.existsSync(configPath(root, CONFIG_FILES.agents)) &&
309
+ object(result.workflows) &&
310
+ object(result.agents)) {
311
+ const legacy = {
312
+ behavior: {
313
+ ...PRESET.agents.reviewer,
314
+ description: "Review runtime correctness and security",
315
+ instructions: "Independently review the supplied scope for runtime correctness, authorization, persistence and concurrency defects. Stay read-only; report concrete evidence and coverage limitations using the supplied result schema.",
316
+ },
317
+ reuse: {
318
+ ...PRESET.agents.dry,
319
+ description: "Review reuse and complexity",
320
+ instructions: "Independently search existing source and dependencies for concrete reuse opportunities in the supplied scope. Stay read-only; distinguish behavioral overlap from superficial similarity and report paths, symbols, findings and coverage limitations using the supplied result schema.",
321
+ },
322
+ quality: {
323
+ ...PRESET.agents.reviewer,
324
+ description: "Review implementation quality and validation",
325
+ instructions: "Independently review the supplied scope for type contracts, maintainability, failure handling and meaningful validation. Stay read-only; report evidence-backed findings and coverage limitations using the supplied result schema.",
326
+ },
327
+ };
328
+ for (const workflow of Object.values(result.workflows))
329
+ if (object(workflow) && Array.isArray(workflow.steps))
330
+ for (const step of workflow.steps)
331
+ if (object(step) &&
332
+ typeof step.agent === "string" &&
333
+ Object.hasOwn(legacy, step.agent))
334
+ result.agents[step.agent] = structuredClone(legacy[step.agent]);
335
+ }
204
336
  validateAutomation(result);
205
337
  return result;
206
338
  }
@@ -218,12 +350,13 @@ export function initAutomation(root, provider) {
218
350
  writeJson(configPath(root, relative), {
219
351
  version: 1,
220
352
  [kind]: preset[kind],
353
+ ...(kind === "workflows" ? { default: preset.defaultWorkflow } : {}),
221
354
  });
222
355
  }
223
356
  export function hasAutomation(root) {
224
357
  return Object.values(CONFIG_FILES).some((relative) => fs.existsSync(configPath(root, relative)));
225
358
  }
226
- export function setAutomation(root, kind, name, value, addOnly = false) {
359
+ export function setAutomation(root, kind, name, value, addOnly = false, makeDefault = false) {
227
360
  identifier(name);
228
361
  const config = readAutomation(root);
229
362
  if (addOnly && Object.hasOwn(config[kind], name))
@@ -234,9 +367,15 @@ export function setAutomation(root, kind, name, value, addOnly = false) {
234
367
  writable: true,
235
368
  configurable: true,
236
369
  });
370
+ if (makeDefault)
371
+ config.defaultWorkflow = name;
237
372
  validateAutomation(config);
238
373
  writeJson(configPath(root, CONFIG_FILES[kind]), {
239
374
  version: 1,
240
375
  [kind]: config[kind],
376
+ ...(kind === "workflows" ? { default: config.defaultWorkflow } : {}),
241
377
  });
242
378
  }
379
+ export function defaultWorkflow(config) {
380
+ return config.defaultWorkflow ?? Object.keys(config.workflows)[0];
381
+ }
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { findExecutable, setupDependencies } from "./dependencies.js";
6
+ import { installRl } from "./install.js";
7
+ import { explicitHelp } from "./help.js";
8
+ const argv = process.argv.slice(2);
9
+ const selected = argv[0] === "--repo"
10
+ ? argv.slice(2)
11
+ : argv[0]?.startsWith("--repo=")
12
+ ? argv.slice(1)
13
+ : argv;
14
+ if (selected[0] === "setup" &&
15
+ ["dependencies", "install"].includes(selected[1])) {
16
+ try {
17
+ if (selected.includes("--help") || selected.includes("-h"))
18
+ console.log(explicitHelp(selected));
19
+ else
20
+ process.exitCode =
21
+ selected[1] === "install"
22
+ ? await installRl(selected.slice(2))
23
+ : await setupDependencies(selected.slice(2));
24
+ }
25
+ catch (error) {
26
+ console.error(`rl: ${error instanceof Error ? error.message : String(error)}`);
27
+ process.exitCode = 1;
28
+ }
29
+ }
30
+ else if (argv[0] === "--version" || argv[0] === "-V") {
31
+ console.log(JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
32
+ }
33
+ else {
34
+ const zsh = findExecutable("zsh");
35
+ if (!zsh) {
36
+ console.error("rl: zsh is required. Run 'rl setup dependencies' to install the RL toolchain.");
37
+ process.exitCode = 1;
38
+ }
39
+ else {
40
+ const child = spawn(zsh, [
41
+ "-f",
42
+ fileURLToPath(new URL("../lib/entry.zsh", import.meta.url)),
43
+ ...argv,
44
+ ], {
45
+ stdio: "inherit",
46
+ env: {
47
+ ...process.env,
48
+ PATH: `${process.env.PATH ? process.env.PATH + path.delimiter : ""}${path.dirname(process.execPath)}`,
49
+ },
50
+ });
51
+ const interrupt = () => child.kill("SIGINT");
52
+ const terminate = () => child.kill("SIGTERM");
53
+ process.on("SIGINT", interrupt);
54
+ process.on("SIGTERM", terminate);
55
+ await new Promise((resolve) => {
56
+ const cleanup = () => {
57
+ process.off("SIGINT", interrupt);
58
+ process.off("SIGTERM", terminate);
59
+ };
60
+ child.on("error", (error) => {
61
+ cleanup();
62
+ console.error(`rl: ${error.message}`);
63
+ process.exitCode = 1;
64
+ resolve();
65
+ });
66
+ child.on("close", (code, signal) => {
67
+ cleanup();
68
+ process.exitCode =
69
+ code ?? (signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1);
70
+ resolve();
71
+ });
72
+ });
73
+ }
74
+ }
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { main as git } from "./git-workflow.js";
10
10
  import { main as pr } from "./pr-workflow.js";
11
11
  import { main as agent } from "./agent-launch.js";
12
12
  import { main as mcp } from "./mcp.js";
13
+ import { main as mcpSetup } from "./mcp-setup.js";
13
14
  import { main as task } from "./task.js";
14
15
  import { main as storage } from "./storage.js";
15
16
  import { main as prUpdate } from "./pr-update.js";
@@ -25,6 +26,7 @@ const commands = {
25
26
  pr,
26
27
  agent,
27
28
  mcp,
29
+ "mcp-setup": mcpSetup,
28
30
  task,
29
31
  storage,
30
32
  "pr-update": prUpdate,
@@ -49,7 +49,7 @@ export function resolveWorkspace(context, selector) {
49
49
  const [root, branch] = context.store.worktree(context.repo);
50
50
  return { path: root, name: "@", branch };
51
51
  }
52
- const choices = candidates(context.repo, context.managedDir);
52
+ const choices = candidates(context.repo, context.managedDir, context.store);
53
53
  const matches = choices.filter((item) => item.name === selector ||
54
54
  item.branch === selector ||
55
55
  (path.isAbsolute(selector) && item.path === canonical(selector)));