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
@@ -1,9 +1,14 @@
1
- import { readGlobalConfig, saveGlobalConfig, type Config, type StoredOAuth } from "../agent/state";
1
+ import * as path from "node:path";
2
+ import * as os from "node:os";
3
+ import * as fs from "node:fs/promises";
4
+ import { readGlobalConfig, readRawGlobalConfig, saveConfigPatch, type StoredOAuth } from "../agent/state";
5
+ import { jeoEnv } from "../util/env";
2
6
 
3
- export type AuthProvider = "anthropic" | "openai" | "gemini";
7
+
8
+ export type AuthProvider = "anthropic" | "openai" | "gemini" | "antigravity";
4
9
 
5
10
  export type Credential =
6
- | { kind: "oauth"; provider: AuthProvider; token: string }
11
+ | { kind: "oauth"; provider: AuthProvider; token: string; projectId?: string }
7
12
  | { kind: "api_key"; provider: AuthProvider; token: string }
8
13
  | { kind: "none"; provider: AuthProvider };
9
14
 
@@ -17,17 +22,123 @@ export interface AuthSnapshot {
17
22
  }
18
23
 
19
24
  const inFlightRefresh = new Map<AuthProvider, Promise<any>>();
25
+ function getLockPath(provider: AuthProvider): string {
26
+ const dir = jeoEnv("CONFIG_DIR") || path.join(os.homedir(), ".jeo");
27
+ return path.join(dir, `oauth-${provider}.lock`);
28
+ }
29
+
30
+ export async function acquireLock(provider: AuthProvider, timeoutMs = 5000): Promise<void> {
31
+ const lockPath = getLockPath(provider);
32
+ const dir = path.dirname(lockPath);
33
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
34
+
35
+ // `timeoutMs` is the STALENESS threshold for a dead holder's lock file; the
36
+ // acquisition wait itself is bounded at 2× that. The previous unbounded 50ms
37
+ // retry loop spun forever when the stale lock could not be unlinked (or was
38
+ // recreated under churn by concurrent sessions) — a mid-turn OAuth refresh
39
+ // then froze the whole agent turn with no diagnostic.
40
+ const deadline = Date.now() + Math.max(timeoutMs * 2, 1_000);
41
+ while (true) {
42
+ try {
43
+ const handle = await fs.open(lockPath, "wx");
44
+ const info = JSON.stringify({ pid: process.pid, createdAt: Date.now() });
45
+ await handle.writeFile(info, "utf-8");
46
+ await handle.close();
47
+ return;
48
+ } catch (err: any) {
49
+ if (err.code !== "EEXIST") {
50
+ throw err;
51
+ }
52
+ try {
53
+ const content = await fs.readFile(lockPath, "utf-8");
54
+ const info = JSON.parse(content);
55
+ if (typeof info.createdAt === "number" && info.createdAt + timeoutMs < Date.now()) {
56
+ await fs.unlink(lockPath).catch(() => {});
57
+ }
58
+ } catch {
59
+ try {
60
+ const stat = await fs.stat(lockPath);
61
+ if (stat.mtimeMs + timeoutMs < Date.now()) {
62
+ await fs.unlink(lockPath).catch(() => {});
63
+ }
64
+ } catch {}
65
+ }
66
+ }
67
+ if (Date.now() >= deadline) {
68
+ // Deadline reached: the holder is dead or wedged. Steal once — the lock
69
+ // guards a short config read-modify-write, so waiting longer only hangs
70
+ // the caller (and with it the live turn that triggered the refresh).
71
+ await fs.unlink(lockPath).catch(() => {});
72
+ const handle = await fs.open(lockPath, "wx").catch(() => null);
73
+ if (handle) {
74
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), stolen: true }), "utf-8");
75
+ await handle.close();
76
+ return;
77
+ }
78
+ throw new Error(`OAuth lock for ${provider} could not be acquired within ${Math.max(timeoutMs * 2, 1_000)}ms (${lockPath})`);
79
+ }
80
+ await new Promise((resolve) => setTimeout(resolve, 50));
81
+ }
82
+ }
83
+
84
+ export async function releaseLock(provider: AuthProvider): Promise<void> {
85
+ const lockPath = getLockPath(provider);
86
+ await fs.unlink(lockPath).catch(() => {});
87
+ }
88
+
20
89
 
