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