jeo-code 0.1.0 → 0.4.4
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.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +804 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +562 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
package/src/commands/team.ts
CHANGED
|
@@ -1,37 +1,73 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
|
|
3
4
|
import {
|
|
4
5
|
readWorkflowState,
|
|
6
|
+
readWorkflowStateStrict,
|
|
5
7
|
writeWorkflowState,
|
|
8
|
+
acquireWorkflowRunLock,
|
|
9
|
+
type WorkflowState,
|
|
6
10
|
} from "../agent/state";
|
|
7
11
|
import { runAgentLoop } from "../agent/engine";
|
|
12
|
+
import { maybeCompact } from "../agent/compaction";
|
|
13
|
+
import { catalogMetadata } from "../ai";
|
|
8
14
|
import { readGlobalConfig } from "../agent/state";
|
|
9
15
|
import {
|
|
10
16
|
defaultSubagentRole,
|
|
17
|
+
getSubagentRole,
|
|
11
18
|
resolveSubagentModel,
|
|
12
19
|
resolveSubagentMaxSteps,
|
|
13
20
|
subagentSystemPrompt,
|
|
14
21
|
subagentToolset,
|
|
22
|
+
subagentRoleIds,
|
|
23
|
+
validateSubagentDoneReason,
|
|
15
24
|
} from "../agent/subagents";
|
|
16
25
|
import type { Message } from "../agent/loop";
|
|
26
|
+
import { loadProjectContext, withProjectContext } from "../agent/context-files";
|
|
27
|
+
import { categoryBadge } from "../tui/components/category-index";
|
|
17
28
|
|
|
18
29
|
export type RalphStreamKind = "step" | "complete" | "error";
|
|
19
30
|
|
|
20
|
-
export
|
|
31
|
+
export interface RalphRenderOptions {
|
|
32
|
+
color?: boolean;
|
|
33
|
+
indexed?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatRalphTodoGuide(
|
|
37
|
+
tasks: string[],
|
|
38
|
+
activeIndex = 0,
|
|
39
|
+
completed: readonly string[] = [],
|
|
40
|
+
opts: RalphRenderOptions = {},
|
|
41
|
+
): string[] {
|
|
21
42
|
const done = new Set(completed);
|
|
43
|
+
const color = opts.color === true;
|
|
44
|
+
const badge = opts.indexed || color ? `${categoryBadge("subagent", { color })} ` : "";
|
|
45
|
+
const green = color ? chalk.green.bold : (s: string) => s;
|
|
46
|
+
const yellow = color ? chalk.yellow.bold : (s: string) => s;
|
|
47
|
+
const gray = color ? chalk.gray : (s: string) => s;
|
|
22
48
|
const lines = [
|
|
23
|
-
"[RALPH] Subagent guidance: follow todos in order; stream every step, complete, and error event
|
|
49
|
+
`${badge || "[RALPH] " }Subagent guidance: follow todos in order; stream every step, complete, and error event.`,
|
|
24
50
|
];
|
|
25
51
|
tasks.forEach((task, index) => {
|
|
26
|
-
const mark = done.has(task) ? "x" : index === activeIndex ? ">" : " ";
|
|
52
|
+
const mark = done.has(task) ? green("x") : index === activeIndex ? yellow(">") : gray(" ");
|
|
27
53
|
lines.push(`[TODO] ${index + 1}/${tasks.length} [${mark}] ${task}`);
|
|
28
54
|
});
|
|
29
55
|
return lines;
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
export function formatRalphStreamEvent(kind: RalphStreamKind, message: string): string {
|
|
58
|
+
export function formatRalphStreamEvent(kind: RalphStreamKind, message: string, opts: RalphRenderOptions = {}): string {
|
|
33
59
|
const label = kind === "complete" ? "complete" : kind === "error" ? "error" : "step";
|
|
34
|
-
return ` └─ stream:${label} ${message}`;
|
|
60
|
+
if (!opts.color && !opts.indexed) return ` └─ stream:${label} ${message}`;
|
|
61
|
+
const color = opts.color === true;
|
|
62
|
+
const badge = `${categoryBadge("subagent", { color })} `;
|
|
63
|
+
const tint = color
|
|
64
|
+
? kind === "complete"
|
|
65
|
+
? chalk.green.bold
|
|
66
|
+
: kind === "error"
|
|
67
|
+
? chalk.red.bold
|
|
68
|
+
: chalk.cyan.bold
|
|
69
|
+
: (s: string) => s;
|
|
70
|
+
return ` ${badge}${tint(`stream:${label}`)} ${message}`;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
export interface RalphSubagentPromptContext {
|
|
@@ -58,280 +94,372 @@ export function buildRalphSubagentPrompt(ctx: RalphSubagentPromptContext): strin
|
|
|
58
94
|
].join("\n");
|
|
59
95
|
}
|
|
60
96
|
|
|
97
|
+
export function activeStepIndex(totalTasks: number, pendingTasks: readonly string[] | undefined): number {
|
|
98
|
+
if (totalTasks <= 0) return 0;
|
|
99
|
+
const pending = pendingTasks?.length ?? totalTasks;
|
|
100
|
+
return Math.max(0, Math.min(totalTasks - pending, totalTasks - 1));
|
|
101
|
+
}
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
const ARCHITECT_STATUS_VALUES = new Set(["CLEAR", "WATCH", "BLOCK"]);
|
|
104
|
+
const ARCHITECT_REVIEW_VALUES = new Set(["APPROVE", "COMMENT", "REQUEST CHANGES"]);
|
|
105
|
+
|
|
106
|
+
function extractLineValue(reason: string, label: string): string | undefined {
|
|
107
|
+
// Strip leading/trailing markdown emphasis/quoting/heading chars so the gate
|
|
108
|
+
// accepts e.g. `**Architectural Status:** CLEAR` or `> Architectural Status: CLEAR`.
|
|
109
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
|
+
for (const rawLine of reason.split(/\r?\n/)) {
|
|
111
|
+
const stripped = rawLine.replace(/[*_`>#]+/g, "").trim();
|
|
112
|
+
const m = stripped.match(new RegExp(`^${escaped}\\s*:\\s*(.+)$`, "i"));
|
|
113
|
+
if (m) return m[1]!.trim();
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeArchitectVerdict(raw: string): string {
|
|
119
|
+
// Drop trailing `(caveats)` or `- comments`; collapse whitespace; uppercase.
|
|
120
|
+
return raw.split(/\s*[(\-—–]/, 1)[0]!.replace(/\s+/g, " ").trim().toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseRoleGateVerdict(roleId: string, reason: string): { ok: boolean; message?: string } {
|
|
124
|
+
const trimmed = reason.trim();
|
|
125
|
+
if (roleId === "architect") {
|
|
126
|
+
const statusRaw = extractLineValue(trimmed, "Architectural Status");
|
|
127
|
+
const reviewRaw = extractLineValue(trimmed, "Code Review Recommendation");
|
|
128
|
+
if (!statusRaw || !reviewRaw) {
|
|
129
|
+
return { ok: false, message: "architect report missing Architectural Status or Code Review Recommendation" };
|
|
130
|
+
}
|
|
131
|
+
const status = normalizeArchitectVerdict(statusRaw);
|
|
132
|
+
const review = normalizeArchitectVerdict(reviewRaw);
|
|
133
|
+
if (!ARCHITECT_STATUS_VALUES.has(status)) {
|
|
134
|
+
return { ok: false, message: `architect Architectural Status invalid (expected CLEAR|WATCH|BLOCK, got ${JSON.stringify(statusRaw)})` };
|
|
135
|
+
}
|
|
136
|
+
if (!ARCHITECT_REVIEW_VALUES.has(review)) {
|
|
137
|
+
return { ok: false, message: `architect Code Review Recommendation invalid (expected APPROVE|COMMENT|REQUEST CHANGES, got ${JSON.stringify(reviewRaw)})` };
|
|
138
|
+
}
|
|
139
|
+
if (status === "BLOCK" || review === "REQUEST CHANGES") {
|
|
140
|
+
return { ok: false, message: `architect gated execution (${status} / ${review})` };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
if (roleId === "critic") {
|
|
145
|
+
// Fail-closed: only an explicit [OKAY] first line approves. Anything else
|
|
146
|
+
// (malformed, missing verdict, wrong case) gates so a buggy/spoofed reason
|
|
147
|
+
// cannot silently pass review.
|
|
148
|
+
const firstLine = trimmed.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
149
|
+
if (firstLine === "[OKAY]") return { ok: true };
|
|
150
|
+
if (firstLine === "[REJECT]" || firstLine === "[ITERATE]") {
|
|
151
|
+
return { ok: false, message: `critic gated execution (${firstLine})` };
|
|
152
|
+
}
|
|
153
|
+
return { ok: false, message: `critic verdict missing or malformed (expected [OKAY]|[ITERATE]|[REJECT], got ${JSON.stringify(firstLine.slice(0, 40))})` };
|
|
154
|
+
}
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
export interface TeamEngineOptions {
|
|
160
|
+
cwd?: string;
|
|
161
|
+
signal?: AbortSignal;
|
|
162
|
+
onProgress?: (e: { skill: string; phase: string; detail?: string }) => void;
|
|
163
|
+
io?: {
|
|
164
|
+
output?: (line: string) => void;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok: boolean; reason?: string }> {
|
|
169
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
170
|
+
|
|
171
|
+
const log = (msg?: any) => {
|
|
172
|
+
const str = msg !== undefined ? String(msg) : "";
|
|
173
|
+
if (opts.io?.output) {
|
|
174
|
+
const lines = str.split("\n");
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
opts.io.output(line);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.log(str);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (opts.onProgress) {
|
|
184
|
+
opts.onProgress({ skill: "team", phase: "start" });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (opts.signal?.aborted) {
|
|
188
|
+
return { ok: false, reason: "aborted" };
|
|
189
|
+
}
|
|
64
190
|
|
|
65
191
|
// Read ralplan state
|
|
66
192
|
const planState = await readWorkflowState("ralplan", cwd);
|
|
67
193
|
if (!planState || planState.current_phase !== "complete" || !planState.plan_path) {
|
|
68
|
-
|
|
69
|
-
`[ERROR] No completed plan found. Please run '
|
|
194
|
+
log(
|
|
195
|
+
`[ERROR] No completed plan found. Please run 'jeo ralplan' to generate a plan first.`
|
|
70
196
|
);
|
|
71
|
-
return;
|
|
197
|
+
return { ok: false, reason: "No completed plan found" };
|
|
72
198
|
}
|
|
73
199
|
|
|
74
200
|
if (!planState.approved) {
|
|
75
|
-
|
|
201
|
+
log(
|
|
76
202
|
`[ERROR] Plan is not approved. Please approve the plan before executing.`
|
|
77
203
|
);
|
|
78
|
-
return;
|
|
204
|
+
return { ok: false, reason: "Plan is not approved" };
|
|
79
205
|
}
|
|
80
206
|
|
|
81
207
|
const planPath = planState.plan_path;
|
|
82
|
-
|
|
83
|
-
|
|
208
|
+
log(`\n=== Starting Team Execution Stage ===`);
|
|
209
|
+
log(`Reading plan from: ${planPath}`);
|
|
84
210
|
|
|
85
211
|
let planContent = "";
|
|
86
212
|
try {
|
|
87
213
|
planContent = await fs.readFile(planPath, "utf-8");
|
|
88
214
|
} catch (err: any) {
|
|
89
|
-
|
|
90
|
-
return;
|
|
215
|
+
log(`[ERROR] Failed to read plan file: ${err.message}`);
|
|
216
|
+
return { ok: false, reason: err.message };
|
|
91
217
|
}
|
|
92
218
|
|
|
93
219
|
let rawPlan: any;
|
|
94
220
|
try {
|
|
95
221
|
rawPlan = parseYaml(planContent);
|
|
96
222
|
} catch (err: any) {
|
|
97
|
-
|
|
98
|
-
return;
|
|
223
|
+
log(`[ERROR] Failed to parse plan YAML: ${err.message}`);
|
|
224
|
+
return { ok: false, reason: err.message };
|
|
99
225
|
}
|
|
100
226
|
|
|
101
|
-
const parsed = PlanSchema.safeParse(rawPlan);
|
|
227
|
+
const parsed = PlanSchema.safeParse(normalizePlanShape(rawPlan));
|
|
102
228
|
if (!parsed.success) {
|
|
103
|
-
|
|
104
|
-
|
|
229
|
+
const shape = Array.isArray(rawPlan) ? "a top-level list" : typeof rawPlan;
|
|
230
|
+
log(
|
|
231
|
+
`[ERROR] The plan is not in the expected shape — it needs a top-level object with a 'steps:' list ` +
|
|
232
|
+
`(each step: { name, role?, ... }), but the plan file is ${shape}.\n` +
|
|
233
|
+
` The planning model likely produced malformed YAML. Review ${planPath} or re-run 'jeo ralplan' (with a more capable model).`
|
|
234
|
+
);
|
|
235
|
+
return { ok: false, reason: "Plan is not in the expected shape" };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const teamCfg = await readGlobalConfig();
|
|
239
|
+
const unknownRoles = parsed.data.steps
|
|
240
|
+
.map(step => step.role?.trim())
|
|
241
|
+
.filter((role): role is string => !!role && !getSubagentRole(role, teamCfg));
|
|
242
|
+
if (unknownRoles.length > 0) {
|
|
243
|
+
const unique = [...new Set(unknownRoles)];
|
|
244
|
+
log(
|
|
245
|
+
`[ERROR] Plan references unknown subagent role(s): ${unique.join(", ")}. ` +
|
|
246
|
+
`Known roles: ${subagentRoleIds(teamCfg).join(", ")}. Fix ${planPath} or re-run 'jeo ralplan'.`
|
|
247
|
+
);
|
|
248
|
+
return { ok: false, reason: "Plan references unknown subagent role(s)" };
|
|
105
249
|
}
|
|
106
250
|
|
|
107
251
|
const tasks = parsed.data.steps.map(step => step.name);
|
|
252
|
+
const roleByIndex = parsed.data.steps.map(step => getSubagentRole(step.role, teamCfg)?.id);
|
|
108
253
|
|
|
109
|
-
|
|
254
|
+
log(`Loaded ${tasks.length} tasks for execution.`);
|
|
110
255
|
|
|
256
|
+
// Round-8 (architect ref 7-Round7Workflow): cross-process run lock — two
|
|
257
|
+
// concurrent `jeo team` runs would each pop pending_tasks[0] and last-writer-
|
|
258
|
+
// wins the state file (tasks executed twice, completions lost).
|
|
259
|
+
let releaseLock: () => Promise<void>;
|
|
260
|
+
try {
|
|
261
|
+
releaseLock = await acquireWorkflowRunLock("team", cwd);
|
|
262
|
+
} catch (err: any) {
|
|
263
|
+
log(`[ERROR] ${err.message}`);
|
|
264
|
+
return { ok: false, reason: "another team run holds the lock" };
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
let teamState: WorkflowState;
|
|
268
|
+
try {
|
|
269
|
+
teamState = (await readWorkflowStateStrict("team", cwd)) ?? {
|
|
270
|
+
active: true,
|
|
271
|
+
current_phase: "executing",
|
|
272
|
+
skill: "team" as const,
|
|
273
|
+
slug: planState.slug,
|
|
274
|
+
plan_path: planPath,
|
|
275
|
+
completed_tasks: [],
|
|
276
|
+
pending_tasks: [...tasks],
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
log(
|
|
280
|
+
`[ERROR] .jeo/state/team-state.json is corrupt. Fix or delete it before re-running 'jeo team' ` +
|
|
281
|
+
`(refusing to silently restart and re-run already-completed tasks).`,
|
|
282
|
+
);
|
|
283
|
+
return { ok: false, reason: "team-state.json is corrupt" };
|
|
284
|
+
}
|
|
111
285
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
286
|
+
// Round-7 #1 (architect ref 7-Round7Workflow): a team-state left over from a
|
|
287
|
+
// PREVIOUS plan must never be reused — pending=[] from plan A would make plan B
|
|
288
|
+
// no-op into a false "all executed" success, and a mid-flight leftover would run
|
|
289
|
+
// plan-A task text under plan-B roles. A different plan reinitializes execution.
|
|
290
|
+
if (teamState.plan_path !== planPath || teamState.slug !== planState.slug) {
|
|
291
|
+
log(`${categoryBadge("progress")} New plan detected (${planPath}) — restarting execution from its task list.`);
|
|
292
|
+
teamState = {
|
|
293
|
+
active: true,
|
|
294
|
+
current_phase: "executing",
|
|
295
|
+
skill: "team" as const,
|
|
296
|
+
slug: planState.slug,
|
|
297
|
+
plan_path: planPath,
|
|
298
|
+
completed_tasks: [],
|
|
299
|
+
pending_tasks: [...tasks],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Round-8: a previous run halted on a task — its partial edits may still be
|
|
304
|
+
// on disk. Warn loudly before re-running on top of them, then clear the marker.
|
|
305
|
+
if (teamState.current_phase === "failed" && teamState.failed_task) {
|
|
306
|
+
log(
|
|
307
|
+
`[WARN] The previous run FAILED on "${teamState.failed_task}" and may have left partial edits on disk. ` +
|
|
308
|
+
`Review the working tree before trusting this re-run — executing the task again on top of partial work can duplicate changes.`,
|
|
309
|
+
);
|
|
310
|
+
teamState.current_phase = "executing";
|
|
311
|
+
delete teamState.failed_task;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
315
|
+
const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
|
|
316
|
+
for (const line of formatRalphTodoGuide(tasks, activeStepIndex(tasks.length, teamState.pending_tasks), teamState.completed_tasks ?? [], renderOpts)) log(line);
|
|
317
|
+
|
|
318
|
+
while (teamState.pending_tasks && teamState.pending_tasks.length > 0) {
|
|
319
|
+
if (opts.signal?.aborted) {
|
|
320
|
+
return { ok: false, reason: "aborted" };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const currentTask = teamState.pending_tasks[0];
|
|
324
|
+
log(`\n${categoryBadge("progress")} Current task: "${currentTask}"`);
|
|
325
|
+
const activeIndex = activeStepIndex(tasks.length, teamState.pending_tasks);
|
|
326
|
+
for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [], renderOpts)) log(line);
|
|
327
|
+
|
|
328
|
+
if (opts.onProgress) {
|
|
329
|
+
opts.onProgress({ skill: "team", phase: "executing", detail: `Current task: ${currentTask}` });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const success = await executeTaskWithAgent({
|
|
333
|
+
task: currentTask,
|
|
334
|
+
tasks,
|
|
335
|
+
activeIndex,
|
|
336
|
+
completed: teamState.completed_tasks ?? [],
|
|
337
|
+
cwd,
|
|
338
|
+
roleId: roleByIndex[activeIndex],
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (opts.signal?.aborted) {
|
|
342
|
+
return { ok: false, reason: "aborted" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (success) {
|
|
346
|
+
teamState.completed_tasks = [...(teamState.completed_tasks ?? []), currentTask];
|
|
347
|
+
teamState.pending_tasks = teamState.pending_tasks.slice(1);
|
|
348
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
349
|
+
log(`${categoryBadge("done")} Completed: "${currentTask}"`);
|
|
350
|
+
} else {
|
|
351
|
+
// Round-8: persist a failed marker so the NEXT run can warn about the
|
|
352
|
+
// partial edits this halted task may have left behind.
|
|
353
|
+
teamState.current_phase = "failed";
|
|
354
|
+
teamState.failed_task = currentTask;
|
|
355
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
356
|
+
log(`${categoryBadge("error")} Failed on task: "${currentTask}". Halting execution.`);
|
|
357
|
+
return { ok: false, reason: `Failed on task: "${currentTask}"` };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
122
360
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
while (teamState.pending_tasks && teamState.pending_tasks.length > 0) {
|
|
127
|
-
const currentTask = teamState.pending_tasks[0];
|
|
128
|
-
console.log(`\n[TASK] Current: "${currentTask}"`);
|
|
129
|
-
const activeIndex = tasks.indexOf(currentTask);
|
|
130
|
-
for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [])) console.log(line);
|
|
131
|
-
|
|
132
|
-
// Run the Executor loop
|
|
133
|
-
const success = await executeTaskWithAgent({
|
|
134
|
-
task: currentTask,
|
|
135
|
-
tasks,
|
|
136
|
-
activeIndex,
|
|
137
|
-
completed: teamState.completed_tasks ?? [],
|
|
138
|
-
cwd,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (success) {
|
|
142
|
-
teamState.completed_tasks = [...(teamState.completed_tasks ?? []), currentTask];
|
|
143
|
-
teamState.pending_tasks = teamState.pending_tasks.slice(1);
|
|
361
|
+
if (teamState.pending_tasks && teamState.pending_tasks.length === 0) {
|
|
362
|
+
teamState.current_phase = "complete";
|
|
363
|
+
teamState.active = false; // execution finished — the flag must not read as "in progress"
|
|
144
364
|
await writeWorkflowState("team", teamState, cwd);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
365
|
+
log(`\n${categoryBadge("done")} All tasks in the plan executed successfully!`);
|
|
366
|
+
log("Run 'jeo ultragoal' to run verify tests and evaluate metrics.");
|
|
367
|
+
if (opts.onProgress) {
|
|
368
|
+
opts.onProgress({ skill: "team", phase: "complete" });
|
|
369
|
+
}
|
|
149
370
|
}
|
|
371
|
+
return { ok: true };
|
|
372
|
+
} finally {
|
|
373
|
+
await releaseLock();
|
|
150
374
|
}
|
|
375
|
+
}
|
|
151
376
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log("Run 'joc ultragoal' to run verify tests and evaluate metrics.");
|
|
377
|
+
export async function runTeamCommand(): Promise<void> {
|
|
378
|
+
const res = await runTeamEngine();
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
process.exitCode = 1;
|
|
157
381
|
}
|
|
158
382
|
}
|
|
159
383
|
|
|
160
|
-
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string }): Promise<boolean> {
|
|
384
|
+
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string }): Promise<boolean> {
|
|
161
385
|
const config = await readGlobalConfig();
|
|
162
|
-
const role = defaultSubagentRole();
|
|
386
|
+
const role = getSubagentRole(ctx.roleId, config) ?? defaultSubagentRole();
|
|
387
|
+
const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
|
|
163
388
|
const model = resolveSubagentModel(role.id, config);
|
|
164
389
|
const maxSteps = resolveSubagentMaxSteps(role.id, config);
|
|
165
390
|
console.log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
|
|
166
391
|
|
|
392
|
+
const contextTokens = catalogMetadata(model)?.contextTokens;
|
|
393
|
+
|
|
394
|
+
const projectContext = await loadProjectContext(ctx.cwd);
|
|
167
395
|
const history: Message[] = [
|
|
168
|
-
{ role: "system", content: subagentSystemPrompt(role) },
|
|
396
|
+
{ role: "system", content: withProjectContext(subagentSystemPrompt(role), projectContext) },
|
|
169
397
|
{ role: "user", content: buildRalphSubagentPrompt(ctx) },
|
|
170
398
|
];
|
|
171
399
|
|
|
400
|
+
try {
|
|
401
|
+
await maybeCompact(history, { model, contextTokens });
|
|
402
|
+
} catch (err) {
|
|
403
|
+
// LLM summary failure does not halt team
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let mutationsOk = 0; // round-8 parent audit: successful write/edit/bash count
|
|
172
407
|
const result = await runAgentLoop(history, {
|
|
173
408
|
cwd: ctx.cwd,
|
|
174
409
|
model,
|
|
175
410
|
maxSteps,
|
|
411
|
+
// Bounded delegation: ralph/team subagents keep an exact step contract; the
|
|
412
|
+
// orchestrator owns retries, so the gjc step-extension flow is disabled here.
|
|
413
|
+
budget: { maxExtensions: 0 },
|
|
176
414
|
tools: subagentToolset(role),
|
|
177
415
|
events: {
|
|
178
416
|
onAssistant: (_raw, invocation) => {
|
|
179
417
|
if (!invocation) {
|
|
180
|
-
console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying"));
|
|
418
|
+
console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
|
|
181
419
|
} else if (invocation.tool !== "done") {
|
|
182
|
-
console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested
|
|
420
|
+
console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
onStep: async step => {
|
|
424
|
+
console.log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`, renderOpts));
|
|
425
|
+
try {
|
|
426
|
+
await maybeCompact(history, { model, contextTokens });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
// LLM summary failure does not halt team
|
|
183
429
|
}
|
|
184
430
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
431
|
+
onToolResult: (tool, ok) => {
|
|
432
|
+
if (ok && (tool === "write" || tool === "edit" || tool === "bash")) mutationsOk++;
|
|
433
|
+
console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
|
|
434
|
+
},
|
|
435
|
+
onNotice: msg => console.log(formatRalphStreamEvent("step", msg, renderOpts)),
|
|
188
436
|
},
|
|
189
437
|
});
|
|
190
438
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
console.log(formatRalphStreamEvent("error", `${role.title} did not converge within ${result.steps} steps`));
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
export const StepSchema = z.object({
|
|
199
|
-
name: z.string(),
|
|
200
|
-
}).passthrough();
|
|
201
|
-
|
|
202
|
-
export const PlanSchema = z.object({
|
|
203
|
-
name: z.string(),
|
|
204
|
-
steps: z.array(StepSchema),
|
|
205
|
-
}).passthrough();
|
|
206
|
-
|
|
207
|
-
function parseValue(v: string): any {
|
|
208
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
209
|
-
return v.slice(1, -1);
|
|
439
|
+
const reason = result.doneReason?.trim() || `${role.title} did not converge within ${result.steps} steps`;
|
|
440
|
+
if (!result.done) {
|
|
441
|
+
console.log(formatRalphStreamEvent("error", reason, renderOpts));
|
|
442
|
+
return false;
|
|
210
443
|
}
|
|
211
|
-
if (v === "true") return true;
|
|
212
|
-
if (v === "false") return false;
|
|
213
|
-
if (v === "null") return null;
|
|
214
|
-
if (v === "") return "";
|
|
215
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
216
|
-
return v;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function parseYaml(yamlStr: string): any {
|
|
220
|
-
const lines = yamlStr.split(/\r?\n/).map(line => {
|
|
221
|
-
const commentIdx = line.indexOf('#');
|
|
222
|
-
const cleanLine = commentIdx !== -1 ? line.slice(0, commentIdx) : line;
|
|
223
|
-
return {
|
|
224
|
-
raw: cleanLine,
|
|
225
|
-
trimmed: cleanLine.trim(),
|
|
226
|
-
indent: cleanLine.length - cleanLine.trimStart().length
|
|
227
|
-
};
|
|
228
|
-
}).filter(l => l.trimmed !== '');
|
|
229
|
-
|
|
230
|
-
let idx = 0;
|
|
231
|
-
|
|
232
|
-
function parseBlock(baseIndent: number): any {
|
|
233
|
-
let result: any = null;
|
|
234
|
-
let isArray = false;
|
|
235
|
-
|
|
236
|
-
if (idx < lines.length) {
|
|
237
|
-
if (lines[idx].trimmed.startsWith('-')) {
|
|
238
|
-
isArray = true;
|
|
239
|
-
result = [];
|
|
240
|
-
} else {
|
|
241
|
-
result = {};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
while (idx < lines.length) {
|
|
246
|
-
const line = lines[idx];
|
|
247
|
-
if (line.indent < baseIndent) {
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (isArray) {
|
|
252
|
-
if (!line.trimmed.startsWith('-')) {
|
|
253
|
-
if (result.length > 0 && typeof result[result.length - 1] === 'object') {
|
|
254
|
-
const colonIdx = line.trimmed.indexOf(':');
|
|
255
|
-
if (colonIdx !== -1) {
|
|
256
|
-
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
257
|
-
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
258
|
-
if (rawVal === '') {
|
|
259
|
-
idx++;
|
|
260
|
-
result[result.length - 1][k] = parseBlock(line.indent + 1);
|
|
261
|
-
continue;
|
|
262
|
-
} else {
|
|
263
|
-
result[result.length - 1][k] = parseValue(rawVal);
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
throw new Error(`Invalid line inside array block: "${line.trimmed}"`);
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
throw new Error(`Invalid line in array: "${line.trimmed}"`);
|
|
270
|
-
}
|
|
271
|
-
idx++;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const rest = line.trimmed.slice(1).trim();
|
|
276
|
-
if (rest === '') {
|
|
277
|
-
idx++;
|
|
278
|
-
const nested = parseBlock(line.indent + 1);
|
|
279
|
-
result.push(nested);
|
|
280
|
-
} else if (rest.includes(':')) {
|
|
281
|
-
const colonIdx = rest.indexOf(':');
|
|
282
|
-
const k = rest.slice(0, colonIdx).trim();
|
|
283
|
-
const rawVal = rest.slice(colonIdx + 1).trim();
|
|
284
|
-
if (rawVal === '') {
|
|
285
|
-
idx++;
|
|
286
|
-
const nestedObj = { [k]: parseBlock(line.indent + 2) };
|
|
287
|
-
result.push(nestedObj);
|
|
288
|
-
} else {
|
|
289
|
-
const item: any = { [k]: parseValue(rawVal) };
|
|
290
|
-
result.push(item);
|
|
291
|
-
idx++;
|
|
292
|
-
while (idx < lines.length && !lines[idx].trimmed.startsWith('-') && lines[idx].indent >= line.indent + 2) {
|
|
293
|
-
const subLine = lines[idx];
|
|
294
|
-
const subColonIdx = subLine.trimmed.indexOf(':');
|
|
295
|
-
if (subColonIdx !== -1) {
|
|
296
|
-
const subK = subLine.trimmed.slice(0, subColonIdx).trim();
|
|
297
|
-
const rawSubVal = subLine.trimmed.slice(subColonIdx + 1).trim();
|
|
298
|
-
if (rawSubVal === '') {
|
|
299
|
-
idx++;
|
|
300
|
-
item[subK] = parseBlock(subLine.indent + 1);
|
|
301
|
-
} else {
|
|
302
|
-
item[subK] = parseValue(rawSubVal);
|
|
303
|
-
idx++;
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
throw new Error(`Invalid sub-line in block mapping: "${subLine.trimmed}"`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
result.push(parseValue(rest));
|
|
312
|
-
idx++;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
const colonIdx = line.trimmed.indexOf(':');
|
|
316
|
-
if (colonIdx === -1) {
|
|
317
|
-
throw new Error(`Invalid line: "${line.trimmed}"`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
321
|
-
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
322
444
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
idx++;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
445
|
+
const contract = validateSubagentDoneReason(role, reason);
|
|
446
|
+
if (!contract.ok) {
|
|
447
|
+
console.log(formatRalphStreamEvent("error", `${role.title} report incomplete: missing ${contract.missing?.join(", ")}`, renderOpts));
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
332
450
|
|
|
333
|
-
|
|
451
|
+
const gate = parseRoleGateVerdict(role.id, reason);
|
|
452
|
+
if (!gate.ok) {
|
|
453
|
+
console.log(formatRalphStreamEvent("error", gate.message ?? `${role.title} blocked execution`, renderOpts));
|
|
454
|
+
return false;
|
|
334
455
|
}
|
|
335
456
|
|
|
336
|
-
|
|
457
|
+
if (!role.readOnly && mutationsOk === 0) {
|
|
458
|
+
// Round-8: a mutating role finished without ONE successful mutation — the
|
|
459
|
+
// task may be legitimately read-only, but its "Changed Files:" claim is
|
|
460
|
+
// unverified; warn instead of silently trusting the report.
|
|
461
|
+
console.log(formatRalphStreamEvent("error", `${role.title} completed WITHOUT any successful write/edit/bash — treat its changed-files claim as unverified.`, renderOpts));
|
|
462
|
+
}
|
|
463
|
+
console.log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
|
|
464
|
+
return true;
|
|
337
465
|
}
|