21
90
  function accessOf(stored: string | StoredOAuth | undefined): string | undefined {
22
91
  if (!stored) return undefined;
23
92
  return typeof stored === "string" ? stored : stored.access;
24
93
  }
25
94
 
26
- /** Single point of resolution: OAuth bearer beats API key when both exist. */
95
+ /** Raw credential resolver: returns refreshable OAuth first; execution/status may override with an API key when both exist. */
27
96
  export async function resolveCredential(provider: AuthProvider): Promise<Credential> {
28
97
  const cfg = await readGlobalConfig();
29
- const stored = cfg.oauth?.[provider];
98
+ let stored = cfg.oauth?.[provider];
99
+
100
+ // Auto-import gemini-cli credentials (~/.gemini/oauth_creds.json) when jeo has no
101
+ // gemini OAuth of its own — the out-of-the-box antigravity/gemini unlock. Hermetic
102
+ // under tests/custom config sandboxes: if JEO_CONFIG_DIR is explicitly set, only an
103
+ // explicit JEO_GEMINI_CREDS_PATH opts in, so tests never read the developer's real
104
+ // credentials or refresh real tokens.
105
+ const credsOverride = jeoEnv("GEMINI_CREDS_PATH");
106
+ const configDirOverridden = !!jeoEnv("CONFIG_DIR");
107
+ if (!stored && provider === "gemini" && (credsOverride || !configDirOverridden)) {
108
+ try {
109
+ const credsPath = jeoEnv("GEMINI_CREDS_PATH") || path.join(os.homedir(), ".gemini", "oauth_creds.json");
110
+ const content = await fs.readFile(credsPath, "utf-8");
111
+ const creds = JSON.parse(content);
112
+ if (creds && creds.access_token) {
113
+ let email: string | undefined;
114
+ if (creds.id_token) {
115
+ const parts = creds.id_token.split(".");
116
+ if (parts.length >= 2) {
117
+ try {
118
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
119
+ const payload = JSON.parse(atob(base64));
120
+ email = payload.email;
121
+ } catch {}
122
+ }
123
+ }
124
+ let expires = typeof creds.expiry_date === "number" ? creds.expiry_date : (typeof creds.expiry_date === "string" ? parseInt(creds.expiry_date, 10) : undefined);
125
+ if (isNaN(expires as number)) expires = undefined;
30
126
 
127
+ const imported: StoredOAuth = {
128
+ access: creds.access_token,
129
+ refresh: creds.refresh_token,
130
+ expires,
131
+ email,
132
+ };
133
+ await setOauthCredential("gemini", imported);
134
+ // stderr, NOT stdout: --json consumers (doctor, models) parse stdout.
135
+ console.error(`[NOTICE] Transparently imported Gemini OAuth credentials from ~/.gemini/oauth_creds.json`);
136
+ stored = imported;
137
+ }
138
+ } catch {
139
+ // never break existing resolution
140
+ }
141
+ }
31
142
  if (stored) {
32
143
  // Auto-refresh refreshable credentials that are past their expiry.
33
144
  if (typeof stored !== "string" && stored.refresh && stored.expires && stored.expires <= Date.now()) {
@@ -39,9 +150,10 @@ export async function resolveCredential(provider: AuthProvider): Promise<Credent
39
150
  return refreshOAuthToken(provider);
40
151
  })();
41
152
  inFlightRefresh.set(provider, refreshPromise);
42
- refreshPromise.finally(() => {
153
+ // Cleanup must not create its own unobserved rejection if the refresh rejects.
154
+ void refreshPromise.finally(() => {
43
155
  inFlightRefresh.delete(provider);
44
- });
156
+ }).catch(() => {});
45
157
  }
46
158
  const result = await refreshPromise;
47
159
  if (result.refreshed && result.credential.kind === "oauth") {
@@ -52,7 +164,14 @@ export async function resolveCredential(provider: AuthProvider): Promise<Credent
52
164
  }
53
165
  }
54
166
  const token = accessOf(stored);
