neko-harness 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gateway.js CHANGED
@@ -16604,6 +16604,42 @@ import { basename as basename4, dirname as dirname9, join as join13 } from "path
16604
16604
  import { createHost as createHost2 } from "@earendil-works/chord";
16605
16605
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
16606
16606
 
16607
+ // packages/core/src/codemode-mode.ts
16608
+ var CODEMODE_NAME = "codemode";
16609
+ var CODEMODE_MODES = ["off", "on", "only"];
16610
+ var DEFAULT_CODEMODE_MODE = "off";
16611
+ var isCodemodeMode = (value) => typeof value === "string" && CODEMODE_MODES.includes(value);
16612
+ var CODEMODE_LIMITS = {
16613
+ memoryLimitBytes: 256 * 1024 * 1024,
16614
+ maxWorkers: 2,
16615
+ waitMs: 60000
16616
+ };
16617
+ var CODEMODE_VIEW_LIMITS = {
16618
+ maxCalls: 100,
16619
+ argsChars: 200,
16620
+ errorChars: 200
16621
+ };
16622
+ function parseCodemodeMode(value, field = "codemode.mode") {
16623
+ if (!isCodemodeMode(value))
16624
+ throw new Error(`"${field}" must be one of ${CODEMODE_MODES.join(", ")}.`);
16625
+ return value;
16626
+ }
16627
+ function parseCodemodeSettings(value, prefix = "codemode") {
16628
+ if (value === null || typeof value !== "object" || Array.isArray(value))
16629
+ throw new Error(`"${prefix}" must be an object.`);
16630
+ const { mode, ...rest } = value;
16631
+ const unknown = Object.keys(rest)[0];
16632
+ if (unknown !== undefined)
16633
+ throw new Error(`"${prefix}.${unknown}" is not a known setting.`);
16634
+ return mode === undefined ? {} : { mode: parseCodemodeMode(mode, `${prefix}.mode`) };
16635
+ }
16636
+ function resolveCodemodeSettings(config, saved) {
16637
+ if (isCodemodeMode(saved))
16638
+ return { effective: { mode: saved }, source: "saved" };
16639
+ if (config?.mode !== undefined)
16640
+ return { effective: { mode: config.mode }, source: "config" };
16641
+ return { effective: { mode: DEFAULT_CODEMODE_MODE }, source: "default" };
16642
+ }
16607
16643
  // packages/core/src/config.ts
16608
16644
  import { existsSync, readFileSync } from "fs";
16609
16645
  import { homedir } from "os";
@@ -16934,6 +16970,13 @@ function validate(config, source) {
16934
16970
  throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
16935
16971
  }
16936
16972
  }
16973
+ if (config.codemode !== undefined) {
16974
+ try {
16975
+ parseCodemodeSettings(config.codemode);
16976
+ } catch (error) {
16977
+ throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
16978
+ }
16979
+ }
16937
16980
  }
16938
16981
  // packages/core/src/credentials.ts
16939
16982
  import { chmod, mkdir, open, readFile, stat, unlink } from "fs/promises";
