jeopi-cli 16.2.30 → 16.4.0

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 (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/cli.js +2824 -2736
  3. package/dist/types/config/settings-schema.d.ts +32 -2
  4. package/dist/types/goals/runtime.d.ts +16 -0
  5. package/dist/types/goals/verifier.d.ts +9 -0
  6. package/dist/types/memories/index.d.ts +9 -0
  7. package/dist/types/memory-backend/types.d.ts +11 -0
  8. package/dist/types/session/agent-session.d.ts +22 -7
  9. package/dist/types/slash-commands/helpers/skill-verify.d.ts +5 -0
  10. package/dist/types/task/model-tier-classifier.d.ts +8 -0
  11. package/dist/types/tools/builtin-names.d.ts +1 -1
  12. package/dist/types/tools/index.d.ts +1 -0
  13. package/dist/types/tools/learn.d.ts +4 -0
  14. package/dist/types/tools/path-write-lock.d.ts +10 -0
  15. package/dist/types/tools/vision-verify.d.ts +34 -0
  16. package/package.json +12 -12
  17. package/src/autolearn/controller.ts +21 -5
  18. package/src/autoresearch/index.ts +60 -6
  19. package/src/config/settings-schema.ts +38 -2
  20. package/src/edit/index.ts +2 -1
  21. package/src/goals/runtime.ts +51 -0
  22. package/src/goals/verifier.ts +100 -0
  23. package/src/internal-urls/docs-index.generated.txt +2 -2
  24. package/src/memories/index.ts +10 -2
  25. package/src/memory-backend/local-backend.ts +34 -0
  26. package/src/memory-backend/types.ts +11 -0
  27. package/src/prompts/agents/designer.md +1 -0
  28. package/src/prompts/agents/goal-verifier.md +51 -0
  29. package/src/prompts/tools/learn.md +2 -0
  30. package/src/prompts/tools/vision-verify-system.md +16 -0
  31. package/src/prompts/tools/vision-verify.md +20 -0
  32. package/src/sdk.ts +2 -0
  33. package/src/session/agent-session.ts +51 -8
  34. package/src/slash-commands/builtin-registry.ts +9 -0
  35. package/src/slash-commands/helpers/skill-verify.ts +171 -0
  36. package/src/task/agents.ts +2 -0
  37. package/src/task/index.ts +26 -1
  38. package/src/task/model-tier-classifier.ts +43 -0
  39. package/src/tools/builtin-names.ts +1 -0
  40. package/src/tools/index.ts +4 -0
  41. package/src/tools/learn.ts +16 -1
  42. package/src/tools/path-write-lock.ts +44 -0
  43. package/src/tools/vision-verify.ts +347 -0
  44. package/src/tools/write.ts +12 -9
@@ -189,12 +189,12 @@ export declare const SETTINGS_SCHEMA: {
189
189
  };
190
190
  readonly "advisor.includeThinking": {
191
191
  readonly type: "boolean";
192
- readonly default: true;
192
+ readonly default: false;
193
193
  readonly ui: {
194
194
  readonly tab: "model";
195
195
  readonly group: "Advisor";
196
196
  readonly label: "Advisor Sees Thinking";
197
- readonly description: "Include the primary agent's thinking blocks in advisor transcript updates. Some provider classifiers (e.g. Anthropic's reasoning_extraction) refuse payloads that replay thinking as text; the advisor also auto-disables this for the session after such a refusal.";
197
+ readonly description: "Include the primary agent's thinking blocks in advisor transcript updates. Off by default: a reviewer that sees the maker's own reasoning trail tends to anchor on conclusions the maker already committed to instead of bringing an independent angle (the advisor's own directive is to 'bring the angle they skipped, never re-run reasoning they already have'); the advisor still sees full messages, tool calls, results, and diffs. Some provider classifiers (e.g. Anthropic's reasoning_extraction) also refuse payloads that replay thinking as text if enabled; the advisor auto-disables this for the session after such a refusal.";
198
198
  readonly condition: "advisorEnabled";
199
199
  };
200
200
  };
@@ -3690,6 +3690,16 @@ export declare const SETTINGS_SCHEMA: {
3690
3690
  readonly description: "Enable the inspect_image tool, delegating image understanding to a vision-capable model";
3691
3691
  };
3692
3692
  };