55
- if (token) return { kind: "oauth", provider, token };
167
+ if (token) {
168
+ return {
169
+ kind: "oauth",
170
+ provider,
171
+ token,
172
+ projectId: typeof stored === "object" ? stored.projectId : undefined,
173
+ };
174
+ }
56
175
  }
57
176
 
58
177
  const apiKey = cfg.providers[provider];
@@ -81,33 +200,37 @@ export async function getStoredOAuth(provider: AuthProvider): Promise<StoredOAut
81
200
 
82
201
  /** Persist a plain bearer token (legacy / manual paste — no refresh metadata). */
83
202
  export async function setOauthToken(provider: AuthProvider, token: string): Promise<void> {
84
- const cfg = await readGlobalConfig();
85
- const next: Config = JSON.parse(JSON.stringify(cfg));
86
- next.oauth = next.oauth ?? {};
87
- next.oauth[provider] = token;
88
- await saveGlobalConfig(next);
203
+ // Persist onto the RAW on-disk config (not env-overlaid) so a short-lived
204
+ // *_OAUTH_TOKEN env / OLLAMA_HOST / role tier is never baked into config.json.
205
+ await saveConfigPatch(raw => ({ oauth: { ...(raw.oauth ?? {}), [provider]: token } }));
206
+ }
207
+
208
+ /** Persist a full OAuth credential set (access + refresh + expiry). */
209
+ export async function setOauthCredentialNoLock(provider: AuthProvider, cred: StoredOAuth): Promise<void> {
210
+ await saveConfigPatch(raw => ({ oauth: { ...(raw.oauth ?? {}), [provider]: cred } }));
89
211
  }
90
212
 
91
213
  /** Persist a full OAuth credential set (access + refresh + expiry). */
92
214
  export async function setOauthCredential(provider: AuthProvider, cred: StoredOAuth): Promise<void> {
93
- const cfg = await readGlobalConfig();
94
- const next: Config = JSON.parse(JSON.stringify(cfg));
95
- next.oauth = next.oauth ?? {};
96
- next.oauth[provider] = cred;
97
- await saveGlobalConfig(next);
215
+ await acquireLock(provider);
216
+ try {
217
+ await setOauthCredentialNoLock(provider, cred);
218
+ } finally {
219
+ await releaseLock(provider);
220
+ }
98
221
  }
99
222
 
100
223
  export async function clearOauthToken(provider: AuthProvider): Promise<boolean> {
101
- const cfg = await readGlobalConfig();
102
- if (!cfg.oauth?.[provider]) return false;
103
- delete cfg.oauth[provider];
104
- await saveGlobalConfig(cfg);
224
+ const raw = await readRawGlobalConfig();
225
+ if (!raw.oauth?.[provider]) return false;
226
+ await saveConfigPatch(r => {
227
+ const oauth = { ...(r.oauth ?? {}) };
228
+ delete oauth[provider];
229
+ return { oauth };
230
+ });
105
231
  return true;
106
232
  }
107
233
 
108
234
  export async function setApiKey(provider: AuthProvider, key: string): Promise<void> {
109
- const cfg = await readGlobalConfig();
110
- const next: Config = JSON.parse(JSON.stringify(cfg));
111
- next.providers[provider] = key;
112
- await saveGlobalConfig(next);
235
+ await saveConfigPatch(raw => ({ providers: { ...(raw.providers ?? {}), [provider]: key } }));
113
236
  }
package/src/auth/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** OAuth flow types, mirroring gjc's oauth/types.ts (trimmed to joc's surface). */
1
+ /** OAuth flow types, mirroring gjc's oauth/types.ts (trimmed to jeo's surface). */
2
2
 
