jeopi-cli 16.2.13 → 16.2.15
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 +21 -3
- package/README.md +1 -1
- package/dist/cli.js +3087 -3060
- package/dist/types/advisor/watchdog.d.ts +2 -2
- package/dist/types/autolearn/managed-skills.d.ts +2 -2
- package/dist/types/capability/types.d.ts +2 -2
- package/dist/types/cli/commands/migrate-legacy.d.ts +1 -0
- package/dist/types/cli/config-cli.d.ts +1 -1
- package/dist/types/cli/flag-tables.d.ts +1 -1
- package/dist/types/cli/grievances-cli.d.ts +1 -1
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +2 -2
- package/dist/types/cli/worktree-cli.d.ts +1 -1
- package/dist/types/commit/index.d.ts +1 -1
- package/dist/types/commit/pipeline.d.ts +1 -1
- package/dist/types/commit/types.d.ts +1 -1
- package/dist/types/config/prompt-templates.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +16 -6
- package/dist/types/config.d.ts +7 -7
- package/dist/types/discovery/helpers.d.ts +8 -8
- package/dist/types/discovery/omp-extension-roots.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +1 -1
- package/dist/types/extensibility/hooks/loader.d.ts +1 -1
- package/dist/types/extensibility/plugins/loader.d.ts +2 -2
- package/dist/types/extensibility/plugins/types.d.ts +13 -3
- package/dist/types/lsp/config.d.ts +2 -2
- package/dist/types/mcp/client.d.ts +1 -1
- package/dist/types/mcp/config-writer.d.ts +1 -1
- package/dist/types/mcp/oauth-flow.d.ts +1 -1
- package/dist/types/mcp/transports/http.d.ts +1 -1
- package/dist/types/memories/okf.d.ts +36 -0
- package/dist/types/modes/components/__tests__/assistant-agent-label.test.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +4 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/sdk.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/auth-broker-config.d.ts +4 -4
- package/dist/types/session/snapcompact-savings-journal.d.ts +1 -1
- package/dist/types/task/commands.d.ts +1 -1
- package/dist/types/task/critic-gate.d.ts +76 -0
- package/dist/types/task/critic-gate.test.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +1 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/acp-bridge.d.ts +2 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +1 -1
- package/dist/types/tools/index.d.ts +6 -1
- package/dist/types/tools/plan-mode-guard.d.ts +8 -0
- package/dist/types/tools/report-tool-issue.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +2 -2
- package/dist/types/utils/image-loading.d.ts +4 -4
- package/dist/types/utils/image-resize.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +1 -1
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +1 -1
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/extensions/tools.ts +1 -1
- package/examples/hooks/README.md +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/06-extensions.ts +3 -3
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-redis-sessions.ts +1 -1
- package/examples/sdk/13-sql-sessions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +12 -12
- package/scripts/jeopi +1 -1
- package/src/advisor/__tests__/config.test.ts +4 -4
- package/src/advisor/watchdog.ts +8 -8
- package/src/autolearn/managed-skills.ts +4 -4
- package/src/autoresearch/storage.ts +2 -2
- package/src/capability/types.ts +2 -2
- package/src/cli/agents-cli.ts +2 -2
- package/src/cli/args.ts +3 -3
- package/src/cli/auth-broker-cli.ts +5 -5
- package/src/cli/auth-gateway-cli.ts +4 -4
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/commands/migrate-legacy.ts +24 -0
- package/src/cli/config-cli.ts +20 -5
- package/src/cli/flag-tables.ts +1 -1
- package/src/cli/gallery-fixtures/misc.ts +7 -7
- package/src/cli/gallery-screenshot.ts +2 -2
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +2 -2
- package/src/cli/profile-alias.ts +8 -8
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +2 -2
- package/src/cli/update-cli.ts +27 -27
- package/src/cli/worktree-cli.ts +3 -3
- package/src/cli.ts +5 -5
- package/src/commands/agents.ts +2 -2
- package/src/commands/auth-broker.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/dry-balance.ts +6 -6
- package/src/commands/launch.ts +2 -2
- package/src/commands/models.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +5 -5
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/worktree.ts +3 -3
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/commit/index.ts +1 -1
- package/src/commit/pipeline.ts +1 -1
- package/src/commit/types.ts +1 -1
- package/src/config/mcp-schema.json +5 -5
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/prompt-templates.ts +2 -2
- package/src/config/settings-schema.ts +20 -8
- package/src/config/settings.ts +1 -1
- package/src/config.ts +10 -10
- package/src/dap/session.ts +2 -2
- package/src/debug/protocol-probe.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +3 -3
- package/src/debug/raw-sse.ts +2 -2
- package/src/discovery/builtin.ts +9 -9
- package/src/discovery/helpers.ts +13 -13
- package/src/discovery/omp-extension-roots.ts +4 -4
- package/src/discovery/omp-plugins.ts +2 -2
- package/src/discovery/substitute-plugin-root.ts +3 -3
- package/src/eval/py/runtime.ts +1 -1
- package/src/eval/rb/runner.rb +6 -6
- package/src/export/custom-share.ts +1 -1
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +4 -4
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/plugins/loader.ts +16 -10
- package/src/extensibility/plugins/manager.ts +10 -9
- package/src/extensibility/plugins/types.ts +15 -4
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/config.ts +4 -4
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +1 -1
- package/src/mcp/client.ts +1 -1
- package/src/mcp/config-writer.ts +2 -2
- package/src/mcp/manager.ts +1 -1
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/mcp/timeout.ts +2 -2
- package/src/mcp/transports/http.ts +1 -1
- package/src/memories/index.ts +82 -11
- package/src/memories/okf.ts +129 -0
- package/src/modes/components/__tests__/assistant-agent-label.test.ts +87 -0
- package/src/modes/components/assistant-message.ts +27 -0
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/modes/controllers/omfg-controller.ts +2 -2
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +3 -1
- package/src/modes/setup-wizard/index.ts +1 -1
- package/src/prompts/agents/jeo.md +20 -10
- package/src/prompts/system/autolearn-guidance.md +2 -2
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/learn.md +1 -1
- package/src/prompts/tools/manage-skill.md +1 -1
- package/src/sdk.ts +8 -6
- package/src/secrets/index.ts +2 -2
- package/src/session/agent-session.ts +86 -6
- package/src/session/auth-broker-config.ts +6 -6
- package/src/session/snapcompact-savings-journal.ts +1 -1
- package/src/slash-commands/helpers/mcp.ts +1 -1
- package/src/system-prompt.test.ts +14 -14
- package/src/task/commands.ts +1 -1
- package/src/task/critic-gate.test.ts +181 -0
- package/src/task/critic-gate.ts +148 -0
- package/src/task/discovery.ts +5 -5
- package/src/task/executor.ts +1 -1
- package/src/task/index.ts +35 -2
- package/src/tools/acp-bridge.ts +2 -2
- package/src/tools/browser/launch.ts +1 -1
- package/src/tools/browser/tab-protocol.ts +1 -1
- package/src/tools/grep.ts +2 -2
- package/src/tools/index.ts +6 -1
- package/src/tools/plan-mode-guard.ts +16 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/report-tool-issue.ts +2 -2
- package/src/utils/changelog.ts +2 -2
- package/src/utils/image-loading.ts +4 -4
- package/src/utils/image-resize.ts +5 -5
- package/src/web/search/index.ts +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { Settings } from "../config/settings";
|
|
3
|
+
import type { ToolSession } from "../tools";
|
|
4
|
+
import { enforceCriticGateWrite } from "../tools/plan-mode-guard";
|
|
5
|
+
import {
|
|
6
|
+
CRITIC_GATE_MAX_STRIKES,
|
|
7
|
+
type CriticGateState,
|
|
8
|
+
evaluateCriticGateSpawn,
|
|
9
|
+
parseCriticVerdict,
|
|
10
|
+
updateCriticGateState,
|
|
11
|
+
} from "./critic-gate";
|
|
12
|
+
import * as taskDiscovery from "./discovery";
|
|
13
|
+
import * as executor from "./executor";
|
|
14
|
+
import { TaskTool } from "./index";
|
|
15
|
+
import type { AgentDefinition, SingleResult } from "./types";
|
|
16
|
+
|
|
17
|
+
const criticAgent = {
|
|
18
|
+
name: "critic",
|
|
19
|
+
description: "Plan gate.",
|
|
20
|
+
systemPrompt: "Gate plans.",
|
|
21
|
+
source: "bundled",
|
|
22
|
+
tools: ["read", "grep", "glob", "web_search", "ast_grep"],
|
|
23
|
+
} satisfies AgentDefinition;
|
|
24
|
+
|
|
25
|
+
const workerAgent = {
|
|
26
|
+
name: "task",
|
|
27
|
+
description: "General worker.",
|
|
28
|
+
systemPrompt: "Do work.",
|
|
29
|
+
source: "bundled",
|
|
30
|
+
} satisfies AgentDefinition;
|
|
31
|
+
|
|
32
|
+
const planAgent = {
|
|
33
|
+
name: "plan",
|
|
34
|
+
description: "Read-only planner.",
|
|
35
|
+
systemPrompt: "Plan.",
|
|
36
|
+
source: "bundled",
|
|
37
|
+
tools: ["read", "grep", "glob"],
|
|
38
|
+
} satisfies AgentDefinition;
|
|
39
|
+
|
|
40
|
+
function engagedGate(overrides: Partial<CriticGateState> = {}): CriticGateState {
|
|
41
|
+
return {
|
|
42
|
+
verdict: "reject",
|
|
43
|
+
agentId: "Critic1",
|
|
44
|
+
at: Date.now(),
|
|
45
|
+
requiredFixes: ["name the target file"],
|
|
46
|
+
strikes: 1,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeSession(gate: { state?: CriticGateState }): ToolSession {
|
|
52
|
+
const settings = Settings.isolated({
|
|
53
|
+
"async.enabled": false,
|
|
54
|
+
"task.batch": true,
|
|
55
|
+
"task.isolation.mode": "none",
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
hasUI: false,
|
|
60
|
+
settings,
|
|
61
|
+
getSessionFile: () => null,
|
|
62
|
+
getSessionSpawns: () => "*",
|
|
63
|
+
getCriticGateState: () => gate.state,
|
|
64
|
+
setCriticGateState: state => {
|
|
65
|
+
gate.state = state;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function criticResult(output: string): SingleResult {
|
|
71
|
+
return {
|
|
72
|
+
index: 0,
|
|
73
|
+
id: "Critic1",
|
|
74
|
+
agent: "critic",
|
|
75
|
+
agentSource: "bundled",
|
|
76
|
+
task: "gate the plan",
|
|
77
|
+
exitCode: 0,
|
|
78
|
+
output,
|
|
79
|
+
stderr: "",
|
|
80
|
+
truncated: false,
|
|
81
|
+
durationMs: 5,
|
|
82
|
+
tokens: 1,
|
|
83
|
+
requests: 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("critic gate verdict lifecycle", () => {
|
|
88
|
+
it("parses schema-shaped verdicts and ignores non-critic payloads", () => {
|
|
89
|
+
expect(parseCriticVerdict(JSON.stringify({ verdict: "iterate", required_fixes: ["fix a", 3] }))).toEqual({
|
|
90
|
+
verdict: "iterate",
|
|
91
|
+
requiredFixes: ["fix a"],
|
|
92
|
+
summary: undefined,
|
|
93
|
+
});
|
|
94
|
+
expect(parseCriticVerdict("not json")).toBeUndefined();
|
|
95
|
+
expect(parseCriticVerdict(JSON.stringify({ verdict: "maybe" }))).toBeUndefined();
|
|
96
|
+
expect(parseCriticVerdict(JSON.stringify({ findings: [] }))).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("engages on non-okay, counts consecutive strikes, and clears on okay", () => {
|
|
100
|
+
const first = updateCriticGateState(undefined, { verdict: "iterate", requiredFixes: ["x"] }, "Critic1");
|
|
101
|
+
expect(first).toMatchObject({ verdict: "iterate", strikes: 1, requiredFixes: ["x"] });
|
|
102
|
+
|
|
103
|
+
const second = updateCriticGateState(first, { verdict: "reject", requiredFixes: [] }, "Critic2");
|
|
104
|
+
expect(second).toMatchObject({ verdict: "reject", strikes: 2, agentId: "Critic2" });
|
|
105
|
+
|
|
106
|
+
expect(updateCriticGateState(second, { verdict: "okay", requiredFixes: [] }, "Critic3")).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("blocks non-read-only spawns while engaged, keeps read-only lanes open", () => {
|
|
110
|
+
const state = engagedGate();
|
|
111
|
+
expect(evaluateCriticGateSpawn(undefined, "task", false)).toBeUndefined();
|
|
112
|
+
expect(evaluateCriticGateSpawn(state, "plan", true)).toBeUndefined();
|
|
113
|
+
expect(evaluateCriticGateSpawn(state, "critic", true)).toBeUndefined();
|
|
114
|
+
|
|
115
|
+
const blocked = evaluateCriticGateSpawn(state, "task", false);
|
|
116
|
+
expect(blocked).toContain("`reject`");
|
|
117
|
+
expect(blocked).toContain("name the target file");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("closes the iterate loop after the strike budget is exhausted", () => {
|
|
121
|
+
const state = engagedGate({ verdict: "iterate", strikes: CRITIC_GATE_MAX_STRIKES });
|
|
122
|
+
const blocked = evaluateCriticGateSpawn(state, "critic", true);
|
|
123
|
+
expect(blocked).toContain("Do NOT re-submit to critic");
|
|
124
|
+
// Other read-only lanes stay open so the agent can still gather what it
|
|
125
|
+
// needs to write the report.
|
|
126
|
+
expect(evaluateCriticGateSpawn(state, "plan", true)).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("critic gate write guard", () => {
|
|
131
|
+
it("rejects working-tree writes while engaged and allows them once cleared", () => {
|
|
132
|
+
const gate: { state?: CriticGateState } = { state: engagedGate() };
|
|
133
|
+
const session = makeSession(gate);
|
|
134
|
+
expect(() => enforceCriticGateWrite(session, "src/index.ts")).toThrow(/Critic gate engaged/);
|
|
135
|
+
|
|
136
|
+
gate.state = undefined;
|
|
137
|
+
expect(() => enforceCriticGateWrite(session, "src/index.ts")).not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("critic gate task-tool enforcement", () => {
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
vi.restoreAllMocks();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("refuses to spawn a non-read-only agent while the gate is engaged", async () => {
|
|
147
|
+
vi.spyOn(taskDiscovery, "discoverAgents").mockResolvedValue({
|
|
148
|
+
agents: [criticAgent, workerAgent, planAgent],
|
|
149
|
+
projectAgentsDir: null,
|
|
150
|
+
});
|
|
151
|
+
const runSpy = vi.spyOn(executor, "runSubprocess").mockResolvedValue(criticResult("{}"));
|
|
152
|
+
|
|
153
|
+
const tool = await TaskTool.create(makeSession({ state: engagedGate() }));
|
|
154
|
+
const result = await tool.execute("call-1", { agent: "task", assignment: "implement the fix" });
|
|
155
|
+
|
|
156
|
+
const text = result.content.find(part => part.type === "text")?.text ?? "";
|
|
157
|
+
expect(text).toContain("Critic gate engaged");
|
|
158
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("records a reject verdict from a critic run and clears it on okay", async () => {
|
|
162
|
+
vi.spyOn(taskDiscovery, "discoverAgents").mockResolvedValue({
|
|
163
|
+
agents: [criticAgent, workerAgent],
|
|
164
|
+
projectAgentsDir: null,
|
|
165
|
+
});
|
|
166
|
+
const runSpy = vi
|
|
167
|
+
.spyOn(executor, "runSubprocess")
|
|
168
|
+
.mockResolvedValue(criticResult(JSON.stringify({ verdict: "reject", required_fixes: ["broken step 2"] })));
|
|
169
|
+
|
|
170
|
+
const gate: { state?: CriticGateState } = {};
|
|
171
|
+
const tool = await TaskTool.create(makeSession(gate));
|
|
172
|
+
await tool.execute("call-1", { agent: "critic", assignment: "gate the plan" });
|
|
173
|
+
|
|
174
|
+
expect(gate.state).toMatchObject({ verdict: "reject", strikes: 1, requiredFixes: ["broken step 2"] });
|
|
175
|
+
|
|
176
|
+
runSpy.mockResolvedValue(criticResult(JSON.stringify({ verdict: "okay" })));
|
|
177
|
+
await tool.execute("call-2", { agent: "critic", assignment: "gate the revised plan" });
|
|
178
|
+
|
|
179
|
+
expect(gate.state).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critic gate — the hard, code-enforced counterpart of the prompt rule
|
|
3
|
+
* "You NEVER execute a plan whose latest critic verdict is not `okay`".
|
|
4
|
+
*
|
|
5
|
+
* When a `critic` subagent completes with a schema-validated verdict, the
|
|
6
|
+
* parent session records it here. While the latest verdict is `iterate` or
|
|
7
|
+
* `reject` the runtime — not the prompt — blocks execution:
|
|
8
|
+
*
|
|
9
|
+
* - `task` refuses to spawn non-read-only agents (read-only lanes such as
|
|
10
|
+
* `plan`, `explore`, and `critic` itself stay open so the plan can be
|
|
11
|
+
* revised and re-gated).
|
|
12
|
+
* - Working-tree mutations through `write`/`edit`/patch are rejected; the
|
|
13
|
+
* `local://` artifact sandbox stays writable for seeds, plans, and notes.
|
|
14
|
+
*
|
|
15
|
+
* The gate clears in exactly two ways: a subsequent critic verdict of `okay`,
|
|
16
|
+
* or the next user-authored prompt (the user regaining control is the only
|
|
17
|
+
* override — mirroring jeo-code's `approve` hard gate, scoped to a turn-chain
|
|
18
|
+
* instead of a state file).
|
|
19
|
+
*
|
|
20
|
+
* A strike counter bounds the iterate loop: after
|
|
21
|
+
* {@link CRITIC_GATE_MAX_STRIKES} consecutive non-okay verdicts without user
|
|
22
|
+
* input, even re-submitting to `critic` is refused — the agent must stop and
|
|
23
|
+
* report the surviving gaps instead of burning tokens on convergence-free
|
|
24
|
+
* re-planning.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Agent name whose structured verdicts drive the gate. */
|
|
28
|
+
export const CRITIC_AGENT_NAME = "critic";
|
|
29
|
+
|
|
30
|
+
/** Consecutive non-okay verdicts allowed before the gate forces a stop-and-report. */
|
|
31
|
+
export const CRITIC_GATE_MAX_STRIKES = 3;
|
|
32
|
+
|
|
33
|
+
/** Non-okay verdicts that engage the gate. */
|
|
34
|
+
export type CriticGateVerdict = "iterate" | "reject";
|
|
35
|
+
|
|
36
|
+
/** Gate state held by the parent session while the latest verdict is not `okay`. */
|
|
37
|
+
export interface CriticGateState {
|
|
38
|
+
/** Latest non-okay verdict. */
|
|
39
|
+
verdict: CriticGateVerdict;
|
|
40
|
+
/** Agent id of the critic run that produced the verdict. */
|
|
41
|
+
agentId: string;
|
|
42
|
+
/** Epoch ms when the verdict was recorded. */
|
|
43
|
+
at: number;
|
|
44
|
+
/** `required_fixes` from the critic's structured output (empty for reject without fixes). */
|
|
45
|
+
requiredFixes: string[];
|
|
46
|
+
/** Consecutive non-okay verdicts since the last okay/user prompt. */
|
|
47
|
+
strikes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Verdict parsed from a critic subagent's validated structured output. */
|
|
51
|
+
export interface ParsedCriticVerdict {
|
|
52
|
+
verdict: "okay" | CriticGateVerdict;
|
|
53
|
+
requiredFixes: string[];
|
|
54
|
+
summary?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse a critic verdict from the subagent's final output payload (the
|
|
59
|
+
* JSON-serialized, schema-validated yield data). Returns undefined when the
|
|
60
|
+
* payload is not a critic-shaped verdict — the gate then stays untouched.
|
|
61
|
+
*/
|
|
62
|
+
export function parseCriticVerdict(output: string): ParsedCriticVerdict | undefined {
|
|
63
|
+
let data: unknown;
|
|
64
|
+
try {
|
|
65
|
+
data = JSON.parse(output);
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (data === null || typeof data !== "object") return undefined;
|
|
70
|
+
const record = data as Record<string, unknown>;
|
|
71
|
+
const verdict = record.verdict;
|
|
72
|
+
if (verdict !== "okay" && verdict !== "iterate" && verdict !== "reject") return undefined;
|
|
73
|
+
const rawFixes = record.required_fixes;
|
|
74
|
+
const requiredFixes = Array.isArray(rawFixes)
|
|
75
|
+
? rawFixes.filter((fix): fix is string => typeof fix === "string")
|
|
76
|
+
: [];
|
|
77
|
+
const summary = typeof record.summary === "string" ? record.summary : undefined;
|
|
78
|
+
return { verdict, requiredFixes, summary };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fold a new verdict into the gate state. `okay` clears the gate; a non-okay
|
|
83
|
+
* verdict engages it and increments the consecutive-strike counter.
|
|
84
|
+
*/
|
|
85
|
+
export function updateCriticGateState(
|
|
86
|
+
previous: CriticGateState | undefined,
|
|
87
|
+
parsed: ParsedCriticVerdict,
|
|
88
|
+
agentId: string,
|
|
89
|
+
now: number = Date.now(),
|
|
90
|
+
): CriticGateState | undefined {
|
|
91
|
+
if (parsed.verdict === "okay") return undefined;
|
|
92
|
+
return {
|
|
93
|
+
verdict: parsed.verdict,
|
|
94
|
+
agentId,
|
|
95
|
+
at: now,
|
|
96
|
+
requiredFixes: parsed.requiredFixes,
|
|
97
|
+
strikes: (previous?.strikes ?? 0) + 1,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatRequiredFixes(state: CriticGateState): string {
|
|
102
|
+
if (state.requiredFixes.length === 0) return "";
|
|
103
|
+
const bullets = state.requiredFixes.map(fix => `- ${fix}`).join("\n");
|
|
104
|
+
return `\nUnresolved required fixes:\n${bullets}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function gateSummaryLine(state: CriticGateState): string {
|
|
108
|
+
return `Critic gate engaged: the latest critic verdict (from \`${state.agentId}\`) was \`${state.verdict}\`, not \`okay\`.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decide whether a spawn is allowed under the current gate state. Returns the
|
|
113
|
+
* rejection message when blocked, undefined when allowed.
|
|
114
|
+
*
|
|
115
|
+
* Read-only agents (plan revision, exploration, re-gating via critic) stay
|
|
116
|
+
* allowed — except that once the strike budget is exhausted, even a fresh
|
|
117
|
+
* critic run is refused so the loop terminates at the user instead of cycling.
|
|
118
|
+
*/
|
|
119
|
+
export function evaluateCriticGateSpawn(
|
|
120
|
+
state: CriticGateState | undefined,
|
|
121
|
+
agentName: string,
|
|
122
|
+
agentIsReadOnly: boolean,
|
|
123
|
+
): string | undefined {
|
|
124
|
+
if (!state) return undefined;
|
|
125
|
+
if (agentName === CRITIC_AGENT_NAME && state.strikes >= CRITIC_GATE_MAX_STRIKES) {
|
|
126
|
+
return (
|
|
127
|
+
`Critic gate: ${state.strikes} consecutive non-okay critic verdicts without user input — the iterate loop is closed. ` +
|
|
128
|
+
`Do NOT re-submit to critic. Stop and report the surviving gaps to the user; the gate clears on the next user message.${formatRequiredFixes(state)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (agentIsReadOnly) return undefined;
|
|
132
|
+
return (
|
|
133
|
+
`${gateSummaryLine(state)} Spawning non-read-only agent "${agentName}" is blocked by the runtime, not just the prompt. ` +
|
|
134
|
+
`To proceed: revise the plan (read-only \`plan\` agent), re-submit to a fresh \`critic\`, and execute only after an \`okay\` verdict — or stop and report to the user (the gate clears on the next user message).${formatRequiredFixes(state)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Error text for a working-tree mutation attempted while the gate is engaged.
|
|
140
|
+
* The `local://` sandbox is exempt (seeds/plans/notes live there); callers
|
|
141
|
+
* check that before invoking this.
|
|
142
|
+
*/
|
|
143
|
+
export function criticGateWriteMessage(state: CriticGateState): string {
|
|
144
|
+
return (
|
|
145
|
+
`${gateSummaryLine(state)} The working tree is locked until a critic returns \`okay\` for the revised plan ` +
|
|
146
|
+
`(or the user sends a new message). Write plans and notes to local:// files instead.${formatRequiredFixes(state)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
package/src/task/discovery.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Agent discovery from filesystem.
|
|
3
3
|
*
|
|
4
4
|
* Discovers agent definitions from OMP-native task-agent roots:
|
|
5
|
-
* - ~/.
|
|
6
|
-
* - .
|
|
5
|
+
* - ~/.jeopi/agent/agents/*.md (user-level)
|
|
6
|
+
* - .jeopi/agents/*.md (project-level)
|
|
7
7
|
*
|
|
8
8
|
* Claude Code marketplace plugin agents are discovered separately via the
|
|
9
9
|
* claude-plugins provider. Direct cross-harness roots such as .claude/agents
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
import * as fs from "node:fs/promises";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import { logger } from "jeopi-utils";
|
|
18
|
+
import { CONFIG_DIR_NAME, logger } from "jeopi-utils";
|
|
19
19
|
import { isProviderEnabled } from "../capability";
|
|
20
20
|
import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config";
|
|
21
21
|
import { listClaudePluginRoots } from "../discovery/helpers";
|
|
22
22
|
import { loadBundledAgents, parseAgent } from "./agents";
|
|
23
23
|
import type { AgentDefinition, AgentSource } from "./types";
|
|
24
24
|
|
|
25
|
-
const TASK_AGENT_CONFIG_SOURCE =
|
|
25
|
+
const TASK_AGENT_CONFIG_SOURCE = CONFIG_DIR_NAME;
|
|
26
26
|
|
|
27
27
|
/** Result of agent discovery */
|
|
28
28
|
export interface DiscoveryResult {
|
|
@@ -55,7 +55,7 @@ async function loadAgentsFromDir(dir: string, source: AgentSource): Promise<Agen
|
|
|
55
55
|
/**
|
|
56
56
|
* Discover agents from filesystem and merge with bundled agents.
|
|
57
57
|
*
|
|
58
|
-
* Precedence (highest wins): project .
|
|
58
|
+
* Precedence (highest wins): project .jeopi, user .jeopi, Claude plugin agents, then bundled
|
|
59
59
|
* @param cwd - Current working directory for project agent discovery
|
|
60
60
|
*/
|
|
61
61
|
export async function discoverAgents(cwd: string, home: string = os.homedir()): Promise<DiscoveryResult> {
|
package/src/task/executor.ts
CHANGED
|
@@ -342,7 +342,7 @@ export interface ExecutorOptions {
|
|
|
342
342
|
preloadedExtensionPaths?: string[];
|
|
343
343
|
/**
|
|
344
344
|
* Parent's discovered custom-tool source paths. Forwarded to skip the
|
|
345
|
-
* `.
|
|
345
|
+
* `.jeopi/tools/` FS scan in the subagent; the subagent then re-binds each
|
|
346
346
|
* tool against its own `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
347
347
|
*/
|
|
348
348
|
preloadedCustomToolPaths?: ToolPathWithSource[];
|
package/src/task/index.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Discovers agent definitions from:
|
|
5
5
|
* - Bundled agents (shipped with omp-coding-agent)
|
|
6
|
-
* - ~/.
|
|
7
|
-
* - .
|
|
6
|
+
* - ~/.jeopi/agent/agents/*.md (user-level)
|
|
7
|
+
* - .jeopi/agents/*.md (project-level)
|
|
8
8
|
*
|
|
9
9
|
* Supports:
|
|
10
10
|
* - Single agent spawn per call (parallelism = parallel task calls)
|
|
@@ -48,6 +48,7 @@ import type { AsyncJobManager } from "../async";
|
|
|
48
48
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
49
49
|
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
50
50
|
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
51
|
+
import { CRITIC_AGENT_NAME, evaluateCriticGateSpawn, parseCriticVerdict, updateCriticGateState } from "./critic-gate";
|
|
51
52
|
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
52
53
|
import { runSubprocess } from "./executor";
|
|
53
54
|
import {
|
|
@@ -1118,6 +1119,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1118
1119
|
};
|
|
1119
1120
|
}
|
|
1120
1121
|
|
|
1122
|
+
// Critic gate: while the latest critic verdict is non-okay, only
|
|
1123
|
+
// read-only agents may spawn (plan revision / re-gating); execution
|
|
1124
|
+
// lanes are blocked by the runtime, not just the prompt.
|
|
1125
|
+
const criticGateRejection = evaluateCriticGateSpawn(
|
|
1126
|
+
this.session.getCriticGateState?.(),
|
|
1127
|
+
agentName,
|
|
1128
|
+
isReadOnlyAgent(agent),
|
|
1129
|
+
);
|
|
1130
|
+
if (criticGateRejection) {
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: criticGateRejection }],
|
|
1133
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1121
1137
|
const planModeState = this.session.getPlanModeState?.();
|
|
1122
1138
|
const planModeBaseTools = ["read", "grep", "glob", "lsp", "web_search"];
|
|
1123
1139
|
const planModeTools = [
|
|
@@ -1388,6 +1404,23 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1388
1404
|
|
|
1389
1405
|
const result = await runTask();
|
|
1390
1406
|
|
|
1407
|
+
// Record critic verdicts into the session's hard execution gate. Only
|
|
1408
|
+
// clean, schema-validated completions move the gate — a failed or
|
|
1409
|
+
// aborted critic run leaves the previous state untouched.
|
|
1410
|
+
if (
|
|
1411
|
+
agent.name === CRITIC_AGENT_NAME &&
|
|
1412
|
+
this.session.setCriticGateState &&
|
|
1413
|
+
result.exitCode === 0 &&
|
|
1414
|
+
!result.aborted
|
|
1415
|
+
) {
|
|
1416
|
+
const parsed = parseCriticVerdict(result.output);
|
|
1417
|
+
if (parsed) {
|
|
1418
|
+
this.session.setCriticGateState(
|
|
1419
|
+
updateCriticGateState(this.session.getCriticGateState?.(), parsed, result.id),
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1391
1424
|
let mergeSummary = "";
|
|
1392
1425
|
let changesApplied: boolean | null = null;
|
|
1393
1426
|
let mergedBranchForNestedPatches = false;
|
package/src/tools/acp-bridge.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When an ACP client (e.g. Zed) advertises the `fs.writeTextFile` capability,
|
|
5
5
|
* all write-mode tools must route through it so the editor's open buffer is
|
|
6
|
-
* updated immediately. Internal artifacts ('/Users/theo/.
|
|
6
|
+
* updated immediately. Internal artifacts ('/Users/theo/.jeopi/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local' plan files, other scheme
|
|
7
7
|
* URLs) are always written directly to disk — those are OMP-owned and should
|
|
8
8
|
* never be pushed into the editor.
|
|
9
9
|
*/
|
|
@@ -16,7 +16,7 @@ import { ToolError } from "./tool-errors";
|
|
|
16
16
|
/**
|
|
17
17
|
* Return `true` when an ACP client bridge write is appropriate for this path.
|
|
18
18
|
*
|
|
19
|
-
* Returns `false` for internal-URL paths (e.g. `'/Users/theo/.
|
|
19
|
+
* Returns `false` for internal-URL paths (e.g. `'/Users/theo/.jeopi/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local/PLAN.md'`) and for the
|
|
20
20
|
* active plan file while plan mode is enabled — both are OMP-internal artifacts
|
|
21
21
|
* that must stay off the editor's buffer.
|
|
22
22
|
*/
|
|
@@ -101,7 +101,7 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
|
101
101
|
/**
|
|
102
102
|
* Lazily download Chromium on first browser launch via @puppeteer/browsers.
|
|
103
103
|
* Skipped when a system Chromium (NixOS) or PUPPETEER_EXECUTABLE_PATH is set.
|
|
104
|
-
* The browser is cached under ~/.
|
|
104
|
+
* The browser is cached under ~/.jeopi/puppeteer (getPuppeteerDir).
|
|
105
105
|
*/
|
|
106
106
|
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
107
107
|
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
@@ -38,7 +38,7 @@ export interface ScreenshotResult {
|
|
|
38
38
|
export interface SessionSnapshot {
|
|
39
39
|
cwd: string;
|
|
40
40
|
browserScreenshotDir?: string;
|
|
41
|
-
/** Force non-WebP screenshot encoding (e.g. for Ollama). Unset honors `
|
|
41
|
+
/** Force non-WebP screenshot encoding (e.g. for Ollama). Unset honors `JEOPI_NO_WEBP`. */
|
|
42
42
|
excludeWebP?: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
package/src/tools/grep.ts
CHANGED
|
@@ -342,7 +342,7 @@ interface IndexedContentLines {
|
|
|
342
342
|
starts: number[];
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
const
|
|
345
|
+
const INTERNAL_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
|
|
346
346
|
|
|
347
347
|
function normalizeSearchLine(line: string): string {
|
|
348
348
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
@@ -721,7 +721,7 @@ async function expandVirtualInternalResource(
|
|
|
721
721
|
context: ResolveContext,
|
|
722
722
|
ranges: readonly LineRange[] | undefined,
|
|
723
723
|
): Promise<VirtualSearchResource[]> {
|
|
724
|
-
if (
|
|
724
|
+
if (INTERNAL_ROOT_URL_RE.test(rawPath)) {
|
|
725
725
|
const completions = await internalRouter.complete("omp", "");
|
|
726
726
|
if (completions && completions.length > 0) {
|
|
727
727
|
const resources: VirtualSearchResource[] = [];
|
package/src/tools/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { CustomMessage } from "../session/messages";
|
|
|
27
27
|
import type { UsageStatistics } from "../session/session-entries";
|
|
28
28
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
29
29
|
import { TaskTool } from "../task";
|
|
30
|
+
import type { CriticGateState } from "../task/critic-gate";
|
|
30
31
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
31
32
|
import { canSpawnAtDepth } from "../task/types";
|
|
32
33
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
@@ -187,7 +188,7 @@ export interface ToolSession {
|
|
|
187
188
|
*/
|
|
188
189
|
extensionPaths?: string[];
|
|
189
190
|
/**
|
|
190
|
-
* Pre-discovered custom-tool source paths from `.
|
|
191
|
+
* Pre-discovered custom-tool source paths from `.jeopi/tools/`, `.claude/tools/`,
|
|
191
192
|
* plugins, etc. Forwarded to subagents so they skip the FS scan but still
|
|
192
193
|
* re-bind tools to their own session-scoped `CustomToolAPI`.
|
|
193
194
|
*/
|
|
@@ -273,6 +274,10 @@ export interface ToolSession {
|
|
|
273
274
|
getPlanModeState?: () => PlanModeState | undefined;
|
|
274
275
|
/** Path of the session's active plan reference (e.g. `local://<title>.md`); defaults to `local://PLAN.md`. */
|
|
275
276
|
getPlanReferencePath?: () => string;
|
|
277
|
+
/** Critic gate state — engaged after a non-okay `critic` verdict; cleared by an okay verdict or the next user prompt. */
|
|
278
|
+
getCriticGateState?: () => CriticGateState | undefined;
|
|
279
|
+
/** Replace the critic gate state (undefined clears the gate). */
|
|
280
|
+
setCriticGateState?: (state: CriticGateState | undefined) => void;
|
|
276
281
|
/** Goal mode state (if active or paused) */
|
|
277
282
|
getGoalModeState?: () => GoalModeState | undefined;
|
|
278
283
|
/** Goal runtime for the active agent session. */
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveLocalUrlToPath,
|
|
8
8
|
resolveVaultUrlToPath,
|
|
9
9
|
} from "../internal-urls";
|
|
10
|
+
import { criticGateWriteMessage } from "../task/critic-gate";
|
|
10
11
|
import type { ToolSession } from ".";
|
|
11
12
|
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
12
13
|
import { ToolError } from "./tool-errors";
|
|
@@ -136,6 +137,7 @@ export function enforcePlanModeWrite(
|
|
|
136
137
|
targetPath: string,
|
|
137
138
|
options?: { move?: string; op?: "create" | "update" | "delete" },
|
|
138
139
|
): void {
|
|
140
|
+
enforceCriticGateWrite(session, targetPath);
|
|
139
141
|
const state = session.getPlanModeState?.();
|
|
140
142
|
if (!state?.enabled) return;
|
|
141
143
|
|
|
@@ -153,3 +155,17 @@ export function enforcePlanModeWrite(
|
|
|
153
155
|
"Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.",
|
|
154
156
|
);
|
|
155
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Critic gate — while the session's latest critic verdict is not `okay`,
|
|
161
|
+
* working-tree mutations are rejected at the runtime level. The `local://`
|
|
162
|
+
* artifact sandbox stays writable (seeds, plans, and scratch notes live
|
|
163
|
+
* there). Runs inside {@link enforcePlanModeWrite} so every mutating tool
|
|
164
|
+
* (`write`, `edit` replace/patch/hashline) hits one choke point.
|
|
165
|
+
*/
|
|
166
|
+
export function enforceCriticGateWrite(session: ToolSession, targetPath: string): void {
|
|
167
|
+
const gate = session.getCriticGateState?.();
|
|
168
|
+
if (!gate) return;
|
|
169
|
+
if (targetsLocalSandbox(session, targetPath)) return;
|
|
170
|
+
throw new ToolError(criticGateWriteMessage(gate));
|
|
171
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -1053,7 +1053,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1053
1053
|
const sessionFile = this.session.getSessionFile();
|
|
1054
1054
|
root = sessionFile?.endsWith(".jsonl")
|
|
1055
1055
|
? sessionFile.slice(0, -6)
|
|
1056
|
-
: path.join(os.tmpdir(), "
|
|
1056
|
+
: path.join(os.tmpdir(), "jeopi-read-pdf-images");
|
|
1057
1057
|
}
|
|
1058
1058
|
const basename = path.basename(absolutePdfPath).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1059
1059
|
return path.join(root, "read-pdf-images", `${basename}-${Bun.hash(absolutePdfPath).toString(36)}`);
|
|
@@ -187,7 +187,7 @@ let cachedDb: Database | null = null;
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Open (or return the cached handle for) the auto-QA SQLite database at
|
|
190
|
-
* `~/.
|
|
190
|
+
* `~/.jeopi/agent/autoqa.db`. Idempotently runs schema creation, the
|
|
191
191
|
* `pushed`-column migration, and index setup so every consumer — tool
|
|
192
192
|
* execute path, manual `jeopi grievances push`, future debug scripts —
|
|
193
193
|
* sees the same prepared schema. Returns `null` only on a hard open
|
|
@@ -474,7 +474,7 @@ export function createReportToolIssueTool(session: ToolSession, activeBuiltinNam
|
|
|
474
474
|
intent: "omit",
|
|
475
475
|
async execute(_toolCallId, rawParams) {
|
|
476
476
|
// Save is unconditional: the row lives in the user's own SQLite
|
|
477
|
-
// at ~/.
|
|
477
|
+
// at ~/.jeopi/agent/autoqa.db regardless of consent — they always
|
|
478
478
|
// own their local data and can inspect or wipe it via `jeopi grievances`.
|
|
479
479
|
// Consent only gates whether the row is *shipped* to the shared
|
|
480
480
|
// backend; that decision rides on `dev.autoqa.consent` and is
|
package/src/utils/changelog.ts
CHANGED
|
@@ -106,8 +106,8 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
|
|
|
106
106
|
export { getChangelogPath } from "../config";
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Last
|
|
110
|
-
* marker file (`~/.
|
|
109
|
+
* Last jeopi version whose changelog the user has seen. Stored as a plain-text
|
|
110
|
+
* marker file (`~/.jeopi/agent/last-changelog-version`) rather than in
|
|
111
111
|
* `config.yml`, so version bumps never dirty user-tracked config files.
|
|
112
112
|
*/
|
|
113
113
|
export async function readLastChangelogVersion(agentDir?: string): Promise<string | undefined> {
|
|
@@ -11,7 +11,7 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
|
11
11
|
* Ollama and its local-backend family decode image input through llama.cpp /
|
|
12
12
|
* `stb_image`, which is compiled without WebP support, so a WebP upload fails
|
|
13
13
|
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
|
14
|
-
* to PNG/JPEG instead — the automatic equivalent of `
|
|
14
|
+
* to PNG/JPEG instead — the automatic equivalent of `JEOPI_NO_WEBP=1`.
|
|
15
15
|
*/
|
|
16
16
|
export function modelLacksWebpSupport(
|
|
17
17
|
model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
|
|
@@ -30,7 +30,7 @@ export function modelLacksWebpSupport(
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* `true` when `model` cannot decode WebP, otherwise `undefined` so the
|
|
33
|
-
* `
|
|
33
|
+
* `JEOPI_NO_WEBP` env fallback in {@link resizeImage} still applies. Feed straight
|
|
34
34
|
* into {@link ImageResizeOptions.excludeWebP}.
|
|
35
35
|
*/
|
|
36
36
|
export function webpExclusionForModel(model: Pick<Model, "provider" | "api"> | undefined): true | undefined {
|
|
@@ -44,7 +44,7 @@ export interface LoadImageInputOptions {
|
|
|
44
44
|
maxBytes?: number;
|
|
45
45
|
resolvedPath?: string;
|
|
46
46
|
detectedMimeType?: string;
|
|
47
|
-
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `
|
|
47
|
+
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `JEOPI_NO_WEBP`. */
|
|
48
48
|
excludeWebP?: boolean;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -55,7 +55,7 @@ export interface LoadImageAttachmentInputOptions {
|
|
|
55
55
|
uri: string;
|
|
56
56
|
autoResize: boolean;
|
|
57
57
|
maxBytes?: number;
|
|
58
|
-
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `
|
|
58
|
+
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `JEOPI_NO_WEBP`. */
|
|
59
59
|
excludeWebP?: boolean;
|
|
60
60
|
}
|
|
61
61
|
|