3693
+ readonly "vision_verify.enabled": {
3694
+ readonly type: "boolean";
3695
+ readonly default: false;
3696
+ readonly ui: {
3697
+ readonly tab: "tools";
3698
+ readonly group: "Available Tools";
3699
+ readonly label: "Vision Verify";
3700
+ readonly description: "Enable the vision_verify tool, checking a screenshot/artifact against a stated goal with a vision-capable model";
3701
+ };
3702
+ };
3693
3703
  readonly "checkpoint.enabled": {
3694
3704
  readonly type: "boolean";
3695
3705
  readonly default: false;
@@ -4056,6 +4066,16 @@ export declare const SETTINGS_SCHEMA: {
4056
4066
  readonly description: "Run modes where active goals may auto-continue between turns";
4057
4067
  };
4058
4068
  };
4069
+ readonly "goal.verifyCompletion": {
4070
+ readonly type: "boolean";
4071
+ readonly default: true;
4072
+ readonly ui: {
4073
+ readonly tab: "tasks";
4074
+ readonly group: "Modes";
4075
+ readonly label: "Verify Goal Completion";
4076
+ readonly description: "Spawn an independent goal-verifier sub-agent to grade a completion claim against the objective and diff before accepting it";
4077
+ };
4078
+ };
4059
4079
  readonly "title.refreshOnReplan": {
4060
4080
  readonly type: "boolean";
4061
4081
  readonly default: true;
@@ -4247,6 +4267,16 @@ export declare const SETTINGS_SCHEMA: {
4247
4267
  readonly description: "Allow subagents spawned via the task tool to use the lsp tool. Off by default to keep subagents cheap; enable when LSP-aware delegation is worth the extra tokens.";
4248
4268
  };
4249
4269
  };
4270
+ readonly "task.autoModelTier": {
4271
+ readonly type: "boolean";
4272
+ readonly default: true;
4273
+ readonly ui: {
4274
+ readonly tab: "tasks";
4275
+ readonly group: "Subagents";
4276
+ readonly label: "Auto Model Tier for Simple Tasks";
4277
+ readonly description: "Classify a task() spawn's assignment text and route trivially-simple work to the cheap 'smol' model tier instead of the default 'task' tier, when the caller has not explicitly overridden the model. Off-by-default agent-type model pins (named specialist agents like reviewer/critic/architect) are never affected — this only applies to the generic default 'task' agent.";
4278
+ };
4279
+ };
4250
4280
  readonly "task.maxRecursionDepth": {
4251
4281
  readonly type: "number";
4252
4282
  readonly default: 2;
@@ -1,5 +1,11 @@
1
1
  import { GoalEvidenceTracker } from "./evidence-gate";
2
2
  import type { Goal, GoalBudgetSteering, GoalModeState, GoalRuntimeEvent, GoalTokenUsage } from "./state";
3
+ export interface GoalCompletionVerdict {
4
+ verdict: "okay" | "iterate" | "reject";
5
+ justification: string;
6
+ summary?: string;
7
+ requiredFixes: string[];
8
+ }
3
9
  export interface GoalRuntimeHost {
4
10
  getState(): GoalModeState | undefined;
5
11
  setState(state: GoalModeState | undefined): void;
@@ -11,6 +17,16 @@ export interface GoalRuntimeHost {
11
17
  content: string;
12
18
  deliverAs?: "steer" | "followUp" | "nextTurn";
13
19
  }): Promise<void>;
20
+ /**
21
+ * Spawn an independent grader sub-agent to verdict a completion claim against
22
+ * the objective and the actual repo diff (article gap: "the agent that wrote
23
+ * the code is not the agent that grades it"). The sub-agent sees only the
24
+ * objective + evidence text passed here plus its own repo inspection — never
25
+ * this session's transcript/reasoning. Optional: hosts that don't wire this
26
+ * (e.g. test harnesses, `goal.verifyCompletion` disabled) skip independent
27
+ * grading and fall back to the deterministic evidence gate alone.
28
+ */
29
+ verifyCompletion?(objective: string, evidence: string): Promise<GoalCompletionVerdict>;
14
30
  now?(): number;
15
31
  }
