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
package/bin/rl
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/zsh -f
|
|
2
|
+
emulate -L zsh
|
|
3
|
+
readonly rl_home="${${(%):-%x}:A:h:h}"
|
|
4
|
+
if [[ "$1" == __hook ]]; then
|
|
5
|
+
# Hooks resolve the installed CLI on PATH, not a version-specific package path.
|
|
6
|
+
# Their explicit repository/state arguments must not be replaced by user config.
|
|
7
|
+
shift
|
|
8
|
+
exec node "$rl_home/dist/cli.js" state "$@"
|
|
9
|
+
fi
|
|
10
|
+
if [[ "$1" == --version || "$1" == -V ]]; then
|
|
11
|
+
exec node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).version' "$rl_home/package.json"
|
|
12
|
+
fi
|
|
13
|
+
if [[ "$1" == init ]]; then
|
|
14
|
+
if (( $# == 1 )); then
|
|
15
|
+
exec node "$rl_home/dist/help.js" init
|
|
16
|
+
fi
|
|
17
|
+
for argument in "$@"; do
|
|
18
|
+
[[ "$argument" == -- ]] && break
|
|
19
|
+
if [[ "$argument" == --help || "$argument" == -h ]]; then
|
|
20
|
+
exec node "$rl_home/dist/help.js" "$@"
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
if [[ "$2" != zsh && "$2" != bash || $# != 2 ]]; then
|
|
24
|
+
print -u2 'Usage: rl setup shell <zsh|bash>'
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
if [[ "$2" == zsh ]]; then
|
|
28
|
+
print -r -- "source ${(q)rl_home}/shell/rl.zsh"
|
|
29
|
+
else
|
|
30
|
+
print -r -- "_rl_executable=${(qq)rl_home}/bin/rl"
|
|
31
|
+
cat <<'BASH'
|
|
32
|
+
rl() {
|
|
33
|
+
local destination_file destination result previous="$PWD"
|
|
34
|
+
local -a arguments=("$@")
|
|
35
|
+
if [[ "$1" == switch && "${2:-}" == - ]]; then
|
|
36
|
+
[[ -n "${RL_PREVIOUS_WORKSPACE:-}" ]] || { printf '%s\n' 'No previous workspace in this shell' >&2; return 1; }
|
|
37
|
+
arguments[1]="$RL_PREVIOUS_WORKSPACE"
|
|
38
|
+
fi
|
|
39
|
+
destination_file=$(mktemp "${TMPDIR:-/tmp}/rl-shell.XXXXXXXX") || return 1
|
|
40
|
+
if RL_SHELL_OUTPUT="$destination_file" command "$_rl_executable" "${arguments[@]}"; then result=0; else result=$?; fi
|
|
41
|
+
destination=$(cat -- "$destination_file")
|
|
42
|
+
rm -f -- "$destination_file"
|
|
43
|
+
if [[ -n "$destination" ]]; then
|
|
44
|
+
builtin cd -- "$destination" || return 1
|
|
45
|
+
RL_PREVIOUS_WORKSPACE="$previous"
|
|
46
|
+
fi
|
|
47
|
+
return "$result"
|
|
48
|
+
}
|
|
49
|
+
BASH
|
|
50
|
+
fi
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
source "$rl_home/lib/rl.zsh" || exit 1
|
|
54
|
+
_rl_main "$@"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copy to ~/.config/rl/config.zsh (or use RL_CONFIG).
|
|
2
|
+
RL_REPO_ROOT="$HOME/Documents/GitHub/my-project"
|
|
3
|
+
RL_WORKTREE_DIR="$HOME/.rl/worktrees"
|
|
4
|
+
RL_REPO_NAME="my-project"
|
|
5
|
+
RL_DEFAULT_BASE="origin/test"
|
|
6
|
+
RL_STATE_DIR="$HOME/.rl/instances"
|
|
7
|
+
# Optional registered repository alias; rl --repo <alias> overrides this.
|
|
8
|
+
# Without it, RL selects by current worktree, then `rl repo use` default.
|
|
9
|
+
# RL_REPO="frontend"
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { timestamp } from "./common.js";
|
|
2
|
+
/** Historical sessions/creation provide a fallback without mistaking metadata edits for use. */
|
|
3
|
+
export function lastAccessed(data) {
|
|
4
|
+
if (!data)
|
|
5
|
+
return undefined;
|
|
6
|
+
const candidates = [
|
|
7
|
+
data.lastAccessedAt,
|
|
8
|
+
data.createdAt,
|
|
9
|
+
...Object.values(data.sessions)
|
|
10
|
+
.flat()
|
|
11
|
+
.map((session) => session.lastUsedAt),
|
|
12
|
+
];
|
|
13
|
+
let latest;
|
|
14
|
+
for (const value of candidates) {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
if (!latest || timestamp(value) > timestamp(latest)) {
|
|
19
|
+
timestamp(value);
|
|
20
|
+
latest = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* Legacy records may not have an access timestamp. */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return latest;
|
|
28
|
+
}
|
|
29
|
+
export function accessLabel(value, now = Date.now()) {
|
|
30
|
+
if (!value)
|
|
31
|
+
return "—";
|
|
32
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(value)) / 1000));
|
|
33
|
+
if (seconds < 60)
|
|
34
|
+
return "just now";
|
|
35
|
+
if (seconds < 3600)
|
|
36
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
37
|
+
if (seconds < 86400)
|
|
38
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
39
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
40
|
+
}
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { object, timestamp } from "./common.js";
|
|
2
|
+
export const STALE_AFTER_MS = 30 * 60 * 1000;
|
|
3
|
+
export function activityView(items = [], at = Date.now()) {
|
|
4
|
+
return items.map((item) => ({
|
|
5
|
+
...item,
|
|
6
|
+
state: item.state !== "ended" &&
|
|
7
|
+
at - Date.parse(item.observedAt) > STALE_AFTER_MS
|
|
8
|
+
? "stale"
|
|
9
|
+
: item.state,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export function recordActivity(data, agent, event, stamp, contextHash) {
|
|
13
|
+
const records = ((data.activity ??= {})[agent] ??= []);
|
|
14
|
+
let item = records.find((entry) => entry.sessionId === event.sessionId && entry.childId === event.childId);
|
|
15
|
+
if (!item) {
|
|
16
|
+
item = {
|
|
17
|
+
sessionId: event.sessionId,
|
|
18
|
+
childId: event.childId,
|
|
19
|
+
state: "idle",
|
|
20
|
+
lastEvent: event.nativeEvent,
|
|
21
|
+
observedAt: stamp,
|
|
22
|
+
resumable: event.resumable,
|
|
23
|
+
};
|
|
24
|
+
records.push(item);
|
|
25
|
+
}
|
|
26
|
+
// Arrival order is the only portable ordering available. Do not invent a
|
|
27
|
+
// source timestamp or claim this is a process-liveness signal.
|
|
28
|
+
item.state =
|
|
29
|
+
event.type === "context.restored" ||
|
|
30
|
+
event.type === "notification" ||
|
|
31
|
+
event.type === "tool.before" ||
|
|
32
|
+
event.type === "tool.after"
|
|
33
|
+
? item.state
|
|
34
|
+
: event.type === "session.ended"
|
|
35
|
+
? "ended"
|
|
36
|
+
: ["turn.started", "child.started"].includes(event.type)
|
|
37
|
+
? "working"
|
|
38
|
+
: "idle";
|
|
39
|
+
item.lastEvent = event.nativeEvent;
|
|
40
|
+
item.observedAt = stamp;
|
|
41
|
+
if (contextHash)
|
|
42
|
+
item.contextHash = contextHash;
|
|
43
|
+
// Keep one snapshot per identity so ownership remains known across sessions.
|
|
44
|
+
records.sort((a, b) => a.observedAt.localeCompare(b.observedAt));
|
|
45
|
+
((data.hookHealth ??= {})[agent] ??= {}).lastSuccess = {
|
|
46
|
+
event: event.nativeEvent,
|
|
47
|
+
at: stamp,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function validateTracking(data) {
|
|
51
|
+
if (data.activity !== undefined) {
|
|
52
|
+
if (!object(data.activity))
|
|
53
|
+
throw new Error("Invalid activity");
|
|
54
|
+
for (const items of Object.values(data.activity)) {
|
|
55
|
+
if (!Array.isArray(items))
|
|
56
|
+
throw new Error("Invalid activity");
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (!object(item) ||
|
|
59
|
+
typeof item.sessionId !== "string" ||
|
|
60
|
+
(item.childId !== undefined && typeof item.childId !== "string") ||
|
|
61
|
+
!["idle", "working", "ended"].includes(String(item.state)) ||
|
|
62
|
+
typeof item.lastEvent !== "string" ||
|
|
63
|
+
typeof item.observedAt !== "string" ||
|
|
64
|
+
typeof item.resumable !== "boolean" ||
|
|
65
|
+
(item.contextHash !== undefined &&
|
|
66
|
+
typeof item.contextHash !== "string"))
|
|
67
|
+
throw new Error("Invalid activity");
|
|
68
|
+
timestamp(item.observedAt);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (data.hookHealth !== undefined) {
|
|
73
|
+
if (!object(data.hookHealth))
|
|
74
|
+
throw new Error("Invalid hook health");
|
|
75
|
+
for (const health of Object.values(data.hookHealth)) {
|
|
76
|
+
if (!object(health))
|
|
77
|
+
throw new Error("Invalid hook health");
|
|
78
|
+
for (const key of ["lastSuccess", "lastError"]) {
|
|
79
|
+
const entry = health[key];
|
|
80
|
+
if (entry === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
if (!object(entry) ||
|
|
83
|
+
typeof entry.at !== "string" ||
|
|
84
|
+
typeof entry[key === "lastSuccess" ? "event" : "message"] !== "string")
|
|
85
|
+
throw new Error("Invalid hook health");
|
|
86
|
+
timestamp(entry.at);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sessionId } from "./common.js";
|
|
2
|
+
import { contextResponse, lifecycleEvents, parseEvent, } from "./agents.js";
|
|
3
|
+
export const claude = {
|
|
4
|
+
name: "claude",
|
|
5
|
+
configPath: ".claude/settings.local.json",
|
|
6
|
+
events: { ...lifecycleEvents },
|
|
7
|
+
extraEvents: { StopFailure: "turn.failed", Notification: "notification" },
|
|
8
|
+
observedId: sessionId,
|
|
9
|
+
resumeArgs: (id) => ["claude", "--resume", sessionId(id)],
|
|
10
|
+
startArgs: (argv) => ["claude", ...argv],
|
|
11
|
+
runArgs: (prompt, argv) => ["claude", "--print", ...argv, "--", prompt],
|
|
12
|
+
parse(payload) {
|
|
13
|
+
return parseEvent(claude, payload);
|
|
14
|
+
},
|
|
15
|
+
response: contextResponse,
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sessionId } from "./common.js";
|
|
2
|
+
import { contextResponse, lifecycleEvents, parseEvent, } from "./agents.js";
|
|
3
|
+
export const codex = {
|
|
4
|
+
name: "codex",
|
|
5
|
+
configPath: ".codex/hooks.json",
|
|
6
|
+
events: { ...lifecycleEvents },
|
|
7
|
+
extraEvents: { Interrupt: "turn.interrupted" },
|
|
8
|
+
observedId(value) {
|
|
9
|
+
// The hook contract also documents thread IDs. Record these for diagnostics,
|
|
10
|
+
// but don't send them to CLI resume until that contract is verified.
|
|
11
|
+
if (typeof value === "string" && /^thr_[A-Za-z0-9_-]{1,128}$/.test(value))
|
|
12
|
+
return value;
|
|
13
|
+
return sessionId(value);
|
|
14
|
+
},
|
|
15
|
+
resumeArgs: (id) => ["codex", "resume", sessionId(id)],
|
|
16
|
+
startArgs: (argv) => ["codex", ...argv],
|
|
17
|
+
runArgs: (prompt, argv) => ["codex", "exec", ...argv, "--", prompt],
|
|
18
|
+
parse(payload) {
|
|
19
|
+
return parseEvent(codex, payload);
|
|
20
|
+
},
|
|
21
|
+
response: contextResponse,
|
|
22
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { agentAdapter } from "./agent-registry.js";
|
|
3
|
+
import { args, arity, execute } from "./common.js";
|
|
4
|
+
import { commandContext, contextOptions, resolveWorkspace, } from "./command-context.js";
|
|
5
|
+
import { readWorkflowConfig, workflowEnvironment } from "./workflow-config.js";
|
|
6
|
+
import { contextText } from "./store.js";
|
|
7
|
+
export async function launchAgent(store, name, argv = [], options = {}) {
|
|
8
|
+
const adapter = agentAdapter(name);
|
|
9
|
+
const [, data] = await store.locked(() => store.current(options.cwd));
|
|
10
|
+
const headless = options.mode === "run";
|
|
11
|
+
const prompt = options.prompt ?? "";
|
|
12
|
+
if (headless && !prompt.trim())
|
|
13
|
+
throw new Error("Agent run requires a non-empty --prompt");
|
|
14
|
+
if (!headless && options.prompt !== undefined)
|
|
15
|
+
throw new Error("Use agent run for --prompt");
|
|
16
|
+
const command = headless
|
|
17
|
+
? adapter.runArgs(prompt, argv)
|
|
18
|
+
: adapter.startArgs(argv);
|
|
19
|
+
const result = await execute(command, data.worktree, {
|
|
20
|
+
interactive: !headless,
|
|
21
|
+
terminalError: headless,
|
|
22
|
+
env: {
|
|
23
|
+
...workflowEnvironment(store.repo, data.worktree, readWorkflowConfig(store.repo)),
|
|
24
|
+
RL_STATE_DIR: store.stateDir,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (headless)
|
|
28
|
+
process.stdout.write(result.stdout);
|
|
29
|
+
return result.code;
|
|
30
|
+
}
|
|
31
|
+
export async function handoff(store, target, cwd) {
|
|
32
|
+
const adapter = target ? agentAdapter(target) : undefined;
|
|
33
|
+
return store.locked(() => {
|
|
34
|
+
const [file, data] = store.current(cwd);
|
|
35
|
+
return ([
|
|
36
|
+
adapter
|
|
37
|
+
? `Handoff to ${adapter.name}. Start with: rl agent start ${adapter.name}`
|
|
38
|
+
: "RL agent handoff",
|
|
39
|
+
"Shared task notes follow. Agent conversations remain in their original sessions.",
|
|
40
|
+
contextText(file, data),
|
|
41
|
+
].join("\n\n") + "\n");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function main(argv) {
|
|
45
|
+
const separator = argv.indexOf("--");
|
|
46
|
+
const extra = separator < 0 ? [] : argv.slice(separator + 1);
|
|
47
|
+
const { values, positionals } = args(separator < 0 ? argv : argv.slice(0, separator), [...contextOptions, "prompt", "workspace", "output"], ["stdout"]);
|
|
48
|
+
const context = commandContext(values);
|
|
49
|
+
const target = resolveWorkspace(context, typeof values.workspace === "string" ? values.workspace : undefined);
|
|
50
|
+
const [command, name, ...rest] = positionals;
|
|
51
|
+
arity(rest, 0);
|
|
52
|
+
if (command === "handoff") {
|
|
53
|
+
if (extra.length ||
|
|
54
|
+
values.prompt !== undefined ||
|
|
55
|
+
(values.stdout && values.output !== undefined))
|
|
56
|
+
throw new Error("Usage: rl handoff [agent] [--stdout | --output path]");
|
|
57
|
+
const text = await handoff(context.store, name, target.path);
|
|
58
|
+
if (typeof values.output === "string")
|
|
59
|
+
fs.writeFileSync(values.output, text, { flag: "wx", mode: 0o600 });
|
|
60
|
+
else
|
|
61
|
+
process.stdout.write(text);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if ((command !== "start" && command !== "run") ||
|
|
65
|
+
!name ||
|
|
66
|
+
values.output !== undefined ||
|
|
67
|
+
values.stdout)
|
|
68
|
+
throw new Error("Usage: rl agent start <codex|claude> [-- args] | run <agent> --prompt text [-- args]");
|
|
69
|
+
return launchAgent(context.store, name, extra, {
|
|
70
|
+
mode: command,
|
|
71
|
+
prompt: typeof values.prompt === "string" ? values.prompt : undefined,
|
|
72
|
+
cwd: target.path,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const PRESET = {
|
|
2
|
+
agents: {
|
|
3
|
+
behavior: {
|
|
4
|
+
provider: "inherit",
|
|
5
|
+
reasoning: "high",
|
|
6
|
+
description: "Runtime correctness, user journeys, persistence and security",
|
|
7
|
+
instructions: "Investigate supported end-to-end user journeys: input, validation, serialization, persistence, queries, subscriptions, caches, rendering and downstream consumers. Own runtime bugs, regressions, concurrency, error behavior, tenant isolation and demonstrable runtime performance failures. Check current code and intended behavior; history and old comments are leads, not proof. Distinguish current defects from fixed regressions, intentional boundaries and unverified hypotheses. Findings need prerequisites, reproducible steps, expected versus actual behavior, and evidence; label code-inferred reproductions. Do not review type hygiene without runtime impact, test design, debug cleanup, duplication or structural simplification. Attach regression advice to the corresponding bug rather than filing duplicate missing-test findings.",
|
|
8
|
+
},
|
|
9
|
+
reuse: {
|
|
10
|
+
provider: "inherit",
|
|
11
|
+
reasoning: "high",
|
|
12
|
+
description: "Reuse, duplicated behavior and avoidable complexity",
|
|
13
|
+
instructions: "Search the repository for existing components, hooks, services, utilities, dependencies and alternate implementations before judging a new capability. Own duplicated business behavior, unnecessary abstractions/features, redundant state and misplaced responsibilities with concrete maintenance cost. Prefer reuse, extension, composition, then a small focused implementation. Cite both the reviewed implementation and existing counterpart, the behavioral overlap and a viable smaller solution. For pure simplification, name removable machinery and explain how requirements are preserved. Small duplication and different presentations can be justified. Do not investigate runtime/security bugs, type-only duplication, logging cleanup or test quality. Labels such as DRY, YAGNI and code smell are not evidence.",
|
|
14
|
+
},
|
|
15
|
+
quality: {
|
|
16
|
+
provider: "inherit",
|
|
17
|
+
reasoning: "high",
|
|
18
|
+
description: "Type contracts, static checks, debug leftovers and test quality",
|
|
19
|
+
instructions: "Own unjustified any, casts and suppressions, overly broad contracts, failure to reuse applicable types, accidental debug leftovers, attributable static-check failures, and tests that cannot protect their claimed behavior. Distinguish safe unknown narrowing and justified dependency escape hatches from weakened checking. Preserve intentional operational logging. For test findings name a specific material contract and a useful assertion, not a coverage percentage. Do not review runtime correctness/security, architecture or duplicated business behavior. A test failure is not automatically a product bug. Request configured checks from the coordinator instead of running duplicate commands. Omit style preferences and broad cleanup of existing code.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
policies: {
|
|
23
|
+
review: {
|
|
24
|
+
instructions: "Report only evidence-backed problems caused or materially worsened by the reviewed changes. Separate required fixes from optional quick wins with reasons. Critical means security exposure, destructive data loss or outage; high means broken core behavior or substantial structural cost; medium means a contained defect or worthwhile required quality fix; low means an optional quick win. Do not block on stylistic preference, tiny duplication, justified type escape hatches, intentional operational logging, speculative flexibility or generic demands for tests. Missing dependencies, unverified critical paths and unresolved material questions are coverage limitations. Do not invent findings or treat a score as a probability of correctness.",
|
|
25
|
+
weights: { critical: 50, high: 20, medium: 10, low: 2 },
|
|
26
|
+
minimumScore: 0,
|
|
27
|
+
blockRequired: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
workflows: {
|
|
31
|
+
"triple-review": {
|
|
32
|
+
description: "Three independent reviewers, owner follow-ups and a policy-based verdict",
|
|
33
|
+
policy: "review",
|
|
34
|
+
concurrency: 3,
|
|
35
|
+
retries: 1,
|
|
36
|
+
timeoutSeconds: 600,
|
|
37
|
+
triggers: [{ event: "before_finish", changes: true }],
|
|
38
|
+
steps: [
|
|
39
|
+
{ id: "behavior", agent: "behavior", after: [] },
|
|
40
|
+
{ id: "reuse", agent: "reuse", after: [] },
|
|
41
|
+
{ id: "quality", agent: "quality", after: [] },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { codex } from "./agent-codex.js";
|
|
2
|
+
import { claude } from "./agent-claude.js";
|
|
3
|
+
export const adapters = { codex, claude };
|
|
4
|
+
export function agentAdapter(name) {
|
|
5
|
+
if (name !== "codex" && name !== "claude")
|
|
6
|
+
throw new Error("Agent must be codex or claude");
|
|
7
|
+
return adapters[name];
|
|
8
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { agentAdapter } from "./agent-registry.js";
|
|
2
|
+
import { syncCodexSessions } from "./codex-sessions.js";
|
|
3
|
+
import { execute, message, now, sessionId, timestamp } from "./common.js";
|
|
4
|
+
import { readWorkflowConfig, workflowEnvironment } from "./workflow-config.js";
|
|
5
|
+
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
export async function resumeSessions(store, kind, options = {}) {
|
|
7
|
+
if (kind !== "claude")
|
|
8
|
+
await syncCodexSessions(store);
|
|
9
|
+
const [, data] = await store.locked(() => store.current());
|
|
10
|
+
const kinds = kind ? [kind] : ["codex", "claude"];
|
|
11
|
+
const choices = kinds
|
|
12
|
+
.flatMap((agent) => data.sessions[agent].map((session) => ({ ...session, agent })))
|
|
13
|
+
.sort((a, b) => {
|
|
14
|
+
const recent = timestamp(b.lastUsedAt) - timestamp(a.lastUsedAt);
|
|
15
|
+
return recent > 0n
|
|
16
|
+
? 1
|
|
17
|
+
: recent < 0n
|
|
18
|
+
? -1
|
|
19
|
+
: a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id);
|
|
20
|
+
});
|
|
21
|
+
if (!choices.length)
|
|
22
|
+
throw new Error(`No ${kind ? `${kind} ` : ""}session recorded for ${data.id}. Use 'rl session save' to save a session first`);
|
|
23
|
+
let selected = choices[0];
|
|
24
|
+
if (choices.length > 1 && !options.latest) {
|
|
25
|
+
const visible = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
26
|
+
const lines = choices.map((choice) => `${choice.agent}:${choice.id}\t${choice.agent}\t${choice.id}\tLast used: ${visible(choice.lastUsedAt)}\tStarted: ${visible(choice.createdAt)}`);
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = await execute([
|
|
30
|
+
"fzf",
|
|
31
|
+
"--delimiter=\t",
|
|
32
|
+
"--with-nth=2..",
|
|
33
|
+
"--no-multi",
|
|
34
|
+
"--no-sort",
|
|
35
|
+
"--height=70%",
|
|
36
|
+
"--border=rounded",
|
|
37
|
+
"--reverse",
|
|
38
|
+
"--prompt=Resume session › ",
|
|
39
|
+
`--header=${visible(data.id)} · Agent / session UUID / last used / started\nMost recently used first · Enter resume · Esc cancel`,
|
|
40
|
+
], data.worktree, { input: lines.join("\n") + "\n", terminalError: true });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`Cannot open session selector: ${message(error)}. Install fzf to choose a saved session`);
|
|
44
|
+
}
|
|
45
|
+
if ([1, 130].includes(result.code))
|
|
46
|
+
return 0;
|
|
47
|
+
if (result.code)
|
|
48
|
+
throw new Error(`Session selector failed (exit ${result.code})`);
|
|
49
|
+
const index = lines.indexOf(result.stdout.replace(/\r?\n$/, ""));
|
|
50
|
+
if (index < 0)
|
|
51
|
+
throw new Error("Session selector returned an unknown selection");
|
|
52
|
+
selected = choices[index];
|
|
53
|
+
}
|
|
54
|
+
return resumeAgent(store, selected.agent, {
|
|
55
|
+
cwd: data.worktree,
|
|
56
|
+
sessionId: selected.id,
|
|
57
|
+
expectedKey: data.key,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function resumeAgent(store, kind, options = {}) {
|
|
61
|
+
const cwd = options.cwd ?? process.cwd();
|
|
62
|
+
if (kind === "codex" && options.sessionId === undefined)
|
|
63
|
+
await syncCodexSessions(store, cwd);
|
|
64
|
+
const [file, data, sid] = await store.locked(() => {
|
|
65
|
+
const [file, data] = store.current(cwd);
|
|
66
|
+
if (options.expectedKey && data.key !== options.expectedKey)
|
|
67
|
+
throw new Error("Instance changed before agent launch");
|
|
68
|
+
const sessions = data.sessions[kind];
|
|
69
|
+
const item = options.sessionId !== undefined
|
|
70
|
+
? sessions.find((item) => item.id === sessionId(options.sessionId))
|
|
71
|
+
: sessions.reduce((a, b) => !a || timestamp(b.lastUsedAt) > timestamp(a.lastUsedAt) ? b : a, undefined);
|
|
72
|
+
if (!item)
|
|
73
|
+
throw new Error(`No ${kind} session recorded for ${data.id}. Use 'rl session save ${kind}${kind === "claude" ? " <UUID>" : ""}'`);
|
|
74
|
+
return [file, data, sessionId(item.id)];
|
|
75
|
+
});
|
|
76
|
+
const stamp = now();
|
|
77
|
+
console.error(`Resuming ${kind} session ${sid} for ${data.id}`);
|
|
78
|
+
const command = agentAdapter(kind).resumeArgs(sid);
|
|
79
|
+
if (options.prompt !== undefined)
|
|
80
|
+
command.push(options.prompt);
|
|
81
|
+
const result = await execute(command, data.worktree, {
|
|
82
|
+
interactive: true,
|
|
83
|
+
env: {
|
|
84
|
+
...workflowEnvironment(store.repo, data.worktree, readWorkflowConfig(store.repo)),
|
|
85
|
+
RL_STATE_DIR: store.stateDir,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (result.code === 0)
|
|
89
|
+
await store.locked(() => {
|
|
90
|
+
const [currentFile, current] = store.current(data.worktree);
|
|
91
|
+
if (currentFile !== file)
|
|
92
|
+
throw new Error("Instance changed while the agent was running");
|
|
93
|
+
store.recordSession(file, current, kind, sid, stamp);
|
|
94
|
+
});
|
|
95
|
+
return result.code;
|
|
96
|
+
}
|