jeo-code 0.6.41 → 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/CHANGELOG.md +23 -0
- package/README.ja.md +30 -37
- package/README.ko.md +30 -37
- package/README.md +31 -37
- package/README.zh.md +30 -37
- package/package.json +4 -2
- package/src/agent/compaction.ts +2 -1
- package/src/agent/computer-supervisor.ts +59 -0
- package/src/agent/config-schema.ts +5 -0
- package/src/agent/engine.ts +9 -4
- package/src/agent/json.ts +75 -8
- package/src/agent/loop-guards.ts +16 -5
- package/src/agent/memory-okf.ts +2 -0
- package/src/agent/memory.ts +25 -8
- package/src/agent/state.ts +38 -7
- package/src/agent/task-tool.ts +12 -3
- package/src/agent/tool-schemas.ts +19 -0
- package/src/ai/model-catalog.ts +4 -0
- package/src/ai/providers/anthropic.ts +10 -6
- package/src/ai/providers/openai-compatible-catalog.ts +2 -0
- package/src/ai/types.ts +2 -1
- package/src/auth/index.ts +1 -1
- package/src/auth/refresh.ts +47 -2
- package/src/auth/storage.ts +11 -5
- package/src/cli/runner.ts +9 -0
- package/src/cli.ts +16 -0
- package/src/commands/approve.ts +17 -1
- package/src/commands/computer.ts +334 -0
- package/src/commands/launch/input.ts +19 -6
- package/src/commands/launch.ts +8 -8
- package/src/commands/ralplan.ts +4 -0
- package/src/commands/team.ts +13 -0
- package/src/index.ts +1 -0
- package/src/prompts/agents/executor.md +6 -4
- package/src/prompts/skills/ralplan/SKILL.md +5 -0
- package/src/prompts/skills/team/SKILL.md +5 -0
- package/src/tui/components/slash.ts +1 -0
- package/src/util/terminal-restore.ts +41 -0
package/src/commands/approve.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import {
|
|
4
5
|
readGlobalConfig,
|
|
5
6
|
readWorkflowState,
|
|
@@ -75,8 +76,10 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
|
|
|
75
76
|
// rubber stamp — validate the plan against the exact contract `jeo team`
|
|
76
77
|
// enforces, so a schema-invalid/unknown-role plan is refused HERE instead of
|
|
77
78
|
// aborting later at execution time.
|
|
79
|
+
let planContent = "";
|
|
78
80
|
try {
|
|
79
|
-
|
|
81
|
+
planContent = await fs.readFile(resolvedInputPath, "utf-8");
|
|
82
|
+
const parsed = PlanSchema.safeParse(normalizePlanShape(parseYaml(planContent)));
|
|
80
83
|
if (!parsed.success) {
|
|
81
84
|
console.log(
|
|
82
85
|
`[ERROR] Refusing to approve: the plan is not in the shape 'jeo team' executes (top-level 'steps:' list of { name, role? }).\n` +
|
|
@@ -114,6 +117,19 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
|
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
// Round-13: verify the plan's hash matches the consensus hash to prevent silent edits
|
|
121
|
+
if (ralplanState.consensus_hash) {
|
|
122
|
+
const currentHash = createHash("sha256").update(planContent).digest("hex");
|
|
123
|
+
if (currentHash !== ralplanState.consensus_hash) {
|
|
124
|
+
console.log(
|
|
125
|
+
`[ERROR] Refusing to approve: the plan file has been modified since the consensus critic reviewed it.\n` +
|
|
126
|
+
` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve again.`
|
|
127
|
+
);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
// Update ralplan-state.json to approved: true
|
|
118
134
|
ralplanState.approved = true;
|
|
119
135
|
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { computerSupervisor } from "../agent/computer-supervisor";
|
|
5
|
+
import { readGlobalConfig } from "../agent/state";
|
|
6
|
+
|
|
7
|
+
export interface ComputerAction {
|
|
8
|
+
action: "screenshot" | "click" | "double_click" | "move" | "drag" | "scroll" | "type" | "keypress" | "wait" | "batch";
|
|
9
|
+
x?: number;
|
|
10
|
+
y?: number;
|
|
11
|
+
text?: string;
|
|
12
|
+
key?: string;
|
|
13
|
+
deltaX?: number;
|
|
14
|
+
deltaY?: number;
|
|
15
|
+
duration?: number;
|
|
16
|
+
actions?: ComputerAction[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function logAudit(action: ComputerAction, success: boolean, error?: string) {
|
|
20
|
+
try {
|
|
21
|
+
const auditDir = path.join(process.cwd(), ".jeo");
|
|
22
|
+
await fs.mkdir(auditDir, { recursive: true });
|
|
23
|
+
const auditPath = path.join(auditDir, "computer-audit.jsonl");
|
|
24
|
+
const logEntry = JSON.stringify({
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
action,
|
|
27
|
+
success,
|
|
28
|
+
error,
|
|
29
|
+
}) + "\n";
|
|
30
|
+
await fs.appendFile(auditPath, logEntry, "utf-8");
|
|
31
|
+
} catch {
|
|
32
|
+
// best-effort
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runCommand(cmd: string[]): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
37
|
+
try {
|
|
38
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
39
|
+
await proc.exited;
|
|
40
|
+
const stdout = await new Response(proc.stdout).text();
|
|
41
|
+
const stderr = await new Response(proc.stderr).text();
|
|
42
|
+
return {
|
|
43
|
+
success: proc.exitCode === 0,
|
|
44
|
+
stdout,
|
|
45
|
+
stderr,
|
|
46
|
+
};
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
stdout: "",
|
|
51
|
+
stderr: err.message || String(err),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function executeComputerAction(action: ComputerAction): Promise<{ success: boolean; output: string; error?: string }> {
|
|
57
|
+
const config = await readGlobalConfig().catch(() => null);
|
|
58
|
+
if (!config?.computer?.enabled) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
output: "",
|
|
62
|
+
error: "Computer use is disabled. Enable it by setting 'computer.enabled: true' in ~/.jeo/config.json",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read-only actions don't need supervisor inputAllowed check
|
|
67
|
+
const isReadOnly = action.action === "screenshot" || action.action === "wait";
|
|
68
|
+
if (!isReadOnly) {
|
|
69
|
+
if (!computerSupervisor.inputAllowed) {
|
|
70
|
+
const err = "Action blocked by fail-closed supervisor (kill switch not live, heartbeat stale, or suspended).";
|
|
71
|
+
await logAudit(action, false, err);
|
|
72
|
+
return { success: false, output: "", error: err };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isMac = os.platform() === "darwin";
|
|
77
|
+
|
|
78
|
+
switch (action.action) {
|
|
79
|
+
case "screenshot": {
|
|
80
|
+
const tmpDir = os.tmpdir();
|
|
81
|
+
const screenPath = path.join(tmpDir, `jeo-screen-${Date.now()}.png`);
|
|
82
|
+
let cmd: string[];
|
|
83
|
+
if (isMac) {
|
|
84
|
+
cmd = ["screencapture", "-x", screenPath];
|
|
85
|
+
} else {
|
|
86
|
+
cmd = ["scrot", "-z", screenPath];
|
|
87
|
+
}
|
|
88
|
+
const res = await runCommand(cmd);
|
|
89
|
+
if (!res.success) {
|
|
90
|
+
await logAudit(action, false, res.stderr);
|
|
91
|
+
return { success: false, output: "", error: `Failed to capture screenshot: ${res.stderr}` };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const bytes = await fs.readFile(screenPath);
|
|
95
|
+
const base64 = bytes.toString("base64");
|
|
96
|
+
await fs.unlink(screenPath).catch(() => {});
|
|
97
|
+
await logAudit(action, true);
|
|
98
|
+
return { success: true, output: base64 };
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
await logAudit(action, false, err.message);
|
|
101
|
+
return { success: false, output: "", error: `Failed to read screenshot file: ${err.message}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "click": {
|
|
106
|
+
if (action.x === undefined || action.y === undefined) {
|
|
107
|
+
return { success: false, output: "", error: "Missing coordinates x and y for click action." };
|
|
108
|
+
}
|
|
109
|
+
let cmd: string[];
|
|
110
|
+
if (isMac) {
|
|
111
|
+
cmd = ["cliclick", `c:${action.x},${action.y}`];
|
|
112
|
+
} else {
|
|
113
|
+
cmd = ["xdotool", "mousemove", String(action.x), String(action.y), "click", "1"];
|
|
114
|
+
}
|
|
115
|
+
const res = await runCommand(cmd);
|
|
116
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
117
|
+
return res.success
|
|
118
|
+
? { success: true, output: `Clicked at (${action.x}, ${action.y})` }
|
|
119
|
+
: { success: false, output: "", error: `Click failed: ${res.stderr}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "double_click": {
|
|
123
|
+
if (action.x === undefined || action.y === undefined) {
|
|
124
|
+
return { success: false, output: "", error: "Missing coordinates x and y for double_click action." };
|
|
125
|
+
}
|
|
126
|
+
let cmd: string[];
|
|
127
|
+
if (isMac) {
|
|
128
|
+
cmd = ["cliclick", `dc:${action.x},${action.y}`];
|
|
129
|
+
} else {
|
|
130
|
+
cmd = ["xdotool", "mousemove", String(action.x), String(action.y), "click", "--repeat", "2", "--delay", "100", "1"];
|
|
131
|
+
}
|
|
132
|
+
const res = await runCommand(cmd);
|
|
133
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
134
|
+
return res.success
|
|
135
|
+
? { success: true, output: `Double clicked at (${action.x}, ${action.y})` }
|
|
136
|
+
: { success: false, output: "", error: `Double click failed: ${res.stderr}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case "move": {
|
|
140
|
+
if (action.x === undefined || action.y === undefined) {
|
|
141
|
+
return { success: false, output: "", error: "Missing coordinates x and y for move action." };
|
|
142
|
+
}
|
|
143
|
+
let cmd: string[];
|
|
144
|
+
if (isMac) {
|
|
145
|
+
cmd = ["cliclick", `m:${action.x},${action.y}`];
|
|
146
|
+
} else {
|
|
147
|
+
cmd = ["xdotool", "mousemove", String(action.x), String(action.y)];
|
|
148
|
+
}
|
|
149
|
+
const res = await runCommand(cmd);
|
|
150
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
151
|
+
return res.success
|
|
152
|
+
? { success: true, output: `Moved cursor to (${action.x}, ${action.y})` }
|
|
153
|
+
: { success: false, output: "", error: `Move failed: ${res.stderr}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "drag": {
|
|
157
|
+
if (action.x === undefined || action.y === undefined) {
|
|
158
|
+
return { success: false, output: "", error: "Missing coordinates x and y for drag action." };
|
|
159
|
+
}
|
|
160
|
+
let cmd: string[];
|
|
161
|
+
if (isMac) {
|
|
162
|
+
cmd = ["cliclick", `dd:${action.x},${action.y}`];
|
|
163
|
+
} else {
|
|
164
|
+
cmd = ["xdotool", "mousemove", String(action.x), String(action.y), "mousedown", "1"];
|
|
165
|
+
}
|
|
166
|
+
const res = await runCommand(cmd);
|
|
167
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
168
|
+
return res.success
|
|
169
|
+
? { success: true, output: `Dragged to (${action.x}, ${action.y})` }
|
|
170
|
+
: { success: false, output: "", error: `Drag failed: ${res.stderr}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "scroll": {
|
|
174
|
+
let cmd: string[];
|
|
175
|
+
const dx = action.deltaX ?? 0;
|
|
176
|
+
const dy = action.deltaY ?? 0;
|
|
177
|
+
if (isMac) {
|
|
178
|
+
// cliclick scroll deltaY deltaX
|
|
179
|
+
cmd = ["cliclick", `w:${dy},${dx}`];
|
|
180
|
+
} else {
|
|
181
|
+
// xdotool click 4 (scroll up), 5 (scroll down), 6 (scroll left), 7 (scroll right)
|
|
182
|
+
const actions: string[] = [];
|
|
183
|
+
if (dy > 0) {
|
|
184
|
+
for (let i = 0; i < dy; i++) actions.push("click", "4");
|
|
185
|
+
} else if (dy < 0) {
|
|
186
|
+
for (let i = 0; i < -dy; i++) actions.push("click", "5");
|
|
187
|
+
}
|
|
188
|
+
if (dx > 0) {
|
|
189
|
+
for (let i = 0; i < dx; i++) actions.push("click", "7");
|
|
190
|
+
} else if (dx < 0) {
|
|
191
|
+
for (let i = 0; i < -dx; i++) actions.push("click", "6");
|
|
192
|
+
}
|
|
193
|
+
cmd = ["xdotool", ...actions];
|
|
194
|
+
}
|
|
195
|
+
const res = await runCommand(cmd);
|
|
196
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
197
|
+
return res.success
|
|
198
|
+
? { success: true, output: `Scrolled by deltaX=${dx}, deltaY=${dy}` }
|
|
199
|
+
: { success: false, output: "", error: `Scroll failed: ${res.stderr}` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "type": {
|
|
203
|
+
if (action.text === undefined) {
|
|
204
|
+
return { success: false, output: "", error: "Missing text for type action." };
|
|
205
|
+
}
|
|
206
|
+
let cmd: string[];
|
|
207
|
+
if (isMac) {
|
|
208
|
+
cmd = ["cliclick", `t:${action.text}`];
|
|
209
|
+
} else {
|
|
210
|
+
cmd = ["xdotool", "type", action.text];
|
|
211
|
+
}
|
|
212
|
+
const res = await runCommand(cmd);
|
|
213
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
214
|
+
return res.success
|
|
215
|
+
? { success: true, output: `Typed text: ${action.text}` }
|
|
216
|
+
: { success: false, output: "", error: `Type failed: ${res.stderr}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "keypress": {
|
|
220
|
+
if (action.key === undefined) {
|
|
221
|
+
return { success: false, output: "", error: "Missing key for keypress action." };
|
|
222
|
+
}
|
|
223
|
+
let cmd: string[];
|
|
224
|
+
if (isMac) {
|
|
225
|
+
cmd = ["cliclick", `kp:${action.key}`];
|
|
226
|
+
} else {
|
|
227
|
+
cmd = ["xdotool", "key", action.key];
|
|
228
|
+
}
|
|
229
|
+
const res = await runCommand(cmd);
|
|
230
|
+
await logAudit(action, res.success, res.success ? undefined : res.stderr);
|
|
231
|
+
return res.success
|
|
232
|
+
? { success: true, output: `Pressed key: ${action.key}` }
|
|
233
|
+
: { success: false, output: "", error: `Keypress failed: ${res.stderr}` };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case "wait": {
|
|
237
|
+
const ms = (action.duration ?? 1) * 1000;
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
239
|
+
await logAudit(action, true);
|
|
240
|
+
return { success: true, output: `Waited for ${action.duration ?? 1} seconds` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case "batch": {
|
|
244
|
+
if (!action.actions || !Array.isArray(action.actions)) {
|
|
245
|
+
return { success: false, output: "", error: "Missing actions array for batch action." };
|
|
246
|
+
}
|
|
247
|
+
const results: string[] = [];
|
|
248
|
+
for (let i = 0; i < action.actions.length; i++) {
|
|
249
|
+
const subAction = action.actions[i];
|
|
250
|
+
const res = await executeComputerAction(subAction);
|
|
251
|
+
if (!res.success) {
|
|
252
|
+
const err = `Batch failed at step ${i}: ${res.error}`;
|
|
253
|
+
await logAudit(action, false, err);
|
|
254
|
+
return { success: false, output: results.join("\n"), error: err };
|
|
255
|
+
}
|
|
256
|
+
results.push(`Step ${i}: ${res.output}`);
|
|
257
|
+
}
|
|
258
|
+
await logAudit(action, true);
|
|
259
|
+
return { success: true, output: results.join("\n") };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
return { success: false, output: "", error: `Unknown computer action: ${(action as any).action}` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function runComputerCommand(args: string[]): Promise<void> {
|
|
268
|
+
const actionName = args[0];
|
|
269
|
+
if (!actionName) {
|
|
270
|
+
console.log("Usage: jeo computer <action> [args]");
|
|
271
|
+
console.log("Actions: screenshot, click, double_click, move, drag, scroll, type, keypress, wait, batch");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const action: ComputerAction = { action: actionName as any };
|
|
276
|
+
|
|
277
|
+
// Parse arguments based on action
|
|
278
|
+
if (actionName === "click" || actionName === "double_click" || actionName === "move" || actionName === "drag") {
|
|
279
|
+
const x = parseInt(args[1] ?? "");
|
|
280
|
+
const y = parseInt(args[2] ?? "");
|
|
281
|
+
if (isNaN(x) || isNaN(y)) {
|
|
282
|
+
console.error(`Error: ${actionName} requires x and y coordinates.`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
action.x = x;
|
|
286
|
+
action.y = y;
|
|
287
|
+
} else if (actionName === "type") {
|
|
288
|
+
const text = args.slice(1).join(" ");
|
|
289
|
+
if (!text) {
|
|
290
|
+
console.error("Error: type requires text.");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
action.text = text;
|
|
294
|
+
} else if (actionName === "keypress") {
|
|
295
|
+
const key = args[1];
|
|
296
|
+
if (!key) {
|
|
297
|
+
console.error("Error: keypress requires a key name.");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
action.key = key;
|
|
301
|
+
} else if (actionName === "scroll") {
|
|
302
|
+
const dx = parseInt(args[1] ?? "0");
|
|
303
|
+
const dy = parseInt(args[2] ?? "0");
|
|
304
|
+
action.deltaX = dx;
|
|
305
|
+
action.deltaY = dy;
|
|
306
|
+
} else if (actionName === "wait") {
|
|
307
|
+
const duration = parseFloat(args[1] ?? "1");
|
|
308
|
+
action.duration = duration;
|
|
309
|
+
} else if (actionName === "batch") {
|
|
310
|
+
const jsonStr = args.slice(1).join(" ");
|
|
311
|
+
try {
|
|
312
|
+
action.actions = JSON.parse(jsonStr);
|
|
313
|
+
} catch (err: any) {
|
|
314
|
+
console.error(`Error: batch requires a valid JSON array of actions. ${err.message}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Heartbeat to supervisor for CLI execution
|
|
320
|
+
computerSupervisor.setKillSwitchLive(true);
|
|
321
|
+
computerSupervisor.heartbeat();
|
|
322
|
+
|
|
323
|
+
const res = await executeComputerAction(action);
|
|
324
|
+
if (res.success) {
|
|
325
|
+
if (actionName === "screenshot") {
|
|
326
|
+
console.log(res.output); // base64 output
|
|
327
|
+
} else {
|
|
328
|
+
console.log(res.output);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
console.error(`Error: ${res.error}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -24,6 +24,8 @@ interface AbortHarnessOptions {
|
|
|
24
24
|
* Without this hook the byte would be swallowed into the buffered input queue,
|
|
25
25
|
* which is why Ctrl+O historically "did nothing" while the TUI owned stdin. */
|
|
26
26
|
onDetailKey?: () => void;
|
|
27
|
+
/** Invoked when Ctrl+\\ (\u001c) is pressed mid-turn — the safety kill-switch binding. */
|
|
28
|
+
onKillSwitch?: () => void;
|
|
27
29
|
/** Invoked when an arrow / PageUp / PageDown key arrives mid-turn — scrolls the
|
|
28
30
|
* open Ctrl+O detail panel. dir -1 = up/back, +1 = down/forward; page = full jump. */
|
|
29
31
|
onScrollKey?: (dir: -1 | 1, page: boolean) => void;
|
|
@@ -65,16 +67,23 @@ export function isGenuineMultilineDraft(line: string): boolean {
|
|
|
65
67
|
return line.includes(MULTILINE_SENTINEL);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
/** Whether an Up/Down keystroke should
|
|
69
|
-
* (textarea feel) instead of
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
/** Whether an Up/Down keystroke should be CONSIDERED for moving the caret between the
|
|
71
|
+
* box's visual rows (textarea feel) instead of going straight to readline. True for any
|
|
72
|
+
* non-empty draft when no slash dropdown or Ctrl+O history panel owns the arrows.
|
|
73
|
+
*
|
|
74
|
+
* This is a gate, NOT the final decision: the caller still consults
|
|
75
|
+
* `verticalCursorOffset`, which returns a target offset only when there is a visual row
|
|
76
|
+
* to move to. So ↑/↓ edit a MULTI-ROW draft — whether it spans rows via explicit
|
|
77
|
+
* Shift+Enter breaks OR a long line the box soft-wraps — yet still fall through to
|
|
78
|
+
* input-history recall at the boundaries: ↑ on the TOP visual row, ↓ on the BOTTOM row,
|
|
79
|
+
* and any single-visual-row draft (where `verticalCursorOffset` yields null). That
|
|
80
|
+
* boundary-aware split gives full arrow-key cursor movement for editing multi-row input
|
|
81
|
+
* while preserving "↑ recalls the previous prompt" once the caret can climb no higher. */
|
|
73
82
|
export function shouldBoxVerticalNav(
|
|
74
83
|
line: string,
|
|
75
84
|
opts: { slashMatchCount: number; historyPanelOpen: boolean },
|
|
76
85
|
): boolean {
|
|
77
|
-
return
|
|
86
|
+
return line.length > 0 && opts.slashMatchCount === 0 && !opts.historyPanelOpen;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
|
|
@@ -372,6 +381,10 @@ export function createInFlightAbortHarness(opts: AbortHarnessOptions = {}): InFl
|
|
|
372
381
|
opts.onDetailKey?.();
|
|
373
382
|
return;
|
|
374
383
|
}
|
|
384
|
+
if (text === "\u001c") {
|
|
385
|
+
opts.onKillSwitch?.();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
375
388
|
if (text === "\u001b[A") { opts.onScrollKey?.(-1, false); return; }
|
|
376
389
|
if (text === "\u001b[B") { opts.onScrollKey?.(1, false); return; }
|
|
377
390
|
if (text === "\u001b[5~") { opts.onScrollKey?.(-1, true); return; }
|
package/src/commands/launch.ts
CHANGED
|
@@ -1469,14 +1469,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1469
1469
|
// Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
|
|
1470
1470
|
const combo = matchCursorCombo(data, i);
|
|
1471
1471
|
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
1472
|
-
// Up/Down
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1472
|
+
// Up/Down move the caret BETWEEN the box's visual rows (textarea feel) for any
|
|
1473
|
+
// MULTI-ROW draft — explicit Shift+Enter breaks (→ SENTINEL) OR a long line the
|
|
1474
|
+
// box soft-wraps. The decision is boundary-aware: verticalCursorOffset returns a
|
|
1475
|
+
// target only when a visual row exists to move to, so ↑ on the TOP row, ↓ on the
|
|
1476
|
+
// BOTTOM row, and any single-visual-row draft fall through to readline and recall
|
|
1477
|
+
// input history — the dominant REPL expectation once the caret can climb no
|
|
1478
|
+
// higher. Skipped when a slash list or Ctrl+O history panel owns ↑/↓.
|
|
1479
|
+
|
|
1480
1480
|
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
1481
1481
|
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
1482
1482
|
const line = activeRl?.line ?? "";
|
package/src/commands/ralplan.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { callLlm, type Message } from "../agent/loop";
|
|
4
5
|
import {
|
|
5
6
|
readWorkflowState,
|
|
@@ -268,6 +269,9 @@ export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise
|
|
|
268
269
|
ralplanState.plan_path = planPath;
|
|
269
270
|
ralplanState.consensus = gate.verdict;
|
|
270
271
|
ralplanState.consensus_detail = gate.detail.slice(0, 600);
|
|
272
|
+
if (gate.verdict === "okay") {
|
|
273
|
+
ralplanState.consensus_hash = createHash("sha256").update(cleanPlan).digest("hex");
|
|
274
|
+
}
|
|
271
275
|
if (gate.verdict !== "okay") {
|
|
272
276
|
ralplanState.approved = false;
|
|
273
277
|
await writeWorkflowState("ralplan", ralplanState, cwd);
|
package/src/commands/team.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import chalk from "chalk";
|
|
3
4
|
import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
|
|
4
5
|
import {
|
|
@@ -221,6 +222,18 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
221
222
|
return { ok: false, reason: err.message };
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
// Round-13: verify the plan's hash matches the consensus hash to prevent silent edits after approval
|
|
226
|
+
if (planState.consensus_hash) {
|
|
227
|
+
const currentHash = createHash("sha256").update(planContent).digest("hex");
|
|
228
|
+
if (currentHash !== planState.consensus_hash) {
|
|
229
|
+
log(
|
|
230
|
+
`[ERROR] Plan file has been modified since it was reviewed by the consensus critic.\n` +
|
|
231
|
+
` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve and execute again.`
|
|
232
|
+
);
|
|
233
|
+
return { ok: false, reason: "Plan file modified since consensus review" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
224
237
|
let rawPlan: any;
|
|
225
238
|
try {
|
|
226
239
|
rawPlan = parseYaml(planContent);
|
package/src/index.ts
CHANGED
|
@@ -16,10 +16,12 @@ Turn a bounded task into a working, verified outcome with the smallest correct c
|
|
|
16
16
|
|
|
17
17
|
<execution_loop>
|
|
18
18
|
1. Inspect the relevant files and conventions.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
21
|
-
4.
|
|
22
|
-
5.
|
|
19
|
+
2. Decompose a large task into ordered subgoals; finish and verify one before starting the next.
|
|
20
|
+
3. Make the minimum change that satisfies the current subgoal.
|
|
21
|
+
4. Run focused verification with the available tools.
|
|
22
|
+
5. When a step fails, extract the lesson, feed it into the next attempt, and split a stuck subgoal into a smaller one rather than retrying unchanged.
|
|
23
|
+
6. Remove debug leftovers.
|
|
24
|
+
7. Call `done` only after verification evidence is available.
|
|
23
25
|
</execution_loop>
|
|
24
26
|
|
|
25
27
|
<tool_protocol>
|
|
@@ -9,3 +9,8 @@ when: When requirements are clear (e.g. from deep-interview) and you need a robu
|
|
|
9
9
|
Executes a multi-agent critique and planning process to generate a structured implementation plan.
|
|
10
10
|
Combines views from a Planner, an Architect, and a Critic to identify risks, define tasks, and specify files.
|
|
11
11
|
Saves the blueprint for execution.
|
|
12
|
+
|
|
13
|
+
When the roles disagree, do not collapse the split to a single verdict: record each
|
|
14
|
+
contested decision with the competing options and their rationale, and flag the points
|
|
15
|
+
that need a human judgment call rather than auto-resolving them.
|
|
16
|
+
|
|
@@ -9,3 +9,8 @@ when: When you have a blueprint/plan and need to execute the concrete implementa
|
|
|
9
9
|
Coordinates execution of individual tasks defined in the blueprint.
|
|
10
10
|
Spawns per-task executor subagents or loops to implement code changes.
|
|
11
11
|
Ensures task-level isolation and tracks implementation status.
|
|
12
|
+
|
|
13
|
+
Each task is a bounded subgoal: decompose large work into ordered subgoals, verify
|
|
14
|
+
one before starting the next, and feed the facts a failed task exposes into the
|
|
15
|
+
inputs of the following task instead of retrying the same approach unchanged.
|
|
16
|
+
|
|
@@ -29,6 +29,7 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
29
29
|
{ command: "/rename", usage: "/rename <title>", description: "Rename the current session", group: "session" },
|
|
30
30
|
{ command: "/resume", usage: "/resume [id]", description: "Resume a saved session in this REPL", group: "session" },
|
|
31
31
|
{ command: "/retry", usage: "/retry", description: "Retry the last request", group: "session" },
|
|
32
|
+
{ command: "/undo", usage: "/undo", description: "Undo the last jeo auto-commit and restore the working tree", group: "session" },
|
|
32
33
|
{ command: "/history", usage: "/history [n|all]", description: "Re-print the worked turn history (prompts, tool steps, replies) into scrollback", group: "session" },
|
|
33
34
|
{ command: "/export", usage: "/export [path] [json]", description: "Export the session transcript to a file", group: "session" },
|
|
34
35
|
{ command: "/dump", usage: "/dump", description: "Copy the session transcript to the clipboard", group: "session" },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// Restoring the terminal on an ABNORMAL exit (uncaught exception, unhandled
|
|
4
|
+
// rejection, fatal error) is what keeps the user's shell usable afterwards. The
|
|
5
|
+
// failure mode this guards against: a background `fetch()` stream rejects after a
|
|
6
|
+
// turn ends, Bun reports it as an unhandled rejection and tears the process down
|
|
7
|
+
// WITHOUT running the readline cleanup — leaving stdin in raw mode and bracketed
|
|
8
|
+
// paste enabled, so the shell echoes nothing and never submits a line (the
|
|
9
|
+
// "prints an error and then input is dead" bug).
|
|
10
|
+
//
|
|
11
|
+
// `process.stdout.write` is async/buffered and is NOT guaranteed to flush from an
|
|
12
|
+
// `exit` handler or a crashing tick, so the escape sequences are emitted with the
|
|
13
|
+
// SYNCHRONOUS `writeSync(1, …)` — the only reliable way to hand the terminal back
|
|
14
|
+
// in a sane line-discipline before the process disappears. `setRawMode(false)` is
|
|
15
|
+
// a synchronous ioctl and is safe to call in the same context.
|
|
16
|
+
|
|
17
|
+
type RawCapableStdin = NodeJS.ReadStream & { isRaw?: boolean; setRawMode?(raw: boolean): void };
|
|
18
|
+
|
|
19
|
+
let restored = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Synchronously return the terminal to cooked input, a visible cursor, and
|
|
23
|
+
* bracketed-paste OFF. Idempotent and safe to call from crash/exit paths.
|
|
24
|
+
*/
|
|
25
|
+
export function restoreTerminalState(): void {
|
|
26
|
+
if (restored) return;
|
|
27
|
+
restored = true;
|
|
28
|
+
const stdin = process.stdin as RawCapableStdin;
|
|
29
|
+
try {
|
|
30
|
+
if (stdin.isTTY && stdin.isRaw && typeof stdin.setRawMode === "function") stdin.setRawMode(false);
|
|
31
|
+
} catch { /* terminal already gone */ }
|
|
32
|
+
try {
|
|
33
|
+
// \x1b[?2004l = disable bracketed paste, \x1b[?25h = show cursor.
|
|
34
|
+
if (process.stdout.isTTY) writeSync(1, "\x1b[?2004l\x1b[?25h");
|
|
35
|
+
} catch { /* terminal already gone */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Test-only: reset the idempotency latch so a fresh restore can be observed. */
|
|
39
|
+
export function resetTerminalRestoreLatch(): void {
|
|
40
|
+
restored = false;
|
|
41
|
+
}
|