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.
Files changed (182) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/README.md +1 -1
  3. package/dist/cli.js +3087 -3060
  4. package/dist/types/advisor/watchdog.d.ts +2 -2
  5. package/dist/types/autolearn/managed-skills.d.ts +2 -2
  6. package/dist/types/capability/types.d.ts +2 -2
  7. package/dist/types/cli/commands/migrate-legacy.d.ts +1 -0
  8. package/dist/types/cli/config-cli.d.ts +1 -1
  9. package/dist/types/cli/flag-tables.d.ts +1 -1
  10. package/dist/types/cli/grievances-cli.d.ts +1 -1
  11. package/dist/types/cli/models-cli.d.ts +1 -1
  12. package/dist/types/cli/update-cli.d.ts +2 -2
  13. package/dist/types/cli/worktree-cli.d.ts +1 -1
  14. package/dist/types/commit/index.d.ts +1 -1
  15. package/dist/types/commit/pipeline.d.ts +1 -1
  16. package/dist/types/commit/types.d.ts +1 -1
  17. package/dist/types/config/prompt-templates.d.ts +1 -1
  18. package/dist/types/config/settings-schema.d.ts +16 -6
  19. package/dist/types/config.d.ts +7 -7
  20. package/dist/types/discovery/helpers.d.ts +8 -8
  21. package/dist/types/discovery/omp-extension-roots.d.ts +2 -2
  22. package/dist/types/eval/py/runtime.d.ts +1 -1
  23. package/dist/types/extensibility/custom-tools/loader.d.ts +2 -2
  24. package/dist/types/extensibility/extensions/loader.d.ts +1 -1
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -1
  26. package/dist/types/extensibility/plugins/loader.d.ts +2 -2
  27. package/dist/types/extensibility/plugins/types.d.ts +13 -3
  28. package/dist/types/lsp/config.d.ts +2 -2
  29. package/dist/types/mcp/client.d.ts +1 -1
  30. package/dist/types/mcp/config-writer.d.ts +1 -1
  31. package/dist/types/mcp/oauth-flow.d.ts +1 -1
  32. package/dist/types/mcp/transports/http.d.ts +1 -1
  33. package/dist/types/memories/okf.d.ts +36 -0
  34. package/dist/types/modes/components/__tests__/assistant-agent-label.test.d.ts +1 -0
  35. package/dist/types/modes/components/assistant-message.d.ts +4 -0
  36. package/dist/types/modes/interactive-mode.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +5 -5
  38. package/dist/types/session/agent-session.d.ts +3 -0
  39. package/dist/types/session/auth-broker-config.d.ts +4 -4
  40. package/dist/types/session/snapcompact-savings-journal.d.ts +1 -1
  41. package/dist/types/task/commands.d.ts +1 -1
  42. package/dist/types/task/critic-gate.d.ts +76 -0
  43. package/dist/types/task/critic-gate.test.d.ts +1 -0
  44. package/dist/types/task/discovery.d.ts +1 -1
  45. package/dist/types/task/executor.d.ts +1 -1
  46. package/dist/types/tools/acp-bridge.d.ts +2 -2
  47. package/dist/types/tools/browser/tab-protocol.d.ts +1 -1
  48. package/dist/types/tools/index.d.ts +6 -1
  49. package/dist/types/tools/plan-mode-guard.d.ts +8 -0
  50. package/dist/types/tools/report-tool-issue.d.ts +1 -1
  51. package/dist/types/utils/changelog.d.ts +2 -2
  52. package/dist/types/utils/image-loading.d.ts +4 -4
  53. package/dist/types/utils/image-resize.d.ts +1 -1
  54. package/dist/types/web/search/index.d.ts +1 -1
  55. package/examples/custom-tools/README.md +1 -1
  56. package/examples/extensions/README.md +1 -1
  57. package/examples/extensions/pirate.ts +1 -1
  58. package/examples/extensions/plan-mode.ts +1 -1
  59. package/examples/extensions/tools.ts +1 -1
  60. package/examples/hooks/README.md +1 -1
  61. package/examples/sdk/01-minimal.ts +1 -1
  62. package/examples/sdk/04-skills.ts +1 -1
  63. package/examples/sdk/06-extensions.ts +3 -3
  64. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  65. package/examples/sdk/12-redis-sessions.ts +1 -1
  66. package/examples/sdk/13-sql-sessions.ts +1 -1
  67. package/examples/sdk/README.md +1 -1
  68. package/package.json +12 -12
  69. package/scripts/jeopi +1 -1
  70. package/src/advisor/__tests__/config.test.ts +4 -4
  71. package/src/advisor/watchdog.ts +8 -8
  72. package/src/autolearn/managed-skills.ts +4 -4
  73. package/src/autoresearch/storage.ts +2 -2
  74. package/src/capability/types.ts +2 -2
  75. package/src/cli/agents-cli.ts +2 -2
  76. package/src/cli/args.ts +3 -3
  77. package/src/cli/auth-broker-cli.ts +5 -5
  78. package/src/cli/auth-gateway-cli.ts +4 -4
  79. package/src/cli/claude-trace-cli.ts +1 -1
  80. package/src/cli/commands/migrate-legacy.ts +24 -0
  81. package/src/cli/config-cli.ts +20 -5
  82. package/src/cli/flag-tables.ts +1 -1
  83. package/src/cli/gallery-fixtures/misc.ts +7 -7
  84. package/src/cli/gallery-screenshot.ts +2 -2
  85. package/src/cli/grievances-cli.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +2 -2
  88. package/src/cli/profile-alias.ts +8 -8
  89. package/src/cli/read-cli.ts +1 -1
  90. package/src/cli/ssh-cli.ts +2 -2
  91. package/src/cli/update-cli.ts +27 -27
  92. package/src/cli/worktree-cli.ts +3 -3
  93. package/src/cli.ts +5 -5
  94. package/src/commands/agents.ts +2 -2
  95. package/src/commands/auth-broker.ts +1 -1
  96. package/src/commands/config.ts +1 -1
  97. package/src/commands/dry-balance.ts +6 -6
  98. package/src/commands/launch.ts +2 -2
  99. package/src/commands/models.ts +1 -1
  100. package/src/commands/say.ts +1 -1
  101. package/src/commands/token.ts +5 -5
  102. package/src/commands/ttsr.ts +2 -2
  103. package/src/commands/worktree.ts +3 -3
  104. package/src/commit/agentic/prompts/system.md +1 -1
  105. package/src/commit/cli.ts +1 -1
  106. package/src/commit/index.ts +1 -1
  107. package/src/commit/pipeline.ts +1 -1
  108. package/src/commit/types.ts +1 -1
  109. package/src/config/mcp-schema.json +5 -5
  110. package/src/config/models-config-schema.ts +1 -1
  111. package/src/config/prompt-templates.ts +2 -2
  112. package/src/config/settings-schema.ts +20 -8
  113. package/src/config/settings.ts +1 -1
  114. package/src/config.ts +10 -10
  115. package/src/dap/session.ts +2 -2
  116. package/src/debug/protocol-probe.ts +1 -1
  117. package/src/debug/raw-sse-buffer.ts +3 -3
  118. package/src/debug/raw-sse.ts +2 -2
  119. package/src/discovery/builtin.ts +9 -9
  120. package/src/discovery/helpers.ts +13 -13
  121. package/src/discovery/omp-extension-roots.ts +4 -4
  122. package/src/discovery/omp-plugins.ts +2 -2
  123. package/src/discovery/substitute-plugin-root.ts +3 -3
  124. package/src/eval/py/runtime.ts +1 -1
  125. package/src/eval/rb/runner.rb +6 -6
  126. package/src/export/custom-share.ts +1 -1
  127. package/src/extensibility/custom-tools/loader.ts +3 -3
  128. package/src/extensibility/extensions/loader.ts +4 -4
  129. package/src/extensibility/hooks/loader.ts +1 -1
  130. package/src/extensibility/plugins/loader.ts +16 -10
  131. package/src/extensibility/plugins/manager.ts +10 -9
  132. package/src/extensibility/plugins/types.ts +15 -4
  133. package/src/internal-urls/docs-index.generated.txt +1 -1
  134. package/src/lsp/config.ts +4 -4
  135. package/src/lsp/index.ts +1 -1
  136. package/src/main.ts +1 -1
  137. package/src/mcp/client.ts +1 -1
  138. package/src/mcp/config-writer.ts +2 -2
  139. package/src/mcp/manager.ts +1 -1
  140. package/src/mcp/oauth-flow.ts +1 -1
  141. package/src/mcp/timeout.ts +2 -2
  142. package/src/mcp/transports/http.ts +1 -1
  143. package/src/memories/index.ts +82 -11
  144. package/src/memories/okf.ts +129 -0
  145. package/src/modes/components/__tests__/assistant-agent-label.test.ts +87 -0
  146. package/src/modes/components/assistant-message.ts +27 -0
  147. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  148. package/src/modes/controllers/mcp-command-controller.ts +2 -2
  149. package/src/modes/controllers/omfg-controller.ts +2 -2
  150. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  151. package/src/modes/interactive-mode.ts +3 -1
  152. package/src/modes/setup-wizard/index.ts +1 -1
  153. package/src/prompts/agents/jeo.md +20 -10
  154. package/src/prompts/system/autolearn-guidance.md +2 -2
  155. package/src/prompts/system/system-prompt.md +10 -1
  156. package/src/prompts/tools/learn.md +1 -1
  157. package/src/prompts/tools/manage-skill.md +1 -1
  158. package/src/sdk.ts +8 -6
  159. package/src/secrets/index.ts +2 -2
  160. package/src/session/agent-session.ts +86 -6
  161. package/src/session/auth-broker-config.ts +6 -6
  162. package/src/session/snapcompact-savings-journal.ts +1 -1
  163. package/src/slash-commands/helpers/mcp.ts +1 -1
  164. package/src/system-prompt.test.ts +14 -14
  165. package/src/task/commands.ts +1 -1
  166. package/src/task/critic-gate.test.ts +181 -0
  167. package/src/task/critic-gate.ts +148 -0
  168. package/src/task/discovery.ts +5 -5
  169. package/src/task/executor.ts +1 -1
  170. package/src/task/index.ts +35 -2
  171. package/src/tools/acp-bridge.ts +2 -2
  172. package/src/tools/browser/launch.ts +1 -1
  173. package/src/tools/browser/tab-protocol.ts +1 -1
  174. package/src/tools/grep.ts +2 -2
  175. package/src/tools/index.ts +6 -1
  176. package/src/tools/plan-mode-guard.ts +16 -0
  177. package/src/tools/read.ts +1 -1
  178. package/src/tools/report-tool-issue.ts +2 -2
  179. package/src/utils/changelog.ts +2 -2
  180. package/src/utils/image-loading.ts +4 -4
  181. package/src/utils/image-resize.ts +5 -5
  182. 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
