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.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +808 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +624 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -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 joc-mcp tools (${TOOLS.length}):`);
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 'joc mcp' subcommand: ${sub}`);
16
- console.log("Usage: joc mcp [serve|tools]");
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
+ }
@@ -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
- getLocalJocDir,
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
- export async function runRalplanCommand(): Promise<void> {
12
- const cwd = process.cwd();
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
- console.log(
18
- `[ERROR] No crystallized requirements found. Please run 'joc deep-interview' to crystallize requirements first.`
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
- console.log(`\n=== Starting Ralplan Planning Stage ===`);
25
- console.log(`Reading requirements seed from: ${seedPath}`);
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
- console.log(`[ERROR] Failed to read seed file: ${err.message}`);
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
- console.log("Generating resilient task sequence plan (Critiqued by Planner/Architect/Critic)...");
134
+ log("Running Planner Architect drafting passes + a repo-grounded Critic consensus gate…");
46
135
 
47
- // Standard multi-role system prompt
48
- const systemPrompt =
49
- `You are the Ralplan Orchestrator, combining three expert roles:\n` +
50
- `1. Planner: Focuses on sequencing tasks into a highly logical, outcome-based progression.\n` +
51
- `2. Architect: Reviews technical feasibility, structural directories, and patterns.\n` +
52
- `3. Critic: Critiques the plan for vagueness, redundant copies, and missing steps.\n\n` +
53
- `Analyze the given crystallized spec (seed.yaml) and generate a step-by-step implementation plan.\n` +
54
- `Output the final plan in YAML format. Ensure it contains a clear sequence of tasks with descriptive names and target files.\n` +
55
- `Output ONLY the YAML. Do not include markdown wraps or code blocks.`;
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 messages = [
58
- { role: "user" as const, content: `Here is the crystallized spec (seed.yaml):\n\n${seedContent}` }
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 rawPlan = await callLlm(messages, { systemPrompt });
63
- const cleanPlan = rawPlan.replace(/```yaml|```/g, "").trim();
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
- const planDir = path.join(getLocalJocDir(cwd), "plans");
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
- ralplanState.current_phase = "complete";
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
- console.log("\nPlan preview:");
78
- console.log("-----------------------------------------");
79
- console.log(cleanPlan);
80
- console.log("-----------------------------------------");
81
- console.log("\n[Handoff Ready] The blueprint is prepared. Run 'joc team' to execute the plan.");
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
- console.log(`[ERROR calling LLM during Planning]: ${error.message}`);
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { runLaunchCommand } from "./launch";
2
2
 
3
- /** `joc resume [id]` — resume the latest (or a specific) interactive session. */
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 (`joc setup`). Extracted from
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("gemini");
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;