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.
- package/LICENSE +21 -0
- package/README.md +735 -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 +70 -0
- package/dist/agent-registry.js +8 -0
- package/dist/agent-resume.js +96 -0
- package/dist/agent-runner.js +386 -0
- package/dist/agent-workflows.js +275 -0
- package/dist/agents.js +73 -0
- package/dist/automation-config.js +381 -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 +89 -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 +650 -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-collaboration.js +531 -0
- package/dist/workflow-config.js +177 -0
- package/dist/workflow.js +211 -0
- package/lib/rl.zsh +329 -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
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
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { object, writeJson } from "./common.js";
|
|
4
|
+
import { readJsonObject } from "./json.js";
|
|
5
|
+
import { PRESET } from "./agent-presets.js";
|
|
6
|
+
export const CONFIG_FILES = {
|
|
7
|
+
agents: ".rl/agents.json",
|
|
8
|
+
policies: ".rl/policies.json",
|
|
9
|
+
workflows: ".rl/agent-workflows.json",
|
|
10
|
+
};
|
|
11
|
+
export const SEVERITIES = ["critical", "high", "medium", "low"];
|
|
12
|
+
const ID = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
13
|
+
export function identifier(value) {
|
|
14
|
+
if (typeof value !== "string" ||
|
|
15
|
+
!ID.test(value) ||
|
|
16
|
+
value === "constructor" ||
|
|
17
|
+
value.startsWith("followup-"))
|
|
18
|
+
throw new Error("Expected an identifier using lowercase letters, numbers, hyphens or underscores");
|
|
19
|
+
}
|
|
20
|
+
export function fields(value, allowed, required = allowed) {
|
|
21
|
+
if (!object(value) ||
|
|
22
|
+
Object.keys(value).some((key) => !allowed.includes(key)) ||
|
|
23
|
+
required.some((key) => !(key in value)))
|
|
24
|
+
throw new Error(`Expected fields: ${allowed.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
function nonempty(value) {
|
|
27
|
+
if (typeof value !== "string" ||
|
|
28
|
+
!value.trim() ||
|
|
29
|
+
value.includes("\0") ||
|
|
30
|
+
value.length > 64000)
|
|
31
|
+
throw new Error("Expected nonempty text (up to 64000 characters)");
|
|
32
|
+
}
|
|
33
|
+
function integer(value, min, max) {
|
|
34
|
+
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max)
|
|
35
|
+
throw new Error(`Expected an integer from ${min} to ${max}`);
|
|
36
|
+
}
|
|
37
|
+
export function validateAutomation(value) {
|
|
38
|
+
fields(value, ["agents", "policies", "workflows", "defaultWorkflow"], ["agents", "policies", "workflows"]);
|
|
39
|
+
for (const kind of ["agents", "policies", "workflows"]) {
|
|
40
|
+
const entries = value[kind];
|
|
41
|
+
if (!object(entries) ||
|
|
42
|
+
!Object.keys(entries).length ||
|
|
43
|
+
Object.keys(entries).length > 64)
|
|
44
|
+
throw new Error(`Expected 1–64 ${kind}`);
|
|
45
|
+
for (const [name, item] of Object.entries(entries)) {
|
|
46
|
+
identifier(name);
|
|
47
|
+
if (kind === "agents") {
|
|
48
|
+
fields(item, [
|
|
49
|
+
"provider",
|
|
50
|
+
"description",
|
|
51
|
+
"instructions",
|
|
52
|
+
"model",
|
|
53
|
+
"reasoning",
|
|
54
|
+
"access",
|
|
55
|
+
], ["description", "instructions"]);
|
|
56
|
+
if (item.provider !== undefined &&
|
|
57
|
+
!["inherit", "codex", "claude"].includes(String(item.provider)))
|
|
58
|
+
throw new Error("Agent provider must be inherit, codex or claude");
|
|
59
|
+
nonempty(item.description);
|
|
60
|
+
nonempty(item.instructions);
|
|
61
|
+
if (item.access !== undefined &&
|
|
62
|
+
item.access !== "read-only" &&
|
|
63
|
+
item.access !== "workspace-write")
|
|
64
|
+
throw new Error("Agent access must be read-only or workspace-write");
|
|
65
|
+
if (item.model !== undefined) {
|
|
66
|
+
nonempty(item.model);
|
|
67
|
+
if (item.model.startsWith("-"))
|
|
68
|
+
throw new Error("Invalid model");
|
|
69
|
+
}
|
|
70
|
+
if (item.reasoning !== undefined &&
|
|
71
|
+
!(item.provider !== "claude"
|
|
72
|
+
? ["low", "medium", "high", "xhigh"]
|
|
73
|
+
: ["low", "medium", "high", "max"]).includes(String(item.reasoning)))
|
|
74
|
+
throw new Error("Unsupported reasoning setting for this provider");
|
|
75
|
+
}
|
|
76
|
+
else if (kind === "policies") {
|
|
77
|
+
fields(item, [
|
|
78
|
+
"instructions",
|
|
79
|
+
"weights",
|
|
80
|
+
"minimumScore",
|
|
81
|
+
"blockRequired",
|
|
82
|
+
"rating",
|
|
83
|
+
], ["instructions", "weights", "minimumScore", "blockRequired"]);
|
|
84
|
+
nonempty(item.instructions);
|
|
85
|
+
fields(item.weights, [...SEVERITIES]);
|
|
86
|
+
for (const weight of Object.values(item.weights))
|
|
87
|
+
integer(weight, 0, 100);
|
|
88
|
+
integer(item.minimumScore, 0, 100);
|
|
89
|
+
if (typeof item.blockRequired !== "boolean")
|
|
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
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
fields(item, [
|
|
104
|
+
"description",
|
|
105
|
+
"policy",
|
|
106
|
+
"concurrency",
|
|
107
|
+
"retries",
|
|
108
|
+
"timeoutSeconds",
|
|
109
|
+
"steps",
|
|
110
|
+
"triggers",
|
|
111
|
+
"collaboration",
|
|
112
|
+
], [
|
|
113
|
+
"description",
|
|
114
|
+
"policy",
|
|
115
|
+
"concurrency",
|
|
116
|
+
"retries",
|
|
117
|
+
"timeoutSeconds",
|
|
118
|
+
"steps",
|
|
119
|
+
]);
|
|
120
|
+
if (item.triggers !== undefined) {
|
|
121
|
+
if (!Array.isArray(item.triggers) || item.triggers.length > 32)
|
|
122
|
+
throw new Error("Expected up to 32 workflow triggers");
|
|
123
|
+
for (const trigger of item.triggers) {
|
|
124
|
+
fields(trigger, ["event", "tools", "paths", "changes"], ["event"]);
|
|
125
|
+
nonempty(trigger.event);
|
|
126
|
+
if (!/^[a-z][a-z0-9_.-]{0,63}$/.test(trigger.event))
|
|
127
|
+
throw new Error("Invalid trigger event");
|
|
128
|
+
for (const key of ["tools", "paths"])
|
|
129
|
+
if (trigger[key] !== undefined &&
|
|
130
|
+
(!Array.isArray(trigger[key]) ||
|
|
131
|
+
!trigger[key].length ||
|
|
132
|
+
trigger[key].length > 64 ||
|
|
133
|
+
trigger[key].some((text) => typeof text !== "string" ||
|
|
134
|
+
!text ||
|
|
135
|
+
text.length > 512 ||
|
|
136
|
+
/[\x00-\x1f]/.test(text))))
|
|
137
|
+
throw new Error(`Invalid trigger ${key}`);
|
|
138
|
+
if (trigger.changes !== undefined &&
|
|
139
|
+
typeof trigger.changes !== "boolean")
|
|
140
|
+
throw new Error("Trigger changes must be boolean");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
nonempty(item.description);
|
|
144
|
+
identifier(item.policy);
|
|
145
|
+
if (!object(value.policies) ||
|
|
146
|
+
!Object.hasOwn(value.policies, item.policy))
|
|
147
|
+
throw new Error(`Unknown policy: ${item.policy}`);
|
|
148
|
+
integer(item.concurrency, 1, 8);
|
|
149
|
+
integer(item.retries, 0, 3);
|
|
150
|
+
integer(item.timeoutSeconds, 1, 3600);
|
|
151
|
+
if (!Array.isArray(item.steps) ||
|
|
152
|
+
!item.steps.length ||
|
|
153
|
+
item.steps.length > 32)
|
|
154
|
+
throw new Error("Expected 1–32 workflow steps");
|
|
155
|
+
const ids = new Set();
|
|
156
|
+
for (const step of item.steps) {
|
|
157
|
+
fields(step, ["id", "after", "agent", "command"], ["id", "after"]);
|
|
158
|
+
identifier(step.id);
|
|
159
|
+
if (ids.has(step.id))
|
|
160
|
+
throw new Error(`Duplicate step: ${step.id}`);
|
|
161
|
+
ids.add(step.id);
|
|
162
|
+
if (!Array.isArray(step.after) ||
|
|
163
|
+
step.after.some((dep) => typeof dep !== "string") ||
|
|
164
|
+
new Set(step.after).size !== step.after.length)
|
|
165
|
+
throw new Error("Step after must contain unique step names");
|
|
166
|
+
if ((step.agent !== undefined) === (step.command !== undefined))
|
|
167
|
+
throw new Error("Each step requires exactly one agent or command");
|
|
168
|
+
if (step.agent !== undefined &&
|
|
169
|
+
(!object(value.agents) ||
|
|
170
|
+
typeof step.agent !== "string" ||
|
|
171
|
+
!Object.hasOwn(value.agents, step.agent)))
|
|
172
|
+
throw new Error(`Unknown agent: ${step.agent}`);
|
|
173
|
+
if (step.command !== undefined &&
|
|
174
|
+
(!Array.isArray(step.command) ||
|
|
175
|
+
!step.command.length ||
|
|
176
|
+
step.command.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
|
|
177
|
+
!step.command[0]))
|
|
178
|
+
throw new Error("Commands must be nonempty argv arrays");
|
|
179
|
+
}
|
|
180
|
+
const remaining = new Map(item.steps.map((step) => [String(step.id), step.after]));
|
|
181
|
+
const done = new Set();
|
|
182
|
+
while (remaining.size) {
|
|
183
|
+
const ready = [...remaining].filter(([, deps]) => deps.every((dep) => done.has(dep)));
|
|
184
|
+
if (!ready.length)
|
|
185
|
+
throw new Error("Workflow dependencies contain a cycle or unknown step");
|
|
186
|
+
for (const [id] of ready) {
|
|
187
|
+
remaining.delete(id);
|
|
188
|
+
done.add(id);
|
|
189
|
+
}
|
|
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
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
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");
|
|
262
|
+
}
|
|
263
|
+
function configPath(root, relative) {
|
|
264
|
+
const directory = path.join(root, ".rl"), file = path.join(root, relative);
|
|
265
|
+
if (fs.lstatSync(directory, { throwIfNoEntry: false })?.isSymbolicLink() ||
|
|
266
|
+
fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
267
|
+
throw new Error("Agent configuration must not be a symlink");
|
|
268
|
+
return file;
|
|
269
|
+
}
|
|
270
|
+
export function readAutomation(root) {
|
|
271
|
+
const result = {};
|
|
272
|
+
for (const [kind, relative] of Object.entries(CONFIG_FILES)) {
|
|
273
|
+
const file = configPath(root, relative);
|
|
274
|
+
if (!fs.existsSync(file)) {
|
|
275
|
+
result[kind] = structuredClone(PRESET[kind]);
|
|
276
|
+
if (kind === "workflows")
|
|
277
|
+
result.defaultWorkflow = PRESET.defaultWorkflow;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const data = readJsonObject(file);
|
|
281
|
+
fields(data, kind === "workflows" ? ["version", kind, "default"] : ["version", kind], ["version", kind]);
|
|
282
|
+
if (data.version !== 1)
|
|
283
|
+
throw new Error("Unsupported agent configuration version");
|
|
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;
|
|
292
|
+
}
|
|
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
|
+
}
|
|
336
|
+
validateAutomation(result);
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
export function initAutomation(root, provider) {
|
|
340
|
+
const paths = Object.values(CONFIG_FILES).map((file) => configPath(root, file));
|
|
341
|
+
if (paths.some((file) => fs.existsSync(file)))
|
|
342
|
+
throw new Error("Agent configuration already exists; edit the existing files");
|
|
343
|
+
const preset = structuredClone(PRESET);
|
|
344
|
+
for (const profile of Object.values(preset.agents)) {
|
|
345
|
+
profile.provider = provider;
|
|
346
|
+
if (provider === "claude")
|
|
347
|
+
profile.reasoning = "high";
|
|
348
|
+
}
|
|
349
|
+
for (const [kind, relative] of Object.entries(CONFIG_FILES))
|
|
350
|
+
writeJson(configPath(root, relative), {
|
|
351
|
+
version: 1,
|
|
352
|
+
[kind]: preset[kind],
|
|
353
|
+
...(kind === "workflows" ? { default: preset.defaultWorkflow } : {}),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
export function hasAutomation(root) {
|
|
357
|
+
return Object.values(CONFIG_FILES).some((relative) => fs.existsSync(configPath(root, relative)));
|
|
358
|
+
}
|
|
359
|
+
export function setAutomation(root, kind, name, value, addOnly = false, makeDefault = false) {
|
|
360
|
+
identifier(name);
|
|
361
|
+
const config = readAutomation(root);
|
|
362
|
+
if (addOnly && Object.hasOwn(config[kind], name))
|
|
363
|
+
throw new Error(`${name} already exists; use set to update it`);
|
|
364
|
+
Object.defineProperty(config[kind], name, {
|
|
365
|
+
value,
|
|
366
|
+
enumerable: true,
|
|
367
|
+
writable: true,
|
|
368
|
+
configurable: true,
|
|
369
|
+
});
|
|
370
|
+
if (makeDefault)
|
|
371
|
+
config.defaultWorkflow = name;
|
|
372
|
+
validateAutomation(config);
|
|
373
|
+
writeJson(configPath(root, CONFIG_FILES[kind]), {
|
|
374
|
+
version: 1,
|
|
375
|
+
[kind]: config[kind],
|
|
376
|
+
...(kind === "workflows" ? { default: config.defaultWorkflow } : {}),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
export function defaultWorkflow(config) {
|
|
380
|
+
return config.defaultWorkflow ?? Object.keys(config.workflows)[0];
|
|
381
|
+
}
|
package/dist/base.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { git } from "./common.js";
|
|
2
|
+
/** Resolve cached refs only; never fetch or infer a base from a feature's upstream. */
|
|
3
|
+
export function resolveBase(repo, requested) {
|
|
4
|
+
const refs = [
|
|
5
|
+
requested,
|
|
6
|
+
requested.startsWith("origin/")
|
|
7
|
+
? requested.slice(7)
|
|
8
|
+
: "origin/" + requested,
|
|
9
|
+
];
|
|
10
|
+
for (const ref of refs) {
|
|
11
|
+
try {
|
|
12
|
+
return {
|
|
13
|
+
ref,
|
|
14
|
+
oid: git(repo, "rev-parse", "--verify", "--end-of-options", ref + "^{commit}"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* Try the corresponding local or remote ref. */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function repositoryBase(repo, configured) {
|
|
24
|
+
const configuredBase = resolveBase(repo, configured);
|
|
25
|
+
if (configuredBase)
|
|
26
|
+
return configuredBase;
|
|
27
|
+
try {
|
|
28
|
+
const head = git(repo, "symbolic-ref", "--short", "refs/remotes/origin/HEAD");
|
|
29
|
+
const resolved = resolveBase(repo, head);
|
|
30
|
+
if (resolved)
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* A local clone may not have origin/HEAD. */
|
|
35
|
+
}
|
|
36
|
+
for (const ref of ["origin/main", "origin/master", "origin/develop"]) {
|
|
37
|
+
const resolved = resolveBase(repo, ref);
|
|
38
|
+
if (resolved)
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** A PR target is a branch, not a tag or the feature branch's upstream. */
|
|
44
|
+
export function pullRequestBase(repo, pr) {
|
|
45
|
+
if (typeof pr?.baseRefName !== "string" || !pr.baseRefName)
|
|
46
|
+
return null;
|
|
47
|
+
const target = pr.baseRefName;
|
|
48
|
+
for (const [ref, fullRef] of [
|
|
49
|
+
[`origin/${target}`, `refs/remotes/origin/${target}`],
|
|
50
|
+
[target, `refs/heads/${target}`],
|
|
51
|
+
]) {
|
|
52
|
+
try {
|
|
53
|
+
return {
|
|
54
|
+
target,
|
|
55
|
+
resolved: {
|
|
56
|
+
ref,
|
|
57
|
+
oid: git(repo, "rev-parse", "--verify", "--end-of-options", `${fullRef}^{commit}`),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* The local target branch can exist without a cached remote ref. */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { target, resolved: null };
|
|
66
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/** Metadata, credentials, and task instructions are never cleanup targets. */
|
|
3
|
+
export function protectedCleanupName(name) {
|
|
4
|
+
return ([
|
|
5
|
+
".git",
|
|
6
|
+
".rl",
|
|
7
|
+
".codex",
|
|
8
|
+
".claude",
|
|
9
|
+
".ssh",
|
|
10
|
+
".aws",
|
|
11
|
+
".azure",
|
|
12
|
+
".config",
|
|
13
|
+
".npmrc",
|
|
14
|
+
".yarnrc",
|
|
15
|
+
".yarnrc.yml",
|
|
16
|
+
".pypirc",
|
|
17
|
+
".netrc",
|
|
18
|
+
"agents.md",
|
|
19
|
+
"claude.md",
|
|
20
|
+
].includes(name.toLowerCase()) ||
|
|
21
|
+
/^(?:\.env(?:\.|$)|credentials(?:\.|$)|secrets(?:\.|$)|\.rl-clean-)/i.test(name));
|
|
22
|
+
}
|
|
23
|
+
export function validateCleanupPath(value) {
|
|
24
|
+
if (typeof value !== "string" ||
|
|
25
|
+
!value ||
|
|
26
|
+
value.trim() !== value ||
|
|
27
|
+
path.isAbsolute(value) ||
|
|
28
|
+
/[\x00-\x1f\x7f\\*?\[\]{}]/.test(value) ||
|
|
29
|
+
value
|
|
30
|
+
.split("/")
|
|
31
|
+
.some((part) => !part || part === "." || part === ".." || protectedCleanupName(part)))
|
|
32
|
+
throw new Error(`Unsafe cleanup path: ${String(value)}`);
|
|
33
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { message } from "./common.js";
|
|
2
|
+
import { invocation, topicHelp, usageHint } from "./help.js";
|
|
3
|
+
import { main as state } from "./state.js";
|
|
4
|
+
import { main as repos } from "./repos.js";
|
|
5
|
+
import { main as remove } from "./delete.js";
|
|
6
|
+
import { main as summary } from "./summary.js";
|
|
7
|
+
import { main as picker } from "./picker.js";
|
|
8
|
+
import { main as workflow } from "./workflow.js";
|
|
9
|
+
import { main as git } from "./git-workflow.js";
|
|
10
|
+
import { main as pr } from "./pr-workflow.js";
|
|
11
|
+
import { main as agent } from "./agent-launch.js";
|
|
12
|
+
import { main as mcp } from "./mcp.js";
|
|
13
|
+
import { main as task } from "./task.js";
|
|
14
|
+
import { main as storage } from "./storage.js";
|
|
15
|
+
import { main as prUpdate } from "./pr-update.js";
|
|
16
|
+
import { main as agentWorkflows } from "./agent-workflows.js";
|
|
17
|
+
const commands = {
|
|
18
|
+
state,
|
|
19
|
+
repos,
|
|
20
|
+
delete: remove,
|
|
21
|
+
summary,
|
|
22
|
+
picker,
|
|
23
|
+
workflow,
|
|
24
|
+
git,
|
|
25
|
+
pr,
|
|
26
|
+
agent,
|
|
27
|
+
mcp,
|
|
28
|
+
task,
|
|
29
|
+
storage,
|
|
30
|
+
"pr-update": prUpdate,
|
|
31
|
+
automation: agentWorkflows,
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const [command, ...argv] = process.argv.slice(2);
|
|
35
|
+
if (!Object.hasOwn(commands, command))
|
|
36
|
+
throw new Error("Unknown internal command");
|
|
37
|
+
const separator = argv.indexOf("--");
|
|
38
|
+
const options = separator < 0 ? argv : argv.slice(0, separator);
|
|
39
|
+
if (options.includes("--help") || options.includes("-h")) {
|
|
40
|
+
console.log(topicHelp(invocation(command, options.filter((arg) => arg !== "--help" && arg !== "-h"))));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.exitCode = await commands[command](argv);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const detail = message(error).replace("; run rl --help", "");
|
|
48
|
+
console.error(`rl: ${detail.startsWith("Usage:") ? "Invalid command arguments" : detail}`);
|
|
49
|
+
const parseError = typeof error === "object" &&
|
|
50
|
+
error !== null &&
|
|
51
|
+
"code" in error &&
|
|
52
|
+
String(error.code).startsWith("ERR_PARSE_ARGS_");
|
|
53
|
+
const argumentsError = parseError ||
|
|
54
|
+
/^(?:Usage:|Invalid command arguments|Unknown option|Missing --|Expected|Unsupported .*option|--.*(?:require|supported|accept))/.test(detail);
|
|
55
|
+
console.error(usageHint(process.argv[2] ?? "", process.argv.slice(3), argumentsError));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|