rl-workspaces 0.0.0-stage → 0.6.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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +735 -2
  3. package/bin/rl +54 -0
  4. package/config.example.zsh +9 -0
  5. package/dist/access.js +40 -0
  6. package/dist/activity.js +90 -0
  7. package/dist/agent-claude.js +16 -0
  8. package/dist/agent-codex.js +22 -0
  9. package/dist/agent-launch.js +74 -0
  10. package/dist/agent-presets.js +70 -0
  11. package/dist/agent-registry.js +8 -0
  12. package/dist/agent-resume.js +96 -0
  13. package/dist/agent-runner.js +386 -0
  14. package/dist/agent-workflows.js +275 -0
  15. package/dist/agents.js +73 -0
  16. package/dist/automation-config.js +381 -0
  17. package/dist/base.js +66 -0
  18. package/dist/cleanup-path.js +33 -0
  19. package/dist/cli.js +57 -0
  20. package/dist/codex-sessions.js +188 -0
  21. package/dist/command-context.js +67 -0
  22. package/dist/common.js +262 -0
  23. package/dist/delete.js +113 -0
  24. package/dist/display.js +134 -0
  25. package/dist/doctor.js +145 -0
  26. package/dist/git-safety.js +24 -0
  27. package/dist/git-workflow.js +89 -0
  28. package/dist/help.js +726 -0
  29. package/dist/hooks.js +159 -0
  30. package/dist/json.js +34 -0
  31. package/dist/mcp.js +336 -0
  32. package/dist/picker.js +182 -0
  33. package/dist/pr-discovery.js +223 -0
  34. package/dist/pr-update.js +419 -0
  35. package/dist/pr-workflow.js +161 -0
  36. package/dist/repos.js +537 -0
  37. package/dist/review-policy.js +150 -0
  38. package/dist/session-workflows.js +650 -0
  39. package/dist/session.js +12 -0
  40. package/dist/state.js +250 -0
  41. package/dist/storage.js +366 -0
  42. package/dist/store.js +379 -0
  43. package/dist/summary.js +164 -0
  44. package/dist/task-state.js +65 -0
  45. package/dist/task.js +192 -0
  46. package/dist/templates.js +4 -0
  47. package/dist/workflow-collaboration.js +531 -0
  48. package/dist/workflow-config.js +177 -0
  49. package/dist/workflow.js +211 -0
  50. package/lib/rl.zsh +329 -0
  51. package/node_modules/graceful-fs/LICENSE +15 -0
  52. package/node_modules/graceful-fs/README.md +143 -0
  53. package/node_modules/graceful-fs/clone.js +23 -0
  54. package/node_modules/graceful-fs/graceful-fs.js +448 -0
  55. package/node_modules/graceful-fs/legacy-streams.js +118 -0
  56. package/node_modules/graceful-fs/package.json +53 -0
  57. package/node_modules/graceful-fs/polyfills.js +355 -0
  58. package/node_modules/proper-lockfile/LICENSE +21 -0
  59. package/node_modules/proper-lockfile/README.md +183 -0
  60. package/node_modules/proper-lockfile/index.js +40 -0
  61. package/node_modules/proper-lockfile/lib/adapter.js +85 -0
  62. package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
  63. package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
  64. package/node_modules/proper-lockfile/package.json +71 -0
  65. package/node_modules/retry/.npmignore +3 -0
  66. package/node_modules/retry/.travis.yml +15 -0
  67. package/node_modules/retry/License +21 -0
  68. package/node_modules/retry/Makefile +18 -0
  69. package/node_modules/retry/README.md +227 -0
  70. package/node_modules/retry/equation.gif +0 -0
  71. package/node_modules/retry/example/dns.js +31 -0
  72. package/node_modules/retry/example/stop.js +40 -0
  73. package/node_modules/retry/index.js +1 -0
  74. package/node_modules/retry/lib/retry.js +100 -0
  75. package/node_modules/retry/lib/retry_operation.js +158 -0
  76. package/node_modules/retry/package.json +32 -0
  77. package/node_modules/retry/test/common.js +10 -0
  78. package/node_modules/retry/test/integration/test-forever.js +24 -0
  79. package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
  80. package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
  81. package/node_modules/retry/test/integration/test-timeouts.js +69 -0
  82. package/node_modules/signal-exit/LICENSE.txt +16 -0
  83. package/node_modules/signal-exit/README.md +39 -0
  84. package/node_modules/signal-exit/index.js +202 -0
  85. package/node_modules/signal-exit/package.json +38 -0
  86. package/node_modules/signal-exit/signals.js +53 -0
  87. package/package.json +57 -4
  88. package/shell/rl.zsh +11 -0
