jerob 1.0.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/CLI/cli.ts +42 -0
- package/README.md +137 -0
- package/SETUP.md +584 -0
- package/agent/action-tracker.ts +45 -0
- package/agent/agent-tools.ts +111 -0
- package/agent/approval.ts +137 -0
- package/agent/diff-view.ts +26 -0
- package/agent/orchestrator.ts +186 -0
- package/agent/tool-executor.ts +463 -0
- package/agent/types.ts +69 -0
- package/ask/orchestrator.ts +244 -0
- package/auth/auth.ts +567 -0
- package/auth/config-store.ts +77 -0
- package/auth/crypto.ts +51 -0
- package/auth/env-writer.ts +82 -0
- package/bin/jerob.js +28 -0
- package/config/ai.config.ts +163 -0
- package/email_ops/email-tools.ts +178 -0
- package/email_ops/email_functions.ts +443 -0
- package/email_ops/email_init.ts +92 -0
- package/email_ops/email_pass_store.ts +61 -0
- package/email_ops/email_server.ts +29 -0
- package/email_ops/types.ts +88 -0
- package/index.ts +176 -0
- package/package.json +88 -0
- package/plan/browser-agent/README.md +118 -0
- package/plan/browser-agent/USAGE.md +308 -0
- package/plan/browser-agent/evaluator.ts +353 -0
- package/plan/browser-agent/executor.ts +372 -0
- package/plan/browser-agent/index.ts +13 -0
- package/plan/browser-agent/orchestrator.ts +323 -0
- package/plan/browser-agent/planner.ts +200 -0
- package/plan/browser-agent/types.ts +62 -0
- package/plan/browser-tool.ts +128 -0
- package/plan/index.ts +12 -0
- package/plan/orchestrator.ts +214 -0
- package/plan/planner.ts +183 -0
- package/plan/selection.ts +50 -0
- package/plan/types.ts +13 -0
- package/plan/web-tools.ts +119 -0
- package/scheduler/ARCHITECTURE.md +263 -0
- package/scheduler/README.md +200 -0
- package/scheduler/SETUP-READY.sql +84 -0
- package/scheduler/check-status.sql +124 -0
- package/scheduler/config-sync.ts +91 -0
- package/scheduler/db-migrate.ts +271 -0
- package/scheduler/db.ts +162 -0
- package/scheduler/debug.ts +184 -0
- package/scheduler/orchestrator.ts +438 -0
- package/scheduler/planner.ts +170 -0
- package/scheduler/update-task-email.ts +70 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/deploy.ps1 +50 -0
- package/supabase/functions/scheduler-tick/index.ts +496 -0
- package/supabase/supabase/.temp/linked-project.json +1 -0
- package/tsconfig.json +33 -0
- package/tui/spinner.ts +33 -0
- package/tui/spinup.ts +67 -0
- package/tui/terminal-render.ts +16 -0
- package/utils/llm-error.ts +185 -0
- package/utils/model-validator.ts +247 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolExecutor } from "./tool-executor";
|
|
4
|
+
|
|
5
|
+
export function createAgentTools(executor: ToolExecutor) {
|
|
6
|
+
return {
|
|
7
|
+
read_file: tool({
|
|
8
|
+
description:
|
|
9
|
+
"Read a text file from the workspace. Use a path relative to the project root.",
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
path: z.string().describe("Relative file path"),
|
|
12
|
+
}),
|
|
13
|
+
execute: async ({ path: p }) => executor.readFile(p),
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
create_file: tool({
|
|
17
|
+
description:
|
|
18
|
+
"Stage creation of a new file (not written until the user approves).",
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
path: z.string(),
|
|
21
|
+
content: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ path: p, content }) => executor.createFile(p, content),
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
modify_file: tool({
|
|
27
|
+
description:
|
|
28
|
+
"Stage a full-file replacement for an existing file (pending approval).",
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
path: z.string(),
|
|
31
|
+
content: z.string().describe("Complete new file contents"),
|
|
32
|
+
}),
|
|
33
|
+
execute: async ({ path: p, content }) => executor.modifyFile(p, content),
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
delete_file: tool({
|
|
37
|
+
description: "Stage deletion of a file (pending approval).",
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
path: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
execute: async ({ path: p }) => executor.deleteFile(p),
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
create_folder: tool({
|
|
45
|
+
description:
|
|
46
|
+
"Stage creation of a directory tree (pending approval). Uses mkdir -p on apply.",
|
|
47
|
+
inputSchema: z.object({
|
|
48
|
+
path: z.string().describe("Relative directory path"),
|
|
49
|
+
}),
|
|
50
|
+
execute: async ({ path: p }) => executor.createFolder(p),
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
list_files: tool({
|
|
54
|
+
description: "List files and directories under a path.",
|
|
55
|
+
inputSchema: z.object({
|
|
56
|
+
path: z.string(),
|
|
57
|
+
recursive: z.boolean().optional().default(false),
|
|
58
|
+
}),
|
|
59
|
+
execute: async ({ path: p, recursive }) =>
|
|
60
|
+
executor.listFiles(p, recursive),
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
search_files: tool({
|
|
64
|
+
description:
|
|
65
|
+
'Find files matching a glob pattern (e.g. "*.ts", "**/*.md"). Optional content substring filter.',
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
root: z.string().describe("Directory to search, relative to root"),
|
|
68
|
+
pattern: z
|
|
69
|
+
.string()
|
|
70
|
+
.describe("Glob-like pattern using * and ** (forward slashes)"),
|
|
71
|
+
content_contains: z.string().optional(),
|
|
72
|
+
}),
|
|
73
|
+
execute: async ({ root, pattern, content_contains }) =>
|
|
74
|
+
executor.searchFiles(root, pattern, content_contains),
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
analyze_codebase: tool({
|
|
78
|
+
description:
|
|
79
|
+
"Summarize structure: file counts, size, extensions. Read-only.",
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
path: z.string().default("."),
|
|
82
|
+
}),
|
|
83
|
+
execute: async ({ path: p }) => executor.analyzeCodebase(p),
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
execute_shell: tool({
|
|
87
|
+
description:
|
|
88
|
+
"Queue a shell command to run in the workspace after user approval. Use with care.",
|
|
89
|
+
inputSchema: z.object({
|
|
90
|
+
command: z.string().describe("Single command; runs with shell: true"),
|
|
91
|
+
}),
|
|
92
|
+
execute: async ({ command }) => executor.queueShell(command),
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
list_skills: tool({
|
|
96
|
+
description:
|
|
97
|
+
"List absolute paths to SKILL.md files under configured skill directories (Cursor / Claude).",
|
|
98
|
+
inputSchema: z.object({}),
|
|
99
|
+
execute: async () => executor.listSkills(),
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
read_skill: tool({
|
|
103
|
+
description:
|
|
104
|
+
"Read a SKILL.md file. Path must be absolute and under skill roots, or use a path returned by list_skills.",
|
|
105
|
+
inputSchema: z.object({
|
|
106
|
+
path: z.string(),
|
|
107
|
+
}),
|
|
108
|
+
execute: async ({ path: p }) => executor.readSkill(p),
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { select, isCancel } from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import type { ActionTracker } from "./action-tracker.ts";
|
|
4
|
+
import type { ActionLog } from "./types.ts";
|
|
5
|
+
import { composeBeforeAfter, formatPatch } from "./diff-view.ts";
|
|
6
|
+
import { renderHTMLMarkdown } from "../tui/terminal-render.ts";
|
|
7
|
+
// import { renderTerminalMarkdown } from "../../tui/terminal-md.ts";
|
|
8
|
+
|
|
9
|
+
interface ReviewGroup {
|
|
10
|
+
label: string;
|
|
11
|
+
actionIds: string[];
|
|
12
|
+
patch: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function groupPending(pending: ActionLog[]): ReviewGroup[] {
|
|
16
|
+
const byPath = new Map<string, ActionLog[]>();
|
|
17
|
+
const shells: ActionLog[] = [];
|
|
18
|
+
|
|
19
|
+
for (const a of pending) {
|
|
20
|
+
if (a.type === "tool_execute") {
|
|
21
|
+
shells.push(a);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const key = a.path;
|
|
25
|
+
if (!byPath.has(key)) byPath.set(key, []);
|
|
26
|
+
byPath.get(key)!.push(a);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const groups: ReviewGroup[] = [];
|
|
30
|
+
|
|
31
|
+
const pathEntries = [...byPath.entries()].sort(([a], [b]) =>
|
|
32
|
+
a.localeCompare(b),
|
|
33
|
+
);
|
|
34
|
+
for (const [p, acts] of pathEntries) {
|
|
35
|
+
const sorted = acts.sort(
|
|
36
|
+
(x, y) => x.timestamp.getTime() - y.timestamp.getTime(),
|
|
37
|
+
);
|
|
38
|
+
const ids = sorted.map((x) => x.id);
|
|
39
|
+
|
|
40
|
+
if (sorted.every((x) => x.type === "folder_create")) {
|
|
41
|
+
groups.push({
|
|
42
|
+
label: `Create folder: ${p}`,
|
|
43
|
+
actionIds: ids,
|
|
44
|
+
patch: null,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { before, after } = composeBeforeAfter(sorted);
|
|
50
|
+
const patch = formatPatch(p, before, after);
|
|
51
|
+
const kinds = [...new Set(sorted.map((x) => x.type))].join(", ");
|
|
52
|
+
groups.push({ label: `${p} (${kinds})`, actionIds: ids, patch });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const s of shells) {
|
|
56
|
+
groups.push({
|
|
57
|
+
label: `Shell: ${s.details.command ?? "(no command)"}`,
|
|
58
|
+
actionIds: [s.id],
|
|
59
|
+
patch: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return groups;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runApprovalFlow(
|
|
67
|
+
tracker: ActionTracker,
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
const pending = tracker.getPendingMutations();
|
|
70
|
+
|
|
71
|
+
if (pending.length === 0) {
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.dim("\nNo staged file, folder, or shell changes to review.\n"),
|
|
74
|
+
);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const choice = await select({
|
|
79
|
+
message: "Apply staged changes?",
|
|
80
|
+
options: [
|
|
81
|
+
{ value: "all", label: "Approve and apply all" },
|
|
82
|
+
{ value: "select", label: "Review one by one" },
|
|
83
|
+
{ value: "cancel", label: "Cancel" },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (isCancel(choice) || choice === "cancel") {
|
|
88
|
+
for (const a of pending) tracker.updateStatus(a.id, "rejected", false);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (choice === "all") {
|
|
93
|
+
for (const a of pending) tracker.updateStatus(a.id, "approved", true);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const g of groupPending(pending)) {
|
|
98
|
+
while (true) {
|
|
99
|
+
const opt = await select({
|
|
100
|
+
message: chalk.bold(g.label),
|
|
101
|
+
options: [
|
|
102
|
+
{ value: "accept", label: "Accept" },
|
|
103
|
+
{ value: "diff", label: "Show diff", hint: g.patch ? "" : "N/A" },
|
|
104
|
+
{ value: "reject", label: "Reject" },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (isCancel(opt)) {
|
|
109
|
+
for (const a of pending) tracker.updateStatus(a.id, "rejected", false);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (opt === "diff") {
|
|
114
|
+
if (g.patch) {
|
|
115
|
+
console.log(
|
|
116
|
+
"\n" +
|
|
117
|
+
renderHTMLMarkdown("```diff\n" + g.patch + "\n```\n") +
|
|
118
|
+
"\n",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const id of g.actionIds) {
|
|
126
|
+
tracker.updateStatus(
|
|
127
|
+
id,
|
|
128
|
+
opt === "accept" ? "approved" : "rejected",
|
|
129
|
+
opt === "accept",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return tracker.getActions().some((a) => a.status === "approved");
|
|
137
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createTwoFilesPatch } from "diff";
|
|
2
|
+
import type { ActionLog } from "./types";
|
|
3
|
+
|
|
4
|
+
export function formatPatch(
|
|
5
|
+
filePath: string,
|
|
6
|
+
before: string,
|
|
7
|
+
after: string,
|
|
8
|
+
): string {
|
|
9
|
+
return createTwoFilesPatch(filePath, filePath, before, after, "", "", {
|
|
10
|
+
context: 3,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function composeBeforeAfter(sorted: ActionLog[]): {
|
|
15
|
+
before: string;
|
|
16
|
+
after: string;
|
|
17
|
+
} {
|
|
18
|
+
const first = sorted[0]!;
|
|
19
|
+
const last = sorted[sorted.length - 1]!;
|
|
20
|
+
if (last.type === "file_delete")
|
|
21
|
+
return { before: last.details.before ?? "", after: "" };
|
|
22
|
+
const before =
|
|
23
|
+
first.type === "file_create" ? "" : (first.details.before ?? "");
|
|
24
|
+
const after = last.details.after ?? "";
|
|
25
|
+
return { before, after };
|
|
26
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { isCancel, text, confirm, select } from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { defaultAgentConfig } from "./types";
|
|
4
|
+
import { ActionTracker } from "./action-tracker";
|
|
5
|
+
import { ToolExecutor } from "./tool-executor";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { stepCountIs, ToolLoopAgent } from "ai";
|
|
8
|
+
import { getAgentModel } from "../config/ai.config";
|
|
9
|
+
import { createAgentTools } from "./agent-tools";
|
|
10
|
+
import { renderHTMLMarkdown } from "../tui/terminal-render";
|
|
11
|
+
import { withSpinner } from "../tui/spinner";
|
|
12
|
+
import { runApprovalFlow } from "./approval";
|
|
13
|
+
import { withLLMRetry, printLLMError } from "../utils/llm-error";
|
|
14
|
+
|
|
15
|
+
export async function runAgentMode() {
|
|
16
|
+
console.log(chalk.bold("\n🤖 Agent Mode\n"));
|
|
17
|
+
|
|
18
|
+
const config = defaultAgentConfig();
|
|
19
|
+
|
|
20
|
+
while (true) {
|
|
21
|
+
const tracker = new ActionTracker();
|
|
22
|
+
const executor = new ToolExecutor(tracker, config);
|
|
23
|
+
const tools = createAgentTools(executor);
|
|
24
|
+
const agent = new ToolLoopAgent({
|
|
25
|
+
model: getAgentModel(),
|
|
26
|
+
instructions: `
|
|
27
|
+
WorkDir: ${config.codebasePath}
|
|
28
|
+
You are a helpful AI coding agent. You MUST use the provided file tools to stage all workspace changes — never describe or print code as plain text.
|
|
29
|
+
Rules:
|
|
30
|
+
- Use create_file to create new files with their full content. NEVER print file contents in plain text.
|
|
31
|
+
- Use modify_file to update existing files with full new content. NEVER print file contents in plain text.
|
|
32
|
+
- Use create_folder to create directory structures.
|
|
33
|
+
- Use delete_file to delete files.
|
|
34
|
+
- Use read_file and list_files to inspect the workspace before writing.
|
|
35
|
+
- For project scaffolding (React, Vite, Next.js, etc.) use execute_shell to run scaffold commands (e.g. "npx create-react-app my-app", "bun create vite my-app", "npx create-next-app@latest").
|
|
36
|
+
- When using execute_shell for scaffolding, also stage key resulting files using create_file so the user can review them.
|
|
37
|
+
- New standalone projects go in a new subfolder. Enhancements to existing code stay in the current workspace root.
|
|
38
|
+
- Do not create a new top-level folder unless the user explicitly requests a new project.
|
|
39
|
+
- Always show file paths as tool calls, never only in plain text.
|
|
40
|
+
- Stage all changes pending user approval. Do not finalize anything before approval.
|
|
41
|
+
`.trim(),
|
|
42
|
+
tools,
|
|
43
|
+
stopWhen: stepCountIs(30),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const goal = await text({
|
|
47
|
+
message: "What would you like the agent to do?",
|
|
48
|
+
placeholder: "Concrete task for this codebase…",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (isCancel(goal) || !goal.trim()) break;
|
|
52
|
+
|
|
53
|
+
let result;
|
|
54
|
+
try {
|
|
55
|
+
result = await withSpinner("Running agent…", async () =>
|
|
56
|
+
withLLMRetry(
|
|
57
|
+
() =>
|
|
58
|
+
agent.generate({
|
|
59
|
+
prompt: goal.trim(),
|
|
60
|
+
onStepFinish: ({ toolCalls }) => {
|
|
61
|
+
for (const tc of toolCalls) {
|
|
62
|
+
const preview = JSON.stringify(tc.input).slice(0, 160);
|
|
63
|
+
console.log(
|
|
64
|
+
chalk.green(" ✓"),
|
|
65
|
+
chalk.bold(String(tc.toolName)),
|
|
66
|
+
chalk.dim(preview + (preview.length >= 160 ? "..." : "")),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
{ maxRetries: 3, context: "Agent" }
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
printLLMError(error, "Agent");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pending = tracker.getPendingMutations();
|
|
80
|
+
const pendingCount = pending.length;
|
|
81
|
+
if (pendingCount === 0) {
|
|
82
|
+
console.log(chalk.yellow("\nNo staged file or folder changes detected. The agent may have described a change without staging it."));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.dim(`\nStaged changes: ${pendingCount}`));
|
|
85
|
+
for (const p of pending) {
|
|
86
|
+
const kind = p.type;
|
|
87
|
+
const pathLabel = p.path || "(no path)";
|
|
88
|
+
if (kind === "tool_execute") {
|
|
89
|
+
console.log(chalk.cyan(` • Shell queued: ${p.details.command ?? "(unknown)"}`));
|
|
90
|
+
} else if (kind === "folder_create") {
|
|
91
|
+
console.log(chalk.cyan(` • Folder: ${pathLabel}`));
|
|
92
|
+
} else if (kind === "file_create") {
|
|
93
|
+
console.log(chalk.cyan(` • New file staged: ${pathLabel}`));
|
|
94
|
+
} else if (kind === "file_modify") {
|
|
95
|
+
console.log(chalk.cyan(` • Modify staged: ${pathLabel}`));
|
|
96
|
+
} else if (kind === "file_delete") {
|
|
97
|
+
console.log(chalk.cyan(` • Delete staged: ${pathLabel}`));
|
|
98
|
+
} else {
|
|
99
|
+
console.log(chalk.cyan(` • ${kind}: ${pathLabel}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result.text?.trim()) console.log("\n" + renderHTMLMarkdown(result.text) + "\n");
|
|
105
|
+
|
|
106
|
+
const approved = await runApprovalFlow(tracker);
|
|
107
|
+
if (!approved) {
|
|
108
|
+
executor.clearStaging();
|
|
109
|
+
console.log(chalk.yellow("No changes were applied.\n"));
|
|
110
|
+
} else {
|
|
111
|
+
const { errors, newFiles } = executor.applyApprovedFromTracker();
|
|
112
|
+
executor.clearStaging();
|
|
113
|
+
if (errors.length) {
|
|
114
|
+
console.log(chalk.red("\nSome operations reported errors:\n"));
|
|
115
|
+
for (const e of errors) console.log(chalk.red(` • ${e}`));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(chalk.green("\n✓ Applied.\n"));
|
|
118
|
+
}
|
|
119
|
+
// If scaffolding created new files, offer to run a follow-up coding pass
|
|
120
|
+
if (newFiles && newFiles.length) {
|
|
121
|
+
// identify top-level new folders
|
|
122
|
+
const roots = new Set<string>();
|
|
123
|
+
for (const f of newFiles) {
|
|
124
|
+
const seg = f.split(/\//)[0];
|
|
125
|
+
roots.add(seg || f);
|
|
126
|
+
}
|
|
127
|
+
if (roots.size === 1) {
|
|
128
|
+
const folder = Array.from(roots)[0];
|
|
129
|
+
const cont = await confirm({
|
|
130
|
+
message: `Scaffold created folder '${folder}'. Run follow-up to implement: ${goal.trim()} ?`,
|
|
131
|
+
initialValue: true,
|
|
132
|
+
});
|
|
133
|
+
if (!isCancel(cont) && cont) {
|
|
134
|
+
// run follow-up coding inside the new folder
|
|
135
|
+
const followGoal = `Implement the user's requested task: ${goal.trim()} inside this project. Write runnable code, use the project's conventions, and stage files with create_file or modify_file for approval.`;
|
|
136
|
+
const followTracker = new ActionTracker();
|
|
137
|
+
const followConfig = { ...config, codebasePath: path.join(config.codebasePath, folder as string) };
|
|
138
|
+
const followExecutor = new ToolExecutor(followTracker, followConfig);
|
|
139
|
+
const followTools = createAgentTools(followExecutor);
|
|
140
|
+
const followAgent = new ToolLoopAgent({ model: getAgentModel(), instructions: `WorkDir: ${followConfig.codebasePath}
|
|
141
|
+
Use file tools to stage changes. Implement the feature fully.`, tools: followTools, stopWhen: stepCountIs(60) });
|
|
142
|
+
|
|
143
|
+
let followResult;
|
|
144
|
+
try {
|
|
145
|
+
followResult = await withSpinner("Running follow-up agent…", async () =>
|
|
146
|
+
withLLMRetry(
|
|
147
|
+
() => followAgent.generate({ prompt: followGoal }),
|
|
148
|
+
{ maxRetries: 3, context: "Agent follow-up" }
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
printLLMError(err, "Agent follow-up");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (followResult?.text?.trim()) console.log("\n" + renderHTMLMarkdown(followResult.text) + "\n");
|
|
157
|
+
const ok2 = await runApprovalFlow(followTracker);
|
|
158
|
+
if (ok2) {
|
|
159
|
+
const { errors: e2 } = followExecutor.applyApprovedFromTracker();
|
|
160
|
+
followExecutor.clearStaging();
|
|
161
|
+
if (e2.length) {
|
|
162
|
+
console.log(chalk.red("\nFollow-up reported errors:\n"));
|
|
163
|
+
for (const ee of e2) console.log(chalk.red(` • ${ee}`));
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.green("\n✓ Follow-up applied.\n"));
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
followExecutor.clearStaging();
|
|
169
|
+
console.log(chalk.yellow("Follow-up canceled.\n"));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const next = await select({
|
|
177
|
+
message: "What next?",
|
|
178
|
+
options: [
|
|
179
|
+
{ value: "continue", label: "Ask next task" },
|
|
180
|
+
{ value: "exit", label: "Exit Agent Mode" },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (isCancel(next) || next === "exit") break;
|
|
185
|
+
}
|
|
186
|
+
}
|