@@ -17146,6 +17189,36 @@ class FileCredentialStore {
17146
17189
  // packages/core/src/file-changes.ts
17147
17190
  import { isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
17148
17191
 
17192
+ // packages/core/src/change-limits.ts
17193
+ var CHANGE_LIMITS = {
17194
+ files: 500,
17195
+ opsPerFile: 50,
17196
+ diffBytes: 2 * 1024 * 1024,
17197
+ opDiffBytes: 64 * 1024,
17198
+ conversations: 64,
17199
+ nestedRecords: 1000
17200
+ };
17201
+ function clipDiff(diff) {
17202
+ if (Buffer.byteLength(diff, "utf8") <= CHANGE_LIMITS.opDiffBytes)
17203
+ return { diff, truncated: false };
17204
+ const lines = diff.split(`
17205
+ `);
17206
+ const kept = [];
17207
+ let bytes = 0;
17208
+ for (const line of lines) {
17209
+ bytes += Buffer.byteLength(line, "utf8") + 1;
17210
+ if (bytes > CHANGE_LIMITS.opDiffBytes)
17211
+ break;
17212
+ kept.push(line);
17213
+ }
17214
+ return { diff: kept.join(`
17215
+ `), truncated: true };
17216
+ }
17217
+
17218
+ // packages/core/src/nested-changes.ts
17219
+ import { defineDoc, wrapTool } from "@earendil-works/pi-durable";
17220
+ import { createEditTool, createWriteTool } from "@earendil-works/pi-durable/tools";
17221
+
17149
17222
  // packages/core/src/image-prompt.ts
17150
17223
  var DEFAULT_IMAGE_PROMPT = {
17151
17224
  version: 2,
@@ -17310,6 +17383,47 @@ var interactionField = (name, details) => {
17310
17383
  const interaction = toolInteraction(name, details);
17311
17384
  return interaction ? { interaction } : {};
17312
17385
  };
17386
+ var clip2 = (text, max) => text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
17387
+ function codemodeView(calls, fullOutputPath) {
17388
+ const limit = CODEMODE_VIEW_LIMITS.maxCalls;
17389
+ return { calls: calls.slice(0, limit), omitted: Math.max(0, calls.length - limit), fullOutputPath };
17390
+ }
17391
+ function liveCodemodeView(live, callId) {
17392
+ const slots = (live.nestedTools ?? []).filter((slot) => slot.parentCallId === callId);
17393
+ return codemodeView(slots.map((slot, index) => {
17394
+ const key = /\/c(\d+)$/.exec(slot.callId)?.[1];
17395
+ const summary = slot.summary;
17396
+ return {
17397
+ id: key === undefined ? index + 1 : Number(key),
17398
+ name: slot.name,
17399
+ args: clip2(JSON.stringify(slot.arguments) ?? "", CODEMODE_VIEW_LIMITS.argsChars),
17400
+ status: slot.status !== "done" ? "running" : summary === undefined ? "cancelled" : summary.isError ? "error" : "ok",
17401
+ durationMs: summary?.durationMs ?? null,
17402
+ error: summary?.error === undefined ? null : clip2(summary.error, CODEMODE_VIEW_LIMITS.errorChars)
17403
+ };
17404
+ }), null);
17405
+ }
17406
+ function codemodeResultView(details) {
17407
+ if (!isRecord(details) || !Array.isArray(details.calls))
17408
+ return;
17409
+ const calls = details.calls.flatMap((call) => {
17410
+ if (!isRecord(call) || typeof call.name !== "string")
17411
+ return [];
17412
+ const { id, args, status, durationMs, error } = call;
17413
+ return [
17414
+ {
17415
+ id: typeof id === "number" ? id : 0,
17416
+ name: call.name,
17417
+ args: typeof args === "string" ? clip2(args, CODEMODE_VIEW_LIMITS.argsChars) : "",
17418
+ status: status === "ok" || status === "error" || status === "cancelled" ? status : "running",
17419
+ durationMs: typeof durationMs === "number" ? durationMs : null,
17420
+ error: typeof error === "string" ? clip2(error, CODEMODE_VIEW_LIMITS.errorChars) : null
17421
+ }
17422
+ ];
17423
+ });
17424
+ return codemodeView(calls, typeof details.fullOutputPath === "string" ? details.fullOutputPath : null);
17425
+ }
17426
+ var codemodeField = (name, view) => name === CODEMODE_NAME && view ? { codemode: view } : {};
17313
17427
  function summarizeToolCall(name, args) {
17314
17428
  const record = args ?? {};
17315
17429
  switch (name) {
@@ -17342,6 +17456,12 @@ function summarizeToolCall(name, args) {
17342
17456
  `)
17343
17457
  };
17344
17458
  }
17459
+ case "codemode": {
17460
+ const code = typeof record.code === "string" ? record.code : "";
17461
+ const first = code.split(`
17462
+ `).find((line) => line.trim() !== "" && !line.trim().startsWith("// @options")) ?? "";
17463
+ return { args: truncateMiddle(first.trim(), 100), detail: code };
17464
+ }
17345
17465
  case "subagent":
17346
17466
  return {
17347
17467
  args: record.action === "list" ? "List agents" : `${record.agentId ?? record.type}: ${record.title}`,
@@ -17487,7 +17607,8 @@ function projectChatView(view, modelLabel, options = {}) {
17487
17607
  diff,
17488
17608
  diffAdded: counts?.added ?? null,
17489
17609
  diffRemoved: counts?.removed ?? null,
17490
- ...interactionField(message.toolName, message.details)
17610
+ ...interactionField(message.toolName, message.details),
17611
+ ...codemodeField(message.toolName, codemodeResultView(message.details))
17491
17612
  });
17492
17613
  }
17493
17614
  }
@@ -17585,7 +17706,8 @@ function projectChatView(view, modelLabel, options = {}) {
17585
17706
  diffAdded: synthesized?.added ?? null,
17586
17707
  diffRemoved: synthesized?.removed ?? null,
17587
17708
  diffTruncated: synthesized?.truncated ?? false,
17588
- ...interactionField(block.name, slot?.details)
17709
+ ...interactionField(block.name, slot?.details),
17710
+ ...codemodeField(block.name, slot?.status === "running" ? liveCodemodeView(live, block.id) : undefined)
17589
17711
  });
17590
17712
  }
17591
17713
  }
@@ -17708,7 +17830,8 @@ ${block.text}`;
17708
17830
  diff: synthesized?.diff ?? null,
17709
17831
  diffAdded: synthesized?.added ?? null,
17710
17832
  diffRemoved: synthesized?.removed ?? null,
17711
- diffTruncated: synthesized?.truncated ?? false
17833
+ diffTruncated: synthesized?.truncated ?? false,
17834
+ ...codemodeField(block.name, slot?.status === "running" ? liveCodemodeView(live, block.id) : undefined)
17712
17835
  });
17713
17836
  }
17714
17837
  }
@@ -17729,11 +17852,74 @@ function contextTokens(entries) {
17729
17852
  return null;
17730
17853
  }
17731
17854
 
17855
+ // packages/core/src/nested-changes.ts
17856
+ var NestedChangesDoc = defineDoc({
17857
+ kind: "app.neko.nested-changes",
17858
+ checkpointWhen: () => false,
17859
+ version: 1,
17860
+ scope: "conversation",
17861
+ history: "rewindable",
17862
+ fork: "asOf",
17863
+ initial: () => ({ total: 0, dropped: false, items: [] })
17864
+ });
17865
+ var bytesOf = (items) => items.reduce((sum, item) => sum + Buffer.byteLength(item.diff, "utf8"), 0);
17866
+ function addNestedChange(state, change) {
17867
+ const existing = state.items.findIndex((item) => item.id === change.id);
17868
+ if (existing !== -1)
17869
+ state.items.splice(existing, 1);
17870
+ else
17871
+ state.total += 1;
17872
+ state.items.push(change);
17873
+ let bytes = bytesOf(state.items);
17874
+ while (state.items.length > 1 && (state.items.length > CHANGE_LIMITS.nestedRecords || bytes > CHANGE_LIMITS.diffBytes)) {
17875
+ const evicted = state.items.shift();
17876
+ bytes -= Buffer.byteLength(evicted.diff, "utf8");
17877
+ state.dropped = true;
17878
+ }
17879
+ }
17880
+ function describe2(tool, args, details) {
17881
+ if (tool === "edit") {
17882
+ const raw = editResultDiff(details);
17883
+ if (raw === null)
17884
+ return { diff: "", added: 0, removed: 0, truncated: false };
17885
+ return { ...clipDiff(raw), ...countDiffLines(raw) };
17886
+ }
17887
+ return synthesizedWriteDiff(args) ?? { diff: "", added: 0, removed: 0, truncated: false };
17888
+ }
17889
+ function nestedChangeWraps() {
17890
+ const wrap = (reference, tool) => wrapTool(reference, (inner) => ({
17891
+ ...inner,
17892
+ execute: async (args, api, context) => {
17893
+ const result = await inner.execute(args, api, context);
17894
+ const path = args?.path;
17895
+ if (result.isError || typeof path !== "string" || path === "")
17896
+ return result;
17897
+ const task = await api.getTask(api.taskId, context);
17898
+ const input = task?.input;
17899
+ if (input?.kind !== "nested" || typeof input.parentCallId !== "string")
17900
+ return result;
17901
+ const parentCallId = input.parentCallId;
17902
+ const change = {
17903
+ id: api.callId,
17904
+ parentCallId,
17905
+ tool,
17906
+ path,
17907
+ ...describe2(tool, args, result.details)
17908
+ };
17909
+ await api.commit(async (tx) => {
17910
+ addNestedChange(await tx.doc(NestedChangesDoc, api.conversationId), change);
17911
+ }, context);
17912
+ return result;
17913
+ }
17914
+ }));
17915
+ return [wrap(createEditTool(), "edit"), wrap(createWriteTool(), "write")];
17916
+ }
17917
+
17732
17918
  // packages/core/src/subagents.ts
17733
17919
  import {
17734
17920
  AssistantEntry,
17735
17921
  configure,
17736
- defineDoc as defineDoc2,
17922
+ defineDoc as defineDoc4,
17737
17923
  defineExtension,
17738
17924
  defineTask,
17739
17925
  defineTool,
@@ -17744,9 +17930,9 @@ import { Type } from "typebox";
17744
17930
  import { Value } from "typebox/value";
17745
17931
 
17746
17932
  // packages/core/src/plan-doc.ts
17747
- import { defineDoc } from "@earendil-works/pi-durable";
17933
+ import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
17748
17934
  var PLAN_HISTORY = 5;
17749
- var PlanDoc = defineDoc({
17935
+ var PlanDoc = defineDoc2({
17750
17936
  kind: "app.neko.plan",
17751
17937
  checkpointWhen: () => false,
17752
17938
  version: 1,
@@ -17756,6 +17942,198 @@ var PlanDoc = defineDoc({
17756
17942
  initial: () => ({ mode: "normal", revision: 0, items: [] })
17757
17943
  });
17758
17944
  var PLAN_CODER_BLOCKED = "Plan mode is read-only: the coder subagent is unavailable. Use explore or plan subagents, or finish the plan first.";
17945
+ var PLAN_REMOVED_TOOLS = ["edit", "write", "job"];
17946
+ var PLAN_INSTRUCTIONS = [
17947
+ "You are in plan mode: research and plan only, and change nothing.",
17948
+ "- Allowed: read, find_files, search_text, web_search, web_extract, todo, ask_user, read-only subagents (explore, plan) and bash limited to read-only commands (listing, searching, git log/diff/status, version checks). Other commands are rejected.",
17949
+ "- Not available: edit, write, job and any command that modifies files, installs packages or starts processes. Do not try to work around this.",
17950
+ "- When you understand the task, call submit_plan once, alone in its turn, with a short title, ordered concrete steps and an optional note (risks, assumptions, verification). The user then approves it or asks for changes.",
17951
+ "- After submit_plan do nothing else. If the user asks for changes, research what is needed and submit a revised plan.",
17952
+ "- Use ask_user only for decisions you cannot settle by reading the code."
17953
+ ].join(`
17954
+ `);
17955
+
17956
+ // packages/core/src/agent-change.ts
17957
+ var ONLY_MODE_MAIN_TOOLS = ["todo", "ask_user", "subagent"];
17958
+ var named = (name) => ({ name });
17959
+ function onlyModeTools(plan, role) {
17960
+ return [
17961
+ CODEMODE_NAME,
17962
+ ...role === "main" ? [...ONLY_MODE_MAIN_TOOLS, ...plan === "plan" ? ["submit_plan"] : []] : []
17963
+ ];
17964
+ }
17965
+ function agentChangeFor({ plan, codemode, role }) {
17966
+ const planning = plan === "plan";
17967
+ return {
17968
+ tools: planning ? { remove: PLAN_REMOVED_TOOLS.map(named) } : null,
17969
+ modelTools: codemode === "only" ? onlyModeTools(plan, role).map(named) : planning ? { add: [named("submit_plan")] } : null,
17970
+ extensions: codemode === "off" ? { remove: [{ name: CODEMODE_NAME }] } : { add: [{ name: CODEMODE_NAME }] },
17971
+ instructions: planning ? PLAN_INSTRUCTIONS : null
17972
+ };
17973
+ }
17974
+ function enabledTools(agent) {
17975
+ const offered = new Set(agent.tools.map((tool) => tool.name));
17976
+ return [...agent.tools, ...agent.callable.filter((tool) => !offered.has(tool.name))];
17977
+ }
17978
+ var agentInPlanMode = (agent) => agent.instructions === PLAN_INSTRUCTIONS;
17979
+ function agentSyncChange(agent, state) {
17980
+ const desired = agentChangeFor(state);
17981
+ const planMismatch = agentInPlanMode(agent) !== (state.plan === "plan");
17982
+ const hasExtension = agent.extensions.some((extension) => extension.name === CODEMODE_NAME);
17983
+ const callable = new Set(agent.callable.map((tool) => tool.name));
17984
+ const offered = agent.tools.map((tool) => tool.name);
17985
+ let codemodeMismatch;
17986
+ if (state.codemode === "off")
17987
+ codemodeMismatch = hasExtension || callable.has(CODEMODE_NAME);
17988
+ else {
17989
+ codemodeMismatch = !hasExtension || !offered.includes(CODEMODE_NAME);
17990
+ if (state.codemode === "only") {
17991
+ const allowed = new Set(onlyModeTools(state.plan, state.role));
17992
+ codemodeMismatch ||= offered.some((name) => !allowed.has(name));
17993
+ }
17994
+ }
17995
+ if (!planMismatch && !codemodeMismatch)
17996
+ return;
17997
+ return {
17998
+ ...planMismatch ? { tools: desired.tools, instructions: desired.instructions } : {},
17999
+ modelTools: desired.modelTools,
18000
+ ...codemodeMismatch ? { extensions: desired.extensions } : {}
18001
+ };
18002
+ }
18003
+
18004
+ // packages/core/src/codemode.ts
18005
+ import { AsyncLocalStorage } from "async_hooks";
18006
+ import { CodemodeSandbox } from "@earendil-works/pi-codemode";
18007
+ import { defineDoc as defineDoc3, wrapTool as wrapTool2 } from "@earendil-works/pi-durable";
18008
+ import { createCodemodeExtension, createCodemodeTool } from "@earendil-works/pi-durable/codemode";
18009
+ var CodemodeSettingsDoc = defineDoc3({
18010
+ kind: "app.neko.codemode-settings",
18011
+ checkpointWhen: () => false,
18012
+ version: 1,
18013
+ scope: "conversation",
18014
+ history: "latest",
18015
+ fork: "initial",
18016
+ initial: () => ({ mode: null })
18017
+ });
18018
+ var CodemodeDoc = defineDoc3({
18019
+ kind: "app.neko.codemode",
18020
+ checkpointWhen: () => false,
18021
+ version: 1,
18022
+ scope: "conversation",
18023
+ history: "rewindable",
18024
+ fork: "asOf",
18025
+ initial: () => ({ mode: "off" })
18026
+ });
18027
+ var SANDBOX_BUSY = "Codemode sandbox is busy: too many scripts are running. Try again shortly.";
18028
+
18029
+ class WorkerLimiter {
18030
+ #max;
18031
+ #waitMs;
18032
+ #tickets = new AsyncLocalStorage;
18033
+ #waiters = [];
18034
+ #running = 0;
18035
+ #reserved = 0;
18036
+ constructor(limits = CODEMODE_LIMITS) {
18037
+ this.#max = limits.maxWorkers;
18038
+ this.#waitMs = limits.waitMs;
18039
+ }
18040
+ get running() {
18041
+ return this.#running;
18042
+ }
18043
+ get waiting() {
18044
+ return this.#waiters.length;
18045
+ }
18046
+ #free() {
18047
+ return this.#running + this.#reserved < this.#max;
18048
+ }
18049
+ #wake() {
18050
+ while (this.#waiters.length > 0 && this.#free()) {
18051
+ this.#reserved += 1;
18052
+ this.#waiters.shift()?.();
18053
+ }
18054
+ }
18055
+ acquireWorker = () => {
18056
+ const ticket = this.#tickets.getStore();
18057
+ if (ticket && !ticket.claimed) {
18058
+ ticket.claimed = true;
18059
+ this.#reserved -= 1;
18060
+ } else if (!this.#free())
18061
+ throw new Error(SANDBOX_BUSY);
18062
+ this.#running += 1;
18063
+ let released = false;
18064
+ return () => {
18065
+ if (released)
18066
+ return;
18067
+ released = true;
18068
+ this.#running -= 1;
18069
+ this.#wake();
18070
+ };
18071
+ };
18072
+ async#admit(signal) {
18073
+ signal?.throwIfAborted();
18074
+ if (this.#waiters.length === 0 && this.#free()) {
18075
+ this.#reserved += 1;
18076
+ return;
18077
+ }
18078
+ await new Promise((resolve, reject) => {
18079
+ let timer;
18080
+ const leave = () => {
18081
+ clearTimeout(timer);
18082
+ signal?.removeEventListener("abort", onAbort);
18083
+ };
18084
+ const grant = () => {
18085
+ leave();
18086
+ resolve();
18087
+ };
18088
+ const drop = (error) => {
18089
+ const index = this.#waiters.indexOf(grant);
18090
+ if (index === -1)
18091
+ return;
18092
+ this.#waiters.splice(index, 1);
18093
+ leave();
18094
+ reject(error);
18095
+ };
18096
+ const onAbort = () => drop(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
18097
+ this.#waiters.push(grant);
18098
+ timer = setTimeout(() => drop(new Error(SANDBOX_BUSY)), this.#waitMs);
18099
+ signal?.addEventListener("abort", onAbort, { once: true });
18100
+ });
18101
+ }
18102
+ async run(signal, work) {
18103
+ await this.#admit(signal);
18104
+ const ticket = { claimed: false };
18105
+ try {
18106
+ return await this.#tickets.run(ticket, work);
18107
+ } finally {
18108
+ if (!ticket.claimed) {
18109
+ this.#reserved -= 1;
18110
+ this.#wake();
18111
+ }
18112
+ }
18113
+ }
18114
+ }
18115
+ function createCodemodeRuntime(limits = CODEMODE_LIMITS) {
18116
+ const limiter = new WorkerLimiter(limits);
18117
+ const sandbox = new CodemodeSandbox({
18118
+ memoryLimitBytes: CODEMODE_LIMITS.memoryLimitBytes,
18119
+ acquireWorker: limiter.acquireWorker
18120
+ });
18121
+ const options = { sandbox, models: false };
18122
+ const base = createCodemodeExtension(options);
18123
+ const reference = createCodemodeTool(options);
18124
+ const extension = {
18125
+ ...base,
18126
+ wraps: [
18127
+ ...base.wraps ?? [],
18128
+ ...nestedChangeWraps(),
18129
+ wrapTool2(reference, (tool) => ({
18130
+ ...tool,
18131
+ execute: (args, api, context) => limiter.run(context.abortSignal, () => tool.execute(args, api, context))
18132
+ }))
18133
+ ]
18134
+ };
18135
+ return { extension, limiter, close: () => sandbox.close() };
18136
+ }
17759
18137
 
17760
18138
  // packages/core/src/subagent-usage.ts
17761
18139
  var emptySubagentUsage = () => ({ models: {}, tools: {} });
@@ -17801,7 +18179,7 @@ function subagentUsageTotals(state) {
17801
18179
 
17802
18180
  // packages/core/src/subagents.ts
17803
18181
  var agentIdFor = (childId) => `agent-${childId}`;
17804
- var DelegationsDoc = defineDoc2({
18182
+ var DelegationsDoc = defineDoc4({
17805
18183
  kind: "app.neko.delegations",
17806
18184
  checkpointWhen: () => false,
17807
18185
  version: 1,
@@ -17810,7 +18188,7 @@ var DelegationsDoc = defineDoc2({
17810
18188
  fork: "initial",
17811
18189
  initial: () => ({ items: [] })
17812
18190
  });
17813
- var SubagentsDoc = defineDoc2({
18191
+ var SubagentsDoc = defineDoc4({
17814
18192
  kind: "app.neko.subagents",
17815
18193
  checkpointWhen: () => false,
17816
18194
  version: 1,
@@ -17819,7 +18197,7 @@ var SubagentsDoc = defineDoc2({
17819
18197
  fork: "initial",
17820
18198
  initial: () => ({ items: [] })
17821
18199
  });
17822
- var SubagentDoc = defineDoc2({
18200
+ var SubagentDoc = defineDoc4({
17823
18201
  kind: "app.neko.subagent",
17824
18202
  checkpointWhen: () => false,
17825
18203
  version: 1,
@@ -17828,7 +18206,7 @@ var SubagentDoc = defineDoc2({
17828
18206
  fork: "initial",
17829
18207
  initial: () => ({ role: null, parentId: null })
17830
18208
  });
17831
- var SlotsDoc = defineDoc2({
18209
+ var SlotsDoc = defineDoc4({
17832
18210
  kind: "app.neko.subagent-slots",
17833
18211
  checkpointWhen: () => false,
17834
18212
  version: 1,
@@ -17843,7 +18221,7 @@ var ROLE_PROMPTS = {
17843
18221
  var SUBAGENT_DELIVERY = " Work from the assigned task, your own prior conversation context and project resources; you cannot see the main conversation. Do not delegate or ask the user directly. Your final message must be a self-contained delivery to the main agent, including evidence, results and limitations. Skills never expand your allowed tools.";
17844
18222
  var MAIN_ONLY_TOOLS = new Set(["todo", "ask_user", "submit_plan"]);
17845
18223
  var MAIN_ONLY_EXTENSIONS = new Set(["neko-todos", "neko-questions", "neko-plan"]);
17846
- var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
18224
+ var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract", CODEMODE_NAME]);
17847
18225
  async function refreshDelegations(tx, id) {
17848
18226
  const doc = await tx.doc(DelegationsDoc, id);
17849
18227
  const identities = await tx.doc(SubagentsDoc, id);
@@ -18131,6 +18509,7 @@ function createSubagentExtension(prepare, discard, guard) {
18131
18509
  parameters,
18132
18510
  structuredOutputSchema,
18133
18511
  replay: "safe",
18512
+ callers: ["model"],
18134
18513
  execute: async (input, api, context) => {
18135
18514
  if (!Value.Check(actionParameters, input))
18136
18515
  throw new Error("Invalid subagent arguments. create requires type/title/task; continue requires agentId/title/task; list accepts only cursor/limit. Do not mix fields from different actions.");
@@ -18181,12 +18560,16 @@ function createSubagentExtension(prepare, discard, guard) {
18181
18560
  if (!identity) {
18182
18561
  const created = child ?? await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
18183
18562
  const role = args.action === "continue" ? "explore" : args.type;
18563
+ const codemode = (await tx.doc(CodemodeDoc, api.conversationId)).mode;
18564
+ (await tx.doc(CodemodeDoc, created.id)).mode = codemode;
18565
+ const enabled = enabledTools(agent);
18184
18566
  await configure(tx, created.id, {
18185
18567
  model: agent.model,
18186
18568
  thinkingLevel: agent.thinkingLevel,
18187
18569
  cwd: agent.cwd,
18188
18570
  extensions: agent.extensions.filter((e) => e.name !== extension.name && !MAIN_ONLY_EXTENSIONS.has(e.name)),
18189
- tools: agent.tools.filter((t) => t.name !== "subagent" && t.name !== "job" && !MAIN_ONLY_TOOLS.has(t.name) && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
18571
+ modelTools: agentChangeFor({ plan: "normal", codemode, role: "subagent" }).modelTools,
18572
+ tools: enabled.filter((t) => t.name !== "subagent" && t.name !== "job" && !MAIN_ONLY_TOOLS.has(t.name) && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
18190
18573
  });
18191
18574
  Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
18192
18575
  identity = {
@@ -18289,9 +18672,10 @@ async function removeMainOnlyTools(harness, ids, context) {
18289
18672
  if (!(await harness.snapshot(SubagentDoc, id, context))?.role)
18290
18673
  continue;
18291
18674
  const agent = await (await harness.conversation(id, context)).agent(context);
18292
- if (!agent.tools.some((t) => MAIN_ONLY_TOOLS.has(t.name)))
18675
+ const enabled = enabledTools(agent);
18676
+ if (!enabled.some((t) => MAIN_ONLY_TOOLS.has(t.name)))
18293
18677
  continue;
18294
- await harness.commit((tx) => configure(tx, id, { tools: agent.tools.filter((t) => !MAIN_ONLY_TOOLS.has(t.name)) }), context);
18678
+ await harness.commit((tx) => configure(tx, id, { tools: enabled.filter((t) => !MAIN_ONLY_TOOLS.has(t.name)) }), context);
18295
18679
  }
18296
18680
  }
18297
18681
  async function installSubagentCoordinator(harness, parents, context) {
@@ -18338,13 +18722,6 @@ async function installSubagentCoordinator(harness, parents, context) {
18338
18722
  }
18339
18723
 
18340
18724
  // packages/core/src/file-changes.ts
18341
- var CHANGE_LIMITS = {
18342
- files: 500,
18343
- opsPerFile: 50,
18344
- diffBytes: 2 * 1024 * 1024,
18345
- opDiffBytes: 64 * 1024,
18346
- conversations: 64
18347
- };
18348
18725
  var MAIN_AGENT = "main";
18349
18726
  function normalizeChangePath(raw, cwd, workspace) {
18350
18727
  const resolved = raw.startsWith("~") ? raw : resolve3(cwd, raw);
@@ -18355,22 +18732,6 @@ function normalizeChangePath(raw, cwd, workspace) {
18355
18732
  }
18356
18733
  return { path: resolved.split(sep).join("/"), outside: true };
18357
18734
  }
18358
- function clipDiff(diff) {
18359
- if (Buffer.byteLength(diff, "utf8") <= CHANGE_LIMITS.opDiffBytes)
18360
- return { diff, truncated: false };
18361
- const lines = diff.split(`
18362
- `);
18363
- const kept = [];
18364
- let bytes = 0;
18365
- for (const line of lines) {
18366
- bytes += Buffer.byteLength(line, "utf8") + 1;
18367
- if (bytes > CHANGE_LIMITS.opDiffBytes)
18368
- break;
18369
- kept.push(line);
18370
- }
18371
- return { diff: kept.join(`
18372
- `), truncated: true };
18373
- }
18374
18735
 
18375
18736
  class ConversationChanges {
18376
18737
  last = 0;
@@ -18378,6 +18739,7 @@ class ConversationChanges {
18378
18739
  truncated = false;
18379
18740
  bytes = 0;
18380
18741
  files = new Map;
18742
+ #scripts = new Map;
18381
18743
  #pending = new Map;
18382
18744
  #agent;
18383
18745
  #cwd;
@@ -18391,6 +18753,8 @@ class ConversationChanges {
18391
18753
  for (const message of entry.model ?? []) {
18392
18754
  if (message.role === "assistant") {
18393
18755
  for (const block of message.content) {
18756
+ if (block.type === "toolCall" && block.name === CODEMODE_NAME)
18757
+ this.#scripts.set(block.id, entry.id);
18394
18758
  if (block.type !== "toolCall" || block.name !== "edit" && block.name !== "write")
18395
18759
  continue;
18396
18760
  const path = block.arguments?.path;
@@ -18398,6 +18762,8 @@ class ConversationChanges {
18398
18762
  this.#pending.set(block.id, { tool: block.name, path, args: block.arguments });
18399
18763
  }
18400
18764
  } else if (message.role === "toolResult") {
18765
+ if (this.#scripts.has(message.toolCallId))
18766
+ this.#scripts.set(message.toolCallId, entry.id);
18401
18767
  const call = this.#pending.get(message.toolCallId);
18402
18768
  if (!call)
18403
18769
  continue;
@@ -18409,8 +18775,20 @@ class ConversationChanges {
18409
18775
  }
18410
18776
  this.last = entry.id;
18411
18777
  }
18778
+ withNested(nested) {
18779
+ const visible = nested.items.filter((item) => this.#scripts.has(item.parentCallId));
18780
+ const copy = new ConversationChanges(this.#agent, this.#cwd, this.#workspace);
18781
+ copy.last = this.last;
18782
+ copy.total = this.total;
18783
+ copy.truncated = this.truncated || nested.dropped && visible.length > 0;
18784
+ copy.bytes = this.bytes;
18785
+ for (const [path, file] of this.files)
18786
+ copy.files.set(path, { ...file, agents: new Set(file.agents), items: file.items.map((item) => ({ ...item })) });
18787
+ for (const item of visible)
18788
+ copy.#record({ tool: item.tool, path: item.path }, { diff: item.diff, added: item.added, removed: item.removed, truncated: item.truncated }, this.#scripts.get(item.parentCallId));
18789
+ return copy;
18790
+ }
18412
18791
  #add(call, details, seq) {
18413
- const { path, outside } = normalizeChangePath(call.path, this.#cwd, this.#workspace);
18414
18792
  let diff = "";
18415
18793
  let added = 0;
18416
18794
  let removed = 0;
@@ -18430,6 +18808,11 @@ class ConversationChanges {
18430
18808
  ({ diff, added, removed, truncated } = synthesized);
18431
18809
  }
18432
18810
  }
18811
+ this.#record(call, { diff, added, removed, truncated }, seq);
18812
+ }
18813
+ #record(call, shown, seq) {
18814
+ const { path, outside } = normalizeChangePath(call.path, this.#cwd, this.#workspace);
18815
+ const { diff, added, removed, truncated } = shown;
18433
18816
  this.total++;
18434
18817
  let file = this.files.get(path);
18435
18818
  if (!file) {
@@ -18450,7 +18833,7 @@ class ConversationChanges {
18450
18833
  file.added += added;
18451
18834
  file.removed += removed;
18452
18835
  file.agents.add(this.#agent);
18453
- file.lastSeq = seq;
18836
+ file.lastSeq = Math.max(file.lastSeq, seq);
18454
18837
  file.items.push({ seq, agent: this.#agent, tool: call.tool, diff, added, removed, truncated });
18455
18838
  this.bytes += Buffer.byteLength(diff, "utf8");
18456
18839
  if (file.items.length > CHANGE_LIMITS.opsPerFile) {
@@ -18552,10 +18935,12 @@ class ChangeTracker {
18552
18935
  let revision = 0;
18553
18936
  let truncated = false;
18554
18937
  for (const member of await this.#members(rootId)) {
18555
- const state = await this.#sync(member, workspace);
18556
- if (!state)
18938
+ const synced = await this.#sync(member, workspace);
18939
+ if (!synced)
18557
18940
  continue;
18558
- revision += state.total;
18941
+ const nested = await this.#harness.snapshot(NestedChangesDoc, member.id, this.#context);
18942
+ const state = nested && nested.items.length > 0 ? synced.withNested(nested) : synced;
18943
+ revision += synced.total + (nested?.total ?? 0);
18559
18944
  truncated ||= state.truncated;
18560
18945
  for (const file of state.files.values()) {
18561
18946
  const existing = merged.get(file.path);
@@ -18960,7 +19345,7 @@ function validatePlanDecision(input) {
18960
19345
  return { kind: "revise", feedback: text(input.feedback, "feedback", PLAN_LIMITS.feedbackChars) };
18961
19346
  }
18962
19347
  // packages/core/src/jobs.ts
18963
- import { defineDoc as defineDoc3 } from "@earendil-works/pi-durable";
19348
+ import { defineDoc as defineDoc5 } from "@earendil-works/pi-durable";
18964
19349
  var JOB_LIMITS = {
18965
19350
  maxRunning: 4,
18966
19351
  keepTerminal: 20,
@@ -18974,7 +19359,7 @@ var JOB_LIMITS = {
18974
19359
  commandBytes: 32 * 1024,
18975
19360
  maxTimeoutSeconds: 7 * 24 * 3600
18976
19361
  };
18977
- var JobsDoc = defineDoc3({
19362
+ var JobsDoc = defineDoc5({
18978
19363
  kind: "app.neko.jobs",
18979
19364
  checkpointWhen: () => false,
18980
19365
  version: 1,
@@ -19115,7 +19500,7 @@ import { PI_ATTRIBUTION } from "@earendil-works/pi-ai/utils/identity";
19115
19500
  import {
19116
19501
  AssistantEntry as AssistantEntry2,
19117
19502
  createRegistry,
19118
- defineDoc as defineDoc8,
19503
+ defineDoc as defineDoc10,
19119
19504
  Harness,
19120
19505
  InboxDoc as InboxDoc3,
19121
19506
  LiveDoc as importLiveDoc,
@@ -19547,7 +19932,7 @@ var statusWord = {
19547
19932
  stopped: "stopped",
19548
19933
  lost: "lost"
19549
19934
  };
19550
- var describe2 = (job) => {
19935
+ var describe3 = (job) => {
19551
19936
  const parts = [statusWord[job.status]];
19552
19937
  if (job.exitCode !== null)
19553
19938
  parts.push(`exit code ${job.exitCode}`);
@@ -19562,7 +19947,7 @@ var describe2 = (job) => {
19562
19947
  function reportText(job, tail) {
19563
19948
  const body = reportTail(tail);
19564
19949
  return [
19565
- `[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
19950
+ `[Background job ${job.jobId} "${job.title}" ${describe3(job)}]`,
19566
19951
  `Command: ${job.command}`,
19567
19952
  body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
19568
19953
  ${body}` : "It produced no output."
@@ -19772,7 +20157,7 @@ class JobManager {
19772
20157
  if (kind === "stop") {
19773
20158
  const jobId = stringField(args.jobId, "jobId");
19774
20159
  const job = await this.stop(io, api.conversationId, jobId, "model", context);
19775
- return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
20160
+ return this.#text(`Job ${job.jobId} ${describe3(job)}.`);
19776
20161
  }
19777
20162
  if (kind !== "start")
19778
20163
  throw new Error("Invalid job arguments: action must be start, output, stop or list.");
@@ -19800,7 +20185,7 @@ class JobManager {
19800
20185
  #listText(items) {
19801
20186
  if (!items.length)
19802
20187
  return "No jobs.";
19803
- return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
20188
+ return items.map((job) => `${job.jobId} [${describe3(job)}] "${job.title}" \u2014 ${job.command}`).join(`
19804
20189
  `);
19805
20190
  }
19806
20191
  async start(io, conversationId, cwd, args, context) {
@@ -19852,7 +20237,7 @@ class JobManager {
19852
20237
  return this.#render(job, read.text, read.offset, read.dropped);
19853
20238
  }
19854
20239
  #render(job, text, offset, dropped) {
19855
- const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
20240
+ const lines = [`Job ${job.jobId} ${describe3(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
19856
20241
  if (dropped > 0)
19857
20242
  lines.push(`(${dropped} bytes of earlier output were dropped)`);
19858
20243
  lines.push(text ? `Output (next offset ${offset}):
@@ -19926,7 +20311,7 @@ import {
19926
20311
  hook as hook2,
19927
20312
  InboxDoc,
19928
20313
  ToolTask as ToolTask2,
19929
- wrapTool
20314
+ wrapTool as wrapTool3
19930
20315
  } from "@earendil-works/pi-durable";
19931
20316
  import { createBashTool } from "@earendil-works/pi-durable/tools";
19932
20317
  import { Type as Type5 } from "typebox";
@@ -20039,7 +20424,7 @@ function isSafeCommand(command) {
20039
20424
  // packages/core/src/user-questions.ts
20040
20425
  import { randomUUID } from "crypto";
20041
20426
  import {
20042
- defineDoc as defineDoc4,
20427
+ defineDoc as defineDoc6,
20043
20428
  defineDocFamily,
20044
20429
  defineExtension as defineExtension5,
20045
20430
  defineTool as defineTool4
@@ -20055,7 +20440,7 @@ var QuestionDoc = defineDocFamily({
20055
20440
  family: true,
20056
20441
  initial: (seed) => seed
20057
20442
  });
20058
- var QuestionsDoc = defineDoc4({
20443
+ var QuestionsDoc = defineDoc6({
20059
20444
  kind: "app.neko.questions",
20060
20445
  checkpointWhen: () => false,
20061
20446
  version: 1,
@@ -20377,28 +20762,7 @@ function createAskUserTool(service) {
20377
20762
  var createQuestionExtension = (service) => defineExtension5({ name: "neko-questions", tools: [createAskUserTool(service)] });
20378
20763
 
20379
20764
  // packages/core/src/plan.ts
20380
- var PLAN_REMOVED_TOOLS = ["edit", "write", "job"];
20381
- var PLAN_INSTRUCTIONS = [
20382
- "You are in plan mode: research and plan only, and change nothing.",
20383
- "- Allowed: read, find_files, search_text, web_search, web_extract, todo, ask_user, read-only subagents (explore, plan) and bash limited to read-only commands (listing, searching, git log/diff/status, version checks). Other commands are rejected.",
20384
- "- Not available: edit, write, job and any command that modifies files, installs packages or starts processes. Do not try to work around this.",
20385
- "- When you understand the task, call submit_plan once, alone in its turn, with a short title, ordered concrete steps and an optional note (risks, assumptions, verification). The user then approves it or asks for changes.",
20386
- "- After submit_plan do nothing else. If the user asks for changes, research what is needed and submit a revised plan.",
20387
- "- Use ask_user only for decisions you cannot settle by reading the code."
20388
- ].join(`
20389
- `);
20390
20765
  var PLAN_BUSY = "PLAN_BUSY: Wait for the current run to finish or stop it first.";
20391
- function planAgentChange(mode) {
20392
- if (mode === "normal")
20393
- return { tools: null, modelTools: null, instructions: null };
20394
- const named = (name) => ({ name });
20395
- return {
20396
- tools: { remove: PLAN_REMOVED_TOOLS.map(named) },
20397
- modelTools: { add: [named("submit_plan")] },
20398
- instructions: PLAN_INSTRUCTIONS
20399
- };
20400
- }
20401
- var agentInPlanMode = (agent) => agent.instructions === PLAN_INSTRUCTIONS;
20402
20766
  var clone2 = (value) => JSON.parse(JSON.stringify(value));
20403
20767
  var toRecord = ({ taskId: _taskId, dispatched: _dispatched, ...record }) => clone2(record);
20404
20768
  function settle2(state, item, status) {
@@ -20634,7 +20998,7 @@ var createPlanExtension = (service) => defineExtension6({
20634
20998
  })
20635
20999
  ],
20636
21000
  wraps: [
20637
- wrapTool(createBashTool(), (bash) => ({
21001
+ wrapTool3(createBashTool(), (bash) => ({
20638
21002
  ...bash,
20639
21003
  execute: async (args, api, context) => {
20640
21004
  if ((await api.snapshot(PlanDoc, api.conversationId, context))?.mode === "plan" && !isSafeCommand(args.command))
@@ -20786,6 +21150,9 @@ import { defineExtension as defineExtension7, section } from "@earendil-works/pi
20786
21150
  var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
20787
21151
  var PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
20788
21152
  var TOOL_GUIDELINES = {
21153
+ codemode: [
21154
+ "Use codemode to batch independent tool calls (Promise.allSettled), chain them, or filter large output, instead of many separate calls."
21155
+ ],
20789
21156
  subagent: [
20790
21157
  "Delegate only independent, sufficiently scoped work when isolation or parallelism helps. Handle simple tasks directly. Pass necessary context and user constraints; research-only requests must not become coding tasks. For related follow-up work, list and continue a suitable idle subagent using its stable agentId; busy means no task was accepted. Do not change its role. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
20791
21158
  ],
@@ -20850,7 +21217,7 @@ var createNekoPrompt = (loadResources, platform = process.platform) => {
20850
21217
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
20851
21218
  const sections = {
20852
21219
  preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
20853
- rules: buildRules(input.agent.tools.map((tool) => tool.name), platform),
21220
+ rules: buildRules(enabledTools(input.agent).map((tool) => tool.name), platform),
20854
21221
  project_context: renderProjectContext(resources.contextFiles),
20855
21222
  skills: formatSkillsForPrompt(resources.skills) || undefined,
20856
21223
  cwd: cwd?.replace(/\\/g, "/")
@@ -21017,13 +21384,13 @@ var createResourceSnapshots = (load, submission) => {
21017
21384
  import { stripVTControlCharacters } from "util";
21018
21385
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
21019
21386
  import {
21020
- defineDoc as defineDoc5,
21387
+ defineDoc as defineDoc7,
21021
21388
  defineExtension as defineExtension8,
21022
21389
  defineTask as defineTask3,
21023
21390
  InboxDoc as InboxDoc2,
21024
21391
  LiveDoc as LiveDoc2
21025
21392
  } from "@earendil-works/pi-durable";
21026
- var SessionMetadataDoc = defineDoc5({
21393
+ var SessionMetadataDoc = defineDoc7({
21027
21394
  kind: "app.neko.session-metadata",
21028
21395
  checkpointWhen: () => false,
21029
21396
  version: 1,
@@ -21360,9 +21727,9 @@ class SessionMetadataManager {
21360
21727
  }
21361
21728
 
21362
21729
  // packages/core/src/todos.ts
21363
- import { defineDoc as defineDoc6, defineExtension as defineExtension9, defineTool as defineTool6 } from "@earendil-works/pi-durable";
21730
+ import { defineDoc as defineDoc8, defineExtension as defineExtension9, defineTool as defineTool6 } from "@earendil-works/pi-durable";
21364
21731
  import { Type as Type6 } from "typebox";
21365
- var TodosDoc = defineDoc6({
21732
+ var TodosDoc = defineDoc8({
21366
21733
  kind: "app.neko.todos",
21367
21734
  checkpointWhen: () => false,
21368
21735
  version: 1,
@@ -21371,7 +21738,7 @@ var TodosDoc = defineDoc6({
21371
21738
  fork: "asOf",
21372
21739
  initial: () => ({ revision: 0, items: [] })
21373
21740
  });
21374
- var TodoReceiptDoc = defineDoc6({
21741
+ var TodoReceiptDoc = defineDoc8({
21375
21742
  kind: "app.neko.todo-receipt",
21376
21743
  checkpointWhen: () => false,
21377
21744
  version: 1,
@@ -21463,14 +21830,14 @@ import { randomUUID as randomUUID3 } from "crypto";
21463
21830
  import { realpath } from "fs/promises";
21464
21831
  import {
21465
21832
  configure as configure2,
21466
- defineDoc as defineDoc7,
21833
+ defineDoc as defineDoc9,
21467
21834
  defineExtension as defineExtension10,
21468
21835
  hook as hook3,
21469
21836
  ToolTask as ToolTask3,
21470
- wrapTool as wrapTool2
21837
+ wrapTool as wrapTool4
21471
21838
  } from "@earendil-works/pi-durable";
21472
21839
  import { createBashTool as createBashTool2 } from "@earendil-works/pi-durable/tools";
21473
- var ApprovalsDoc = defineDoc7({
21840
+ var ApprovalsDoc = defineDoc9({
21474
21841
  kind: "app.neko.approvals",
21475
21842
  checkpointWhen: () => false,
21476
21843
  version: 1,
@@ -21612,7 +21979,7 @@ function createApprovalExtension(service) {
21612
21979
  })
21613
21980
  ],
21614
21981
  wraps: [
21615
- wrapTool2(createBashTool2(), (bash) => ({
21982
+ wrapTool4(createBashTool2(), (bash) => ({
21616
21983
  ...bash,
21617
21984
  execute: async (args, api, context) => {
21618
21985
  const role = await api.snapshot(SubagentDoc, api.conversationId, context);
@@ -21636,7 +22003,7 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
21636
22003
  const child = await harness.conversation(id, context);
21637
22004
  const agent = await child.agent(context);
21638
22005
  await harness.commit((tx) => configure2(tx, id, {
21639
- tools: [...agent.tools.filter((t) => t.name !== "bash"), createBashTool2()],
22006
+ tools: [...enabledTools(agent).filter((t) => t.name !== "bash"), createBashTool2()],
21640
22007
  extensions: { add: [extension] }
21641
22008
  }), context);
21642
22009
  }
@@ -22645,7 +23012,7 @@ function cancellable(work, signal) {
22645
23012
  var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
22646
23013
  var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
22647
23014
  var neverAborted = () => new AbortController().signal;
22648
- var ArchivedDoc = defineDoc8({
23015
+ var ArchivedDoc = defineDoc10({
22649
23016
  kind: "app.neko.conversation",
22650
23017
  checkpointWhen: () => false,
22651
23018
  version: 1,
@@ -22654,7 +23021,7 @@ var ArchivedDoc = defineDoc8({
22654
23021
  fork: "initial",
22655
23022
  initial: () => ({ archived: false })
22656
23023
  });
22657
- var WorkspacesDoc = defineDoc8({
23024
+ var WorkspacesDoc = defineDoc10({
22658
23025
  kind: "app.neko.workspaces",
22659
23026
  checkpointWhen: () => false,
22660
23027
  version: 1,
@@ -22663,7 +23030,7 @@ var WorkspacesDoc = defineDoc8({
22663
23030
  fork: "initial",
22664
23031
  initial: () => ({ items: [] })
22665
23032
  });
22666
- var RemovedWorkspacesDoc = defineDoc8({
23033
+ var RemovedWorkspacesDoc = defineDoc10({
22667
23034
  kind: "app.neko.workspaces-removed",
22668
23035
  checkpointWhen: () => false,
22669
23036
  version: 1,
@@ -22672,7 +23039,7 @@ var RemovedWorkspacesDoc = defineDoc8({
22672
23039
  fork: "initial",
22673
23040
  initial: () => ({ paths: [] })
22674
23041
  });
22675
- var SearchSettingsDoc = defineDoc8({
23042
+ var SearchSettingsDoc = defineDoc10({
22676
23043
  kind: "app.neko.search-settings",
22677
23044
  checkpointWhen: () => false,
22678
23045
  version: 1,
@@ -22681,7 +23048,7 @@ var SearchSettingsDoc = defineDoc8({
22681
23048
  fork: "initial",
22682
23049
  initial: () => ({ provider: null })
22683
23050
  });
22684
- var ProviderSettingsDoc = defineDoc8({
23051
+ var ProviderSettingsDoc = defineDoc10({
22685
23052
  kind: "app.neko.provider-settings",
22686
23053
  checkpointWhen: () => false,
22687
23054
  version: 1,
@@ -22690,7 +23057,7 @@ var ProviderSettingsDoc = defineDoc8({
22690
23057
  fork: "initial",
22691
23058
  initial: () => ({ enabled: null })
22692
23059
  });
22693
- var DefaultModelDoc = defineDoc8({
23060
+ var DefaultModelDoc = defineDoc10({
22694
23061
  kind: "app.neko.default-model",
22695
23062
  checkpointWhen: () => false,
22696
23063
  version: 1,
@@ -22699,7 +23066,7 @@ var DefaultModelDoc = defineDoc8({
22699
23066
  fork: "initial",
22700
23067
  initial: () => ({ model: null })
22701
23068
  });
22702
- var ContextSettingsDoc = defineDoc8({
23069
+ var ContextSettingsDoc = defineDoc10({
22703
23070
  kind: "app.neko.context-settings",
22704
23071
  checkpointWhen: () => false,
22705
23072
  version: 1,
@@ -22708,7 +23075,7 @@ var ContextSettingsDoc = defineDoc8({
22708
23075
  fork: "initial",
22709
23076
  initial: emptySavedContextSettings
22710
23077
  });
22711
- var ActivityDoc = defineDoc8({
23078
+ var ActivityDoc = defineDoc10({
22712
23079
  kind: "app.neko.activity",
22713
23080
  checkpointWhen: () => false,
22714
23081
  version: 1,
@@ -22815,6 +23182,11 @@ async function createNekoRuntime(options = {}) {
22815
23182
  registry.install(createQuestionExtension(() => questions));
22816
23183
  let plans;
22817
23184
  registry.install(createPlanExtension(() => plans));
23185
+ const codemode = createCodemodeRuntime({
23186
+ maxWorkers: options.codemodeLimits?.maxWorkers ?? CODEMODE_LIMITS.maxWorkers,
23187
+ waitMs: options.codemodeLimits?.waitMs ?? CODEMODE_LIMITS.waitMs
23188
+ });
23189
+ registry.install(codemode.extension);
22818
23190
  for (const extension of options.extensions ?? [])
22819
23191
  registry.install(extension);
22820
23192
  let approvals;
@@ -22868,6 +23240,7 @@ async function createNekoRuntime(options = {}) {
22868
23240
  }
22869
23241
  }
22870
23242
  } catch (error) {
23243
+ await codemode.close();
22871
23244
  await workspaceFiles.close();
22872
23245
  await harness.close(context);
22873
23246
  throw error;
@@ -22882,6 +23255,13 @@ async function createNekoRuntime(options = {}) {
22882
23255
  }
22883
23256
  });
22884
23257
  enabled = new Set(normalizeEnabledProviders((await harness.snapshot(ProviderSettingsDoc, conversation.id, context))?.enabled));
23258
+ const codemodeState = async () => resolveCodemodeSettings(options.codemodeSettings, (await harness.snapshot(CodemodeSettingsDoc, conversation.id, context))?.mode);
23259
+ if (existing.length === 0) {
23260
+ const initial = (await codemodeState()).effective.mode;
23261
+ await harness.commit(async (tx) => {
23262
+ (await tx.doc(CodemodeDoc, conversation.id)).mode = initial;
23263
+ }, context);
23264
+ }
22885
23265
  const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
22886
23266
  if (isSearchProvider(savedSearch))
22887
23267
  searchProvider = savedSearch;
@@ -23008,13 +23388,17 @@ async function createNekoRuntime(options = {}) {
23008
23388
  admissions.delete(id);
23009
23389
  }
23010
23390
  };
23011
- const syncPlanAgent = async (target) => {
23012
- const mode = await plans.mode(target.id);
23013
- if (mode === "plan" !== agentInPlanMode(await target.agent(context)))
23014
- await target.configure(planAgentChange(mode), context);
23391
+ const syncAgent = async (target) => {
23392
+ const change = agentSyncChange(await target.agent(context), {
23393
+ plan: await plans.mode(target.id),
23394
+ codemode: (await harness.snapshot(CodemodeDoc, target.id, context))?.mode ?? "off",
23395
+ role: "main"
23396
+ });
23397
+ if (change)
23398
+ await target.configure(change, context);
23015
23399
  };
23016
23400
  const submitTo = async (target, content, submitOptions, extra) => admit(target.id, async () => {
23017
- await syncPlanAgent(target);
23401
+ await syncAgent(target);
23018
23402
  const agent = await target.agent(context);
23019
23403
  const workspace = executionWorkspace(agent.cwd);
23020
23404
  const duplicate = submitOptions?.requestId !== undefined && await storage.submissionByRequest(target.id, submitOptions.requestId, context);
@@ -23178,7 +23562,7 @@ async function createNekoRuntime(options = {}) {
23178
23562
  if (await isRunning(target.id))
23179
23563
  throw new Error(PLAN_BUSY);
23180
23564
  await plans.setMode(target.id, mode);
23181
- await syncPlanAgent(target);
23565
+ await syncAgent(target);
23182
23566
  return plans.view(target.id);
23183
23567
  });
23184
23568
  },
@@ -23300,7 +23684,7 @@ async function createNekoRuntime(options = {}) {
23300
23684
  const session = await getConversation(id);
23301
23685
  if (!session)
23302
23686
  continue;
23303
- await admit(id, () => syncPlanAgent(session.conversation));
23687
+ await admit(id, () => syncAgent(session.conversation));
23304
23688
  const pending = await plans.undispatched(id);
23305
23689
  if (pending)
23306
23690
  await dispatchPlan(session.conversation, pending);
@@ -23386,6 +23770,8 @@ async function createNekoRuntime(options = {}) {
23386
23770
  }
23387
23771
  const child = at === undefined ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(id, at, { ownership: { kind: "ownerless" } });
23388
23772
  (await tx.doc(DelegationsDoc, child.id)).items = inherited;
23773
+ if (at === undefined)
23774
+ (await tx.doc(CodemodeDoc, child.id)).mode = (await tx.doc(CodemodeDoc, id)).mode;
23389
23775
  if (sourceMetadata) {
23390
23776
  const doc = await tx.doc(SessionMetadataDoc, child.id);
23391
23777
  doc.title = sourceMetadata.title || sourceMetadata.firstPromptPreview;
@@ -23466,12 +23852,17 @@ async function createNekoRuntime(options = {}) {
23466
23852
  const workspace = await addWorkspace(createOptions?.cwd ?? defaultCwd);
23467
23853
  const savedDefault = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
23468
23854
  const effective = createOptions?.model ?? modelOverride ?? savedDefault ?? model;
23855
+ const mode = (await codemodeState()).effective.mode;
23469
23856
  const created = await harness.createConversation({
23470
23857
  ownership: { kind: "ownerless" },
23471
23858
  agent: {
23472
23859
  model: effective,
23473
23860
  thinkingLevel: resolveLevel(effective, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
23474
- cwd: workspace.path
23861
+ cwd: workspace.path,
23862
+ ...agentChangeFor({ plan: "normal", codemode: mode, role: "main" })
23863
+ },
23864
+ init: async (tx, id) => {
23865
+ (await tx.doc(CodemodeDoc, id)).mode = mode;
23475
23866
  }
23476
23867
  }, context);
23477
23868
  await metadata.initialize(created.id);
@@ -23526,6 +23917,20 @@ async function createNekoRuntime(options = {}) {
23526
23917
  async resetContextSettings() {
23527
23918
  return persistContext(emptySavedContextSettings());
23528
23919
  },
23920
+ codemodeSettings: codemodeState,
23921
+ async setCodemodeMode(mode) {
23922
+ const parsed = parseCodemodeMode(mode);
23923
+ await harness.commit(async (tx) => {
23924
+ (await tx.doc(CodemodeSettingsDoc, conversation.id)).mode = parsed;
23925
+ }, context);
23926
+ return codemodeState();
23927
+ },
23928
+ async resetCodemodeMode() {
23929
+ await harness.commit(async (tx) => {
23930
+ (await tx.doc(CodemodeSettingsDoc, conversation.id)).mode = null;
23931
+ }, context);
23932
+ return codemodeState();
23933
+ },
23529
23934
  async searchSettings() {
23530
23935
  return { selected: searchProvider };
23531
23936
  },
@@ -23647,6 +24052,7 @@ async function createNekoRuntime(options = {}) {
23647
24052
  registry.install(extension);
23648
24053
  },
23649
24054
  async close() {
24055
+ await codemode.close();
23650
24056
  await cleanup();
23651
24057
  return harness.close(context);
23652
24058
  }
@@ -23689,6 +24095,18 @@ var image = Type8.Object({
23689
24095
  contentIndex: integer
23690
24096
  });
23691
24097
  var skill = Type8.Object({ name: str, location: str, body: str, args: str });
24098
+ var CodemodeViewSchema = Type8.Object({
24099
+ calls: Type8.Array(Type8.Object({
24100
+ id: integer,
24101
+ name: str,
24102
+ args: str,
24103
+ status: Type8.Enum(["running", "ok", "error", "cancelled"]),
24104
+ durationMs: Type8.Union([num, nil]),
24105
+ error: nullableString
24106
+ })),
24107
+ omitted: integer,
24108
+ fullOutputPath: nullableString
24109
+ });
23692
24110
  var call = Type8.Object({
23693
24111
  callId: str,
23694
24112
  name: str,
@@ -23699,7 +24117,8 @@ var call = Type8.Object({
23699
24117
  diff: nullableString,
23700
24118
  diffAdded: Type8.Union([num, nil]),
23701
24119
  diffRemoved: Type8.Union([num, nil]),
23702
- diffTruncated: Type8.Optional(bool)
24120
+ diffTruncated: Type8.Optional(bool),
24121
+ codemode: Type8.Optional(CodemodeViewSchema)
23703
24122
  });
23704
24123
  var DelegationSchema = Type8.Object({
23705
24124
  parentId: integer,
@@ -24007,7 +24426,8 @@ var ChatViewSchema = Type8.Object({
24007
24426
  diffAdded: Type8.Union([num, nil]),
24008
24427
  diffRemoved: Type8.Union([num, nil]),
24009
24428
  diffTruncated: Type8.Optional(bool),
24010
- interaction: Type8.Optional(ToolInteractionSchema)
24429
+ interaction: Type8.Optional(ToolInteractionSchema),
24430
+ codemode: Type8.Optional(CodemodeViewSchema)
24011
24431
  }),
24012
24432
  Type8.Object({
24013
24433
  type: Type8.Literal("system"),
@@ -24284,6 +24704,24 @@ var NekoContextSettings = contract("neko.context-settings", {
24284
24704
  }
24285
24705
  }
24286
24706
  });
24707
+ var CodemodeSettingsStateSchema = Type8.Object({
24708
+ revision: integer,
24709
+ effective: Type8.Object({ mode: Type8.Enum([...CODEMODE_MODES]) }),
24710
+ source: Type8.Enum(["default", "config", "saved"])
24711
+ });
24712
+ var NekoCodemodeSettings = contract("neko.codemode-settings", {
24713
+ version: 1,
24714
+ input: Type8.Object({}, { additionalProperties: false }),
24715
+ variants: {
24716
+ ready: {
24717
+ state: { state: CodemodeSettingsStateSchema },
24718
+ methods: {
24719
+ set: rpc(Type8.Object({ mode: Type8.Enum([...CODEMODE_MODES]) }, { additionalProperties: false }), nil),
24720
+ reset: rpc(Type8.Object({}, { additionalProperties: false }), nil)
24721
+ }
24722
+ }
24723
+ }
24724
+ });
24287
24725
  var defaultModelSource = Type8.Enum(["cli", "saved", "config", "default"]);
24288
24726
  var DefaultModelStateSchema = Type8.Object({
24289
24727
  effective: model2,
@@ -25090,8 +25528,8 @@ function subagentClient(connect) {
25090
25528
  import { createHash as createHash2 } from "crypto";
25091
25529
 
25092
25530
  // packages/core/src/application-version.ts
25093
- var RUNNING_VERSION = "0.5.2";
25094
- var APPLICATION_PROTOCOL = 9;
25531
+ var RUNNING_VERSION = "0.5.3";
25532
+ var APPLICATION_PROTOCOL = 10;
25095
25533
  function stableVersion(value) {
25096
25534
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
25097
25535
  }
@@ -25228,6 +25666,23 @@ var MobileToolInteractionSchema = Type10.Union([
25228
25666
  note: str2
25229
25667
  })
25230
25668
  ]);
25669
+ var MobileCodemodeSchema = Type10.Object({
25670
+ calls: Type10.Array(Type10.Object({
25671
+ id: int,
25672
+ name: str2,
25673
+ args: Type10.String({ maxLength: CODEMODE_VIEW_LIMITS.argsChars }),
25674
+ status: Type10.Union([
25675
+ Type10.Literal("running"),
25676
+ Type10.Literal("ok"),
25677
+ Type10.Literal("error"),
25678
+ Type10.Literal("cancelled")
25679
+ ]),
25680
+ durationMs: opt(Type10.Number()),
25681
+ error: opt(Type10.String({ maxLength: CODEMODE_VIEW_LIMITS.errorChars }))
25682
+ }), { maxItems: CODEMODE_VIEW_LIMITS.maxCalls }),
25683
+ omitted: int,
25684
+ fullOutputPath: opt(str2)
25685
+ });
25231
25686
  var MobileToolItemSchema = Type10.Object({
25232
25687
  callId: str2,
25233
25688
  name: str2,
@@ -25242,7 +25697,8 @@ var MobileToolItemSchema = Type10.Object({
25242
25697
  diffTruncated: opt(Type10.Boolean()),
25243
25698
  durationMs: opt(Type10.Number()),
25244
25699
  note: opt(str2),
25245
- interaction: opt(MobileToolInteractionSchema)
25700
+ interaction: opt(MobileToolInteractionSchema),
25701
+ codemode: opt(MobileCodemodeSchema)
25246
25702
  });
25247
25703
  var MobileChatItemSchema = Type10.Object({
25248
25704
  key: str2,
@@ -25773,6 +26229,17 @@ function mapChatItem(raw, owner) {
25773
26229
  tool.note = raw.note;
25774
26230
  if (raw.interaction !== undefined)
25775
26231
  tool.interaction = raw.interaction;
26232
+ if (raw.codemode !== undefined) {
26233
+ tool.codemode = {
26234
+ calls: raw.codemode.calls.map(({ durationMs, error, ...call }) => ({
26235
+ ...call,
26236
+ ...durationMs === null ? {} : { durationMs },
26237
+ ...error === null ? {} : { error }
26238
+ })),
26239
+ omitted: raw.codemode.omitted,
26240
+ ...raw.codemode.fullOutputPath === null ? {} : { fullOutputPath: raw.codemode.fullOutputPath }
26241
+ };
26242
+ }
25776
26243
  item = { key: raw.key, type: "tool", tool, ...images(raw.images) };
25777
26244
  break;
25778
26245
  }
@@ -30044,10 +30511,12 @@ var ServerState = local3("neko.server-state", {
30044
30511
  credentials: CredentialStateSchema,
30045
30512
  searchSettings: SearchSettingsStateSchema,
30046
30513
  contextSettings: ContextSettingsStateSchema,
30514
+ codemodeSettings: CodemodeSettingsStateSchema,
30047
30515
  defaultModel: DefaultModelStateSchema,
30048
30516
  refreshCredentials: method2(Type13.Null(), Type13.Null()),
30049
30517
  refreshSearchSettings: method2(Type13.Null(), Type13.Null()),
30050
30518
  refreshContextSettings: method2(Type13.Null(), Type13.Null()),
30519
+ refreshCodemodeSettings: method2(Type13.Null(), Type13.Null()),
30051
30520
  refreshDefaultModel: method2(Type13.Null(), Type13.Null())
30052
30521
  }
30053
30522
  });
@@ -30138,12 +30607,13 @@ async function createNekoServerServices(options) {
30138
30607
  return [spec.id, spec.kind === "model" ? { ...status, enabled: enabled.has(spec.id) } : status];
30139
30608
  })));
30140
30609
  };
30141
- const [sessions, workspaces, statuses, search, contextSettings0, defaultModel0] = await Promise.all([
30610
+ const [sessions, workspaces, statuses, search, contextSettings0, codemodeSettings0, defaultModel0] = await Promise.all([
30142
30611
  list(),
30143
30612
  runtime.listWorkspaces(),
30144
30613
  readStatuses(),
30145
30614
  runtime.searchSettings(),
30146
30615
  runtime.contextSettings(),
30616
+ runtime.codemodeSettings(),
30147
30617
  runtime.defaultModel()
30148
30618
  ]);
30149
30619
  let mutationTail = Promise.resolve();
@@ -30161,6 +30631,10 @@ async function createNekoServerServices(options) {
30161
30631
  revision: 1,
30162
30632
  ...contextSettings0
30163
30633
  });
30634
+ const codemodeSettings = self2.state("codemodeSettings", CodemodeSettingsStateSchema, {
30635
+ revision: 1,
30636
+ ...codemodeSettings0
30637
+ });
30164
30638
  const defaultModel = self2.state("defaultModel", DefaultModelStateSchema, defaultModel0);
30165
30639
  const refreshContextSettings = async (_input, ctx) => {
30166
30640
  const next = await runtime.contextSettings();
@@ -30169,6 +30643,13 @@ async function createNekoServerServices(options) {
30169
30643
  contextSettings.replace(ctx, { revision: revision + 1, ...next });
30170
30644
  return null;
30171
30645
  };
30646
+ const refreshCodemodeSettings = async (_input, ctx) => {
30647
+ const next = await runtime.codemodeSettings();
30648
+ const { revision, ...current } = codemodeSettings.value;
30649
+ if (JSON.stringify(next) !== JSON.stringify(current))
30650
+ codemodeSettings.replace(ctx, { revision: revision + 1, ...next });
30651
+ return null;
30652
+ };
30172
30653
  const refreshDefaultModel = async (_input, ctx) => {
30173
30654
  const next = await runtime.defaultModel();
30174
30655
  if (JSON.stringify(next) !== JSON.stringify(defaultModel.value))
@@ -30204,6 +30685,7 @@ async function createNekoServerServices(options) {
30204
30685
  credentials,
30205
30686
  searchSettings,
30206
30687
  contextSettings,
30688
+ codemodeSettings,
30207
30689
  defaultModel,
30208
30690
  async refreshCredentials(_input, ctx) {
30209
30691
  credentials.replace(ctx, {
@@ -30214,6 +30696,7 @@ async function createNekoServerServices(options) {
30214
30696
  },
30215
30697
  refreshSearchSettings,
30216
30698
  refreshContextSettings,
30699
+ refreshCodemodeSettings,
30217
30700
  refreshDefaultModel
30218
30701
  };
30219
30702
  }, { lifetime: "host" });
@@ -30280,6 +30763,21 @@ async function createNekoServerServices(options) {
30280
30763
  })
30281
30764
  };
30282
30765
  });
30766
+ host.serve(NekoCodemodeSettings, (_input, _principal, self2) => {
30767
+ const hub = shared(self2);
30768
+ return {
30769
+ variant: "ready",
30770
+ state: hub.codemodeSettings,
30771
+ set: (input, ctx) => serialize(async () => {
30772
+ await runtime.setCodemodeMode(input.mode);
30773
+ return hub.refreshCodemodeSettings(null, ctx);
30774
+ }),
30775
+ reset: (_input, ctx) => serialize(async () => {
30776
+ await runtime.resetCodemodeMode();
30777
+ return hub.refreshCodemodeSettings(null, ctx);
30778
+ })
30779
+ };
30780
+ });
30283
30781
  host.serve(NekoDefaultModel, (_input, _principal, self2) => {
30284
30782
  const hub = shared(self2);
30285
30783
  return {
@@ -30608,6 +31106,7 @@ async function startGateway(options = {}) {
30608
31106
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
30609
31107
  ...searchProvider !== undefined ? { searchProvider } : {},
30610
31108
  ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
31109
+ ...fileConfig.codemode !== undefined ? { codemodeSettings: fileConfig.codemode } : {},
30611
31110
  ...fileConfig.defaultModel !== undefined ? { model: fileConfig.defaultModel } : {},
30612
31111
  ...options.model !== undefined ? { modelOverride: options.model } : {},
30613
31112
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",