jeo-code 0.11.2 → 0.11.3
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/CHANGELOG.md +35 -1
- package/README.ja.md +59 -27
- package/README.ko.md +62 -29
- package/README.md +58 -26
- package/README.zh.md +61 -29
- package/package.json +1 -1
- package/src/agent/config-schema.ts +20 -7
- package/src/agent/debug-session.ts +7 -3
- package/src/agent/engine.ts +46 -102
- package/src/agent/loop-guards.ts +11 -13
- package/src/agent/notify/daemon-control.ts +109 -24
- package/src/agent/notify/discord-api.ts +148 -0
- package/src/agent/notify/discord-daemon.ts +443 -0
- package/src/agent/notify/paths.ts +1 -1
- package/src/agent/notify/session-discovery.ts +16 -0
- package/src/agent/notify/session-endpoint.ts +43 -25
- package/src/agent/notify/slack-api.ts +177 -0
- package/src/agent/notify/slack-daemon.ts +361 -0
- package/src/agent/notify/telegram-daemon.ts +215 -83
- package/src/agent/state.ts +13 -1
- package/src/ai/model-discovery.ts +13 -21
- package/src/autopilot.ts +14 -5
- package/src/cli/runner.ts +4 -4
- package/src/commands/approve.ts +21 -16
- package/src/commands/daemon.ts +5 -3
- package/src/commands/launch/agents-slash.ts +8 -3
- package/src/commands/launch.ts +36 -44
- package/src/commands/notify.ts +215 -83
- package/src/commands/ralplan.ts +2 -1
- package/src/commands/team.ts +17 -12
- package/src/commands/ultragoal.ts +2 -1
package/src/autopilot.ts
CHANGED
|
@@ -180,6 +180,7 @@ export function foldBest(goal: Goal, best: number | undefined, score: number): n
|
|
|
180
180
|
/**
|
|
181
181
|
* Single source of truth for the ratchet keep/revert decision. Shared by step,
|
|
182
182
|
* loop, and status so they can never diverge.
|
|
183
|
+
* - every goal: a failed eval always reverts, regardless of its printed score.
|
|
183
184
|
* - gate goal: keep iff the eval passed (score is irrelevant).
|
|
184
185
|
* - min/max goal: a non-measurable (NaN) score can never prove improvement, so
|
|
185
186
|
* it is always reverted; otherwise keep iff it improves on the best so far.
|
|
@@ -190,7 +191,8 @@ export function decideStep(
|
|
|
190
191
|
passed: boolean,
|
|
191
192
|
best: number | undefined,
|
|
192
193
|
): "keep" | "revert" {
|
|
193
|
-
if (
|
|
194
|
+
if (!passed) return "revert";
|
|
195
|
+
if (goal === "gate") return "keep";
|
|
194
196
|
if (Number.isNaN(score)) return "revert";
|
|
195
197
|
return isImprovement(goal, score, best) ? "keep" : "revert";
|
|
196
198
|
}
|
|
@@ -255,8 +257,11 @@ function cmdStep(flags: Record<string, string>): void {
|
|
|
255
257
|
if (decision === "revert" && flags["on-revert"]) {
|
|
256
258
|
try {
|
|
257
259
|
execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
|
|
258
|
-
} catch {
|
|
259
|
-
|
|
260
|
+
} catch (error) {
|
|
261
|
+
appendLog({ type: "stop", reason: "rollback_failed", change, score, passed, prevBest: best ?? null, output, error: String(error) });
|
|
262
|
+
console.error(`jeo autopilot: stop — --on-revert hook failed: ${String(error)}`);
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -295,6 +300,7 @@ function cmdLoop(flags: Record<string, string>): void {
|
|
|
295
300
|
if (!runnerOk) {
|
|
296
301
|
appendLog({ type: "stop", reason: "runner_failed", iteration: i });
|
|
297
302
|
console.log(`jeo autopilot: stop — runner failed at iteration ${i}`);
|
|
303
|
+
process.exitCode = 1;
|
|
298
304
|
return;
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -304,8 +310,11 @@ function cmdLoop(flags: Record<string, string>): void {
|
|
|
304
310
|
if (decision === "revert" && flags["on-revert"]) {
|
|
305
311
|
try {
|
|
306
312
|
execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
|
|
307
|
-
} catch {
|
|
308
|
-
|
|
313
|
+
} catch (error) {
|
|
314
|
+
appendLog({ type: "stop", reason: "rollback_failed", iteration: i, change: `loop#${i}`, score, passed, prevBest: best ?? null, output, error: String(error) });
|
|
315
|
+
console.error(`jeo autopilot: stop — --on-revert hook failed at iteration ${i}: ${String(error)}`);
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
appendLog({ type: "step", iteration: i, change: `loop#${i}`, score, passed, decision, prevBest: best ?? null, output });
|
package/src/cli/runner.ts
CHANGED
|
@@ -112,8 +112,8 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
name: "notify",
|
|
115
|
-
summary: "Configure/inspect
|
|
116
|
-
usage: "notify [setup [--token
|
|
115
|
+
summary: "Configure/inspect Telegram, Discord, and Slack notification channels.",
|
|
116
|
+
usage: "notify [setup|status|health|test] [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID|--channel-id ID] [--allowed-user-ids IDS]",
|
|
117
117
|
loader: async () => {
|
|
118
118
|
const m = await import("../commands/notify");
|
|
119
119
|
return args => m.runNotifyCommand(args);
|
|
@@ -121,7 +121,7 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
name: "daemon",
|
|
124
|
-
summary: "Manage the
|
|
124
|
+
summary: "Manage the shared Telegram/Discord/Slack notification daemon.",
|
|
125
125
|
usage: "daemon [status|start|stop|reload]",
|
|
126
126
|
loader: async () => {
|
|
127
127
|
const m = await import("../commands/daemon");
|
|
@@ -197,7 +197,7 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
name: "notify-daemon-run",
|
|
200
|
-
summary: "(internal) Foreground
|
|
200
|
+
summary: "(internal) Foreground notification worker, spawned by 'jeo daemon start'.",
|
|
201
201
|
usage: "notify-daemon-run",
|
|
202
202
|
loader: async () => {
|
|
203
203
|
const m = await import("../agent/notify/telegram-daemon");
|
package/src/commands/approve.ts
CHANGED
|
@@ -67,11 +67,6 @@ export async function approvePlan(planPathInput: string, cwd: string): Promise<A
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Idempotency: check if already approved
|
|
71
|
-
if (ralplanState.approved) {
|
|
72
|
-
return { ok: true, message: `[SUCCESS] Plan is already approved.` };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
70
|
// Round-10 #4 (architect ref 8-Round10Planning): approval is a GATE, not a
|
|
76
71
|
// rubber stamp — validate the plan against the exact contract `jeo team`
|
|
77
72
|
// enforces, so a schema-invalid/unknown-role plan is refused HERE instead of
|
|
@@ -118,17 +113,27 @@ export async function approvePlan(planPathInput: string, cwd: string): Promise<A
|
|
|
118
113
|
};
|
|
119
114
|
}
|
|
120
115
|
|
|
121
|
-
//
|
|
122
|
-
if (ralplanState.consensus_hash) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
// Approval and idempotent re-approval both require the exact reviewed bytes.
|
|
117
|
+
if (!ralplanState.consensus_hash) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
message:
|
|
121
|
+
`[ERROR] Refusing to approve: the plan lacks a consensus review hash.\n` +
|
|
122
|
+
` Re-run 'jeo ralplan' so the consensus critic can review the plan, then approve again.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const currentHash = createHash("sha256").update(planContent).digest("hex");
|
|
126
|
+
if (currentHash !== ralplanState.consensus_hash) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
message:
|
|
130
|
+
`[ERROR] Refusing to approve: the plan file has been modified since the consensus critic reviewed it.\n` +
|
|
131
|
+
` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve again.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (ralplanState.approved) {
|
|
136
|
+
return { ok: true, message: `[SUCCESS] Plan is already approved.` };
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// Update ralplan-state.json to approved: true
|
package/src/commands/daemon.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `jeo daemon status|start|stop|reload` — manage the
|
|
2
|
+
* `jeo daemon status|start|stop|reload` — manage the shared Telegram/Discord/Slack
|
|
3
3
|
* notification/subagent-control daemon (gjc `gjc daemon` parity, scoped to
|
|
4
4
|
* jeo's one daemon kind). See `src/agent/notify/daemon-control.ts`.
|
|
5
5
|
*/
|
|
@@ -10,8 +10,10 @@ async function printStatus(): Promise<void> {
|
|
|
10
10
|
if (!status.configured) {
|
|
11
11
|
console.log("notifications not configured — run 'jeo notify setup' first.");
|
|
12
12
|
}
|
|
13
|
-
if (status.
|
|
14
|
-
console.log(`
|
|
13
|
+
if (status.pairing) {
|
|
14
|
+
console.log(`pairing owns polling (pid ${status.pid}); daemon not running`);
|
|
15
|
+
} else if (status.running) {
|
|
16
|
+
console.log(`process ${status.ready ? "initialized" : "initializing"} (pid ${status.pid}, started ${new Date(status.startedAt!).toISOString()}); platform connectivity not checked`);
|
|
15
17
|
// Say so when the PID-reuse guard could not actually run. On a host with no `/proc`
|
|
16
18
|
// and no usable `ps` (distroless images, seccomp-hardened sandboxes) "running" is an
|
|
17
19
|
// existence-only guess: the pid is alive, but it may belong to an unrelated process
|
|
@@ -54,12 +54,12 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
54
54
|
return openaiCompatDef(p)?.defaultModel ?? STATIC_PROVIDER_DEFAULT[p] ?? "";
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Pick-list entries for ONE provider
|
|
58
|
-
//
|
|
57
|
+
// Pick-list entries for ONE provider. Antigravity requires live availability;
|
|
58
|
+
// other providers retain static fallbacks. Mirrors launch.ts's `providerPickEntries`
|
|
59
59
|
// (duplicated here rather than imported to avoid a launch.ts <-> launch/*.ts cycle).
|
|
60
60
|
function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
61
61
|
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
62
|
-
if (fromLive.length) return fromLive;
|
|
62
|
+
if (fromLive.length || want === "antigravity") return fromLive;
|
|
63
63
|
const catalog = catalogByProvider(want);
|
|
64
64
|
if (catalog.length) {
|
|
65
65
|
return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
|
|
@@ -253,6 +253,11 @@ export async function runAgentsSlash(input: string, ctx: AgentsSlashCtx): Promis
|
|
|
253
253
|
const forProvider = providerPickEntries(live, want);
|
|
254
254
|
const liveForProvider = live.some(r => r.ok && r.provider === want && r.models.length > 0);
|
|
255
255
|
const explicit = tokens[3];
|
|
256
|
+
if (want === "antigravity" && !forProvider.length && (!explicit || explicit.startsWith("#"))) {
|
|
257
|
+
const reason = live.find(r => r.provider === want)?.error ?? "no account-available models returned";
|
|
258
|
+
console.log(`Cannot list ${want} models: ${reason}. Retry discovery or specify an explicit model id.`);
|
|
259
|
+
return result();
|
|
260
|
+
}
|
|
256
261
|
let chosenModel: string;
|
|
257
262
|
if (explicit && forProvider.length) {
|
|
258
263
|
const sel = resolveSelection(forProvider, explicit);
|
package/src/commands/launch.ts
CHANGED
|
@@ -335,22 +335,22 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
338
|
-
* Pick-list entries for ONE provider. OpenAI availability comes
|
|
339
|
-
* results; other providers retain their offline catalog fallbacks.
|
|
338
|
+
* Pick-list entries for ONE provider. OpenAI and Antigravity availability comes
|
|
339
|
+
* only from live results; other providers retain their offline catalog fallbacks.
|
|
340
340
|
*
|
|
341
341
|
* Live discovery yields ids only for a logged-in, reachable provider, and
|
|
342
342
|
* `catalogOr` backfills the static catalog for OAuth sources and for API-key
|
|
343
343
|
* providers whose models-list endpoint is absent (HTTP 404, e.g. Tencent MaaS).
|
|
344
344
|
* A not-yet-configured provider still has an empty live list, so prefer live ids;
|
|
345
345
|
* else the provider's capability catalog; else its single known default model (all
|
|
346
|
-
* 24 OpenAI-compat providers carry one)
|
|
346
|
+
* 24 OpenAI-compat providers carry one). Antigravity never uses these fallbacks.
|
|
347
347
|
*/
|
|
348
348
|
export function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
349
349
|
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
350
350
|
if (fromLive.length) return fromLive;
|
|
351
|
-
// An OpenAI empty/error response is authoritative for this picker.
|
|
352
|
-
// the static capability catalog here would fabricate account availability.
|
|
353
|
-
if (want === "openai") return [];
|
|
351
|
+
// An OpenAI or Antigravity empty/error response is authoritative for this picker.
|
|
352
|
+
// Offering the static capability catalog here would fabricate account availability.
|
|
353
|
+
if (want === "openai" || want === "antigravity") return [];
|
|
354
354
|
const catalog = catalogByProvider(want);
|
|
355
355
|
if (catalog.length) {
|
|
356
356
|
return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
|
|
@@ -1519,15 +1519,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1519
1519
|
|
|
1520
1520
|
const goalState = await readGoalState(cwd);
|
|
1521
1521
|
if (goalState && goalState.condition) {
|
|
1522
|
-
const reBlockCount = goalState.verdicts.filter(v => v.verdict === "NOT_MET" || v.verdict === "IMPOSSIBLE").length;
|
|
1523
|
-
const MAX_RE_BLOCKS = 2;
|
|
1524
|
-
|
|
1525
|
-
if (reBlockCount >= MAX_RE_BLOCKS) {
|
|
1526
|
-
if (tui) tui.events().onNotice?.(`[Goal Verifier] Re-block cap of ${MAX_RE_BLOCKS} reached. Auto-allowing done.`);
|
|
1527
|
-
else console.log(`[Goal Verifier] Re-block cap of ${MAX_RE_BLOCKS} reached. Auto-allowing done.`);
|
|
1528
|
-
return null;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
1522
|
if (tui) tui.events().onNotice?.("[Goal Verifier] Running goal verification...");
|
|
1532
1523
|
const llmVerdict = await verifyGoal(goalState.condition, history, resolveVerifierModel(turnConfig));
|
|
1533
1524
|
// Deterministic downgrade: a fresh MET from the transcript-only LLM
|
|
@@ -4197,35 +4188,36 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4197
4188
|
// `rl`/`promptInput`'s `pendingRemoteInject`, and `pendingStdinLines`, all
|
|
4198
4189
|
// established by now. See the Tier 2 contract's "Session-side routing".
|
|
4199
4190
|
if (sessionNotifyEndpoint) {
|
|
4200
|
-
sessionNotifyEndpoint.onUserMessage = (msg): void => {
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4191
|
+
sessionNotifyEndpoint.onUserMessage = async (msg): Promise<void | boolean> => {
|
|
4192
|
+
// Resolve any attached photo/document paths (already downloaded to
|
|
4193
|
+
// local temp files by the daemon) into real ImageAttachments via the
|
|
4194
|
+
// SAME path `attachImagePaths` uses for local drag-drop — one image
|
|
4195
|
+
// ingestion mechanism, not a parallel one. Bare paths never contain
|
|
4196
|
+
// whitespace here (daemon-generated temp names), so simple whitespace
|
|
4197
|
+
// joining is a safe token boundary for the path-token scanner.
|
|
4198
|
+
const withPaths = msg.imagePaths?.length ? `${msg.text} ${msg.imagePaths.join(" ")}`.trim() : msg.text;
|
|
4199
|
+
const attached = await attachImagePaths(withPaths, pendingImages.length + 1);
|
|
4200
|
+
const cleanedText = attached.images.length ? attached.text : msg.text;
|
|
4201
|
+
// Attachment normalization must not expose a remote slash command that
|
|
4202
|
+
// bypasses the local approval prompt or changes local execution policy.
|
|
4203
|
+
if (cleanedText.trimStart().startsWith("/")) return false;
|
|
4204
|
+
if (attached.images.length) pendingImages.push(...attached.images);
|
|
4205
|
+
|
|
4206
|
+
if (interactiveTurnActive && currentTurnSteer) {
|
|
4207
|
+
// Busy: steer the running turn exactly like a local mid-turn Enter.
|
|
4208
|
+
currentTurnSteer.push(cleanedText);
|
|
4209
|
+
currentTurnSteer.flushCard(cleanedText);
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
// Idle: never hijack text the user is actively typing locally — only
|
|
4213
|
+
// abort+inject when the readline buffer is confirmed empty (verified
|
|
4214
|
+
// empirically: aborting mid-partial-type corrupts the buffer).
|
|
4215
|
+
const rli = rl as unknown as { line?: string };
|
|
4216
|
+
if (rli.line === "" && pendingRemoteInject?.(cleanedText)) return;
|
|
4217
|
+
// No question pending, or the user is mid-type: queue for the NEXT
|
|
4218
|
+
// prompt cycle instead (same queue `pendingMidTurnCommands`/paste-merge
|
|
4219
|
+
// already feed — consumed at the top of the next `promptInput` call).
|
|
4220
|
+
pendingStdinLines.push(cleanedText);
|
|
4229
4221
|
};
|
|
4230
4222
|
sessionNotifyEndpoint.onConfigCommand = (cmd): void => {
|
|
4231
4223
|
if (cmd.verbosity !== undefined) notifyVerbosity = cmd.verbosity;
|