@@ -0,0 +1,386 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { git, gitRaw, now, object, sessionId, writeJson } from "./common.js";
5
+ import { readJsonObject } from "./json.js";
6
+ import { validateAutomation, } from "./automation-config.js";
7
+ import { evaluate, validateResult, } from "./review-policy.js";
8
+ import { agentAdapter } from "./agent-registry.js";
9
+ import { collaborationAssessment, initializeCollaboration, validateCollaborationState, } from "./workflow-collaboration.js";
10
+ export function captureScope(cwd, base) {
11
+ if (!base || base.startsWith("-") || /[\x00-\x20\x7f]/.test(base))
12
+ throw new Error("Expected a literal base ref");
13
+ const baseCommit = git(cwd, "rev-parse", "--verify", "--end-of-options", `${base}^{commit}`);
14
+ const head = git(cwd, "rev-parse", "HEAD");
15
+ const ancestor = git(cwd, "merge-base", head, baseCommit);
16
+ const patch = gitRaw(cwd, "diff", "--no-ext-diff", "--no-textconv", "--binary", ancestor, "--");
17
+ const stagedPatch = gitRaw(cwd, "diff", "--cached", "--no-ext-diff", "--no-textconv", "--binary", ancestor, "--");
18
+ const unstagedPatch = gitRaw(cwd, "diff", "--no-ext-diff", "--no-textconv", "--binary", "--");
19
+ const changed = gitRaw(cwd, "diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", ancestor, "--")
20
+ .split("\0")
21
+ .filter(Boolean);
22
+ const untracked = gitRaw(cwd, "ls-files", "--others", "--exclude-standard", "-z")
23
+ .split("\0")
24
+ .filter(Boolean);
25
+ const stagedFiles = gitRaw(cwd, "diff", "--cached", "--no-ext-diff", "--no-textconv", "--name-only", "-z", ancestor, "--")
26
+ .split("\0")
27
+ .filter(Boolean);
28
+ const unstagedFiles = gitRaw(cwd, "diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", "--")
29
+ .split("\0")
30
+ .filter(Boolean);
31
+ let size = Buffer.byteLength(patch) +
32
+ Buffer.byteLength(stagedPatch) +
33
+ Buffer.byteLength(unstagedPatch);
34
+ const additional = untracked.map((file) => {
35
+ const absolute = path.join(cwd, file), stat = fs.lstatSync(absolute);
36
+ size += stat.size;
37
+ if (size > 4 * 1024 * 1024)
38
+ throw new Error("Review scope exceeds 4 MiB; narrow the changes before running this workflow");
39
+ if (!stat.isFile() && !stat.isSymbolicLink())
40
+ throw new Error(`Unsupported untracked path: ${file}`);
41
+ return {
42
+ file,
43
+ mode: stat.mode & 0o777,
44
+ kind: stat.isSymbolicLink() ? "symlink" : "file",
45
+ content: stat.isSymbolicLink()
46
+ ? fs.readlinkSync(absolute)
47
+ : fs.readFileSync(absolute).toString("base64"),
48
+ };
49
+ });
50
+ if (size > 4 * 1024 * 1024)
51
+ throw new Error("Review scope exceeds 4 MiB");
52
+ const context = JSON.stringify({
53
+ head,
54
+ base,
55
+ baseCommit,
56
+ mergeBase: ancestor,
57
+ patch,
58
+ stagedPatch,
59
+ unstagedPatch,
60
+ untracked: additional,
61
+ });
62
+ return {
63
+ scope: {
64
+ base,
65
+ baseCommit,
66
+ head,
67
+ fingerprint: createHash("sha256").update(context).digest("hex"),
68
+ files: [
69
+ ...new Set([
70
+ ...changed,
71
+ ...stagedFiles,
72
+ ...unstagedFiles,
73
+ ...untracked,
74
+ ]),
75
+ ],
76
+ },
77
+ context,
78
+ };
79
+ }
80
+ function runDirectory(instanceFile, id) {
81
+ sessionId(id);
82
+ const parent = path.join(path.dirname(instanceFile), "workflow-runs");
83
+ const directory = path.join(parent, id);
84
+ for (const file of [parent, directory, path.join(directory, "run.json")])
85
+ if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink())
86
+ throw new Error("Workflow state must not be a symlink");
87
+ return directory;
88
+ }
89
+ function load(directory) {
90
+ const raw = readJsonObject(path.join(directory, "run.json"));
91
+ if (raw.version !== 1 ||
92
+ typeof raw.id !== "string" ||
93
+ typeof raw.key !== "string" ||
94
+ typeof raw.workspace !== "string" ||
95
+ typeof raw.branch !== "string" ||
96
+ typeof raw.workflow !== "string" ||
97
+ typeof raw.createdAt !== "string" ||
98
+ typeof raw.updatedAt !== "string" ||
99
+ !["running", "paused", "cancelled", "completed"].includes(String(raw.status)) ||
100
+ (raw.controlRequested !== null &&
101
+ raw.controlRequested !== "pause" &&
102
+ raw.controlRequested !== "cancel") ||
103
+ typeof raw.routed !== "boolean" ||
104
+ typeof raw.prompt !== "string" ||
105
+ !object(raw.scope) ||
106
+ typeof raw.scope.fingerprint !== "string" ||
107
+ typeof raw.scope.base !== "string" ||
108
+ typeof raw.scope.head !== "string" ||
109
+ typeof raw.scope.baseCommit !== "string" ||
110
+ !Array.isArray(raw.scope.files) ||
111
+ raw.scope.files.some((file) => typeof file !== "string") ||
112
+ !Array.isArray(raw.stages) ||
113
+ !Array.isArray(raw.assessments))
114
+ throw new Error("Invalid workflow run metadata");
115
+ sessionId(raw.id);
116
+ if (raw.execution !== undefined && raw.execution !== "native")
117
+ throw new Error("Invalid workflow execution mode");
118
+ if (raw.execution === "native") {
119
+ if (!object(raw.owner) ||
120
+ !["codex", "claude"].includes(String(raw.owner.agent)))
121
+ throw new Error("Invalid workflow parent");
122
+ agentAdapter(raw.owner.agent).observedId(raw.owner.sessionId);
123
+ if (raw.requestId !== undefined)
124
+ sessionId(raw.requestId);
125
+ }
126
+ validateAutomation(raw.config);
127
+ if (!Object.hasOwn(raw.config.workflows, raw.workflow))
128
+ throw new Error("Invalid saved workflow");
129
+ const definition = raw.config.workflows[raw.workflow];
130
+ if (definition.collaboration)
131
+ validateCollaborationState(raw.collaboration);
132
+ const seen = new Set();
133
+ for (const stage of raw.stages) {
134
+ if (!object(stage) ||
135
+ typeof stage.id !== "string" ||
136
+ !/^(?:followup-)?[a-z][a-z0-9_-]{0,63}$/.test(stage.id) ||
137
+ !["pending", "running", "completed", "failed", "blocked"].includes(String(stage.status)) ||
138
+ !Number.isSafeInteger(stage.attempts) ||
139
+ Number(stage.attempts) < 0 ||
140
+ typeof stage.durationMs !== "number" ||
141
+ stage.durationMs < 0 ||
142
+ !Array.isArray(stage.after))
143
+ throw new Error("Invalid workflow stage metadata");
144
+ if (seen.has(stage.id))
145
+ throw new Error("Duplicate saved workflow stage");
146
+ seen.add(stage.id);
147
+ const original = definition.steps.find((step) => step.id === stage.id || `followup-${step.id}` === stage.id);
148
+ if (!original ||
149
+ JSON.stringify(stage.command) !== JSON.stringify(original.command) ||
150
+ stage.agent !== original.agent ||
151
+ JSON.stringify(stage.after) !==
152
+ JSON.stringify(stage.id.startsWith("followup-") ? [original.id] : original.after))
153
+ throw new Error("Saved workflow stage does not match its configuration");
154
+ if (stage.claim !== undefined)
155
+ sessionId(stage.claim);
156
+ if (stage.scopeFingerprint !== undefined &&
157
+ typeof stage.scopeFingerprint !== "string")
158
+ throw new Error("Invalid stage scope fingerprint");
159
+ if (stage.startedAt !== undefined &&
160
+ (typeof stage.startedAt !== "string" ||
161
+ !Number.isFinite(Date.parse(stage.startedAt))))
162
+ throw new Error("Invalid stage start time");
163
+ if (raw.execution === "native" &&
164
+ stage.status === "running" &&
165
+ (stage.claim === undefined || stage.startedAt === undefined))
166
+ throw new Error("Running native stage is missing its claim");
167
+ if (stage.result !== undefined)
168
+ validateResult(stage.result);
169
+ }
170
+ if (raw.scope.files.length &&
171
+ definition.steps.some((step) => !seen.has(step.id)))
172
+ throw new Error("Saved workflow is missing configured steps");
173
+ return raw;
174
+ }
175
+ export function readRun(store, requested) {
176
+ const [file, data] = store.current();
177
+ let id = requested;
178
+ if (!id || id === "latest") {
179
+ const parent = path.dirname(runDirectory(file, "00000000-0000-0000-0000-000000000000"));
180
+ const entries = fs.existsSync(parent)
181
+ ? fs
182
+ .readdirSync(parent)
183
+ .filter((id) => /^[a-f0-9-]{36}$/.test(id))
184
+ .map((id) => load(runDirectory(file, id)))
185
+ : [];
186
+ id = entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0]?.id;
187
+ if (!id)
188
+ throw new Error("No workflow runs in this RL workspace");
189
+ }
190
+ const directory = runDirectory(file, id), run = load(directory);
191
+ if (run.id !== id ||
192
+ run.key !== data.key ||
193
+ run.workspace !== data.worktree ||
194
+ run.branch !== data.branch)
195
+ throw new Error("Workflow run belongs to a different RL workspace");
196
+ return { run, directory };
197
+ }
198
+ export async function controlRun(store, id, action) {
199
+ return store.locked(() => {
200
+ const { run, directory } = readRun(store, id);
201
+ if (run.status === "completed")
202
+ throw new Error("Workflow has already completed");
203
+ run.controlRequested = action;
204
+ if (run.execution === "native" || run.status !== "running")
205
+ run.status = action === "pause" ? "paused" : "cancelled";
206
+ run.updatedAt = now();
207
+ writeJson(path.join(directory, "run.json"), run);
208
+ return run;
209
+ });
210
+ }
211
+ export function evaluation(run, policy = run.config.policies[run.config.workflows[run.workflow].policy]) {
212
+ const currentScope = (stage) => stage.scopeFingerprint === undefined ||
213
+ stage.scopeFingerprint === run.scope.fingerprint;
214
+ const reports = run.stages
215
+ .filter((stage) => stage.status === "completed" &&
216
+ stage.result !== undefined &&
217
+ currentScope(stage))
218
+ .map((stage) => ({ owner: stage.id, result: stage.result }));
219
+ const collaboration = collaborationAssessment(run);
220
+ reports.push(...collaboration.reports);
221
+ const limitations = run.stages
222
+ .filter((stage) => stage.status !== "completed")
223
+ .map((stage) => `${stage.id}: ${stage.error ?? stage.status}`);
224
+ if (run.status !== "completed")
225
+ limitations.push(`Run ${run.status}`);
226
+ limitations.push(...collaboration.limitations);
227
+ for (const stage of run.stages.filter((stage) => stage.status === "completed" && !currentScope(stage))) {
228
+ if (stage.agent &&
229
+ run.config.agents[stage.agent].access === "workspace-write")
230
+ continue;
231
+ const refreshed = run.stages.some((other) => other.status === "completed" &&
232
+ currentScope(other) &&
233
+ (stage.agent
234
+ ? other.agent === stage.agent
235
+ : JSON.stringify(other.command) === JSON.stringify(stage.command)));
236
+ if (!refreshed)
237
+ limitations.push(`${stage.id}: result predates workspace changes; rerun this role/check after writing`);
238
+ }
239
+ for (const stage of run.stages)
240
+ if (stage.id.startsWith("followup-") && stage.result?.routingLeads.length)
241
+ limitations.push(`${stage.id}: further routing leads require review`);
242
+ for (const { owner, result } of reports)
243
+ for (const requested of result.checkRequests) {
244
+ const check = run.stages.find((step) => step.id === requested && step.command);
245
+ if (!check || check.status !== "completed" || !currentScope(check))
246
+ limitations.push(`${owner}: requested check ${requested} was not completed`);
247
+ }
248
+ const result = evaluate(policy, reports, limitations);
249
+ if (!run.scope.files.length) {
250
+ result.score = null;
251
+ result.verdict = "incomplete";
252
+ result.limitations.push("No changes to review");
253
+ }
254
+ return result;
255
+ }
256
+ export function runMetrics(run) {
257
+ const agents = run.stages.filter((stage) => stage.agent && stage.attempts);
258
+ const reported = agents.flatMap((stage) => stage.status === "completed" && stage.usage ? [stage.usage] : []);
259
+ const total = (key) => reported.length === agents.length &&
260
+ agents.every((stage) => stage.attempts === 1) &&
261
+ reported.length &&
262
+ reported.every((item) => item[key] !== null)
263
+ ? reported.reduce((sum, item) => sum + item[key], 0)
264
+ : null;
265
+ return {
266
+ execution: run.execution ?? "legacy",
267
+ usageSource: run.execution === "native"
268
+ ? "parent-reported; unreported usage remains unknown"
269
+ : "legacy provider-reported",
270
+ attempts: run.stages.reduce((sum, step) => sum + step.attempts, 0),
271
+ totalStageDurationMs: run.stages.reduce((sum, step) => sum + step.durationMs, 0),
272
+ completed: run.stages.filter((step) => step.status === "completed").length,
273
+ failed: run.stages.filter((step) => step.status === "failed").length,
274
+ inputTokens: total("inputTokens"),
275
+ outputTokens: total("outputTokens"),
276
+ costUsd: total("costUsd"),
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
+ : {}),
291
+ stages: run.stages.map(({ id, status, attempts, durationMs, usage }) => ({
292
+ id,
293
+ status,
294
+ attempts,
295
+ durationMs,
296
+ usage: usage ?? null,
297
+ })),
298
+ };
299
+ }
300
+ export async function rescoreRun(store, id, policy, config) {
301
+ if (!Object.hasOwn(config.policies, policy))
302
+ throw new Error(`Unknown policy: ${policy}`);
303
+ return store.locked(() => {
304
+ const { run, directory } = readRun(store, id);
305
+ if (run.status === "running")
306
+ throw new Error("Wait for the run to stop before rescoring");
307
+ run.evaluation = evaluation(run, config.policies[policy]);
308
+ run.assessments.push({
309
+ at: now(),
310
+ policyName: policy,
311
+ policy: structuredClone(config.policies[policy]),
312
+ evaluation: structuredClone(run.evaluation),
313
+ });
314
+ run.updatedAt = now();
315
+ writeJson(path.join(directory, "run.json"), run);
316
+ return run;
317
+ });
318
+ }
319
+ export function createRunLocked(store, config, workflow, base, prompt) {
320
+ if (!Object.hasOwn(config.workflows, workflow))
321
+ throw new Error(`Unknown workflow: ${workflow}`);
322
+ const [file, data] = store.current();
323
+ const { scope, context } = captureScope(data.worktree, base), id = randomUUID();
324
+ const directory = runDirectory(file, id);
325
+ const run = {
326
+ version: 1,
327
+ id,
328
+ key: data.key,
329
+ workspace: data.worktree,
330
+ branch: data.branch,
331
+ workflow,
332
+ createdAt: now(),
333
+ updatedAt: now(),
334
+ status: "paused",
335
+ controlRequested: null,
336
+ config: structuredClone(config),
337
+ scope,
338
+ prompt,
339
+ routed: false,
340
+ assessments: [],
341
+ stages: config.workflows[workflow].steps.map((step) => ({
342
+ ...step,
343
+ status: "pending",
344
+ attempts: 0,
345
+ durationMs: 0,
346
+ })),
347
+ };
348
+ initializeCollaboration(run);
349
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
350
+ fs.writeFileSync(path.join(directory, "scope.json"), context, {
351
+ mode: 0o600,
352
+ });
353
+ writeJson(path.join(directory, "run.json"), run);
354
+ return { run, directory };
355
+ }
356
+ export function planRun(store, config, workflow, base) {
357
+ if (!Object.hasOwn(config.workflows, workflow))
358
+ throw new Error(`Unknown workflow: ${workflow}`);
359
+ const [, data] = store.current();
360
+ return {
361
+ workspace: data.worktree,
362
+ workflow,
363
+ scope: captureScope(data.worktree, base).scope,
364
+ definition: config.workflows[workflow],
365
+ policy: config.workflows[workflow].policy,
366
+ };
367
+ }
368
+ export function workflowSummary(store) {
369
+ const [file] = store.current();
370
+ const parent = path.dirname(runDirectory(file, "00000000-0000-0000-0000-000000000000"));
371
+ if (!fs.existsSync(parent) ||
372
+ !fs.readdirSync(parent).some((id) => /^[a-f0-9-]{36}$/.test(id)))
373
+ return null;
374
+ const { run } = readRun(store);
375
+ return {
376
+ id: run.id,
377
+ workflow: run.workflow,
378
+ status: run.status,
379
+ completed: run.stages.filter((step) => step.status === "completed").length,
380
+ total: run.stages.length,
381
+ score: run.evaluation?.score ?? null,
382
+ verdict: run.evaluation?.verdict ?? null,
383
+ execution: run.execution,
384
+ owner: run.owner,
385
+ };
386
+ }
@@ -0,0 +1,275 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { args, arity, json } from "./common.js";
5
+ import { commandContext, contextOptions } from "./command-context.js";
6
+ import { CONFIG_FILES, initAutomation, readAutomation, setAutomation, defaultWorkflow, } from "./automation-config.js";
7
+ import { controlRun, planRun, readRun, rescoreRun, runMetrics, } from "./agent-runner.js";
8
+ import { callWorkflowTool, resumeNativeRun } from "./session-workflows.js";
9
+ import { parseJsonObject } from "./json.js";
10
+ const visible = (value) => stripVTControlCharacters(String(value ?? "")).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
11
+ function report(run, asJson) {
12
+ const metrics = runMetrics(run);
13
+ if (asJson) {
14
+ json({ ...run, metrics });
15
+ return;
16
+ }
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
+ for (const stage of run.stages)
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
+ }
26
+ if (run.evaluation) {
27
+ const result = run.evaluation;
28
+ console.log(`\n${result.score === null ? "Not scored" : `Score: ${result.score}/100`} · ${result.verdict}${result.verdict === "incomplete" && result.score !== null ? " (provisional)" : ""}`);
29
+ for (const finding of result.findings) {
30
+ console.log(`\n ${finding.severity.toUpperCase()} · ${finding.required ? "required fix" : "quick win"} · ${visible(finding.title)}\n ${visible(finding.file)}:${finding.line} · ${finding.owners.join(", ")}\n ${visible(finding.evidence)}\n Fix: ${visible(finding.recommendation)}`);
31
+ }
32
+ for (const limitation of result.limitations)
33
+ console.log(` Limitation: ${visible(limitation)}`);
34
+ }
35
+ console.log(`\nMetrics: ${metrics.attempts} attempts · ${(metrics.totalStageDurationMs / 1000).toFixed(1)}s total stage time · usage ${metrics.usageCoverage}`);
36
+ }
37
+ export async function main(argv) {
38
+ const { values, positionals } = args(argv, [
39
+ ...contextOptions,
40
+ "base",
41
+ "prompt",
42
+ "provider",
43
+ "policy",
44
+ "file",
45
+ "instructions",
46
+ "description",
47
+ "model",
48
+ "reasoning",
49
+ "agent",
50
+ "session",
51
+ "step",
52
+ "request",
53
+ "claim",
54
+ ], ["json", "dry-run", "default"]);
55
+ const context = commandContext(values);
56
+ const [namespace, action, ...rest] = positionals;
57
+ arity(rest, 0, 1);
58
+ const [, data] = context.store.current();
59
+ const asJson = !!values.json;
60
+ if (values.file && values.instructions)
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");
65
+ if ((values.description || values.model || values.reasoning) &&
66
+ !(namespace === "agent" &&
67
+ ["add", "set"].includes(action) &&
68
+ values.instructions))
69
+ throw new Error("--description, --model and --reasoning require agent add/set --instructions");
70
+ if (values.provider && !(namespace === "workflow" && action === "init"))
71
+ throw new Error("--provider is only supported by workflow init");
72
+ if (values.request &&
73
+ !(namespace === "workflow" && action === "run" && !values["dry-run"]))
74
+ throw new Error("--request is only supported by workflow run without --dry-run");
75
+ if ((values.base || values.prompt || values["dry-run"]) &&
76
+ !(namespace === "workflow" && action === "run"))
77
+ throw new Error("--base, --prompt and --dry-run are only supported by workflow run");
78
+ if (values.policy && !(namespace === "workflow" && action === "score"))
79
+ throw new Error("--policy is only supported by workflow score");
80
+ if (namespace === "workflow" && action === "init") {
81
+ arity(rest, 0);
82
+ const provider = values.provider ?? "inherit";
83
+ if (provider !== "inherit" && provider !== "codex" && provider !== "claude")
84
+ throw new Error("Provider must be inherit, codex or claude");
85
+ await context.store.locked(() => initAutomation(data.worktree, provider));
86
+ await context.store.adopt(data.worktree);
87
+ const paths = Object.values(CONFIG_FILES).map((file) => path.join(data.worktree, file));
88
+ if (asJson)
89
+ json({ files: paths });
90
+ else
91
+ console.log(`Created separate agent, policy and workflow configurations:\n${paths.map((file) => " " + visible(file)).join("\n")}`);
92
+ return 0;
93
+ }
94
+ const loadInput = () => {
95
+ if (typeof values.file !== "string")
96
+ throw new Error("Missing --file <json|->");
97
+ return parseJsonObject(fs.readFileSync(values.file === "-" ? 0 : values.file, "utf8"));
98
+ };
99
+ if ((namespace === "agent" || namespace === "workflow") &&
100
+ ["add", "set"].includes(action)) {
101
+ arity(rest, 1);
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
+ });
123
+ await context.store.adopt(data.worktree);
124
+ if (asJson)
125
+ json(value);
126
+ else
127
+ console.log(`${namespace} ${action}: ${visible(rest[0])}`);
128
+ return 0;
129
+ }
130
+ const parent = {
131
+ ...(values.agent ? { agent: values.agent } : {}),
132
+ ...(values.session ? { sessionId: values.session } : {}),
133
+ };
134
+ if (namespace === "workflow" &&
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
154
+ ? loadInput()
155
+ : {};
156
+ const result = await callWorkflowTool(context.store, `rl_workflow_${action}`, {
157
+ ...input,
158
+ ...parent,
159
+ ...(rest[0] ? { run: rest[0] } : {}),
160
+ ...(values.step ? { step: values.step } : {}),
161
+ ...(values.claim ? { claim: values.claim } : {}),
162
+ });
163
+ if (asJson || action !== "context")
164
+ json(result);
165
+ else
166
+ console.log(result.instructions);
167
+ return 0;
168
+ }
169
+ const configuration = () => readAutomation(data.worktree);
170
+ if ((namespace === "agent" ||
171
+ namespace === "policy" ||
172
+ namespace === "workflow") &&
173
+ ["list", "show"].includes(action)) {
174
+ const config = configuration();
175
+ const entries = namespace === "agent"
176
+ ? config.agents
177
+ : namespace === "policy"
178
+ ? config.policies
179
+ : config.workflows;
180
+ if (action === "list") {
181
+ arity(rest, 0);
182
+ if (asJson)
183
+ json(entries);
184
+ else
185
+ for (const [name, value] of Object.entries(entries))
186
+ console.log(`${name.padEnd(22)} ${"description" in value ? visible(value.description) : "Review scoring and readiness policy"}`);
187
+ }
188
+ else {
189
+ arity(rest, 1);
190
+ if (!Object.hasOwn(entries, rest[0]))
191
+ throw new Error(`Unknown ${namespace}: ${rest[0]}`);
192
+ const item = entries[rest[0]];
193
+ if (asJson)
194
+ json(item);
195
+ else {
196
+ console.log(`${rest[0]}\n`);
197
+ for (const [key, value] of Object.entries(item))
198
+ console.log(`${key}: ${visible(typeof value === "object" ? JSON.stringify(value) : value)}`);
199
+ }
200
+ }
201
+ return 0;
202
+ }
203
+ if (namespace !== "workflow")
204
+ throw new Error("Usage: rl agent|policy list|show <name>");
205
+ const name = rest[0];
206
+ if (action === "run") {
207
+ const config = configuration();
208
+ const workflow = name ?? defaultWorkflow(config), base = String(values.base ?? context.defaultBase);
209
+ if (values["dry-run"]) {
210
+ const plan = planRun(context.store, config, workflow, base);
211
+ if (asJson)
212
+ json(plan);
213
+ else {
214
+ console.log(`Preview: ${workflow}\nWorkspace: ${visible(plan.workspace)}\nScope: ${visible(base)} · ${plan.scope.files.length} changed files\nPolicy: ${plan.policy}`);
215
+ for (const step of plan.definition.steps)
216
+ console.log(` ${step.id}: ${step.agent ? `${step.agent} (${config.agents[step.agent].provider ?? "inherit"})` : JSON.stringify(step.command)} · after ${step.after.join(", ") || "start"}`);
217
+ }
218
+ return 0;
219
+ }
220
+ const run = (await callWorkflowTool(context.store, "rl_workflow_start", {
221
+ ...parent,
222
+ ...(name || !values.request ? { workflow } : {}),
223
+ ...(values.base || !values.request ? { base } : {}),
224
+ ...(values.request ? { requestId: values.request } : {}),
225
+ ...(values.prompt ? { prompt: values.prompt } : {}),
226
+ }));
227
+ report(run, asJson);
228
+ if (!asJson)
229
+ console.log("Prepared for this session's native subagents. Claim ready steps with rl_workflow_claim or rl workflow claim; RL has not started any agent processes.");
230
+ return 0;
231
+ }
232
+ if (action === "resume") {
233
+ const run = await resumeNativeRun(context.store, name, parent);
234
+ report(run, asJson);
235
+ return 0;
236
+ }
237
+ if (action === "pause" || action === "cancel") {
238
+ const run = await controlRun(context.store, name, action);
239
+ if (asJson)
240
+ json({ id: run.id, status: run.status, requested: action });
241
+ else
242
+ console.log(`${action} requested for ${run.id}`);
243
+ return 0;
244
+ }
245
+ if (action === "status") {
246
+ report(readRun(context.store, name).run, asJson);
247
+ return 0;
248
+ }
249
+ if (action === "metrics") {
250
+ const { run } = readRun(context.store, name);
251
+ const metrics = runMetrics(run);
252
+ if (asJson)
253
+ json(metrics);
254
+ else {
255
+ console.log(`Metrics: ${run.id}`);
256
+ for (const [key, value] of Object.entries(metrics))
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")
263
+ console.log(`${key}: ${value ?? "not reported"}`);
264
+ }
265
+ return 0;
266
+ }
267
+ if (action === "score") {
268
+ const { run: saved } = readRun(context.store, name);
269
+ const policy = String(values.policy ?? saved.config.workflows[saved.workflow].policy);
270
+ const run = await rescoreRun(context.store, saved.id, policy, configuration());
271
+ report(run, asJson);
272
+ return run.evaluation?.verdict === "ready" ? 0 : 1;
273
+ }
274
+ throw new Error("Usage: rl workflow init|list|show|run|status|metrics|pause|cancel|resume|score");
275
+ }