16
32
  export interface GoalTurnSnapshot {
@@ -0,0 +1,9 @@
1
+ import type { ToolSession } from "../tools";
2
+ import type { GoalCompletionVerdict } from "./runtime";
3
+ /**
4
+ * Grade a goal-completion claim with the independent `goal-verifier` sub-agent.
5
+ * Never throws: a spawn failure (depth limit, disabled agent, no model) fails
6
+ * open to `iterate` so it costs one corrective bounce, the same as a genuine
7
+ * gap — never a silent skip of independent grading, never a permanent block.
8
+ */
9
+ export declare function verifyGoalCompletion(session: ToolSession, objective: string, evidence: string): Promise<GoalCompletionVerdict>;
@@ -45,6 +45,15 @@ export declare function clearMemoryData(agentDir: string, cwd: string): Promise<
45
45
  */
46
46
  export declare function enqueueMemoryConsolidation(agentDir: string, cwd: string, sourceUpdatedAt?: number): void;
47
47
  export declare function getMemoryRoot(agentDir: string, cwd: string): string;
48
+ /**
49
+ * Filename of the captured-lessons file under a project's memory root.
50
+ *
51
+ * Written by the `learn` tool via {@link saveLearnedLesson} and read back by
52
+ * {@link buildMemoryToolDeveloperInstructions}. Deliberately distinct from the
53
+ * consolidation artifacts (`MEMORY.md`, `memory_summary.md`, `skills/`) so a
54
+ * consolidation pass never clobbers manually captured lessons.
55
+ */
56
+ export declare const LEARNED_LESSONS_FILE = "learned.md";
48
57
  /**
49
58
  * Append one lesson to the project's `learned.md` (newest-first, deduped,
50
59
  * capped, secret-redacted, injection-neutralized). The file backs the `learn`
@@ -53,6 +53,17 @@ export interface MemoryBackendSaveInput {
53
53
  context?: string;
54
54
  source?: string;
55
55
  importance?: number;
56
+ /**
57
+ * Marks this lesson as independently re-checked against the repo (a
58
+ * command run, a file read, a test executed) rather than merely asserted
59
+ * from inference. Mirrors the "verify" stage of the fail → investigate →
60
+ * verify → distill → consult self-improvement progression: a lesson that
61
+ * skips verification is still worth capturing, but downstream consumers
62
+ * (e.g. `stats()`) should be able to tell the two apart.
63
+ */
64
+ verified?: boolean;
65
+ /** The short check that was run to establish `verified` (a command, a file path, a test name). Ignored when `verified` is not `true`. */
66
+ evidence?: string;
56
67
  }
