jeo-code 0.1.0 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +808 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +624 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
package/src/agent/state.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { parseConfig } from "./config-schema";
|
|
5
|
+
import { jeoEnv } from "../util/env";
|
|
5
6
|
|
|
6
7
|
/** Persisted OAuth credential set (access + refresh + expiry) for a provider. */
|
|
7
8
|
export interface StoredOAuth {
|
|
@@ -14,30 +15,45 @@ export interface StoredOAuth {
|
|
|
14
15
|
projectId?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export interface HookConfig {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
hooks?: Array<{
|
|
21
|
+
event: "pre-tool" | "post-turn" | "post-implementation";
|
|
22
|
+
match?: { tool?: string };
|
|
23
|
+
run: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export interface Config {
|
|
18
29
|
providers: {
|
|
19
30
|
anthropic?: string;
|
|
20
31
|
openai?: string;
|
|
21
32
|
gemini?: string;
|
|
33
|
+
antigravity?: string;
|
|
22
34
|
};
|
|
23
35
|
/**
|
|
24
|
-
* OAuth credentials (
|
|
25
|
-
*
|
|
26
|
-
*
|
|
36
|
+
* OAuth credentials. `resolveCredential()` returns these before API keys so refresh
|
|
37
|
+
* metadata is not lost, but provider execution/status applies the GJC parity rule:
|
|
38
|
+
* an API key is broader and wins whenever both key + OAuth exist.
|
|
27
39
|
*/
|
|
28
40
|
oauth?: {
|
|
29
41
|
anthropic?: string | StoredOAuth;
|
|
30
42
|
openai?: string | StoredOAuth;
|
|
31
43
|
gemini?: string | StoredOAuth;
|
|
44
|
+
antigravity?: string | StoredOAuth;
|
|
32
45
|
};
|
|
33
46
|
/** Base URL for the local Ollama server (keyless). */
|
|
34
47
|
ollamaBaseUrl?: string;
|
|
35
48
|
/** Base URL override for OpenAI-compatible providers (LM Studio, vLLM, llama-cpp-server, ...). */
|
|
36
49
|
openaiBaseUrl?: string;
|
|
37
50
|
defaultModel: string;
|
|
51
|
+
theme?: string;
|
|
38
52
|
thinkingLevel?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
39
53
|
/** Friendly model aliases, e.g. { fast: "ollama/qwen2.5:0.5b" }. Override built-ins. */
|
|
40
54
|
modelAliases?: { [alias: string]: string };
|
|
55
|
+
/** Most-recently-selected models, newest first (MRU; head == defaultModel). */
|
|
56
|
+
recentModels?: string[];
|
|
41
57
|
/**
|
|
42
58
|
* Provider retry budgets (gjc parity). `requestMaxRetries` is the number of
|
|
43
59
|
* retries (excluding the initial request) for a provider request; `maxDelayMs`
|
|
@@ -49,18 +65,57 @@ export interface Config {
|
|
|
49
65
|
streamMaxRetries?: number;
|
|
50
66
|
maxRetries?: number;
|
|
51
67
|
maxDelayMs?: number;
|
|
68
|
+
/** Retries (excluding the initial request) specifically for 429 rate limits. */
|
|
69
|
+
rateLimitRetries?: number;
|
|
70
|
+
/** Minimum backoff (ms) for a 429 when the server sends no Retry-After. */
|
|
71
|
+
rateLimitMinDelayMs?: number;
|
|
72
|
+
/** HTTP statuses to treat as NON-retryable even when defaultRetryable would
|
|
73
|
+
* retry them (e.g. pin 503 to fail fast instead of riding the backoff ladder). */
|
|
74
|
+
failFastStatuses?: number[];
|
|
75
|
+
/** Case-insensitive substrings; an error whose message matches any of these
|
|
76
|
+
* fails fast (non-retryable) even when the chosen predicate would retry it. */
|
|
77
|
+
failFastPatterns?: string[];
|
|
52
78
|
};
|
|
53
79
|
/**
|
|
54
|
-
* Per-subagent-role overrides (gjc role-agent parity)
|
|
55
|
-
* (executor / planner / architect / critic)
|
|
56
|
-
*
|
|
80
|
+
* Per-subagent-role overrides (gjc role-agent parity), keyed by role id.
|
|
81
|
+
* Bundled ids (executor / planner / architect / critic) take model / maxSteps /
|
|
82
|
+
* thinking pins. A NON-bundled id that declares `title`, `description`, or
|
|
83
|
+
* `prompt` becomes a config-defined CUSTOM ROLE (system-driven registry —
|
|
84
|
+
* see rolesFromConfig). Custom roles are read-only unless `readOnly: false`.
|
|
57
85
|
*/
|
|
58
|
-
subagents?: {
|
|
86
|
+
subagents?: {
|
|
87
|
+
[roleId: string]: {
|
|
88
|
+
model?: string;
|
|
89
|
+
maxSteps?: number;
|
|
90
|
+
thinking?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
91
|
+
title?: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
prompt?: string;
|
|
94
|
+
readOnly?: boolean;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
59
97
|
/**
|
|
60
98
|
* Model role tiers (gjc `--smol`/`--slow`/`--plan` parity). Each falls back to
|
|
61
|
-
* `defaultModel`. Env `
|
|
99
|
+
* `defaultModel`. Env `JEO_SMOL_MODEL`/`JEO_SLOW_MODEL`/`JEO_PLAN_MODEL` fill gaps.
|
|
62
100
|
*/
|
|
63
101
|
roles?: { smol?: string; slow?: string; plan?: string };
|
|
102
|
+
hooks?: HookConfig;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WorkflowTopologyComponent {
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
description: string;
|
|
109
|
+
status: "active" | "deferred";
|
|
110
|
+
evidence?: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface WorkflowTopologyState {
|
|
114
|
+
status: "pending" | "confirmed" | "legacy_missing";
|
|
115
|
+
confirmed_at?: string | null;
|
|
116
|
+
components: WorkflowTopologyComponent[];
|
|
117
|
+
deferrals?: Array<{ component_id: string; reason: string; confirmed_at: string }>;
|
|
118
|
+
last_targeted_component_id?: string | null;
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
export interface WorkflowState {
|
|
@@ -72,20 +127,45 @@ export interface WorkflowState {
|
|
|
72
127
|
initial_idea?: string;
|
|
73
128
|
current_ambiguity?: number;
|
|
74
129
|
threshold?: number;
|
|
130
|
+
threshold_source?: string;
|
|
131
|
+
type?: "greenfield" | "brownfield";
|
|
75
132
|
seed_path?: string;
|
|
133
|
+
topology?: WorkflowTopologyState;
|
|
134
|
+
codebase_context?: string;
|
|
135
|
+
language?: string;
|
|
76
136
|
plan_path?: string;
|
|
77
137
|
completed_tasks?: string[];
|
|
78
138
|
pending_tasks?: string[];
|
|
139
|
+
/** team: the task the previous run failed on — surfaces a partial-edits
|
|
140
|
+
* warning on resume (round-8). Cleared when the run resumes past it. */
|
|
141
|
+
failed_task?: string;
|
|
142
|
+
/** ralplan: persisted consensus-critic verdict ("okay" | "iterate" | "reject" |
|
|
143
|
+
* "unverified") — `jeo approve` requires "okay" (round-11 real consensus). */
|
|
144
|
+
consensus?: string;
|
|
145
|
+
/** ralplan: the critic's justification (bounded excerpt) for auditability. */
|
|
146
|
+
consensus_detail?: string;
|
|
79
147
|
approved?: boolean;
|
|
148
|
+
/** ultragoal terminal outcome. */
|
|
149
|
+
status?: string;
|
|
150
|
+
passed?: number;
|
|
151
|
+
total?: number;
|
|
152
|
+
/** ultragoal: whether the global verification suite was green (round-7 honest
|
|
153
|
+
* contract — criteria are recorded, not individually claimed as passed). */
|
|
154
|
+
suite_green?: boolean;
|
|
80
155
|
}
|
|
81
156
|
|
|
157
|
+
/** The built-in default model when neither disk config nor JEO_DEFAULT_MODEL provides one.
|
|
158
|
+
* Shared by envDefaultConfig (runtime) and readRawGlobalConfig (persistence base) so a
|
|
159
|
+
* fresh-install saveConfigPatch never bakes a DIFFERENT default than the runtime resolves. */
|
|
160
|
+
const DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
161
|
+
|
|
82
162
|
/**
|
|
83
163
|
* Resolve the global config directory at call time (not import time) so that a
|
|
84
|
-
* `
|
|
85
|
-
* `
|
|
164
|
+
* `JEO_CONFIG_DIR` override or a runtime `HOME` change is always honored.
|
|
165
|
+
* `JEO_CONFIG_DIR` takes precedence; otherwise `~/.jeo`.
|
|
86
166
|
*/
|
|
87
167
|
function globalConfigDir(): string {
|
|
88
|
-
return
|
|
168
|
+
return jeoEnv("CONFIG_DIR") || path.join(os.homedir(), ".jeo");
|
|
89
169
|
}
|
|
90
170
|
function globalConfigPath(): string {
|
|
91
171
|
return path.join(globalConfigDir(), "config.json");
|
|
@@ -99,20 +179,31 @@ function envOAuth(): NonNullable<Config["oauth"]> {
|
|
|
99
179
|
};
|
|
100
180
|
}
|
|
101
181
|
|
|
102
|
-
/** Merge env-provided
|
|
182
|
+
/** Merge env-provided credentials / base URLs over a config (env fills gaps only;
|
|
183
|
+
* on-disk values always win). Previously the providers API-key map was NOT overlaid
|
|
184
|
+
* when a config file existed, so a provider whose key lived only in the environment
|
|
185
|
+
* (e.g. GEMINI_API_KEY) resolved to "no credential" — breaking provider/model
|
|
186
|
+
* selection (including per-role subagent overrides) despite the key being present. */
|
|
103
187
|
function withEnvOverlay(cfg: Config): Config {
|
|
104
188
|
const envTok = envOAuth();
|
|
105
189
|
const oauth = { ...envTok, ...(cfg.oauth ?? {}) };
|
|
190
|
+
// Disk wins when it is a real key; env fills missing/blank gaps. A hand-edited
|
|
191
|
+
// empty string should not mask a valid environment credential.
|
|
192
|
+
const providers: Config["providers"] = { ...(cfg.providers ?? {}) };
|
|
193
|
+
if (!providers.anthropic && process.env.ANTHROPIC_API_KEY) providers.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
194
|
+
if (!providers.openai && process.env.OPENAI_API_KEY) providers.openai = process.env.OPENAI_API_KEY;
|
|
195
|
+
if (!providers.gemini && process.env.GEMINI_API_KEY) providers.gemini = process.env.GEMINI_API_KEY;
|
|
106
196
|
return {
|
|
107
197
|
...cfg,
|
|
198
|
+
providers,
|
|
108
199
|
oauth,
|
|
109
|
-
defaultModel:
|
|
200
|
+
defaultModel: jeoEnv("DEFAULT_MODEL") || cfg.defaultModel,
|
|
110
201
|
ollamaBaseUrl: cfg.ollamaBaseUrl || process.env.OLLAMA_HOST || "http://localhost:11434",
|
|
111
202
|
openaiBaseUrl: cfg.openaiBaseUrl || process.env.OPENAI_BASE_URL,
|
|
112
203
|
roles: {
|
|
113
|
-
smol: cfg.roles?.smol ||
|
|
114
|
-
slow: cfg.roles?.slow ||
|
|
115
|
-
plan: cfg.roles?.plan ||
|
|
204
|
+
smol: cfg.roles?.smol || jeoEnv("SMOL_MODEL"),
|
|
205
|
+
slow: cfg.roles?.slow || jeoEnv("SLOW_MODEL"),
|
|
206
|
+
plan: cfg.roles?.plan || jeoEnv("PLAN_MODEL"),
|
|
116
207
|
},
|
|
117
208
|
};
|
|
118
209
|
}
|
|
@@ -124,50 +215,122 @@ function envDefaultConfig(): Config {
|
|
|
124
215
|
openai: process.env.OPENAI_API_KEY,
|
|
125
216
|
gemini: process.env.GEMINI_API_KEY,
|
|
126
217
|
},
|
|
127
|
-
defaultModel:
|
|
218
|
+
defaultModel: jeoEnv("DEFAULT_MODEL") || DEFAULT_MODEL,
|
|
128
219
|
thinkingLevel: "medium",
|
|
129
220
|
};
|
|
130
221
|
}
|
|
131
222
|
|
|
132
|
-
|
|
133
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Parsed-config read cache keyed by path and validated by stat (mtimeMs + size).
|
|
225
|
+
* `readGlobalConfig`/`readRawGlobalConfig` run on EVERY model call (resolveCall,
|
|
226
|
+
* credential resolution, per-turn config reads), so an uncached read paid a disk
|
|
227
|
+
* read + JSON.parse + zod validation per agent-loop step. The cache is bounded
|
|
228
|
+
* (≤8 paths — one per JEO_CONFIG_DIR seen) and invalidated by `saveGlobalConfig`;
|
|
229
|
+
* any external write changes mtime/size, so a stale entry is never served. Reads
|
|
230
|
+
* return a structuredClone so callers can never poison the cached object.
|
|
231
|
+
*/
|
|
232
|
+
type ParsedFile =
|
|
233
|
+
| { kind: "ok"; config: Config }
|
|
234
|
+
| { kind: "missing" }
|
|
235
|
+
| { kind: "bad-json"; warned: boolean }
|
|
236
|
+
| { kind: "invalid"; message: string; warned: boolean };
|
|
237
|
+
const configReadCache = new Map<string, { mtimeMs: number; size: number; parsed: ParsedFile }>();
|
|
238
|
+
const CONFIG_CACHE_CAP = 8;
|
|
239
|
+
|
|
240
|
+
/** Drop all cached config reads (tests / explicit invalidation). */
|
|
241
|
+
export function clearConfigReadCache(): void {
|
|
242
|
+
configReadCache.clear();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function readParsedConfigFile(): Promise<ParsedFile> {
|
|
246
|
+
const p = globalConfigPath();
|
|
247
|
+
let st: { mtimeMs: number; size: number };
|
|
134
248
|
try {
|
|
135
|
-
|
|
249
|
+
st = await fs.stat(p);
|
|
136
250
|
} catch {
|
|
137
|
-
|
|
251
|
+
configReadCache.delete(p);
|
|
252
|
+
return { kind: "missing" };
|
|
138
253
|
}
|
|
139
|
-
|
|
140
|
-
|
|
254
|
+
const hit = configReadCache.get(p);
|
|
255
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.parsed;
|
|
256
|
+
let parsed: ParsedFile;
|
|
141
257
|
try {
|
|
142
|
-
raw = JSON.parse(
|
|
258
|
+
const raw = JSON.parse(await fs.readFile(p, "utf-8"));
|
|
259
|
+
const result = parseConfig(raw);
|
|
260
|
+
parsed = result.ok
|
|
261
|
+
? { kind: "ok", config: result.config as Config }
|
|
262
|
+
: { kind: "invalid", message: result.message, warned: false };
|
|
143
263
|
} catch {
|
|
144
|
-
|
|
145
|
-
|
|
264
|
+
parsed = { kind: "bad-json", warned: false };
|
|
265
|
+
}
|
|
266
|
+
if (configReadCache.size >= CONFIG_CACHE_CAP && !configReadCache.has(p)) {
|
|
267
|
+
const oldest = configReadCache.keys().next().value;
|
|
268
|
+
if (oldest !== undefined) configReadCache.delete(oldest);
|
|
146
269
|
}
|
|
270
|
+
configReadCache.set(p, { mtimeMs: st.mtimeMs, size: st.size, parsed });
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
147
273
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
274
|
+
export async function readGlobalConfig(): Promise<Config> {
|
|
275
|
+
const parsed = await readParsedConfigFile();
|
|
276
|
+
if (parsed.kind === "bad-json" || parsed.kind === "invalid") {
|
|
277
|
+
// Warn once per file VERSION (mtime/size change resets the entry), not per read.
|
|
278
|
+
if (!parsed.warned) {
|
|
279
|
+
parsed.warned = true;
|
|
280
|
+
const detail = parsed.kind === "invalid" ? ` is invalid (${parsed.message})` : " is not valid JSON";
|
|
281
|
+
process.stderr.write(`[jeo] ${globalConfigPath()}${detail}; using environment defaults.\n`);
|
|
282
|
+
}
|
|
151
283
|
return withEnvOverlay(envDefaultConfig());
|
|
152
284
|
}
|
|
153
|
-
|
|
285
|
+
if (parsed.kind === "missing") return withEnvOverlay(envDefaultConfig());
|
|
286
|
+
return withEnvOverlay(structuredClone(parsed.config));
|
|
154
287
|
}
|
|
155
288
|
|
|
156
289
|
export async function saveGlobalConfig(config: Config): Promise<void> {
|
|
157
|
-
|
|
158
|
-
await fs.
|
|
159
|
-
|
|
290
|
+
const dir = globalConfigDir();
|
|
291
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
292
|
+
const target = globalConfigPath();
|
|
293
|
+
const tmpPath = `${target}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
294
|
+
try {
|
|
295
|
+
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
296
|
+
await fs.chmod(tmpPath, 0o600).catch(() => {});
|
|
297
|
+
await fs.rename(tmpPath, target);
|
|
298
|
+
configReadCache.delete(target); // the next read re-parses the fresh file
|
|
299
|
+
} catch (err) {
|
|
300
|
+
await fs.unlink(tmpPath).catch(() => {});
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
160
303
|
}
|
|
161
304
|
|
|
162
|
-
|
|
163
|
-
|
|
305
|
+
/** Read the on-disk config WITHOUT the env overlay. Used as the base for
|
|
306
|
+
* persistence so env-only values (OAuth bearer tokens, JEO_DEFAULT_MODEL,
|
|
307
|
+
* JEO_*_MODEL role tiers, OLLAMA_HOST/OPENAI_BASE_URL) are never baked into
|
|
308
|
+
* ~/.jeo/config.json by an unrelated `/agents`/`/roles`/`/model save`. */
|
|
309
|
+
export async function readRawGlobalConfig(): Promise<Config> {
|
|
310
|
+
const clean: Config = { providers: {}, defaultModel: DEFAULT_MODEL, thinkingLevel: "medium" };
|
|
311
|
+
const parsed = await readParsedConfigFile();
|
|
312
|
+
return parsed.kind === "ok" ? structuredClone(parsed.config) : clean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Merge a patch onto the RAW on-disk config and persist. The `build` callback
|
|
316
|
+
* receives the raw config so partial updates (subagents/roles maps) are derived
|
|
317
|
+
* from on-disk state, never from the env-overlaid runtime config. */
|
|
318
|
+
export async function saveConfigPatch(build: (raw: Config) => Partial<Config>): Promise<Config> {
|
|
319
|
+
const raw = await readRawGlobalConfig();
|
|
320
|
+
const next = { ...raw, ...build(raw) };
|
|
321
|
+
await saveGlobalConfig(next);
|
|
322
|
+
return next;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function getLocalJeoDir(cwd: string = process.cwd()): string {
|
|
326
|
+
return path.join(cwd, ".jeo");
|
|
164
327
|
}
|
|
165
328
|
|
|
166
329
|
export async function readWorkflowState(
|
|
167
330
|
skill: "deep-interview" | "ralplan" | "team" | "ultragoal",
|
|
168
331
|
cwd: string = process.cwd()
|
|
169
332
|
): Promise<WorkflowState | null> {
|
|
170
|
-
const statePath = path.join(
|
|
333
|
+
const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
|
|
171
334
|
try {
|
|
172
335
|
const data = await fs.readFile(statePath, "utf-8");
|
|
173
336
|
return JSON.parse(data) as WorkflowState;
|
|
@@ -176,15 +339,49 @@ export async function readWorkflowState(
|
|
|
176
339
|
}
|
|
177
340
|
}
|
|
178
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Like {@link readWorkflowState} but distinguishes a missing file (→ null) from a
|
|
344
|
+
* corrupt/invalid one (→ throws). Security-sensitive callers (the MutationGuard)
|
|
345
|
+
* use this to fail CLOSED: a corrupt lock state must not be treated as "no lock".
|
|
346
|
+
*/
|
|
347
|
+
export async function readWorkflowStateStrict(
|
|
348
|
+
skill: "deep-interview" | "ralplan" | "team" | "ultragoal",
|
|
349
|
+
cwd: string = process.cwd()
|
|
350
|
+
): Promise<WorkflowState | null> {
|
|
351
|
+
const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
|
|
352
|
+
let data: string;
|
|
353
|
+
try {
|
|
354
|
+
data = await fs.readFile(statePath, "utf-8");
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
return JSON.parse(data) as WorkflowState;
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error(`workflow state ${statePath} is corrupt (invalid JSON)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
179
366
|
export async function writeWorkflowState(
|
|
180
367
|
skill: "deep-interview" | "ralplan" | "team" | "ultragoal",
|
|
181
368
|
state: WorkflowState,
|
|
182
369
|
cwd: string = process.cwd()
|
|
183
370
|
): Promise<string> {
|
|
184
|
-
const stateDir = path.join(
|
|
371
|
+
const stateDir = path.join(getLocalJeoDir(cwd), "state");
|
|
185
372
|
await fs.mkdir(stateDir, { recursive: true });
|
|
186
373
|
const statePath = path.join(stateDir, `${skill}-state.json`);
|
|
187
|
-
|
|
374
|
+
// Atomic temp+rename (zeroclaw crash-durability): workflow state is rewritten
|
|
375
|
+
// repeatedly mid-workflow, and the mutation guard fails CLOSED on corrupt JSON —
|
|
376
|
+
// a torn write would otherwise wedge the agent into a permanent mutation block.
|
|
377
|
+
const tmpPath = `${statePath}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
378
|
+
try {
|
|
379
|
+
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
380
|
+
await fs.rename(tmpPath, statePath);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
await fs.unlink(tmpPath).catch(() => {});
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
188
385
|
return statePath;
|
|
189
386
|
}
|
|
190
387
|
|
|
@@ -192,8 +389,58 @@ export async function clearWorkflowState(
|
|
|
192
389
|
skill: "deep-interview" | "ralplan" | "team" | "ultragoal",
|
|
193
390
|
cwd: string = process.cwd()
|
|
194
391
|
): Promise<void> {
|
|
195
|
-
const statePath = path.join(
|
|
392
|
+
const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
|
|
196
393
|
try {
|
|
197
394
|
await fs.unlink(statePath);
|
|
198
395
|
} catch {}
|
|
199
396
|
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Cross-process run lock for a workflow engine (round-8, architect ref
|
|
400
|
+
* 7-Round7Workflow): two concurrent `jeo team` processes would read-modify-write
|
|
401
|
+
* team-state.json last-writer-wins — tasks executed twice, completions lost.
|
|
402
|
+
* O_EXCL lockfile carrying the holder's pid; a DEAD holder's stale lock is taken
|
|
403
|
+
* over once, a LIVE holder refuses with an actionable error. Returns a release fn.
|
|
404
|
+
*/
|
|
405
|
+
export async function acquireWorkflowRunLock(
|
|
406
|
+
skill: "team" | "ultragoal",
|
|
407
|
+
cwd: string = process.cwd(),
|
|
408
|
+
): Promise<() => Promise<void>> {
|
|
409
|
+
const stateDir = path.join(getLocalJeoDir(cwd), "state");
|
|
410
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
411
|
+
const lockPath = path.join(stateDir, `${skill}.lock`);
|
|
412
|
+
const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
|
|
413
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
414
|
+
try {
|
|
415
|
+
await fs.writeFile(lockPath, payload, { flag: "wx" });
|
|
416
|
+
return async () => {
|
|
417
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
418
|
+
};
|
|
419
|
+
} catch {
|
|
420
|
+
let holder: { pid?: number } = {};
|
|
421
|
+
try {
|
|
422
|
+
holder = JSON.parse(await fs.readFile(lockPath, "utf-8")) as { pid?: number };
|
|
423
|
+
} catch { /* unreadable/torn lock → treat as stale */ }
|
|
424
|
+
const alive = typeof holder.pid === "number" && holder.pid > 0 && (() => {
|
|
425
|
+
try {
|
|
426
|
+
process.kill(holder.pid!, 0);
|
|
427
|
+
return true;
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
})();
|
|
432
|
+
if (alive) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`another 'jeo ${skill}' run (pid ${holder.pid}) holds ${lockPath} — wait for it to finish, or delete the lock file if that process is gone.`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
await fs.unlink(lockPath).catch(() => {}); // stale (dead/unreadable) → take over once
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
throw new Error(`could not acquire ${lockPath} even after stale-lock takeover.`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Returns true if the agent is running in development mode (enables self-improvement). */
|
|
444
|
+
export function isDevMode(): boolean {
|
|
445
|
+
return jeoEnv("DEV_MODE") === "1" || process.env.NODE_ENV === "development";
|
|
446
|
+
}
|