jeo-code 0.1.0 → 0.4.5
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 +808 -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 +624 -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/mcp.ts
CHANGED
|
@@ -8,11 +8,11 @@ export async function runMcpCommand(args: string[]): Promise<void> {
|
|
|
8
8
|
}
|
|
9
9
|
if (sub === "tools") {
|
|
10
10
|
const { TOOLS } = await import("../mcp");
|
|
11
|
-
console.log(`Available
|
|
11
|
+
console.log(`Available jeo-mcp tools (${TOOLS.length}):`);
|
|
12
12
|
for (const t of TOOLS) console.log(` ${t.name.padEnd(28)} ${t.description}`);
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
console.log(`unknown '
|
|
16
|
-
console.log("Usage:
|
|
15
|
+
console.log(`unknown 'jeo mcp' subcommand: ${sub}`);
|
|
16
|
+
console.log("Usage: jeo mcp [serve|tools]");
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { syncSpecificationToSeed } from "../agent/dev/spec-automation";
|
|
2
|
+
import { categoryBadge } from "../tui/components/category-index";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* jeo ooo-seed: Syncs .specify/specification.md to an ooo seed.
|
|
7
|
+
*/
|
|
8
|
+
export async function runOooSeedCommand(args: string[]): Promise<void> {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
console.log(`\n=== ${chalk.bold("ooo seed")} Specification Sync ===`);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await syncSpecificationToSeed(cwd);
|
|
14
|
+
console.log(`\n${categoryBadge("done")} ooo seed sync completed.`);
|
|
15
|
+
} catch (err: any) {
|
|
16
|
+
console.error(`\n${categoryBadge("error")} ooo seed sync failed: ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/commands/ralplan.ts
CHANGED
|
@@ -1,35 +1,124 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { callLlm } from "../agent/loop";
|
|
3
|
+
import { callLlm, type Message } from "../agent/loop";
|
|
4
4
|
import {
|
|
5
5
|
readWorkflowState,
|
|
6
6
|
writeWorkflowState,
|
|
7
|
-
|
|
7
|
+
readGlobalConfig,
|
|
8
|
+
getLocalJeoDir,
|
|
8
9
|
type WorkflowState,
|
|
9
10
|
} from "../agent/state";
|
|
11
|
+
import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
|
|
12
|
+
import {
|
|
13
|
+
getSubagentRole,
|
|
14
|
+
subagentSystemPrompt,
|
|
15
|
+
subagentToolset,
|
|
16
|
+
validateSubagentDoneReason,
|
|
17
|
+
resolveSubagentModel,
|
|
18
|
+
resolveSubagentMaxSteps,
|
|
19
|
+
} from "../agent/subagents";
|
|
20
|
+
import { runAgentLoop } from "../agent/engine";
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
/** Round-11 (architect ref 8-Round10Planning #1): the REAL consensus gate. A
|
|
23
|
+
* read-only critic SUBAGENT (repo access via read/search/find) reviews the
|
|
24
|
+
* candidate plan and must return an explicit verdict — unlike the drafting
|
|
25
|
+
* passes, this one can actually BLOCK. Fail-closed: anything but a clean
|
|
26
|
+
* [OKAY] contract is non-approval. */
|
|
27
|
+
export async function runConsensusCriticGate(args: {
|
|
28
|
+
cwd: string;
|
|
29
|
+
seedContent: string;
|
|
30
|
+
plan: string;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}): Promise<{ verdict: "okay" | "iterate" | "reject" | "unverified"; detail: string }> {
|
|
33
|
+
const role = getSubagentRole("critic")!;
|
|
34
|
+
const config = await readGlobalConfig();
|
|
35
|
+
const model = resolveSubagentModel(role.id, config);
|
|
36
|
+
const maxSteps = resolveSubagentMaxSteps(role.id, config);
|
|
37
|
+
const history: Message[] = [
|
|
38
|
+
{ role: "system", content: subagentSystemPrompt(role) },
|
|
39
|
+
{
|
|
40
|
+
role: "user",
|
|
41
|
+
content:
|
|
42
|
+
`Review this implementation plan BEFORE it can be approved for execution.\n\n` +
|
|
43
|
+
`Crystallized spec (seed.yaml):\n${args.seedContent}\n\n` +
|
|
44
|
+
`Proposed plan (YAML — 'jeo team' executes the steps strictly top-to-bottom):\n${args.plan}\n\n` +
|
|
45
|
+
`Verify against the ACTUAL repository (use read/search/find/ls): file targets exist or are sensibly placed, ` +
|
|
46
|
+
`steps are ordered and independently verifiable, and the acceptance criteria are covered. ` +
|
|
47
|
+
`Then call done — your reason MUST start with [OKAY], [ITERATE], or [REJECT] and include a 'Justification:' section.`,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
const result = await runAgentLoop(history, {
|
|
51
|
+
cwd: args.cwd,
|
|
52
|
+
model,
|
|
53
|
+
maxSteps,
|
|
54
|
+
budget: { maxExtensions: 0 },
|
|
55
|
+
signal: args.signal,
|
|
56
|
+
tools: subagentToolset(role),
|
|
57
|
+
});
|
|
58
|
+
const reason = result.doneReason?.trim() ?? "";
|
|
59
|
+
if (!result.done) return { verdict: "unverified", detail: reason || `critic did not converge within ${result.steps} steps` };
|
|
60
|
+
const contract = validateSubagentDoneReason(role, reason);
|
|
61
|
+
if (!contract.ok) return { verdict: "unverified", detail: `critic report incomplete (missing ${contract.missing?.join(", ")}): ${reason.slice(0, 300)}` };
|
|
62
|
+
const firstLine = reason.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
63
|
+
const verdict = firstLine === "[OKAY]" ? "okay" : firstLine === "[ITERATE]" ? "iterate" : firstLine === "[REJECT]" ? "reject" : "unverified";
|
|
64
|
+
return { verdict, detail: reason };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RalplanEngineOptions {
|
|
68
|
+
cwd?: string;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
onProgress?: (e: { skill: string; phase: string; detail?: string }) => void;
|
|
71
|
+
io?: {
|
|
72
|
+
output?: (line: string) => void;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise<{ ok: boolean; reason?: string }> {
|
|
77
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
78
|
+
|
|
79
|
+
const log = (msg?: any) => {
|
|
80
|
+
const str = msg !== undefined ? String(msg) : "";
|
|
81
|
+
if (opts.io?.output) {
|
|
82
|
+
const lines = str.split("\n");
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
opts.io.output(line);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.log(str);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (opts.onProgress) {
|
|
92
|
+
opts.onProgress({ skill: "ralplan", phase: "start" });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (opts.signal?.aborted) {
|
|
96
|
+
return { ok: false, reason: "aborted" };
|
|
97
|
+
}
|
|
13
98
|
|
|
14
99
|
// Read deep-interview state
|
|
15
100
|
const interviewState = await readWorkflowState("deep-interview", cwd);
|
|
16
101
|
if (!interviewState || interviewState.current_phase !== "complete" || !interviewState.seed_path) {
|
|
17
|
-
|
|
18
|
-
`[ERROR] No crystallized requirements found. Please run '
|
|
102
|
+
log(
|
|
103
|
+
`[ERROR] No crystallized requirements found. Please run 'jeo deep-interview' to crystallize requirements first.`
|
|
19
104
|
);
|
|
20
|
-
return;
|
|
105
|
+
return { ok: false, reason: "No crystallized requirements found" };
|
|
21
106
|
}
|
|
22
107
|
|
|
23
108
|
const seedPath = interviewState.seed_path;
|
|
24
|
-
|
|
25
|
-
|
|
109
|
+
log(`\n=== Starting Ralplan Planning Stage ===`);
|
|
110
|
+
log(`Reading requirements seed from: ${seedPath}`);
|
|
26
111
|
|
|
27
112
|
let seedContent = "";
|
|
28
113
|
try {
|
|
29
114
|
seedContent = await fs.readFile(seedPath, "utf-8");
|
|
30
115
|
} catch (err: any) {
|
|
31
|
-
|
|
32
|
-
return;
|
|
116
|
+
log(`[ERROR] Failed to read seed file: ${err.message}`);
|
|
117
|
+
return { ok: false, reason: err.message };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (opts.signal?.aborted) {
|
|
121
|
+
return { ok: false, reason: "aborted" };
|
|
33
122
|
}
|
|
34
123
|
|
|
35
124
|
// Initialize ralplan state
|
|
@@ -42,45 +131,174 @@ export async function runRalplanCommand(): Promise<void> {
|
|
|
42
131
|
};
|
|
43
132
|
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
44
133
|
|
|
45
|
-
|
|
134
|
+
log("Running Planner → Architect drafting passes + a repo-grounded Critic consensus gate…");
|
|
46
135
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
`
|
|
50
|
-
`
|
|
51
|
-
`
|
|
52
|
-
`
|
|
53
|
-
`
|
|
54
|
-
`
|
|
55
|
-
`
|
|
136
|
+
// Shared output contract (the exact shape `team` consumes) included in every pass.
|
|
137
|
+
const SCHEMA_SPEC =
|
|
138
|
+
`Output the plan as YAML with EXACTLY this shape (no prose, no markdown, no code fences):\n` +
|
|
139
|
+
`name: "<short plan name>"\n` +
|
|
140
|
+
`steps:\n` +
|
|
141
|
+
` - name: "<imperative task, e.g. Implement reverse() in src/reverse.ts>"\n` +
|
|
142
|
+
` role: executor # one of: executor | planner | architect | critic\n` +
|
|
143
|
+
` target: "<primary file path>"\n` +
|
|
144
|
+
`Provide 3-8 concrete, ordered steps. Output ONLY the YAML.`;
|
|
56
145
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
146
|
+
const PLANNER = `You are the PLANNER. From the crystallized spec, sequence the work into a logical, outcome-based progression of concrete, ordered tasks.\n` + SCHEMA_SPEC;
|
|
147
|
+
const ARCHITECT = `You are the ARCHITECT. Review the Planner's draft for technical feasibility, correct file targets, directory structure, and any missing setup/wiring/test steps. Return an improved plan (same shape).\n` + SCHEMA_SPEC;
|
|
148
|
+
const CRITIC = `You are the CRITIC. Finalize the plan: remove vague or redundant steps, make each step actionable and independently verifiable, and ensure the acceptance criteria are covered. Return the final plan (same shape).\n` + SCHEMA_SPEC;
|
|
60
149
|
|
|
61
150
|
try {
|
|
62
|
-
const
|
|
63
|
-
|
|
151
|
+
const callRole = async (systemPrompt: string, userContent: string): Promise<string> => {
|
|
152
|
+
const raw = await callLlm([{ role: "user" as const, content: userContent }], { systemPrompt });
|
|
153
|
+
return raw.replace(/```yaml|```/g, "").trim();
|
|
154
|
+
};
|
|
155
|
+
const isValidPlan = (yaml: string): boolean => {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = PlanSchema.safeParse(normalizePlanShape(parseYaml(yaml)));
|
|
158
|
+
if (!parsed.success) return false;
|
|
159
|
+
// Round-10 #2 (architect ref 8-Round10Planning): write-time parity with
|
|
160
|
+
// team's execution gate — an unknown role (e.g. "developer") used to pass
|
|
161
|
+
// here and abort only at `jeo team`, after the planning model was gone.
|
|
162
|
+
return parsed.data.steps.every(s => !s.role?.trim() || !!getSubagentRole(s.role));
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
64
167
|
|
|
65
|
-
|
|
168
|
+
if (opts.signal?.aborted) {
|
|
169
|
+
return { ok: false, reason: "aborted" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Three chained role passes, each consuming the prior output (gjc consensus).
|
|
173
|
+
log(" [1/3] Planner drafting the task sequence…");
|
|
174
|
+
if (opts.onProgress) {
|
|
175
|
+
opts.onProgress({ skill: "ralplan", phase: "planning", detail: "Planner drafting" });
|
|
176
|
+
}
|
|
177
|
+
const draft = await callRole(PLANNER, `Crystallized spec (seed.yaml):\n\n${seedContent}`);
|
|
178
|
+
|
|
179
|
+
if (opts.signal?.aborted) {
|
|
180
|
+
return { ok: false, reason: "aborted" };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
log(" [2/3] Architect reviewing feasibility & structure…");
|
|
184
|
+
if (opts.onProgress) {
|
|
185
|
+
opts.onProgress({ skill: "ralplan", phase: "planning", detail: "Architect reviewing" });
|
|
186
|
+
}
|
|
187
|
+
const reviewed = await callRole(ARCHITECT, `Crystallized spec (seed.yaml):\n\n${seedContent}\n\nPlanner's draft plan:\n\n${draft}\n\nReturn the improved plan.`);
|
|
188
|
+
|
|
189
|
+
if (opts.signal?.aborted) {
|
|
190
|
+
return { ok: false, reason: "aborted" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log(" [3/3] Critic finalizing (tightening + verifiability)…");
|
|
194
|
+
if (opts.onProgress) {
|
|
195
|
+
opts.onProgress({ skill: "ralplan", phase: "planning", detail: "Critic finalizing" });
|
|
196
|
+
}
|
|
197
|
+
let cleanPlan = await callRole(CRITIC, `Crystallized spec (seed.yaml):\n\n${seedContent}\n\nArchitect's plan:\n\n${reviewed}\n\nReturn the final, critiqued plan.`);
|
|
198
|
+
|
|
199
|
+
if (opts.signal?.aborted) {
|
|
200
|
+
return { ok: false, reason: "aborted" };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Self-validate the Critic's output against team's schema (incl. role names);
|
|
204
|
+
// repair once, else fall back to the best valid earlier pass. When NO pass is
|
|
205
|
+
// valid, the plan is saved for inspection but the workflow is NOT marked
|
|
206
|
+
// complete (round-10 #2) — failing here, while the model is still in the loop,
|
|
207
|
+
// beats failing later at `jeo team` with the same plan.
|
|
208
|
+
let planValid = true;
|
|
209
|
+
if (!isValidPlan(cleanPlan)) {
|
|
210
|
+
log("[ralplan] Final plan did not match the required shape; requesting a corrected plan…");
|
|
211
|
+
cleanPlan = await callRole(CRITIC, `Your previous output was not valid for the required schema. Fix it (roles MUST be one of executor|planner|architect|critic).\n\n${SCHEMA_SPEC}\n\nPlan to fix:\n\n${cleanPlan}`);
|
|
212
|
+
if (!isValidPlan(cleanPlan)) {
|
|
213
|
+
const fallback = [reviewed, draft].find(isValidPlan);
|
|
214
|
+
if (fallback) {
|
|
215
|
+
cleanPlan = fallback;
|
|
216
|
+
log("[ralplan] Using an earlier valid pass output (Critic output was unparseable).");
|
|
217
|
+
} else {
|
|
218
|
+
planValid = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const planDir = path.join(getLocalJeoDir(cwd), "plans");
|
|
66
224
|
await fs.mkdir(planDir, { recursive: true });
|
|
67
225
|
const planPath = path.join(planDir, `plan-${interviewState.slug}.yaml`);
|
|
68
226
|
|
|
69
227
|
await fs.writeFile(planPath, cleanPlan, "utf-8");
|
|
70
|
-
console.log(`\n[SUCCESS] Plan successfully created and saved to: ${planPath}`);
|
|
71
228
|
|
|
72
|
-
|
|
229
|
+
if (!planValid) {
|
|
230
|
+
ralplanState.plan_path = planPath; // saved for inspection — but NOT complete
|
|
231
|
+
ralplanState.approved = false;
|
|
232
|
+
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
233
|
+
log(
|
|
234
|
+
`[ERROR] No pass produced a schema/role-valid plan. The last output was saved to ${planPath} for review, ` +
|
|
235
|
+
`but the workflow was NOT marked complete — edit the plan to match the schema (roles: executor|planner|architect|critic) ` +
|
|
236
|
+
`and re-run 'jeo ralplan', or retry with a stronger model.`,
|
|
237
|
+
);
|
|
238
|
+
return { ok: false, reason: "no schema-valid plan produced" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Round-11: REAL consensus gate — a read-only critic subagent with repo
|
|
242
|
+
// access must return [OKAY] before this plan can be marked complete. One
|
|
243
|
+
// [ITERATE] revision round is honored; anything else fails closed.
|
|
244
|
+
log(" [gate] Consensus critic (read-only subagent) reviewing the plan against the repo…");
|
|
245
|
+
if (opts.onProgress) {
|
|
246
|
+
opts.onProgress({ skill: "ralplan", phase: "planning", detail: "Critic gate reviewing" });
|
|
247
|
+
}
|
|
248
|
+
let gate = await runConsensusCriticGate({ cwd, seedContent, plan: cleanPlan, signal: opts.signal });
|
|
249
|
+
if (gate.verdict === "iterate") {
|
|
250
|
+
log(`[ralplan] Critic returned [ITERATE] — revising the plan once to address the justification…`);
|
|
251
|
+
const revised = await callRole(
|
|
252
|
+
CRITIC,
|
|
253
|
+
`The consensus critic returned [ITERATE] on the plan with this justification:\n\n${gate.detail}\n\n` +
|
|
254
|
+
`Revise the plan to address every point.\n\n${SCHEMA_SPEC}\n\nCurrent plan:\n\n${cleanPlan}`,
|
|
255
|
+
);
|
|
256
|
+
if (isValidPlan(revised)) {
|
|
257
|
+
cleanPlan = revised;
|
|
258
|
+
await fs.writeFile(planPath, cleanPlan, "utf-8");
|
|
259
|
+
gate = await runConsensusCriticGate({ cwd, seedContent, plan: cleanPlan, signal: opts.signal });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
73
262
|
ralplanState.plan_path = planPath;
|
|
263
|
+
ralplanState.consensus = gate.verdict;
|
|
264
|
+
ralplanState.consensus_detail = gate.detail.slice(0, 600);
|
|
265
|
+
if (gate.verdict !== "okay") {
|
|
266
|
+
ralplanState.approved = false;
|
|
267
|
+
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
268
|
+
log(
|
|
269
|
+
`[ERROR] Consensus critic did NOT approve the plan (verdict: ${gate.verdict.toUpperCase()}).\n` +
|
|
270
|
+
` Justification:\n${gate.detail.slice(0, 800)}\n` +
|
|
271
|
+
` The plan was saved to ${planPath} but the workflow was NOT marked complete — address the justification and re-run 'jeo ralplan'.`,
|
|
272
|
+
);
|
|
273
|
+
return { ok: false, reason: `critic verdict: ${gate.verdict}` };
|
|
274
|
+
}
|
|
275
|
+
log(` [gate] Critic verdict: [OKAY] — consensus recorded.`);
|
|
276
|
+
|
|
277
|
+
log(`\n[SUCCESS] Plan successfully created and saved to: ${planPath}`);
|
|
278
|
+
|
|
279
|
+
ralplanState.current_phase = "complete";
|
|
74
280
|
ralplanState.approved = false;
|
|
75
281
|
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
76
282
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
283
|
+
log("\nPlan preview:");
|
|
284
|
+
log("-----------------------------------------");
|
|
285
|
+
log(cleanPlan);
|
|
286
|
+
log("-----------------------------------------");
|
|
287
|
+
log(`\n[Handoff Ready] The blueprint is prepared but NOT yet approved.`);
|
|
288
|
+
log(` 1) Review it, then approve: jeo approve "${planPath}"`);
|
|
289
|
+
log(` 2) Execute the plan: jeo team`);
|
|
290
|
+
|
|
291
|
+
if (opts.onProgress) {
|
|
292
|
+
opts.onProgress({ skill: "ralplan", phase: "complete" });
|
|
293
|
+
}
|
|
294
|
+
return { ok: true };
|
|
82
295
|
|
|
83
296
|
} catch (error: any) {
|
|
84
|
-
|
|
297
|
+
log(`[ERROR calling LLM during Planning]: ${error.message}`);
|
|
298
|
+
return { ok: false, reason: error.message };
|
|
85
299
|
}
|
|
86
300
|
}
|
|
301
|
+
|
|
302
|
+
export async function runRalplanCommand(): Promise<void> {
|
|
303
|
+
await runRalplanEngine();
|
|
304
|
+
}
|
package/src/commands/resume.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runLaunchCommand } from "./launch";
|
|
2
2
|
|
|
3
|
-
/** `
|
|
3
|
+
/** `jeo resume [id]` — resume the latest (or a specific) interactive session. */
|
|
4
4
|
export async function runResumeCommand(args: string[] = []): Promise<void> {
|
|
5
5
|
return runLaunchCommand(["--resume", ...args]);
|
|
6
6
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawnSync } from "bun";
|
|
2
|
+
|
|
3
|
+
export type RunTmuxFn = (
|
|
4
|
+
argv: string[]
|
|
5
|
+
) => { exitCode: number; stdout: string; stderr: string } | Promise<{ exitCode: number; stdout: string; stderr: string }>;
|
|
6
|
+
|
|
7
|
+
export const defaultRunTmux: RunTmuxFn = (argv: string[]) => {
|
|
8
|
+
const isAttach = argv[0] === "attach" || argv[0] === "attach-session";
|
|
9
|
+
const proc = Bun.spawnSync(["tmux", ...argv], {
|
|
10
|
+
stdout: isAttach ? "inherit" : "pipe",
|
|
11
|
+
stderr: isAttach ? "inherit" : "pipe",
|
|
12
|
+
stdin: isAttach ? "inherit" : "pipe",
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
exitCode: proc.exitCode,
|
|
16
|
+
stdout: proc.stdout ? proc.stdout.toString() : "",
|
|
17
|
+
stderr: proc.stderr ? proc.stderr.toString() : "",
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface SessionInfo {
|
|
22
|
+
name: string;
|
|
23
|
+
created: string;
|
|
24
|
+
attached: boolean;
|
|
25
|
+
/** Carries the `@jeo-profile` ownership marker set by `jeo --tmux` (gjc `@gjc-profile` parity). */
|
|
26
|
+
owned: boolean;
|
|
27
|
+
/** `@jeo-branch` identity recorded at session creation, when present. */
|
|
28
|
+
branch?: string;
|
|
29
|
+
/** `@jeo-project` (cwd) identity recorded at session creation, when present. */
|
|
30
|
+
project?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printUsage(): void {
|
|
34
|
+
console.log("Usage: jeo session [list] [--json]");
|
|
35
|
+
console.log(" jeo session attach <name>");
|
|
36
|
+
console.log(" jeo session rm/kill <name>");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Exact-name tmux session target: `=name` is exact-matched (never prefix-matched)
|
|
40
|
+
* and accepted by attach-session/kill-session on every supported tmux. (Option
|
|
41
|
+
* commands need the `=name:` form instead — see launch.ts `tmuxProfileCommands`.) */
|
|
42
|
+
function exactTarget(name: string): string {
|
|
43
|
+
return `=${name}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const LIST_FORMAT =
|
|
47
|
+
"#{session_name}\t#{session_created}\t#{session_attached}\t#{@jeo-profile}\t#{@jeo-branch}\t#{@jeo-project}";
|
|
48
|
+
|
|
49
|
+
async function getJeoSessions(runTmux: RunTmuxFn): Promise<SessionInfo[] | null> {
|
|
50
|
+
try {
|
|
51
|
+
const res = await runTmux(["list-sessions", "-F", LIST_FORMAT]);
|
|
52
|
+
if (res.exitCode !== 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const sessions: SessionInfo[] = [];
|
|
56
|
+
const lines = res.stdout.split("\n");
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) continue;
|
|
60
|
+
const parts = trimmed.split("\t");
|
|
61
|
+
if (parts.length < 3) continue;
|
|
62
|
+
const [name, created, attached, marker, branch, project] = parts;
|
|
63
|
+
const owned = marker === "1";
|
|
64
|
+
// Ownership: the `@jeo-profile` marker is authoritative (set by `jeo --tmux`
|
|
65
|
+
// regardless of how the session is named); the `jeo-` name prefix is accepted
|
|
66
|
+
// for directly named sessions.
|
|
67
|
+
if (!owned && !name.startsWith("jeo-")) continue;
|
|
68
|
+
|
|
69
|
+
const createdSeconds = parseInt(created, 10);
|
|
70
|
+
const createdIso = isNaN(createdSeconds) ? "" : new Date(createdSeconds * 1000).toISOString();
|
|
71
|
+
sessions.push({
|
|
72
|
+
name,
|
|
73
|
+
created: createdIso,
|
|
74
|
+
attached: attached === "1",
|
|
75
|
+
owned,
|
|
76
|
+
...(branch ? { branch } : {}),
|
|
77
|
+
...(project ? { project } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return sessions;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function runSessionCommand(args: string[]): Promise<void> {
|
|
87
|
+
await runSessionCommandWith(args, defaultRunTmux);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runSessionCommandWith(args: string[], runTmux: RunTmuxFn): Promise<void> {
|
|
91
|
+
const isHelp = args.includes("--help") || args.includes("-h");
|
|
92
|
+
if (isHelp) {
|
|
93
|
+
printUsage();
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isJson = args.includes("--json");
|
|
99
|
+
const cleanArgs = args.filter(a => a !== "--json");
|
|
100
|
+
const verb = cleanArgs[0];
|
|
101
|
+
|
|
102
|
+
if (!verb || verb === "list") {
|
|
103
|
+
const sessions = await getJeoSessions(runTmux);
|
|
104
|
+
if (!sessions || sessions.length === 0) {
|
|
105
|
+
if (isJson) {
|
|
106
|
+
console.log("[]");
|
|
107
|
+
} else {
|
|
108
|
+
console.log("No active jeo sessions found.");
|
|
109
|
+
}
|
|
110
|
+
process.exitCode = 0;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isJson) {
|
|
115
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
116
|
+
} else {
|
|
117
|
+
const branchOf = (s: SessionInfo): string => s.branch ?? "";
|
|
118
|
+
const nameWidth = Math.max("Name".length, ...sessions.map(s => s.name.length));
|
|
119
|
+
const createdWidth = Math.max("Created".length, ...sessions.map(s => s.created.length));
|
|
120
|
+
const branchWidth = Math.max("Branch".length, ...sessions.map(s => branchOf(s).length));
|
|
121
|
+
|
|
122
|
+
console.log(`${"Name".padEnd(nameWidth)} ${"Created".padEnd(createdWidth)} ${"Branch".padEnd(branchWidth)} Attached`);
|
|
123
|
+
for (const s of sessions) {
|
|
124
|
+
const attachedStr = s.attached ? "yes" : "no";
|
|
125
|
+
console.log(`${s.name.padEnd(nameWidth)} ${s.created.padEnd(createdWidth)} ${branchOf(s).padEnd(branchWidth)} ${attachedStr}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
process.exitCode = 0;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (verb === "attach") {
|
|
133
|
+
const name = cleanArgs[1];
|
|
134
|
+
if (!name) {
|
|
135
|
+
console.log("Error: Session name required.");
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sessions = await getJeoSessions(runTmux);
|
|
141
|
+
const exists = sessions ? sessions.some(s => s.name === name) : false;
|
|
142
|
+
if (!exists) {
|
|
143
|
+
console.log(`Error: Session '${name}' not found.`);
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const res = await runTmux(["attach", "-t", exactTarget(name)]);
|
|
149
|
+
process.exitCode = res.exitCode;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (verb === "rm" || verb === "kill") {
|
|
154
|
+
const name = cleanArgs[1];
|
|
155
|
+
if (!name) {
|
|
156
|
+
console.log("Error: Session name required.");
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ownership gate (gjc parity): the `jeo-` name prefix allows directly,
|
|
162
|
+
// and any session carrying the `@jeo-profile` marker (set by `jeo --tmux`)
|
|
163
|
+
// is jeo-owned regardless of its name. Everything else is refused.
|
|
164
|
+
if (!name.startsWith("jeo-")) {
|
|
165
|
+
const sessions = await getJeoSessions(runTmux);
|
|
166
|
+
const owned = sessions?.some(s => s.name === name && s.owned) ?? false;
|
|
167
|
+
if (!owned) {
|
|
168
|
+
console.log(`Error: Refusing to kill non-jeo session '${name}'.`);
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const res = await runTmux(["kill-session", "-t", exactTarget(name)]);
|
|
175
|
+
process.exitCode = res.exitCode;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Unknown verb
|
|
180
|
+
console.log(`Unknown subcommand: ${verb}`);
|
|
181
|
+
printUsage();
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure helpers for the model/provider setting flow (`
|
|
2
|
+
* Pure helpers for the model/provider setting flow (`jeo setup`). Extracted from
|
|
3
3
|
* the readline-driven command so the validation, normalization, recommendation,
|
|
4
4
|
* and summary logic is unit-testable without a TTY.
|
|
5
5
|
*/
|
|
6
6
|
import type { ProviderName } from "../ai/types";
|
|
7
7
|
import { recommendedModel, validateModelId, suggestModels, findCatalogEntry, catalogForProvider } from "../ai/model-catalog-compat";
|
|
8
|
+
import { CODEX_MODELS } from "../ai/model-catalog";
|
|
8
9
|
import { resolveProvider } from "../ai/model-manager";
|
|
9
10
|
import type { Config } from "../agent/state";
|
|
10
11
|
|
|
@@ -56,7 +57,13 @@ export function chooseDefaultModel(typed: string | undefined, provider: Provider
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** Top-N recommended catalog rows for a provider, as `id — note` display lines. */
|
|
59
|
-
export function recommendedModelsFor(provider: ProviderName, n = 5): string[] {
|
|
60
|
+
export function recommendedModelsFor(provider: ProviderName, n = 5, opts: { codex?: boolean } = {}): string[] {
|
|
61
|
+
if (provider === "openai" && opts.codex) {
|
|
62
|
+
return CODEX_MODELS.slice(0, n).map(id => {
|
|
63
|
+
const entry = findCatalogEntry(id);
|
|
64
|
+
return `${id}${entry?.note ? ` — ${entry.note}, Codex OAuth` : " — Codex OAuth"}`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
60
67
|
return catalogForProvider(provider)
|
|
61
68
|
.slice(0, n)
|
|
62
69
|
.map(e => `${e.id}${e.note ? ` — ${e.note}` : ""}`);
|
|
@@ -68,7 +75,7 @@ export function buildEnabledProviders(config: Config): string[] {
|
|
|
68
75
|
const cfg = config as Config & { openaiBaseUrl?: string };
|
|
69
76
|
if (cfg.providers?.anthropic || cfg.oauth?.anthropic) enabled.push("anthropic");
|
|
70
77
|
if (cfg.providers?.openai || cfg.oauth?.openai) enabled.push("openai");
|
|
71
|
-
if (cfg.providers?.gemini || cfg.oauth?.gemini) enabled.push("
|
|
78
|
+
if (cfg.providers?.gemini || cfg.oauth?.gemini || cfg.providers?.antigravity || cfg.oauth?.antigravity) enabled.push("google-oauth");
|
|
72
79
|
if (cfg.ollamaBaseUrl) enabled.push(`ollama(${cfg.ollamaBaseUrl})`);
|
|
73
80
|
if (cfg.openaiBaseUrl) enabled.push(`openai-compatible(${cfg.openaiBaseUrl})`);
|
|
74
81
|
return enabled;
|