+ }
@@ -2,8 +2,8 @@
2
2
  * Agent discovery from filesystem.
3
3
  *
4
4
  * Discovers agent definitions from OMP-native task-agent roots:
5
- * - ~/.omp/agent/agents/*.md (user-level)
6
- * - .omp/agents/*.md (project-level)
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 = ".omp";
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 .omp, user .omp, Claude plugin agents, then bundled
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> {
@@ -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
- * `.omp/tools/` FS scan in the subagent; the subagent then re-binds each
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
- * - ~/.omp/agent/agents/*.md (user-level)
7
- * - .omp/agents/*.md (project-level)
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;
@@ -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/.omp/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local' plan files, other scheme
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/.omp/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local/PLAN.md'`) and for the
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 ~/.omp/puppeteer (getPuppeteerDir).
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 `OMP_NO_WEBP`. */
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 OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
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 (OMP_ROOT_URL_RE.test(rawPath)) {
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[] = [];
@@ -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 `.omp/tools/`, `.claude/tools/`,
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(), "omp-read-pdf-images");
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
- * `~/.omp/agent/autoqa.db`. Idempotently runs schema creation, the
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 ~/.omp/agent/autoqa.db regardless of consent — they always
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
@@ -106,8 +106,8 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
106
106
  export { getChangelogPath } from "../config";
107
107
 
108
108
  /**
109
- * Last omp version whose changelog the user has seen. Stored as a plain-text
110
- * marker file (`~/.omp/agent/last-changelog-version`) rather than in
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 `OMP_NO_WEBP=1`.
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
- * `OMP_NO_WEBP` env fallback in {@link resizeImage} still applies. Feed straight
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 `OMP_NO_WEBP`. */
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 `OMP_NO_WEBP`. */
58
+ /** Force non-WebP output (e.g. for Ollama). Leave unset to honor `JEOPI_NO_WEBP`. */
59
59
  excludeWebP?: boolean;
60
60
  }
61
61