57
68
  export interface MemoryBackendSaveResult {
58
69
  backend: MemoryBackendId;
@@ -35,7 +35,7 @@ import type { ExtensionRunner } from "../extensibility/extensions";
35
35
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
36
36
  import type { Skill, SkillWarning } from "../extensibility/skills";
37
37
  import { type FileSlashCommand } from "../extensibility/slash-commands";
38
- import { GoalRuntime } from "../goals/runtime";
38
+ import { type GoalCompletionVerdict, GoalRuntime } from "../goals/runtime";
39
39
  import type { Goal, GoalModeState } from "../goals/state";
40
40
  import type { HindsightSessionState } from "../hindsight/state";
41
41
  import { type IrcMessage } from "../irc/bus";
@@ -124,13 +124,19 @@ export type AgentSessionEvent = AgentEvent | {
124
124
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
125
125
  /**
126
126
  * Budget for callers on the user-visible `/quit` / `/exit` shutdown path that
127
- * want to cap how long they wait for `MnemopiSessionState.dispose()` to finish
128
- * its consolidate pass. Consolidate fires fresh LLM fact extractions, each a
129
- * 1–3 s round-trip, so interactive shutdown passes this budget to keep the
130
- * UI responsive. Callers that keep the process/session host alive must omit it
131
- * so dispose still awaits the full consolidate-then-close pipeline.
127
+ * want to cap how long they wait for `MnemopiSessionState.dispose()` (and the
128
+ * Hindsight retain-queue flush, see the dispose() call site) to finish writing
129
+ * this session's memory before the process exits. Consolidate fires fresh LLM
130
+ * fact extractions, each a 1-3s round-trip; the retain-queue flush is a single
131
+ * HTTP POST. The prior 1500ms budget was tighter than a single extraction
132
+ * round-trip, so it lost the race on effectively every session with any
133
+ * memory-worthy content — "write before walking away" in name only. 8s still
134
+ * bounds worst-case UI unresponsiveness on /exit while giving a small
135
+ * consolidation batch (1-3 facts) a realistic chance to land before the
136
+ * process exits. Callers that keep the process/session host alive must omit
137
+ * it so dispose still awaits the full write pipeline unbounded.
132
138
  */
133
- export declare const SHUTDOWN_CONSOLIDATE_BUDGET_MS = 1500;
139
+ export declare const SHUTDOWN_CONSOLIDATE_BUDGET_MS = 8000;
134
140
  export interface AgentSessionDisposeOptions {
135
141
  mnemopiConsolidateTimeoutMs?: number;
136
142
  /**
@@ -225,6 +231,15 @@ export interface AgentSessionConfig {
225
231
  }>;
226
232
  /** Rebuild the SSH tool from current capability discovery results. */
227
233
  reloadSshTool?: () => Promise<AgentTool | null>;
234
+ /**
235
+ * Independent-verifier bridge for goal-mode completion (see goals/verifier.ts).
236
+ * Spawns the bundled `goal-verifier` sub-agent to grade a completion claim against
237
+ * the objective and the actual repo diff — a fresh session/context, no exposure to
238
+ * this session's transcript. Wired from sdk.ts (needs the constructed `toolSession`).
239
+ * Undefined when `goal.verifyCompletion` is off or the caller has no tool session
240
+ * (e.g. minimal SDK embedders) — GoalRuntime skips independent grading in that case.
241
+ */
242
+ verifyGoalCompletion?: (objective: string, evidence: string) => Promise<GoalCompletionVerdict>;
228
243
  requestedToolNames?: ReadonlySet<string>;
229
244
  /**
230
245
  * Optional accessor for live MCP server instructions. Read by the session's
@@ -0,0 +1,5 @@
1
+ import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
2
+ /** Extract the bulleted list under a `## Eval Cases` heading, if present. */
3
+ export declare function extractEvalCases(skillBody: string): string[];
4
+ /** `/verify-skill <name>` handler — resolves a skill, runs its `## Eval Cases`, reports pass/fail. */
5
+ export declare function handleSkillCommand(command: ParsedSlashCommand, runtime: SlashCommandRuntime): Promise<SlashCommandResult>;
@@ -0,0 +1,8 @@
1
+ import { type ClassifyDifficultyDeps } from "../auto-thinking/classifier";
2
+ /**
3
+ * Classify `assignment` and return `"smol"` when the work is trivially
4
+ * simple ({@link Effort.Minimal} or {@link Effort.Low}), or `undefined` when
5
+ * the caller should leave normal model resolution in place (not-simple
6
+ * enough, no classifier available, or the classification failed/timed out).
7
+ */
8
+ export declare function classifyDefaultTaskModelTier(assignment: string, deps: ClassifyDifficultyDeps): Promise<"smol" | undefined>;
@@ -1,4 +1,4 @@
1
- export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "ssh", "github", "glob", "grep", "lsp", "inspect_image", "browser", "checkpoint", "rewind", "task", "job", "irc", "subagent", "todo", "web_search", "search_tool_bm25", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
1
+ export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "ssh", "github", "glob", "grep", "lsp", "inspect_image", "vision_verify", "browser", "checkpoint", "rewind", "task", "job", "irc", "subagent", "todo", "web_search", "search_tool_bm25", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
2
2
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
3
3
  /** Return the canonical tool name for current and legacy built-in tool IDs. */
4
4
  export declare function normalizeToolName(name: string): string;
@@ -64,6 +64,7 @@ export * from "./ssh";
64
64
  export * from "./subagent";
65
65
  export * from "./todo";
66
66
  export * from "./tts";
67
+ export * from "./vision-verify";
67
68
  export * from "./write";
68
69
  export * from "./yield";
69
70
  /** Tool type (AgentTool from pi-ai) */
@@ -3,6 +3,8 @@ import type { ToolSession } from ".";
3
3
  declare const learnSchema: import("arktype/internal/variants/object.ts").ObjectType<{
4
4
  memory: string;
5
5
  context?: string | undefined;
6
+ verified?: boolean | undefined;
7
+ evidence?: string | undefined;
6
8
  skill?: {
7
9
  action: "create" | "update";
8
10
  name: string;
@@ -27,6 +29,8 @@ export declare class LearnTool implements AgentTool<typeof learnSchema> {
27
29
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
28
30
  memory: string;
29
31
  context?: string | undefined;
32
+ verified?: boolean | undefined;
33
+ evidence?: string | undefined;
30
34
  skill?: {
31
35
  action: "create" | "update";
32
36
  name: string;
@@ -0,0 +1,10 @@
1
+ import type { WritethroughCallback } from "../lsp";
2
+ /**
3
+ * Wrap a {@link WritethroughCallback} so concurrent writes to the same `dst`
4
+ * are serialized instead of racing. `dst` is used as-is for the chain key:
5
+ * every caller (edit's hashline/patch/replace paths, and the write tool)
6
+ * resolves `dst` to an absolute path via `resolvePlanPath`/`resolveToCwd`
7
+ * before invoking the writethrough, so the same logical file always arrives
8
+ * under the same string.
9
+ */
10
+ export declare function withPathWriteLock(callback: WritethroughCallback): WritethroughCallback;
@@ -0,0 +1,34 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
2
+ import { completeSimple, type ToolExample } from "jeopi-ai";
3
+ import type { ToolSession } from "./index";
4
+ declare const visionVerifySchema: import("arktype/internal/variants/object.ts").ObjectType<{
5
+ goal: string;
6
+ screenshot: string;
7
+ baseline?: string | undefined;
8
+ }, {}>;
9
+ export type VisionVerifyParams = typeof visionVerifySchema.infer;
10
+ export interface VisionVerifyToolDetails {
11
+ model: string;
12
+ matches: boolean;
13
+ gaps: string[];
14
+ }
15
+ export declare class VisionVerifyTool implements AgentTool<typeof visionVerifySchema, VisionVerifyToolDetails> {
16
+ private readonly session;
17
+ private readonly completeImageRequest;
18
+ readonly name = "vision_verify";
19
+ readonly approval: "read";
20
+ readonly label = "VisionVerify";
21
+ readonly loadMode = "discoverable";
22
+ readonly summary = "Check a screenshot/artifact against a stated goal with a vision model";
23
+ readonly description: string;
24
+ readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
25
+ goal: string;
26
+ screenshot: string;
27
+ baseline?: string | undefined;
28
+ }, {}>;
29
+ readonly strict = false;
30
+ readonly examples: readonly ToolExample<typeof visionVerifySchema.infer>[];
31
+ constructor(session: ToolSession, completeImageRequest?: typeof completeSimple);
32
+ execute(_toolCallId: string, params: VisionVerifyParams, signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<VisionVerifyToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<VisionVerifyToolDetails>>;
33
+ }
34
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.2.30",
4
+ "version": "16.4.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -57,17 +57,17 @@
57
57
  "@agentclientprotocol/sdk": "0.25.0",
58
58
  "@babel/parser": "^7.29.7",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "jeopi-hashline": "16.2.30",
61
- "jeopi-stats": "16.2.30",
62
- "jeopi-agent-core": "16.2.30",
63
- "jeopi-ai": "16.2.30",
64
- "jeopi-catalog": "16.2.30",
65
- "jeopi-mnemopi": "16.2.30",
66
- "jeopi-natives": "16.2.30",
67
- "jeopi-tui": "16.2.30",
68
- "jeopi-utils": "16.2.30",
69
- "jeopi-wire": "16.2.30",
70
- "jeopi-snapcompact": "16.2.30",
60
+ "jeopi-hashline": "16.4.0",
61
+ "jeopi-stats": "16.4.0",
62
+ "jeopi-agent-core": "16.4.0",
63
+ "jeopi-ai": "16.4.0",
64
+ "jeopi-catalog": "16.4.0",
65
+ "jeopi-mnemopi": "16.4.0",
66
+ "jeopi-natives": "16.4.0",
67
+ "jeopi-tui": "16.4.0",
68
+ "jeopi-utils": "16.4.0",
69
+ "jeopi-wire": "16.4.0",
70
+ "jeopi-snapcompact": "16.4.0",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@opentelemetry/context-async-hooks": "^2.7.1",
73
73
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Auto-learn session controller (experimental).
3
3
  *
4
- * Subscribes to the session event stream and, after a substantive turn,
5
- * optionally auto-runs a synthetic capture turn. Passive mode is intentionally
6
- * prompt-cache neutral: the standing system guidance remains available, but no
7
- * hidden mid-session reminder is inserted into the conversation.
4
+ * Subscribes to the session event stream and, after a turn becomes eligible
5
+ * for the capture nudge, optionally auto-runs a synthetic capture turn. A
6
+ * turn is eligible via either of two independent paths: it cleared the
7
+ * `autolearn.minToolCalls` volume threshold, OR it saw at least one failed
8
+ * tool call regardless of volume — a single failure is treated as a likely
9
+ * reusable lesson even in an otherwise-short turn. Passive mode is
10
+ * intentionally prompt-cache neutral: the standing system guidance remains
11
+ * available, but no hidden mid-session reminder is inserted into the
12
+ * conversation.
8
13
  *
9
14
  * Installed once per top-level session (taskDepth 0). The subscription lives
10
15
  * for the session's lifetime — `newSession` resets the session in place
@@ -54,6 +59,8 @@ export class AutoLearnController {
54
59
  * would let a goal-continuation turn slip through and get nudged.
55
60
  */
56
61
  #turnStartedInGoalMode = false;
62
+ /** Whether any `tool_execution_end` in the in-flight turn carried `isError === true`. */
63
+ #sawFailure = false;
57
64
  /** Swallow the agent_end produced by an auto-run capture turn so it cannot re-trigger. */
58
65
  #suppressNext = false;
59
66
 
@@ -73,6 +80,7 @@ export class AutoLearnController {
73
80
  }
74
81
  if (event.type === "tool_execution_end") {
75
82
  this.#toolCalls++;
83
+ if (event.isError === true) this.#sawFailure = true;
76
84
  return;
77
85
  }
78
86
  if (event.type === "agent_end") {
@@ -90,6 +98,10 @@ export class AutoLearnController {
90
98
  // observed no agent_start can never inherit a stale value.
91
99
  const startedInGoalMode = this.#turnStartedInGoalMode;
92
100
  this.#turnStartedInGoalMode = false;
101
+ // Snapshot and reset alongside the counter above: a failure describes
102
+ // only the just-finished turn.
103
+ const sawFailure = this.#sawFailure;
104
+ this.#sawFailure = false;
93
105
 
94
106
  if (this.#suppressNext) {
95
107
  this.#suppressNext = false;
@@ -99,7 +111,11 @@ export class AutoLearnController {
99
111
  // the current flag rather than trusting install-time state.
100
112
  if (!this.#settings.get("autolearn.enabled")) return;
101
113
  const minToolCalls = this.#settings.get("autolearn.minToolCalls") ?? DEFAULT_MIN_TOOL_CALLS;
102
- if (toolCalls < minToolCalls) return;
114
+ // Eligible when EITHER the turn cleared the volume threshold OR it saw at
115
+ // least one failed tool call — a single failure is a reusable lesson
116
+ // regardless of how few tools ran, while the volume gate remains the
117
+ // fallback for long clean turns.
118
+ if (toolCalls < minToolCalls && !sawFailure) return;
103
119
  // Never interrupt plan-mode review.
104
120
  if (this.#session.getPlanModeState()?.enabled) return;
105
121
  // Never divert a goal loop. Skip when the turn STARTED in goal mode — a
@@ -30,6 +30,16 @@ import type { AutoresearchRuntime, ExperimentResult, PendingRunSummary } from ".
30
30
 
31
31
  const EXPERIMENT_TOOL_NAMES = ["init_experiment", "run_experiment", "log_experiment", "update_notes"];
32
32
 
33
+ /**
34
+ * Cross-process auto-resume window: a pending (run-but-never-logged) experiment
35
+ * completed within this long ago is resumed automatically on the next session
36
+ * load; older ones are left for the user to inspect and resume by hand. Bounds
37
+ * the "open your laptop, the loop picks back up" pattern to realistic gaps
38
+ * (overnight, a workday) without silently relaunching token-spending work
39
+ * against a session nobody touched in weeks.
40
+ */
41
+ const AUTORESEARCH_RESUME_STALENESS_MS = 24 * 60 * 60 * 1000;
42
+
33
43
  export const createAutoresearchExtension: ExtensionFactory = api => {
34
44
  const runtimeStore = createRuntimeStore();
35
45
  const dashboard = createDashboardController();
@@ -47,7 +57,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
47
57
  return { session, currentBranch };
48
58
  };
49
59
 
50
- const rehydrate = async (ctx: ExtensionContext): Promise<void> => {
60
+ const rehydrate = async (ctx: ExtensionContext, resumeEligible: boolean): Promise<void> => {
51
61
  const runtime = getRuntime(ctx);
52
62
  const control = reconstructControlState(ctx.sessionManager.getBranch());
53
63
  runtime.goal = control.goal;
@@ -69,13 +79,15 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
69
79
  const onActiveBranch = session === null || session.branch === null || session.branch === currentBranch;
70
80
  runtime.autoresearchMode = control.autoresearchMode && onActiveBranch;
71
81
 
82
+ let pendingRow: RunRow | null = null;
72
83
  if (session && onActiveBranch) {
73
84
  const storage = await openAutoresearchStorageIfExists(ctx.cwd);
74
85
  if (storage) {
75
86
  const loggedRuns = storage.listLoggedRuns(session.id);
76
87
  runtime.state = buildExperimentState(session, loggedRuns);
77
88
  runtime.goal = runtime.goal ?? session.goal;
78
- runtime.lastRunSummary = pendingRunSummaryFromRow(storage.getPendingRun(session.id));
89
+ pendingRow = storage.getPendingRun(session.id);
90
+ runtime.lastRunSummary = pendingRunSummaryFromRow(pendingRow);
79
91
  } else {
80
92
  runtime.state = createExperimentState();
81
93
  runtime.lastRunSummary = null;
@@ -102,6 +114,41 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
102
114
  if (toolsChanged) {
103
115
  await api.setActiveTools(nextActiveTools);
104
116
  }
117
+
118
+ // Auto-rearm across a process restart (article gap: "the mode itself has to be
119
+ // manually re-armed with /autoresearch after a restart, since session_shutdown
120
+ // doesn't leave anything wired to auto-relaunch" — the "morning briefing"/
121
+ // nightly-loop pattern requires the NEXT process to pick a pending run back up
122
+ // on its own, not wait for the user to notice and retype /autoresearch).
123
+ // `autoResumeArmed` is deliberately NOT restored here (reset above, matching
124
+ // existing behavior) — that flag is a live in-process signal set by the
125
+ // experiment tools mid-run; it cannot be reconstructed from persisted state
126
+ // and re-arming it unconditionally on every resume would fire even when the
127
+ // prior process ended cleanly with nothing left to continue. The only
128
+ // cross-restart-durable signal is an actual pending (run-but-never-logged)
129
+ // run row, so that's the sole trigger here.
130
+ if (
131
+ resumeEligible &&
132
+ runtime.autoresearchMode &&
133
+ pendingRow?.completedAt !== undefined &&
134
+ pendingRow.completedAt !== null &&
135
+ Date.now() - pendingRow.completedAt <= AUTORESEARCH_RESUME_STALENESS_MS &&
136
+ !ctx.hasPendingMessages()
137
+ ) {
138
+ const pendingRun = runtime.lastRunSummary;
139
+ runtime.lastAutoResumePendingRunNumber = pendingRow.id;
140
+ api.sendMessage(
141
+ {
142
+ customType: "autoresearch-resume",
143
+ content: prompt.render(resumeMessageTemplate, {
144
+ has_pending_run: Boolean(pendingRun),
145
+ }),
146
+ display: false,
147
+ attribution: "agent",
148
+ },
149
+ { deliverAs: "nextTurn", triggerTurn: true },
150
+ );
151
+ }
105
152
  };
106
153
 
107
154
  const setMode = (
@@ -245,10 +292,17 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
245
292
  },
246
293
  });
247
294
 
248
- api.on("session_start", (_event, ctx) => rehydrate(ctx));
249
- api.on("session_switch", (_event, ctx) => rehydrate(ctx));
250
- api.on("session_branch", (_event, ctx) => rehydrate(ctx));
251
- api.on("session_tree", (_event, ctx) => rehydrate(ctx));
295
+ api.on("session_start", (_event, ctx) => rehydrate(ctx, true));
296
+ // "resume" = picked from the /resume session picker or `jeopi -r`, the exact
297
+ // "reopen your laptop, the loop should still be going" moment the article
298
+ // describes. "new"/"fork" start a fresh conversation off old history — no
299
+ // auto-continuation there, matching session_start's "don't fire on plain
300
+ // process launch into an unrelated session" restraint.
301
+ api.on("session_switch", (event, ctx) => rehydrate(ctx, event.reason === "resume"));
302
+ // Mid-conversation navigation (branch off an earlier message, move through the
303
+ // tree), not a process/session restart — never auto-fires a fresh turn here.
304
+ api.on("session_branch", (_event, ctx) => rehydrate(ctx, false));
305
+ api.on("session_tree", (_event, ctx) => rehydrate(ctx, false));
252
306
  api.on("session_shutdown", (_event, ctx) => {
253
307
  dashboard.clear(ctx);
254
308
  runtimeStore.clear(getSessionKey(ctx));
@@ -416,13 +416,13 @@ export const SETTINGS_SCHEMA = {
416
416
  },
417
417
  "advisor.includeThinking": {
418
418
  type: "boolean",
419
- default: true,
419
+ default: false,
420
420
  ui: {
421
421
  tab: "model",
422
422
  group: "Advisor",
423
423
  label: "Advisor Sees Thinking",
424
424
  description:
425
- "Include the primary agent's thinking blocks in advisor transcript updates. Some provider classifiers (e.g. Anthropic's reasoning_extraction) refuse payloads that replay thinking as text; the advisor also auto-disables this for the session after such a refusal.",
425
+ "Include the primary agent's thinking blocks in advisor transcript updates. Off by default: a reviewer that sees the maker's own reasoning trail tends to anchor on conclusions the maker already committed to instead of bringing an independent angle (the advisor's own directive is to 'bring the angle they skipped, never re-run reasoning they already have'); the advisor still sees full messages, tool calls, results, and diffs. Some provider classifiers (e.g. Anthropic's reasoning_extraction) also refuse payloads that replay thinking as text if enabled; the advisor auto-disables this for the session after such a refusal.",
426
426
  condition: "advisorEnabled",
427
427
  },
428
428
  },
@@ -3560,6 +3560,18 @@ export const SETTINGS_SCHEMA = {
3560
3560
  },
3561
3561
  },
3562
3562
 
3563
+ "vision_verify.enabled": {
3564
+ type: "boolean",
3565
+ default: false,
3566
+ ui: {
3567
+ tab: "tools",
3568
+ group: "Available Tools",
3569
+ label: "Vision Verify",
3570
+ description:
3571
+ "Enable the vision_verify tool, checking a screenshot/artifact against a stated goal with a vision-capable model",
3572
+ },
3573
+ },
3574
+
3563
3575
  "checkpoint.enabled": {
3564
3576
  type: "boolean",
3565
3577
  default: false,
@@ -3946,6 +3958,18 @@ export const SETTINGS_SCHEMA = {
3946
3958
  },
3947
3959
  },
3948
3960
 
3961
+ "goal.verifyCompletion": {
3962
+ type: "boolean",
3963
+ default: true,
3964
+ ui: {
3965
+ tab: "tasks",
3966
+ group: "Modes",
3967
+ label: "Verify Goal Completion",
3968
+ description:
3969
+ "Spawn an independent goal-verifier sub-agent to grade a completion claim against the objective and diff before accepting it",
3970
+ },
3971
+ },
3972
+
3949
3973
  "title.refreshOnReplan": {
3950
3974
  type: "boolean",
3951
3975
  default: true,
@@ -4112,6 +4136,18 @@ export const SETTINGS_SCHEMA = {
4112
4136
  },
4113
4137
  },
4114
4138
 
4139
+ "task.autoModelTier": {
4140
+ type: "boolean",
4141
+ default: true,
4142
+ ui: {
4143
+ tab: "tasks",
4144
+ group: "Subagents",
4145
+ label: "Auto Model Tier for Simple Tasks",
4146
+ description:
4147
+ "Classify a task() spawn's assignment text and route trivially-simple work to the cheap 'smol' model tier instead of the default 'task' tier, when the caller has not explicitly overridden the model. Off-by-default agent-type model pins (named specialist agents like reviewer/critic/architect) are never affected — this only applies to the generic default 'task' agent.",
4148
+ },
4149
+ },
4150
+
4115
4151
  "task.maxRecursionDepth": {
4116
4152
  type: "number",
4117
4153
  default: 2,
package/src/edit/index.ts CHANGED
@@ -19,6 +19,7 @@ import replaceDescription from "../prompts/tools/replace.md" with { type: "text"
19
19
  import type { DeferredDiagnosticsEntry, ToolSession } from "../tools";
20
20
  import { truncateForPrompt } from "../tools/approval";
21
21
  import { isInternalUrlPath } from "../tools/path-utils";
22
+ import { withPathWriteLock } from "../tools/path-write-lock";
22
23
  import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
23
24
  import { executeHashlineSingle, hashlineEditParamsSchema } from "./hashline";
24
25
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
@@ -405,7 +406,7 @@ export class EditTool implements AgentTool<TInput> {
405
406
  (session.enableLsp ?? true) &&
406
407
  session.settings.get("lsp.diagnosticsOnEdit") &&
407
408
  session.settings.get("lsp.diagnosticsDeduplicate");
408
- this.#writethrough = createEditWritethrough(session);
409
+ this.#writethrough = withPathWriteLock(createEditWritethrough(session));
409
410
  }
410
411
 
411
412
  get mode(): EditMode {