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
package/src/cli/runner.ts CHANGED
@@ -32,6 +32,15 @@ export const COMMANDS: readonly CommandSpec[] = [
32
32
  return args => m.runAuthCommand(args);
33
33
  },
34
34
  },
35
+ {
36
+ name: "export",
37
+ summary: "Export a saved session transcript to Markdown (or --json).",
38
+ usage: "export [id] [--json] [--system]",
39
+ loader: async () => {
40
+ const m = await import("../commands/export");
41
+ return args => m.runExportCommand(args);
42
+ },
43
+ },
35
44
  {
36
45
  name: "deep-interview",
37
46
  summary: "Execute Socratic requirements interview (locks tools while ambiguity > 20%).",
@@ -84,24 +93,16 @@ export const COMMANDS: readonly CommandSpec[] = [
84
93
  },
85
94
  {
86
95
  name: "mcp",
87
- summary: "Run joc as an MCP stdio server (subcommand: serve|tools).",
96
+ summary: "Run jeo as an MCP stdio server (subcommand: serve|tools).",
88
97
  usage: "mcp [serve|tools]",
89
98
  loader: async () => {
90
99
  const m = await import("../commands/mcp");
91
100
  return args => m.runMcpCommand(args);
92
101
  },
93
102
  },
94
- {
95
- name: "models",
96
- summary: "List model aliases + probe local/compatible models.",
97
- loader: async () => {
98
- const m = await import("../commands/models");
99
- return args => m.runModelsCommand(args);
100
- },
101
- },
102
103
  {
103
104
  name: "skills",
104
- summary: "List bundled workflow skills (joc skills <name> for details).",
105
+ summary: "List bundled workflow skills (jeo skills <name> for details).",
105
106
  usage: "skills [name]",
106
107
  loader: async () => {
107
108
  const m = await import("../commands/skills");
@@ -110,7 +111,7 @@ export const COMMANDS: readonly CommandSpec[] = [
110
111
  },
111
112
  {
112
113
  name: "resume",
113
- summary: "Resume the latest interactive session (or 'joc resume <id>').",
114
+ summary: "Resume the latest interactive session (or 'jeo resume <id>').",
114
115
  usage: "resume [id]",
115
116
  loader: async () => {
116
117
  const m = await import("../commands/resume");
@@ -135,6 +136,95 @@ export const COMMANDS: readonly CommandSpec[] = [
135
136
  return args => m.runEvolveCommand(args);
136
137
  },
137
138
  },
139
+ {
140
+ name: "memory-distill",
141
+ summary: "(internal) Background session-memory distillation worker spawned on exit.",
142
+ usage: "memory-distill <payload.json>",
143
+ loader: async () => {
144
+ const m = await import("../agent/memory");
145
+ return args => m.runMemoryDistillCommand(args);
146
+ },
147
+ },
148
+ {
149
+ name: "state",
150
+ summary: "Read or update workflow state receipts under .jeo/state (gjc-state parity).",
151
+ usage: "state <deep-interview|ralplan|team|ultragoal> <read|write|clear|handoff> [--input '<json>'] [--to <skill>] [--json]",
152
+ loader: async () => {
153
+ const m = await import("../commands/state");
154
+ return args => m.runStateCommand(args);
155
+ },
156
+ },
157
+ {
158
+ name: "session",
159
+ summary: "List, attach, or remove jeo-managed tmux sessions.",
160
+ usage: "session [list|attach <name>|rm <name>] [--json]",
161
+ loader: async () => {
162
+ const m = await import("../commands/session");
163
+ return args => m.runSessionCommand(args);
164
+ },
165
+ },
166
+ {
167
+ name: "update",
168
+ summary: "Check for (and install) a newer jeo-code release from the npm registry.",
169
+ usage: "update [--check|--install] [--json] [--strict]",
170
+ loader: async () => {
171
+ const m = await import("../commands/update");
172
+ return args => m.runUpdateCommand(args);
173
+ },
174
+ },
175
+ {
176
+ name: "gjc",
177
+ summary: "Run the gjc workflow skill as an autonomous build loop (plan → implement → verify).",
178
+ usage: "gjc \"<intent>\"",
179
+ loader: async () => {
180
+ const m = await import("../commands/gjc");
181
+ return args => m.runGjcCommand(args);
182
+ },
183
+ },
184
+ {
185
+ name: "ooo-seed",
186
+ summary: "Generate an immutable ooo seed from a specification (spec-first automation).",
187
+ usage: "ooo-seed [args]",
188
+ loader: async () => {
189
+ const m = await import("../commands/ooo-seed");
190
+ return args => m.runOooSeedCommand(args);
191
+ },
192
+ },
193
+ {
194
+ name: "status",
195
+ summary: "Show evolution status + engine performance metrics.",
196
+ loader: async () => {
197
+ const m = await import("../commands/status");
198
+ return async () => m.runStatusCommand();
199
+ },
200
+ },
201
+ {
202
+ name: "evolve-core",
203
+ summary: "Trigger a self-evolution turn using gjc as a guide (Dev Mode).",
204
+ usage: "evolve-core [args]",
205
+ loader: async () => {
206
+ const m = await import("../commands/evolve-core");
207
+ return args => m.runEvolveCoreCommand(args);
208
+ },
209
+ },
210
+ {
211
+ name: "autopilot",
212
+ summary: "Autonomous build loop (autopilot × autoresearch ratchet).",
213
+ usage: "autopilot <subcommand> [flags]",
214
+ loader: async () => {
215
+ const m = await import("../autopilot");
216
+ return args => Promise.resolve(m.runAutopilot(args));
217
+ },
218
+ },
219
+ {
220
+ name: "ledger",
221
+ summary: "Cross-plan append-only ledger (ledger / review / cleanup).",
222
+ usage: "ledger <subcommand> [flags]",
223
+ loader: async () => {
224
+ const m = await import("../ledger");
225
+ return args => Promise.resolve(m.runLedger(args));
226
+ },
227
+ },
138
228
  ];
139
229
 
140
230
  export function findCommand(name: string): CommandSpec | undefined {
@@ -190,6 +280,10 @@ export function renderHelp(ctx: DispatchContext): string {
190
280
  lines.push("Options:");
191
281
  lines.push(" -v, --version Show version.");
192
282
  lines.push(" -h, --help Show help.");
283
+ lines.push(" --model <id> Use a session model for launch/one-shot.");
284
+ lines.push(" --provider <name> Start launch on a provider default (anthropic/openai/gemini/antigravity/ollama).");
285
+ lines.push(" --smol|--slow|--plan Start launch with the configured model role tier.");
286
+ lines.push(" --thinking <level> Set thinking budget (minimal/low/medium/high/xhigh).");
193
287
  lines.push("");
194
288
  return lines.join("\n");
195
289
  }
@@ -204,6 +298,38 @@ export function renderCommandHelp(spec: CommandSpec, ctx: DispatchContext): stri
204
298
  ].join("\n");
205
299
  }
206
300
 
301
+ const VALUE_FLAGS = new Set(["--worktree", "--model", "--provider", "--thinking", "--max-steps", "--append-system-prompt", "--skills", "--tools", "--system-prompt"]);
302
+ const OPTIONAL_UUID_FLAGS = new Set(["--resume", "--continue", "-c"]);
303
+ const VALUE_PREFIXES = ["--worktree=", "--model=", "--provider=", "--thinking=", "--max-steps=", "--append-system-prompt=", "--skills=", "--tools=", "--system-prompt="];
304
+ const LAUNCH_ONLY_FLAGS = new Set(["--tmux", "--no-tui", "--no-session", "--list", "--smol", "--slow", "--plan", "-p", "--print", "--no-skills", "--no-tools"]);
305
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
306
+
307
+ function flagName(arg: string): string {
308
+ const eq = arg.indexOf("=");
309
+ return eq === -1 ? arg : arg.slice(0, eq);
310
+ }
311
+
312
+
313
+ function leadingGlobalFlag(argv: string[], targets: readonly string[]): boolean {
314
+ const wanted = new Set(targets);
315
+ for (let i = 0; i < argv.length; i++) {
316
+ const a = argv[i]!;
317
+ if (a === "--") break;
318
+ const name = flagName(a);
319
+ if (wanted.has(a) || wanted.has(name)) return true;
320
+ if (LAUNCH_ONLY_FLAGS.has(name)) continue;
321
+ if (VALUE_FLAGS.has(name) || VALUE_PREFIXES.some(prefix => a.startsWith(prefix))) {
322
+ if (!a.includes("=") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) i++;
323
+ continue;
324
+ }
325
+ if (OPTIONAL_UUID_FLAGS.has(name)) {
326
+ if (argv[i + 1] && UUID_REGEX.test(argv[i + 1]!)) i++;
327
+ continue;
328
+ }
329
+ break;
330
+ }
331
+ return false;
332
+ }
207
333
  export async function dispatch(argv: string[], ctx: DispatchContext): Promise<number> {
208
334
  const first = argv[0];
209
335
 
@@ -215,8 +341,16 @@ export async function dispatch(argv: string[], ctx: DispatchContext): Promise<nu
215
341
  console.log(renderHelp(ctx));
216
342
  return 0;
217
343
  }
218
- // Bare invocation or a leading global flag (e.g. `joc`, `joc --tmux`,
219
- // `joc --tmux --worktree <path>`) routes to the interactive agent — gjc parity.
344
+ if (leadingGlobalFlag(argv, ["--version", "-v"])) {
345
+ console.log(`${ctx.appName} v${ctx.version}`);
346
+ return 0;
347
+ }
348
+ if (leadingGlobalFlag(argv, ["--help", "-h"])) {
349
+ console.log(renderHelp(ctx));
350
+ return 0;
351
+ }
352
+ // Bare invocation or a leading global flag (e.g. `jeo`, `jeo --tmux`,
353
+ // `jeo --tmux --worktree <path>`) routes to the interactive agent — gjc parity.
220
354
  if (!first || first.startsWith("-")) {
221
355
  const run = await findCommand("launch")!.loader();
222
356
  await run(argv);
@@ -232,7 +366,7 @@ export async function dispatch(argv: string[], ctx: DispatchContext): Promise<nu
232
366
  return 1;
233
367
  }
234
368
 
235
- // Per-command help: `joc <cmd> --help`.
369
+ // Per-command help: `jeo <cmd> --help`.
236
370
  const rest = argv.slice(1);
237
371
  if (rest.includes("--help") || rest.includes("-h")) {
238
372
  console.log(renderCommandHelp(spec, ctx));
package/src/cli.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
  import { dispatch } from "./cli/runner";
3
+ import pkg from "../package.json";
3
4
 
4
- const APP_NAME = "joc";
5
- const VERSION = "0.1.0";
5
+ const APP_NAME = "jeo";
6
+ // Single source of truth: package.json. A hardcoded copy here drifted from the
7
+ // published version (`jeo update` compares the local version against the registry).
8
+ const VERSION = pkg.version;
6
9
  const MIN_BUN_VERSION = "1.3.14";
7
10
 
8
11
  if (typeof Bun !== "undefined" && Bun.semver?.order(Bun.version, MIN_BUN_VERSION) < 0) {
@@ -13,5 +16,11 @@ if (typeof Bun !== "undefined" && Bun.semver?.order(Bun.version, MIN_BUN_VERSION
13
16
  }
14
17
  process.title = APP_NAME;
15
18
 
16
- const code = await dispatch(process.argv.slice(2), { appName: APP_NAME, version: VERSION });
17
- if (code !== 0) process.exit(code);
19
+ try {
20
+ const code = await dispatch(process.argv.slice(2), { appName: APP_NAME, version: VERSION });
21
+ if (code !== 0) process.exit(code);
22
+ } catch (err) {
23
+ // Service-readiness: never surface a raw stack trace to users; clean error + non-zero exit.
24
+ process.stderr.write(`error: ${(err as Error)?.message ?? String(err)}\n`);
25
+ process.exit(1);
26
+ }
@@ -0,0 +1,40 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
+
4
+ # commands
5
+
6
+ ## Purpose
7
+ Concrete implementations of `jeo` subcommands (e.g., launch, setup, team, ultragoal).
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `launch.ts` | The primary interactive/one-shot execution command |
13
+ | `setup.ts` | Guided configuration command |
14
+ | `team.ts` | Multi-agent coordination command |
15
+ | `status.ts` / `update.ts` | Inspection and maintenance commands |
16
+
17
+ ## Subdirectories
18
+ *(None)*
19
+
20
+ ## For AI Agents
21
+
22
+ ### Working In This Directory
23
+ - Commands should handle their own specific setup but delegate core logic to `src/agent/` or `src/tui/`.
24
+ - Maintain clean separation between interactive (TTY) and non-interactive modes.
25
+
26
+ ### Testing Requirements
27
+ - Mock standard streams (stdout/stdin) to test command outputs.
28
+
29
+ ### Common Patterns
30
+ - Command handlers take parsed options, initialize context, and run the main loop or utility function.
31
+
32
+ ## Dependencies
33
+
34
+ ### Internal
35
+ - Connects `src/cli/` routing to `src/agent/` and `src/tui/`.
36
+
37
+ ### External
38
+ *(None)*
39
+
40
+ <!-- MANUAL: -->
@@ -1,9 +1,12 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import {
4
+ readGlobalConfig,
4
5
  readWorkflowState,
5
6
  writeWorkflowState,
6
7
  } from "../agent/state";
8
+ import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
9
+ import { getSubagentRole, subagentRoleIds } from "../agent/subagents";
7
10
 
8
11
  export async function runApproveCommand(args: string[] = []): Promise<void> {
9
12
  const cwd = process.cwd();
@@ -29,7 +32,7 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
29
32
  // Read ralplan state
30
33
  const ralplanState = await readWorkflowState("ralplan", cwd);
31
34
  if (!ralplanState) {
32
- console.log(`[ERROR] No ralplan workflow state found. Please run 'joc ralplan' first.`);
35
+ console.log(`[ERROR] No ralplan workflow state found. Please run 'jeo ralplan' first.`);
33
36
  process.exitCode = 1;
34
37
  return;
35
38
  }
@@ -40,10 +43,23 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
40
43
  return;
41
44
  }
42
45
 
46
+ // Compare canonical (symlink-resolved) paths so a relative arg, an absolute arg,
47
+ // or a /var↔/private/var (macOS) form all match the stored plan path.
48
+ const canonical = async (p: string): Promise<string> => {
49
+ try {
50
+ return await fs.realpath(p);
51
+ } catch {
52
+ return p;
53
+ }
54
+ };
43
55
  const resolvedStatePath = path.resolve(cwd, ralplanState.plan_path);
44
- if (resolvedStatePath !== resolvedInputPath) {
56
+ const [canonInput, canonState] = await Promise.all([canonical(resolvedInputPath), canonical(resolvedStatePath)]);
57
+ if (canonInput !== canonState) {
45
58
  console.log(
46
- `[ERROR] Provided plan path does not match the active plan in the ralplan state.`
59
+ `[ERROR] Provided plan path does not match the active plan in the ralplan state.\n` +
60
+ ` provided: ${resolvedInputPath}\n` +
61
+ ` active: ${resolvedStatePath}\n` +
62
+ ` Run 'jeo approve "${resolvedStatePath}"' to approve the active plan.`
47
63
  );
48
64
  process.exitCode = 1;
49
65
  return;
@@ -55,6 +71,49 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
55
71
  return;
56
72
  }
57
73
 
74
+ // Round-10 #4 (architect ref 8-Round10Planning): approval is a GATE, not a
75
+ // rubber stamp — validate the plan against the exact contract `jeo team`
76
+ // enforces, so a schema-invalid/unknown-role plan is refused HERE instead of
77
+ // aborting later at execution time.
78
+ try {
79
+ const parsed = PlanSchema.safeParse(normalizePlanShape(parseYaml(await fs.readFile(resolvedInputPath, "utf-8"))));
80
+ if (!parsed.success) {
81
+ console.log(
82
+ `[ERROR] Refusing to approve: the plan is not in the shape 'jeo team' executes (top-level 'steps:' list of { name, role? }).\n` +
83
+ ` ${parsed.error.issues[0]?.message ?? "schema mismatch"}\n` +
84
+ ` Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
85
+ );
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ const cfg = await readGlobalConfig();
90
+ const unknown = [...new Set(parsed.data.steps.map(s => s.role?.trim()).filter((r): r is string => !!r && !getSubagentRole(r, cfg)))];
91
+ if (unknown.length > 0) {
92
+ console.log(
93
+ `[ERROR] Refusing to approve: plan references unknown subagent role(s): ${unknown.join(", ")}.\n` +
94
+ ` Known roles: ${subagentRoleIds(cfg).join(", ")}. Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
95
+ );
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ } catch (err: any) {
100
+ console.log(`[ERROR] Refusing to approve: the plan file is not parseable YAML (${err.message}). Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`);
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+
105
+ // Round-11: approval also requires the PERSISTED consensus verdict — a plan
106
+ // that never passed (or failed) the critic gate cannot be approved. States
107
+ // from older ralplan runs lack the field; re-running ralplan heals them.
108
+ if (ralplanState.consensus !== "okay") {
109
+ console.log(
110
+ `[ERROR] Refusing to approve: the plan lacks an [OKAY] consensus verdict (recorded: ${ralplanState.consensus ?? "none"}).\n` +
111
+ ` Re-run 'jeo ralplan' so the consensus critic can review the plan, then approve again.`,
112
+ );
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+
58
117
  // Update ralplan-state.json to approved: true
59
118
  ralplanState.approved = true;
60
119
  await writeWorkflowState("ralplan", ralplanState, cwd);
@@ -1,5 +1,10 @@
1
+ import * as path from "node:path";
2
+ import * as os from "node:os";
3
+ import * as fs from "node:fs/promises";
4
+ import { type StoredOAuth } from "../agent/state";
1
5
  import { createInterface } from "node:readline/promises";
2
6
  import { readGlobalConfig } from "../agent/state";
7
+ import { jeoEnv } from "../util/env";
3
8
  import {
4
9
  OAUTH_FLOWS,
5
10
  OAUTH_FLOW_REGISTRY,
@@ -17,12 +22,24 @@ export async function runAuthCommand(args: string[]): Promise<void> {
17
22
  const sub = args[0];
18
23
  if (!sub || sub === "status") return runAuthStatus();
19
24
  if (sub === "login") return runAuthLogin(args.slice(1));
25
+ if (sub === "import") return runAuthImport(args.slice(1));
20
26
  if (sub === "logout") return runAuthLogout(args[1] as AuthProvider | undefined);
21
27
  if (sub === "refresh") return runAuthRefresh(args[1] as AuthProvider | undefined);
22
- console.log(`Unknown auth subcommand: ${sub}\nUsage: joc auth [login|logout|refresh|status] [provider] [--token <bearer>]`);
28
+ console.log(`Unknown auth subcommand: ${sub}\nUsage: jeo auth [login|logout|refresh|status|import] [provider] [--token <bearer>] [--import]`);
23
29
  process.exitCode = 1;
24
30
  }
25
31
 
32
+ const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity"]
33
+ /** True (and prints an error + sets exit code) when `p` is given but not a known provider. */
34
+ function rejectInvalidProvider(p: string | undefined): boolean {
35
+ if (p !== undefined && !(CLOUD_PROVIDERS as readonly string[]).includes(p)) {
36
+ console.log(`Unknown provider '${p}'. Use one of: ${CLOUD_PROVIDERS.join(", ")}.`);
37
+ process.exitCode = 1;
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
26
43
  function fmtExpiry(expires?: number): string {
27
44
  if (!expires) return "";
28
45
  const ms = expires - Date.now();
@@ -34,13 +51,20 @@ function fmtExpiry(expires?: number): string {
34
51
 
35
52
  async function runAuthStatus(): Promise<void> {
36
53
  const cfg = await readGlobalConfig();
37
- console.log("\n=== joc auth status ===");
54
+ console.log("\n=== jeo auth status ===");
38
55
  console.log("Provider API key OAuth");
39
- for (const p of ["anthropic", "openai", "gemini"] as AuthProvider[]) {
56
+ for (const p of ["anthropic", "openai", "gemini", "antigravity"] as AuthProvider[]) {
40
57
  const snap = await snapshotProvider(p);
41
- const key = snap.apiKey ? "set" : "—";
58
+ const key = p === "antigravity" ? "—" : (snap.apiKey ? "set" : "—");
42
59
  let oauth = "—";
43
- if (snap.oauth) {
60
+ if (p === "antigravity") {
61
+ const fallback = snap.oauth ? snap : await snapshotProvider("gemini");
62
+ if (fallback.oauth) {
63
+ oauth = snap.oauth
64
+ ? `set (refreshable)${fmtExpiry(snap.oauthExpires)}${snap.oauthEmail ? ` <${snap.oauthEmail}>` : ""}`
65
+ : "via gemini fallback";
66
+ }
67
+ } else if (snap.oauth) {
44
68
  oauth = snap.oauthHasRefresh ? "set (refreshable)" : "set (manual)";
45
69
  oauth += fmtExpiry(snap.oauthExpires);
46
70
  if (snap.oauthEmail) oauth += ` <${snap.oauthEmail}>`;
@@ -55,7 +79,19 @@ async function runAuthStatus(): Promise<void> {
55
79
  async function runAuthLogin(rest: string[]): Promise<void> {
56
80
  const tokenIdx = rest.indexOf("--token");
57
81
  const manualToken = tokenIdx >= 0 ? rest[tokenIdx + 1] : undefined;
58
- const provider = rest.find((a, i) => a !== "--token" && rest[i - 1] !== "--token") as AuthProvider | undefined;
82
+ const isImport = rest.includes("--import");
83
+ const provider = rest.find((a, i) => a !== "--token" && rest[i - 1] !== "--token" && a !== "--import") as AuthProvider | undefined;
84
+
85
+ if (rejectInvalidProvider(provider)) return;
86
+
87
+ if (isImport) {
88
+ if (provider !== "gemini") {
89
+ console.log(`[FAILED] Import is only supported for 'gemini' provider.`);
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ return runAuthImport(["gemini"]);
94
+ }
59
95
 
60
96
  const rl = createInterface({ input: process.stdin, output: process.stdout });
61
97
  const chosen = provider ?? (await selectProvider(rl));
@@ -72,37 +108,142 @@ async function runAuthLogin(rest: string[]): Promise<void> {
72
108
  return;
73
109
  }
74
110
 
75
- const flow = OAUTH_FLOW_REGISTRY[chosen];
76
- console.log(`\n=== joc auth login ${flow.label} ===`);
77
- if (!flow.verifiedEndToEnd && flow.note) console.log(`Note: ${flow.note}`);
78
- for (const line of OAUTH_FLOWS[chosen].instructions) console.log(" " + line);
79
- console.log("");
111
+ try {
112
+ const { email } = await interactiveOAuthLogin(chosen, rl);
113
+ console.log(`\n[SUCCESS] OAuth login complete for ${chosen}${email ? ` (${email})` : ""}.`);
114
+ console.log("Stored access + refresh tokens in ~/.jeo/config.json; jeo will auto-refresh on expiry.");
115
+ } catch (err) {
116
+ console.log(`\n[FAILED] ${(err as Error).message}`);
117
+ console.log("Tip: paste the redirect URL when prompted, or use 'jeo auth login <provider> --token <bearer>'.");
118
+ process.exitCode = 1;
119
+ } finally {
120
+ rl.close();
121
+ }
122
+ }
123
+
124
+ async function runAuthImport(rest: string[]): Promise<void> {
125
+ const provider = rest.find(a => a !== "--import") as AuthProvider | undefined;
126
+ if (!provider) {
127
+ console.log("Usage: jeo auth import <provider>");
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ if (rejectInvalidProvider(provider)) return;
132
+ if (provider !== "gemini") {
133
+ console.log(`[FAILED] Import is only supported for 'gemini' provider.`);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
80
137
 
138
+ try {
139
+ const credsPath = jeoEnv("GEMINI_CREDS_PATH") || path.join(os.homedir(), ".gemini", "oauth_creds.json");
140
+ let content: string;
141
+ try {
142
+ content = await fs.readFile(credsPath, "utf-8");
143
+ } catch (err: any) {
144
+ console.log(`[FAILED] Failed to read Gemini credentials file: ${err.message}`);
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+
149
+ let creds: any;
150
+ try {
151
+ creds = JSON.parse(content);
152
+ } catch (err: any) {
153
+ console.log(`[FAILED] Failed to parse Gemini credentials JSON: ${err.message}`);
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+
158
+ if (!creds || !creds.access_token) {
159
+ console.log(`[FAILED] Missing access_token in Gemini credentials.`);
160
+ process.exitCode = 1;
161
+ return;
162
+ }
163
+
164
+ let email: string | undefined;
165
+ if (creds.id_token) {
166
+ const parts = creds.id_token.split(".");
167
+ if (parts.length >= 2) {
168
+ try {
169
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
170
+ const payload = JSON.parse(atob(base64));
171
+ email = payload.email;
172
+ } catch {}
173
+ }
174
+ }
175
+
176
+ let expires = typeof creds.expiry_date === "number" ? creds.expiry_date : (typeof creds.expiry_date === "string" ? parseInt(creds.expiry_date, 10) : undefined);
177
+ if (isNaN(expires as number)) expires = undefined;
178
+
179
+ const imported: StoredOAuth = {
180
+ access: creds.access_token,
181
+ refresh: creds.refresh_token,
182
+ expires,
183
+ email,
184
+ };
185
+
186
+ const { setOauthCredential } = await import("../auth/storage");
187
+ await setOauthCredential("gemini", imported);
188
+
189
+ console.log(`[SUCCESS] Imported OAuth credentials for gemini (also usable as the Antigravity fallback when the backend accepts gemini-cli tokens).`);
190
+ if (email) {
191
+ console.log(`Account email: ${email}`);
192
+ }
193
+ const expiryMsg = expires ? fmtExpiry(expires) : "";
194
+ console.log(`Expiry status:${expiryMsg || " valid"}`);
195
+ } catch (err: any) {
196
+ console.log(`[FAILED] ${err.message}`);
197
+ process.exitCode = 1;
198
+ }
199
+ }
200
+
201
+ /** Prompt object the OAuth manual-code fallback needs (a readline interface satisfies it). */
202
+ export interface OAuthPrompt {
203
+ question(query: string, options?: { signal?: AbortSignal }): Promise<string>;
204
+ }
205
+
206
+ /**
207
+ * Run the interactive OAuth login flow for a provider using an existing prompt
208
+ * (readline) interface. Shared by `jeo auth login` and the REPL `/provider login`.
209
+ * Prints flow instructions, opens the browser, and resolves with the account email.
210
+ */
211
+ export async function interactiveOAuthLogin(
212
+ provider: AuthProvider,
213
+ prompt: OAuthPrompt,
214
+ log: (s: string) => void = console.log,
215
+ ): Promise<{ email?: string }> {
216
+ const flow = OAUTH_FLOW_REGISTRY[provider];
217
+ log(`\n=== OAuth login — ${flow.label} ===`);
218
+ if (!flow.verifiedEndToEnd && flow.note) log(`Note: ${flow.note}`);
219
+ for (const line of OAUTH_FLOWS[provider].instructions) log(" " + line);
220
+ log("");
221
+
222
+ // Abort the pending "Paste redirect URL…" question once the flow settles.
223
+ // Without this the dangling readline question survives a SUCCESS/FAILED
224
+ // result, reprints its prompt over the result line, and queues in FRONT of
225
+ // any follow-up question (the `jeo setup` API-key fallback appeared hung).
226
+ const ac = new AbortController();
81
227
  const ctrl: OAuthController = {
228
+ signal: ac.signal,
82
229
  onAuth: ({ url, instructions }) => {
83
- console.log(`Opening browser:\n ${url}\n`);
84
- if (instructions) console.log(instructions + "\n");
230
+ log(`Opening browser:\n ${url}\n`);
231
+ if (instructions) log(instructions + "\n");
85
232
  void openInBrowser(url);
86
233
  },
87
- onProgress: msg => console.log(` … ${msg}`),
234
+ onProgress: msg => log(` … ${msg}`),
88
235
  onManualCodeInput: async () =>
89
- (await rl.question("Paste redirect URL or code (or wait for the browser callback): ")).trim(),
236
+ (await prompt.question("Paste redirect URL or code (or wait for the browser callback): ", { signal: ac.signal })).trim(),
90
237
  };
91
-
92
238
  try {
93
- const { email } = await interactiveLogin(chosen, ctrl);
94
- console.log(`\n[SUCCESS] OAuth login complete for ${chosen}${email ? ` (${email})` : ""}.`);
95
- console.log("Stored access + refresh tokens in ~/.joc/config.json; joc will auto-refresh on expiry.");
96
- } catch (err) {
97
- console.log(`\n[FAILED] ${(err as Error).message}`);
98
- console.log("Tip: paste the redirect URL when prompted, or use 'joc auth login <provider> --token <bearer>'.");
99
- process.exitCode = 1;
239
+ return await interactiveLogin(provider, ctrl);
100
240
  } finally {
101
- rl.close();
241
+ ac.abort();
102
242
  }
103
243
  }
104
244
 
105
245
  async function runAuthLogout(provider?: AuthProvider): Promise<void> {
246
+ if (rejectInvalidProvider(provider)) return;
106
247
  const rl = createInterface({ input: process.stdin, output: process.stdout });
107
248
  const chosen = provider ?? (await selectProvider(rl, "logout"));
108
249
  rl.close();
@@ -113,10 +254,11 @@ async function runAuthLogout(provider?: AuthProvider): Promise<void> {
113
254
 
114
255
  async function runAuthRefresh(provider?: AuthProvider): Promise<void> {
115
256
  if (!provider) {
116
- console.log("Usage: joc auth refresh <provider>");
257
+ console.log("Usage: jeo auth refresh <provider>");
117
258
  process.exitCode = 1;
118
259
  return;
119
260
  }
261
+ if (rejectInvalidProvider(provider)) return;
120
262
  const result = await refreshOAuthToken(provider);
121
263
  console.log(
122
264
  result.refreshed