3
3
  export interface OAuthCredentials {
4
4
  access: string;
@@ -0,0 +1,362 @@
1
+ /**
2
+ * jeo autopilot — autonomous build loop hardened with autoresearch ratcheting.
3
+ *
4
+ * Fuses two skills:
5
+ * - /skill:autopilot : end-to-end plan -> implement -> verify -> stop loop
6
+ * - /skill:autoresearch : frozen evaluator, one change per step, keep-if-improved /
7
+ * revert-otherwise by score, append-only log, baseline-first,
8
+ * convergence/stop discipline.
9
+ *
10
+ * The engine owns the RATCHET BRAIN (decision + evidence ledger), not destructive
11
+ * git ops. Mutation ("make one change") is supplied by the operator/agent via a
12
+ * runner command; reverts run an operator-supplied --on-revert hook.
13
+ *
14
+ * State (per cwd):
15
+ * .jeo/autopilot/session.json frozen contract (immutable for the session)
16
+ * .jeo/autopilot/log.jsonl append-only attempt log (baseline, steps, stops)
17
+ *
18
+ * No external dependencies (Node stdlib only). Runs under Bun or Node.
19
+ */
20
+
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import { execSync } from "node:child_process";
24
+ import { renderAutopilotStatusPanel, type AutopilotStatusPanelData } from "./tui/components/autopilot-status";
25
+
26
+ const AP_DIR = path.join(".jeo", "autopilot");
27
+ const SESSION = path.join(AP_DIR, "session.json");
28
+ const LOG = path.join(AP_DIR, "log.jsonl");
29
+ function getShell(): string | undefined {
30
+ if (process.platform === "win32") return undefined;
31
+ return process.env.SHELL || "/bin/bash";
32
+ }
33
+
34
+ type Goal = "min" | "max" | "gate";
35
+
36
+ interface Session {
37
+ task: string;
38
+ evalCmd: string;
39
+ goal: Goal;
40
+ timeoutSec: number;
41
+ patience: number;
42
+ createdAt: string;
43
+ frozen: true;
44
+ }
45
+
46
+ interface LogEvent {
47
+ ts: string;
48
+ type: "baseline" | "step" | "stop";
49
+ [k: string]: unknown;
50
+ }
51
+
52
+ type LogEventInput = {
53
+ type: LogEvent["type"];
54
+ [k: string]: unknown;
55
+ };
56
+
57
+ function die(msg: string): never {
58
+ console.error(`jeo autopilot: ${msg}`);
59
+ process.exit(1);
60
+ }
61
+
62
+ function parseArgs(argv: string[]): { positionals: string[]; flags: Record<string, string> } {
63
+ const positionals: string[] = [];
64
+ const flags: Record<string, string> = {};
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a.startsWith("--")) {
68
+ const key = a.slice(2);
69
+ const next = argv[i + 1];
70
+ if (next === undefined || next.startsWith("--")) flags[key] = "true";
71
+ else {
72
+ flags[key] = next;
73
+ i++;
74
+ }
75
+ } else positionals.push(a);
76
+ }
77
+ return { positionals, flags };
78
+ }
79
+
80
+ function loadSession(): Session {
81
+ if (!fs.existsSync(SESSION)) die("no session — run: jeo autopilot init --task <t> --eval <cmd>");
82
+ return JSON.parse(fs.readFileSync(SESSION, "utf8")) as Session;
83
+ }
84
+
85
+ function appendLog(ev: LogEventInput): LogEvent {
86
+ const full: LogEvent = { ts: new Date().toISOString(), ...ev };
87
+ fs.appendFileSync(LOG, JSON.stringify(full) + "\n");
88
+ return full;
89
+ }
90
+
91
+ function readLog(): LogEvent[] {
92
+ if (!fs.existsSync(LOG)) return [];
93
+ return fs
94
+ .readFileSync(LOG, "utf8")
95
+ .split("\n")
96
+ .filter((l) => l.trim())
97
+ .map((l) => JSON.parse(l) as LogEvent);
98
+ }
99
+
100
+ /** Run the frozen eval. Returns { score, passed }. score is NaN when no score: line. */
101
+ function runEval(s: Session): { score: number; passed: boolean; output: string } {
102
+ let output = "";
103
+ let passed = true;
104
+ try {
105
+ output = execSync(s.evalCmd, {
106
+ encoding: "utf8",
107
+ timeout: s.timeoutSec * 1000,
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ shell: getShell(),
110
+ });
111
+ } catch (e: unknown) {
112
+ passed = false;
113
+ const err = e as { stdout?: string; stderr?: string };
114
+ output = (err.stdout ?? "") + (err.stderr ?? "");
115
+ }
116
+ const matches = [...output.matchAll(/score:\s*(-?\d+(?:\.\d+)?)/gi)];
117
+ const score = matches.length ? Number(matches[matches.length - 1][1]) : NaN;
118
+ return { score, passed, output: output.trim() };
119
+ }
120
+
121
+ /** Best kept score so far, folding baseline + kept steps. undefined if none. */
122
+ function currentBest(s: Session): number | undefined {
123
+ let best: number | undefined;
124
+ for (const ev of readLog()) {
125
+ if (ev.type === "baseline" || (ev.type === "step" && ev.decision === "keep")) {
126
+ const sc = ev.score as number;
127
+ if (typeof sc === "number" && !Number.isNaN(sc)) {
128
+ if (best === undefined) best = sc;
129
+ else if (s.goal === "min") best = Math.min(best, sc);
130
+ else if (s.goal === "max") best = Math.max(best, sc);
131
+ else best = sc;
132
+ }
133
+ }
134
+ }
135
+ return best;
136
+ }
137
+
138
+ function isImprovement(goal: Goal, score: number, best: number | undefined): boolean {
139
+ if (best === undefined) return true;
140
+ if (goal === "min") return score < best;
141
+ if (goal === "max") return score > best;
142
+ return true; // gate handled via passed, not score
143
+ }
144
+
145
+ function hasBaseline(): boolean {
146
+ return readLog().some((e) => e.type === "baseline");
147
+ }
148
+
149
+ // ── commands ──────────────────────────────────────────────────────────────
150
+
151
+ function cmdInit(flags: Record<string, string>): void {
152
+ if (fs.existsSync(SESSION) && flags.force !== "true") {
153
+ die(`session already frozen at ${SESSION} (use --force to overwrite)`);
154
+ }
155
+ if (!flags.task) die("init requires --task");
156
+ if (!flags.eval) die("init requires --eval <command that prints 'score: N' or exits 0/1>");
157
+ const goal = (flags.goal ?? "min") as Goal;
158
+ if (!["min", "max", "gate"].includes(goal)) die("--goal must be min|max|gate");
159
+ fs.mkdirSync(AP_DIR, { recursive: true });
160
+ const session: Session = {
161
+ task: flags.task,
162
+ evalCmd: flags.eval,
163
+ goal,
164
+ timeoutSec: flags.timeout ? Number(flags.timeout) : 300,
165
+ patience: flags.patience ? Number(flags.patience) : 3,
166
+ createdAt: new Date().toISOString(),
167
+ frozen: true,
168
+ };
169
+ fs.writeFileSync(SESSION, JSON.stringify(session, null, 2) + "\n");
170
+ // fresh log on (re)init
171
+ fs.writeFileSync(LOG, "");
172
+ console.log(`jeo autopilot: session frozen → ${SESSION}`);
173
+ console.log(` task=${session.task}`);
174
+ console.log(` eval=${session.evalCmd} goal=${session.goal} timeout=${session.timeoutSec}s patience=${session.patience}`);
175
+ }
176
+
177
+ function cmdBaseline(): void {
178
+ const s = loadSession();
179
+ if (hasBaseline()) die("baseline already recorded (re-init to reset)");
180
+ const { score, passed, output } = runEval(s);
181
+ appendLog({ type: "baseline", score, passed, output });
182
+ console.log(`jeo autopilot: baseline score=${fmt(score)} passed=${passed}`);
183
+ }
184
+
185
+ function cmdStep(flags: Record<string, string>): void {
186
+ const s = loadSession();
187
+ if (s.goal !== "gate" && !hasBaseline()) die("record a baseline first: jeo autopilot baseline");
188
+ const change = flags.change ?? "(unspecified change)";
189
+ const best = currentBest(s);
190
+ const { score, passed, output } = runEval(s);
191
+
192
+ let decision: "keep" | "revert";
193
+ if (s.goal === "gate") {
194
+ decision = passed ? "keep" : "revert";
195
+ } else if (Number.isNaN(score)) {
196
+ decision = "revert"; // no measurable score => cannot prove improvement
197
+ } else {
198
+ decision = isImprovement(s.goal, score, best) ? "keep" : "revert";
199
+ }
200
+
201
+ if (decision === "revert" && flags["on-revert"]) {
202
+ try {
203
+ execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
204
+ } catch {
205
+ console.error("jeo autopilot: --on-revert hook failed (decision still logged)");
206
+ }
207
+ }
208
+
209
+ appendLog({ type: "step", change, score, passed, decision, prevBest: best ?? null, output });
210
+ const cmp =
211
+ s.goal === "gate"
212
+ ? passed ? "pass" : "fail"
213
+ : `${fmt(score)} vs best ${fmt(best)}`;
214
+ console.log(`jeo autopilot: step ${decision.toUpperCase()} (${cmp}) — ${change}`);
215
+ }
216
+
217
+ function cmdLoop(flags: Record<string, string>): void {
218
+ const s = loadSession();
219
+ const runner = flags.runner;
220
+ if (!runner) die("loop requires --runner <command that makes ONE change>");
221
+ const max = flags.max ? Number(flags.max) : 10;
222
+ if (s.goal !== "gate" && !hasBaseline()) {
223
+ const { score, passed, output } = runEval(s);
224
+ appendLog({ type: "baseline", score, passed, output });
225
+ console.log(`jeo autopilot: auto-baseline score=${fmt(score)}`);
226
+ }
227
+
228
+ let sinceImprove = 0;
229
+ for (let i = 1; i <= max; i++) {
230
+ // mutate: runner makes exactly one change
231
+ let runnerOk = true;
232
+ try {
233
+ execSync(runner, { stdio: "inherit", timeout: s.timeoutSec * 1000, shell: getShell() });
234
+ } catch {
235
+ runnerOk = false;
236
+ }
237
+ if (!runnerOk) {
238
+ appendLog({ type: "stop", reason: "runner_failed", iteration: i });
239
+ console.log(`jeo autopilot: stop — runner failed at iteration ${i}`);
240
+ return;
241
+ }
242
+
243
+ const best = currentBest(s);
244
+ const { score, passed, output } = runEval(s);
245
+ let decision: "keep" | "revert";
246
+ if (s.goal === "gate") decision = passed ? "keep" : "revert";
247
+ else if (Number.isNaN(score)) decision = "revert";
248
+ else decision = isImprovement(s.goal, score, best) ? "keep" : "revert";
249
+
250
+ if (decision === "revert" && flags["on-revert"]) {
251
+ try {
252
+ execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
253
+ } catch {
254
+ /* logged below regardless */
255
+ }
256
+ }
257
+ appendLog({ type: "step", iteration: i, change: `loop#${i}`, score, passed, decision, prevBest: best ?? null, output });
258
+ const improved = decision === "keep" && (s.goal === "gate" || !Number.isNaN(score));
259
+ sinceImprove = improved && (best === undefined || s.goal === "gate" || isImprovement(s.goal, score, best)) ? 0 : sinceImprove + 1;
260
+ console.log(`jeo autopilot: loop ${i}/${max} ${decision.toUpperCase()} score=${fmt(score)} (sinceImprove=${sinceImprove})`);
261
+
262
+ if (s.goal !== "gate" && sinceImprove >= s.patience) {
263
+ appendLog({ type: "stop", reason: "converged", iteration: i, patience: s.patience });
264
+ console.log(`jeo autopilot: stop — converged (no improvement in ${s.patience} steps)`);
265
+ return;
266
+ }
267
+ }
268
+ appendLog({ type: "stop", reason: "max_iterations", iteration: max });
269
+ console.log(`jeo autopilot: stop — reached max ${max} iterations`);
270
+ }
271
+
272
+ function cmdStatus(flags: Record<string, string>): void {
273
+ const s = loadSession();
274
+ const log = readLog();
275
+ const steps = log.filter((e) => e.type === "step");
276
+ const kept = steps.filter((e) => e.decision === "keep").length;
277
+ const reverted = steps.filter((e) => e.decision === "revert").length;
278
+ const baseline = log.find((e) => e.type === "baseline");
279
+ const best = currentBest(s);
280
+ const stop = [...log].reverse().find((e) => e.type === "stop");
281
+
282
+ // convergence: steps since last keep-with-improvement
283
+ let sinceImprove = 0;
284
+ for (const e of steps) {
285
+ if (e.decision === "keep") sinceImprove = 0;
286
+ else sinceImprove++;
287
+ }
288
+ const converged = s.goal !== "gate" && sinceImprove >= s.patience;
289
+
290
+ let recommendation: string;
291
+ if (stop) recommendation = `stopped: ${stop.reason as string}`;
292
+ else if (converged) recommendation = "converged — stop or change strategy";
293
+ else recommendation = "continue";
294
+
295
+ const out: AutopilotStatusPanelData = {
296
+ task: s.task,
297
+ goal: s.goal,
298
+ eval: s.evalCmd,
299
+ baseline: fmt(baseline ? (baseline.score as number) : null),
300
+ best: fmt(best ?? null),
301
+ attempts: steps.length,
302
+ kept,
303
+ reverted,
304
+ sinceImprove,
305
+ converged,
306
+ recommendation,
307
+ };
308
+
309
+ if (flags.json === "true") {
310
+ console.log(JSON.stringify({
311
+ ...out,
312
+ baseline: baseline ? (baseline.score as number) : null,
313
+ best: best ?? null,
314
+ }, null, 2));
315
+ return;
316
+ }
317
+ console.log(renderAutopilotStatusPanel(out, {
318
+ cols: process.stdout.columns || 88,
319
+ color: !!process.stdout.isTTY,
320
+ unicode: true,
321
+ }).join("\n"));
322
+ }
323
+
324
+ function fmt(n: number | null | undefined): string {
325
+ if (n === null || n === undefined || Number.isNaN(n)) return "—";
326
+ return String(n);
327
+ }
328
+
329
+ function help(): void {
330
+ console.log(
331
+ [
332
+ "jeo autopilot — autonomous build loop with autoresearch ratcheting",
333
+ "",
334
+ " init --task <t> --eval <cmd> [--goal min|max|gate] [--timeout S] [--patience N]",
335
+ " baseline",
336
+ " step --change <desc> [--on-revert <cmd>]",
337
+ " loop --runner <cmd> [--max N] [--on-revert <cmd>]",
338
+ " status [--json]",
339
+ "",
340
+ "eval contract: command prints 'score: <number>' (min/max goals) or exits 0/1 (gate goal).",
341
+ ].join("\n"),
342
+ );
343
+ }
344
+
345
+ export function runAutopilot(argv: string[]): void {
346
+ const [cmd, ...rest] = argv;
347
+ const { flags } = parseArgs(rest);
348
+ switch (cmd) {
349
+ case "init": cmdInit(flags); break;
350
+ case "baseline": cmdBaseline(); break;
351
+ case "step": cmdStep(flags); break;
352
+ case "loop": cmdLoop(flags); break;
353
+ case "status": cmdStatus(flags); break;
354
+ case undefined:
355
+ case "help":
356
+ case "--help": help(); break;
357
+ default: die(`unknown subcommand: ${cmd} (try: jeo autopilot help)`);
358
+ }
359
+ }
360
+
361
+ // allow running this module directly: bun src/autopilot.ts <args>
362
+ if (import.meta.main) runAutopilot(process.argv.slice(2));
@@ -0,0 +1,4 @@
1
+ declare module "*.md" {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -0,0 +1,39 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
+
4
+ # cli
5
+
6
+ ## Purpose
7
+ Command-line interface routing, argument parsing, and initialization logic. Defines the shape of the `jeo` binary interface.
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `parser.ts` | Argument parsing and flag validation |
13
+ | `router.ts` | Dispatches CLI commands to their respective implementations |
14
+
15
+ ## Subdirectories
16
+ *(None)*
17
+
18
+ ## For AI Agents
19
+
20
+ ### Working In This Directory
21
+ - Keep parsing logic declarative.
22
+ - Ensure all flags have clear help text and defaults.
23
+ - Delegate actual command execution to `src/commands/`.
24
+
25
+ ### Testing Requirements
26
+ - Test CLI arg parsing with various flag combinations.
27
+
28
+ ### Common Patterns
29
+ - Early exit for `--help` and `--version`.
30
+
31
+ ## Dependencies
32
+
33
+ ### Internal
34
+ - Routes to `src/commands/`.
35
+
36
+ ### External
37
+ - Standard CLI arg parsing libraries or custom minimal parsers.
38
+
39
+ <!-- MANUAL: -->