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