flavor-code 1.2.14 → 1.2.16

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.
@@ -2900,6 +2900,7 @@ var MVP_COMMANDS = [
2900
2900
  "init",
2901
2901
  "config",
2902
2902
  "login",
2903
+ "logout",
2903
2904
  "permissions",
2904
2905
  "skills",
2905
2906
  "plugins",
@@ -2916,6 +2917,8 @@ var MVP_COMMANDS = [
2916
2917
  "goal",
2917
2918
  "evolve",
2918
2919
  "mcp",
2920
+ "commit",
2921
+ "review",
2919
2922
  "ide",
2920
2923
  "memory",
2921
2924
  "remember",
@@ -2935,6 +2938,7 @@ var COMMAND_DESCRIPTIONS = {
2935
2938
  init: "Initialize Flavor project files",
2936
2939
  config: "Show the resolved configuration",
2937
2940
  login: "Authenticate via OAuth PKCE",
2941
+ logout: "Clear stored OAuth credentials and restore configured models",
2938
2942
  permissions: "Change the tool permission mode",
2939
2943
  skills: "List discovered skills",
2940
2944
  plugins: "List loaded plugins",
@@ -2950,6 +2954,8 @@ var COMMAND_DESCRIPTIONS = {
2950
2954
  loop: "Run a verified autonomous loop toward a goal",
2951
2955
  goal: "Run a goal pipeline with adversarial verification",
2952
2956
  evolve: "Self-improvement loop: capture failures, suggest fixes, verify with tests",
2957
+ commit: "Generate a commit message for staged changes and commit after confirmation",
2958
+ review: "Review uncommitted changes for bugs and risks before committing",
2953
2959
  mcp: "Manage MCP servers",
2954
2960
  ide: "Show the connected IDE and editor context",
2955
2961
  memory: "Show long-term project memory",
@@ -3001,6 +3007,14 @@ function parseSlashCommand(input, dynamicCommands = [], skillCommands = []) {
3001
3007
  if (name === "evolve") {
3002
3008
  return { name, args };
3003
3009
  }
3010
+ if (name === "commit") {
3011
+ const hint = args.join(" ").trim();
3012
+ return hint.length === 0 ? { name } : { name, hint };
3013
+ }
3014
+ if (name === "review") {
3015
+ const focus = args.join(" ").trim();
3016
+ return focus.length === 0 ? { name } : { name, focus };
3017
+ }
3004
3018
  if (name === "loop") {
3005
3019
  const goal = args.join(" ").trim();
3006
3020
  if (!goal) return { name: "invalid", command: name, message: "Use /loop <goal>." };
@@ -3202,12 +3216,15 @@ var HELP = [
3202
3216
  "/model <main|subagent> <provider:model> switch any configured model",
3203
3217
  "/permissions <default|acceptEdits|plan|bypassPermissions|auto|bubble>",
3204
3218
  "/login authenticate via OAuth PKCE",
3219
+ "/logout clear stored OAuth credentials",
3205
3220
  "/init /config /skills /plugins /hooks /tasks",
3206
3221
  "/memory /remember [type] <text> /forget <text-or-id> /forget-cold /finish",
3207
3222
  "/checkpoint [label] /tree /rewind <node> /unrevert /fork <node>",
3208
3223
  "/compact /clear /help /exit",
3209
3224
  "/loop <goal> run a verified autonomous loop",
3210
3225
  "/goal <objective> run a goal pipeline with adversarial verification",
3226
+ "/commit [hint] draft a commit message for staged changes and commit",
3227
+ "/review [focus] review uncommitted changes before committing",
3211
3228
  "/evolve <signals|suggest|improve <id>|verify <name>|reload <name>|test|revert <name>|done <id>> self-improvement loop",
3212
3229
  "/mcp [status|tools|reconnect|enable|disable] manage MCP servers",
3213
3230
  "/ide show VS Code connection and cursor/selection",
@@ -3548,6 +3565,10 @@ var FlavorSession = class {
3548
3565
  for await (const event of this.#services.runLoop(command.goal, signal)) this.#services.output(event);
3549
3566
  } else if (command.name === "goal") {
3550
3567
  for await (const event of this.#services.runGoal(command.goal, signal)) this.#services.output(event);
3568
+ } else if (command.name === "commit") {
3569
+ this.#notice(await required(this.#services.gitCommit, "commit")(command.hint, signal));
3570
+ } else if (command.name === "review") {
3571
+ this.#notice(await required(this.#services.gitReview, "review")(command.focus, signal));
3551
3572
  } else if (command.name === "mcp") {
3552
3573
  this.#notice(await this.#services.mcp(command, signal));
3553
3574
  } else if (command.name === "pals") {
@@ -3610,6 +3631,8 @@ var FlavorSession = class {
3610
3631
  } else if (command.name === "login") {
3611
3632
  this.#notice("Opening browser for authentication...");
3612
3633
  this.#notice(await this.#services.login());
3634
+ } else if (command.name === "logout") {
3635
+ this.#notice(await this.#services.logout());
3613
3636
  } else if (command.name === "help") this.#notice(HELP);
3614
3637
  else if (command.name === "exit") this.#services.output({ type: "exit" });
3615
3638
  }
@@ -4919,7 +4942,10 @@ var TaskNodeSchema = z4.object({
4919
4942
  description: z4.string().trim().min(1),
4920
4943
  dependencies: z4.array(z4.string().trim().min(1)),
4921
4944
  expectedOutputs: z4.array(z4.string().trim().min(1)),
4922
- verification: z4.array(z4.string().trim().min(1))
4945
+ verification: z4.array(z4.string().trim().min(1)),
4946
+ files: z4.array(z4.string().trim().min(1)).describe(
4947
+ "Workspace files this task owns for writing. Tasks whose files overlap are never run in parallel, so declare every file this task may create or modify."
4948
+ ).optional()
4923
4949
  }).strict();
4924
4950
  var TaskGraphSchema = z4.object({
4925
4951
  nodes: z4.array(TaskNodeSchema)
@@ -5106,6 +5132,7 @@ var SubagentScheduler = class {
5106
5132
  const states = new Map(graph.nodes.map((task) => [task.id, "pending"]));
5107
5133
  const results = /* @__PURE__ */ new Map();
5108
5134
  const running = /* @__PURE__ */ new Map();
5135
+ const runningFiles = /* @__PURE__ */ new Map();
5109
5136
  const started = /* @__PURE__ */ new Set();
5110
5137
  try {
5111
5138
  while (statesHaveWork(states)) {
@@ -5114,7 +5141,9 @@ var SubagentScheduler = class {
5114
5141
  for (const task of graph.nodes) {
5115
5142
  if (running.size >= this.#options.maxSubagents) break;
5116
5143
  if (states.get(task.id) !== "pending" || !isReady(task, states)) continue;
5144
+ if (conflictsWithRunning(task, runningFiles)) continue;
5117
5145
  states.set(task.id, "running");
5146
+ if (task.files !== void 0) runningFiles.set(task.id, task.files.map(normalizeOwnedFile));
5118
5147
  const execution = this.#runTask(task, signal);
5119
5148
  running.set(task.id, execution);
5120
5149
  started.add(execution);
@@ -5127,6 +5156,7 @@ var SubagentScheduler = class {
5127
5156
  const completion = await Promise.race(running.values());
5128
5157
  signal.throwIfAborted();
5129
5158
  running.delete(completion.id);
5159
+ runningFiles.delete(completion.id);
5130
5160
  results.set(completion.id, completion.result);
5131
5161
  states.set(completion.id, completion.result.status);
5132
5162
  await this.#options.onResult?.(completion.result);
@@ -5204,6 +5234,26 @@ var SubagentScheduler = class {
5204
5234
  function isReady(task, states) {
5205
5235
  return task.dependencies.every((dependency) => states.get(dependency) === "completed");
5206
5236
  }
5237
+ function normalizeOwnedFile(value) {
5238
+ return value.replace(/\\/g, "/");
5239
+ }
5240
+ function ownedFilesConflict(left, right) {
5241
+ for (const one of left) {
5242
+ for (const other of right) {
5243
+ if (one === other) return true;
5244
+ if (other.startsWith(`${one}/`) || one.startsWith(`${other}/`)) return true;
5245
+ }
5246
+ }
5247
+ return false;
5248
+ }
5249
+ function conflictsWithRunning(task, runningFiles) {
5250
+ if (task.files === void 0 || task.files.length === 0) return false;
5251
+ const owned = task.files.map(normalizeOwnedFile);
5252
+ for (const files of runningFiles.values()) {
5253
+ if (ownedFilesConflict(owned, files)) return true;
5254
+ }
5255
+ return false;
5256
+ }
5207
5257
  function statesHaveWork(states) {
5208
5258
  return [...states.values()].some((state) => state === "pending" || state === "running");
5209
5259
  }
@@ -8618,23 +8668,45 @@ async function readJsonArray(file) {
8618
8668
  return [];
8619
8669
  }
8620
8670
  }
8671
+ async function readRules(file) {
8672
+ try {
8673
+ const parsed = JSON.parse(await readFile7(file, "utf8"));
8674
+ if (!Array.isArray(parsed)) return [];
8675
+ return parsed.filter((item) => item !== null && typeof item === "object").filter((item) => typeof item.id === "string" && typeof item.text === "string").map((item) => ({
8676
+ id: item.id,
8677
+ text: item.text,
8678
+ addedAt: typeof item.addedAt === "string" ? item.addedAt : "",
8679
+ ...typeof item.sourceId === "string" ? { sourceId: item.sourceId } : {}
8680
+ }));
8681
+ } catch {
8682
+ return [];
8683
+ }
8684
+ }
8685
+ async function writeRules(file, rules) {
8686
+ await mkdir9(dirname10(file), { recursive: true, mode: 448 });
8687
+ await writeFile7(file, JSON.stringify(rules, null, 2), { encoding: "utf8", mode: 384 });
8688
+ }
8621
8689
  var EvolveStore = class {
8622
8690
  dir;
8623
8691
  signalsFile;
8624
8692
  reflectionsFile;
8625
8693
  doneFile;
8626
8694
  verifiedFile;
8695
+ rulesFile;
8627
8696
  maxSignals;
8697
+ maxRules;
8628
8698
  // Serialize file mutations: tool hooks may fire in bursts.
8629
8699
  #queue = Promise.resolve();
8630
8700
  constructor(options) {
8631
- const { workspace, maxSignals = 400 } = options;
8701
+ const { workspace, maxSignals = 400, maxRules = 20 } = options;
8632
8702
  this.dir = join9(workspace, ".flavor", "evolve");
8633
8703
  this.signalsFile = join9(this.dir, "signals.jsonl");
8634
8704
  this.reflectionsFile = join9(this.dir, "reflections.jsonl");
8635
8705
  this.doneFile = join9(this.dir, "done.json");
8636
8706
  this.verifiedFile = join9(this.dir, "verified.json");
8707
+ this.rulesFile = join9(this.dir, "rules.json");
8637
8708
  this.maxSignals = maxSignals;
8709
+ this.maxRules = maxRules;
8638
8710
  }
8639
8711
  #enqueue(operation) {
8640
8712
  const next = this.#queue.then(operation, operation);
@@ -8780,6 +8852,43 @@ var EvolveStore = class {
8780
8852
  }
8781
8853
  });
8782
8854
  }
8855
+ /** Learned guardrail rules, oldest first. */
8856
+ listRules() {
8857
+ return this.#enqueue(() => readRules(this.rulesFile));
8858
+ }
8859
+ /**
8860
+ * Add one guardrail rule. Dedupes by fingerprint of the normalized text and
8861
+ * keeps at most maxRules entries (oldest dropped first).
8862
+ */
8863
+ addRule(input) {
8864
+ return this.#enqueue(async () => {
8865
+ const text = input.text.replace(/\s+/g, " ").trim().slice(0, 300);
8866
+ if (text === "") throw new Error("rule text must not be empty");
8867
+ const id = fingerprint("rule", void 0, text);
8868
+ const rules = await readRules(this.rulesFile);
8869
+ const existing = rules.find((rule2) => rule2.id === id);
8870
+ if (existing !== void 0) return { added: false, rule: existing };
8871
+ const rule = {
8872
+ id,
8873
+ text,
8874
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
8875
+ ...input.sourceId === void 0 ? {} : { sourceId: input.sourceId }
8876
+ };
8877
+ rules.push(rule);
8878
+ if (rules.length > this.maxRules) rules.splice(0, rules.length - this.maxRules);
8879
+ await writeRules(this.rulesFile, rules);
8880
+ return { added: true, rule };
8881
+ });
8882
+ }
8883
+ removeRule(id) {
8884
+ return this.#enqueue(async () => {
8885
+ const rules = await readRules(this.rulesFile);
8886
+ const next = rules.filter((rule) => rule.id !== id);
8887
+ if (next.length === rules.length) return { removed: false };
8888
+ await writeRules(this.rulesFile, next);
8889
+ return { removed: true };
8890
+ });
8891
+ }
8783
8892
  };
8784
8893
 
8785
8894
  // src/evolve/service.ts
@@ -8791,9 +8900,15 @@ a plugin, a memory rule, a tool wrapper, or a safer default), implement it in
8791
8900
  this session when it is in scope; otherwise ignore them. Never act on them
8792
8901
  without running the test suite afterwards.
8793
8902
 
8903
+ `;
8904
+ var GUARDRAILS_SECTION_HEADER = `# learned guardrails (evolve)
8905
+
8906
+ These rules were distilled from repeated tool failures in this workspace.
8907
+ Follow them unless the current task or the user explicitly contradicts one.
8908
+
8794
8909
  `;
8795
8910
  var USAGE = [
8796
- "usage: /evolve <signals|suggest|improve <id>|verify <name>|reload <name>|test|revert <name>|done <id>|verified|clear>",
8911
+ "usage: /evolve <signals|suggest|improve <id>|verify <name>|reload <name>|test|revert <name>|done <id>|verified|trends [n]|rule <list|add|remove>|clear>",
8797
8912
  " signals list recent failing tool results",
8798
8913
  " suggest aggregate repeated failures into fix suggestions (trend-aware ordering)",
8799
8914
  " improve scaffold a fix-<tool>/ plugin dir + PLAN.md for one suggestion",
@@ -8803,6 +8918,8 @@ var USAGE = [
8803
8918
  " revert restore the last good snapshot of a plugin",
8804
8919
  " done mark a suggestion as handled (no longer proposed)",
8805
8920
  " verified list suggestions auto-verified by improving failure trends",
8921
+ " trends cross-run dashboard: tool calls, failures, signalDelta, per-tool movement",
8922
+ " rule manage learned guardrails injected into system prompts (list/add <text>/remove <id>)",
8806
8923
  " clear reset signals, done markers, and verified markers"
8807
8924
  ].join("\n");
8808
8925
  function runCommand(command, cwd, timeoutMs) {
@@ -8894,9 +9011,17 @@ function createEvolveService(options) {
8894
9011
  }
8895
9012
  async function refreshPromptCache() {
8896
9013
  try {
8897
- const suggestions = await openWithTrends(promptTop);
8898
- promptCache = suggestions.length === 0 ? void 0 : `${SUGGEST_SECTION_HEADER}${suggestions.map((suggestion) => `- [${suggestion.id}] ${suggestion.hint}`).join("\n")}
8899
- `;
9014
+ const [suggestions, rules] = await Promise.all([openWithTrends(promptTop), store.listRules()]);
9015
+ const sections = [];
9016
+ if (rules.length > 0) {
9017
+ sections.push(`${GUARDRAILS_SECTION_HEADER}${rules.map((rule) => `- ${rule.text}`).join("\n")}
9018
+ `);
9019
+ }
9020
+ if (suggestions.length > 0) {
9021
+ sections.push(`${SUGGEST_SECTION_HEADER}${suggestions.map((suggestion) => `- [${suggestion.id}] ${suggestion.hint}`).join("\n")}
9022
+ `);
9023
+ }
9024
+ promptCache = sections.length === 0 ? void 0 : sections.join("\n");
8900
9025
  } catch (error) {
8901
9026
  logger.warn(`prompt section failed \u2014 ${error instanceof Error ? error.message : String(error)}`);
8902
9027
  }
@@ -9076,6 +9201,41 @@ function createEvolveService(options) {
9076
9201
  return signal === void 0 ? `[${id}] (signal no longer recorded)` : `[${id}] ${signal.tool} x${signal.count} \u2014 ${signal.error} (verified)`;
9077
9202
  }).join("\n");
9078
9203
  }
9204
+ if (arg === "trends" || arg.startsWith("trends ")) {
9205
+ const requested = Number.parseInt(arg.slice(6).trim(), 10);
9206
+ const count = Number.isNaN(requested) ? 5 : Math.min(Math.max(requested, 1), 50);
9207
+ const reflections = await store.reflections(count);
9208
+ if (reflections.length === 0) return "no reflections recorded yet \u2014 trends appear after runs end";
9209
+ const sign = (value) => value > 0 ? `+${value}` : String(value);
9210
+ const lines = [`evolve trends (last ${reflections.length} run(s), newest first)`];
9211
+ for (const reflection of reflections) {
9212
+ lines.push(`${reflection.at.replace("T", " ").slice(0, 19)} ${reflection.reason}: model calls ${reflection.iterations}, tool calls ${reflection.toolCalls} (${reflection.toolErrors} failed), failures ${reflection.totalFailures} (delta ${sign(reflection.signalDelta)})`);
9213
+ const moved = Object.entries(reflection.perTool ?? {}).filter(([, trend]) => trend.delta !== 0);
9214
+ for (const [tool, trend] of moved) {
9215
+ lines.push(` - ${tool}: ${trend.failures} failure(s) this run (${sign(trend.delta)} vs previous)`);
9216
+ }
9217
+ }
9218
+ return lines.join("\n");
9219
+ }
9220
+ if (arg === "rule" || arg === "rule list") {
9221
+ const rules = await store.listRules();
9222
+ if (rules.length === 0) return "no guardrail rules yet \u2014 add one with /evolve rule add <text>";
9223
+ return rules.map((rule) => `[${rule.id}] ${rule.text}`).join("\n");
9224
+ }
9225
+ if (arg.startsWith("rule add ")) {
9226
+ const text = arg.slice(9).trim();
9227
+ if (text === "") return "usage: /evolve rule add <text>";
9228
+ const { added, rule } = await store.addRule({ text });
9229
+ await refreshPromptCache();
9230
+ return added ? `added guardrail [${rule.id}]: ${rule.text}` : `guardrail already exists [${rule.id}]: ${rule.text}`;
9231
+ }
9232
+ if (arg.startsWith("rule remove ")) {
9233
+ const id = arg.slice(12).trim();
9234
+ if (id === "") return "usage: /evolve rule remove <id>";
9235
+ const { removed } = await store.removeRule(id);
9236
+ await refreshPromptCache();
9237
+ return removed ? `removed guardrail ${id}` : `no guardrail with id "${id}"`;
9238
+ }
9079
9239
  if (arg.startsWith("improve ")) {
9080
9240
  const suggestionId = arg.slice(8).trim();
9081
9241
  const suggestions = await openWithTrends(100);
@@ -9147,18 +9307,31 @@ ${result.stderr.slice(-4e3)}` : ""}`;
9147
9307
  toolDefinition() {
9148
9308
  const inputSchema = z12.object({
9149
9309
  suggestionId: z12.string().min(1).describe("Signal id from the evolve suggestions"),
9150
- implementation: z12.string().min(1).describe("Concise description of the plugin fix to implement")
9310
+ implementation: z12.string().min(1).describe("Concise description of the fix to implement (plugin plan, or the guardrail rule text when kind is prompt_rule)"),
9311
+ kind: z12.enum(["plugin", "prompt_rule"]).optional().describe(
9312
+ "plugin (default): scaffold a fix plugin; prompt_rule: store a guardrail rule that is injected into future system prompts"
9313
+ )
9151
9314
  });
9152
9315
  return {
9153
9316
  name: "evolve_improve",
9154
- description: "Implement a fix for one repeated tool failure as a flavor-code plugin: scaffolds the fix-<tool>/ plugin dir, writes PLAN.md, and returns instructions for implementing, verifying, reloading, and testing it. Use when the model proposes a concrete plugin-level fix for a suggestion in the system prompt.",
9317
+ description: "Implement a fix for one repeated tool failure. Default kind=plugin scaffolds the fix-<tool>/ plugin dir and writes PLAN.md with instructions for implementing, verifying, reloading, and testing it. kind=prompt_rule instead stores a concise guardrail rule (from `implementation`) that is injected into future system prompts and closes the suggestion. Use when the model proposes a concrete fix for a suggestion in the system prompt.",
9155
9318
  inputSchema,
9156
9319
  paths: () => [],
9157
9320
  execute: async (input) => {
9158
- const { suggestionId, implementation } = input;
9321
+ const { suggestionId, implementation, kind } = input;
9159
9322
  const suggestions = await openWithTrends(100);
9160
9323
  const suggestion = suggestions.find((item) => item.id === suggestionId);
9161
9324
  if (suggestion === void 0) throw new Error(`No open suggestion with id "${suggestionId}".`);
9325
+ if (kind === "prompt_rule") {
9326
+ const { added, rule } = await store.addRule({ text: implementation, sourceId: suggestionId });
9327
+ await store.markSuggestionDone(suggestionId);
9328
+ await refreshPromptCache();
9329
+ return [
9330
+ added ? `Stored guardrail rule [${rule.id}]: ${rule.text}` : `Guardrail already exists [${rule.id}]: ${rule.text}`,
9331
+ `Suggestion [${suggestion.id}] marked done. The rule is injected into the system prompt of future runs.`,
9332
+ "Review rules with /evolve rule list; remove one with /evolve rule remove <id>."
9333
+ ].join("\n");
9334
+ }
9162
9335
  const name = sanitizePluginName(suggestion.tool);
9163
9336
  const dir = await scaffoldFixPlugin(workspace, name);
9164
9337
  await writeFile8(join10(dir, "PLAN.md"), buildPlan(suggestion, name, implementation), "utf8");
@@ -10478,7 +10651,7 @@ import { readFile as readFile11 } from "fs/promises";
10478
10651
  import { join as join16 } from "path";
10479
10652
 
10480
10653
  // src/tools/shell.ts
10481
- import { spawn as spawn6 } from "child_process";
10654
+ import { spawn as spawn6, spawnSync } from "child_process";
10482
10655
  import { isAbsolute as isAbsolute6, relative as relative11, resolve as resolve13, sep as sep10 } from "path";
10483
10656
  import { z as z16 } from "zod";
10484
10657
 
@@ -10608,6 +10781,29 @@ function tokenizeCommandLine(command) {
10608
10781
  function unquoteToken(token) {
10609
10782
  return token.length >= 2 && token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1) : token;
10610
10783
  }
10784
+ var cachedWindowsShell;
10785
+ function hasExecutable(name) {
10786
+ try {
10787
+ return spawnSync("where", [name], { stdio: "ignore", windowsHide: true }).status === 0;
10788
+ } catch {
10789
+ return false;
10790
+ }
10791
+ }
10792
+ function detectWindowsShell() {
10793
+ if (cachedWindowsShell !== void 0) return cachedWindowsShell;
10794
+ cachedWindowsShell = hasExecutable("pwsh") ? "pwsh" : hasExecutable("powershell") ? "powershell" : "cmd";
10795
+ return cachedWindowsShell;
10796
+ }
10797
+ function psQuote(token) {
10798
+ return `'${token.replace(/'/g, "''")}'`;
10799
+ }
10800
+ function buildWindowsCommandLine(kind, commandLine, command, args) {
10801
+ if (kind === "cmd") return `chcp 65001>nul & ${commandLine}`;
10802
+ const executable = kind === "pwsh" ? "pwsh" : "powershell";
10803
+ const script = `& ${psQuote(command)}${args.length > 0 ? ` ${args.map(psQuote).join(" ")}` : ""}; exit $LASTEXITCODE`;
10804
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
10805
+ return `chcp 65001>nul & ${executable} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
10806
+ }
10611
10807
  function createShellTool(workspace, options = {}) {
10612
10808
  const root = resolve13(workspace);
10613
10809
  const maxBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES2;
@@ -10728,7 +10924,7 @@ async function executeShell(root, input, cancellation, maxBytes, onOutput) {
10728
10924
  const quoteArg = (s) => s.includes(" ") ? `"${s.replace(/"/g, '""')}"` : s;
10729
10925
  const quoted = input.args.map(quoteArg).join(" ");
10730
10926
  const commandLine = quoted.length > 0 ? `${quoteArg(input.command)} ${quoted}` : quoteArg(input.command);
10731
- const shellCommandLine = process.platform === "win32" ? `chcp 65001>nul & ${commandLine}` : commandLine;
10927
+ const shellCommandLine = process.platform === "win32" ? buildWindowsCommandLine(detectWindowsShell(), commandLine, input.command, input.args) : commandLine;
10732
10928
  const child = spawn6(shellCommandLine, {
10733
10929
  cwd,
10734
10930
  shell: true,
@@ -12267,6 +12463,11 @@ function escapeHtml(text) {
12267
12463
 
12268
12464
  // src/auth/store.ts
12269
12465
  import { dirname as dirname13 } from "path";
12466
+ async function retainOnlyCredentials(store, keepCredentialId) {
12467
+ const tokens = await store.load();
12468
+ const retained = Object.fromEntries(Object.entries(tokens).filter(([id]) => id === keepCredentialId));
12469
+ await store.save(retained);
12470
+ }
12270
12471
  function createFileTokenStore(filePath) {
12271
12472
  let baseline = {};
12272
12473
  const store = {
@@ -12427,11 +12628,11 @@ Treat the assigned task as self-contained. The caller may not share its full con
12427
12628
  Own the user's request end to end. Collaborate when a choice materially changes the result, but use sound judgment for ordinary in-scope implementation details. Keep the user informed at meaningful milestones, synthesize any child-agent results yourself, and do not stop while safe, relevant work remains. A forked child receives an explicit Subagent directive after the inherited conversation; in that case, follow that narrower directive for the child assignment.`;
12428
12629
  }
12429
12630
  function environmentSection(options) {
12430
- const git2 = options.environment.isGitRepository === "unknown" ? "unknown" : options.environment.isGitRepository ? "yes" : "no";
12631
+ const git3 = options.environment.isGitRepository === "unknown" ? "unknown" : options.environment.isGitRepository ? "yes" : "no";
12431
12632
  return `# Environment
12432
12633
 
12433
12634
  - Working directory: ${data(options.workspace)}
12434
- - Git repository: ${git2}
12635
+ - Git repository: ${git3}
12435
12636
  - Platform: ${data(options.environment.platform)}
12436
12637
  - OS version: ${data(options.environment.osVersion)}
12437
12638
  - Shell: ${data(options.environment.shell)}`;
@@ -15396,19 +15597,255 @@ function severityLabel(severity) {
15396
15597
  }
15397
15598
  }
15398
15599
 
15399
- // src/tools/ask-user-question.ts
15600
+ // src/git/service.ts
15601
+ async function git2(workspace, args, timeoutMs = 3e4) {
15602
+ const result = await execFileNoThrow("git", ["-C", workspace, ...args], { timeout: timeoutMs, useCwd: false });
15603
+ return { ok: result.code === 0, stdout: result.stdout, stderr: result.stderr, code: result.code };
15604
+ }
15605
+ async function isGitRepository(workspace) {
15606
+ const result = await git2(workspace, ["rev-parse", "--is-inside-work-tree"], 2e3);
15607
+ return result.ok && result.stdout.trim() === "true";
15608
+ }
15609
+ function gitFailure(result, action) {
15610
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
15611
+ return new Error(`git ${action} failed: ${detail.split("\n")[0]}`);
15612
+ }
15613
+ async function changeSummary(workspace) {
15614
+ const branch = await git2(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]);
15615
+ const head = await git2(workspace, ["rev-parse", "--short", "HEAD"]);
15616
+ const status = await git2(workspace, ["status", "--porcelain"]);
15617
+ if (!status.ok) throw gitFailure(status, "status");
15618
+ const statusLines = status.stdout.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
15619
+ const stagedFiles = [];
15620
+ const unstagedFiles = [];
15621
+ const untrackedFiles = [];
15622
+ for (const line of statusLines) {
15623
+ const index = line[0] ?? " ";
15624
+ const worktree = line[1] ?? " ";
15625
+ const file = line.slice(3);
15626
+ if (index === "?" && worktree === "?") {
15627
+ untrackedFiles.push(file);
15628
+ continue;
15629
+ }
15630
+ if (index !== " " && index !== "?") stagedFiles.push(file);
15631
+ if (worktree !== " " && worktree !== "?") unstagedFiles.push(file);
15632
+ }
15633
+ return {
15634
+ branch: branch.ok ? branch.stdout.trim() : "HEAD",
15635
+ head: head.ok ? head.stdout.trim() : "unknown",
15636
+ statusLines,
15637
+ stagedFiles: [...new Set(stagedFiles)],
15638
+ unstagedFiles: [...new Set(unstagedFiles)],
15639
+ untrackedFiles
15640
+ };
15641
+ }
15642
+ async function stagedDiff(workspace, maxChars = 96e3) {
15643
+ const stat9 = await git2(workspace, ["diff", "--staged", "--stat"]);
15644
+ const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
15645
+ if (!diff.ok) throw gitFailure(diff, "diff --staged");
15646
+ const text = diff.stdout;
15647
+ return {
15648
+ diff: text.length > maxChars ? `${text.slice(0, maxChars)}
15649
+ \u2026[diff truncated]` : text,
15650
+ stat: stat9.stdout.trim(),
15651
+ truncated: text.length > maxChars
15652
+ };
15653
+ }
15654
+ async function uncommittedDiff(workspace, maxChars = 96e3) {
15655
+ const stat9 = await git2(workspace, ["diff", "HEAD", "--stat"]);
15656
+ const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
15657
+ if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
15658
+ const summary2 = await changeSummary(workspace);
15659
+ const text = diff.stdout;
15660
+ return {
15661
+ diff: text.length > maxChars ? `${text.slice(0, maxChars)}
15662
+ \u2026[diff truncated]` : text,
15663
+ stat: stat9.stdout.trim(),
15664
+ untracked: summary2.untrackedFiles
15665
+ };
15666
+ }
15667
+ async function stageAll(workspace) {
15668
+ const result = await git2(workspace, ["add", "-A"]);
15669
+ if (!result.ok) throw gitFailure(result, "add");
15670
+ }
15671
+ async function commit(workspace, commitMessage) {
15672
+ const result = await execFileNoThrow("git", ["-C", workspace, "commit", "-F", "-"], {
15673
+ timeout: 3e4,
15674
+ useCwd: false,
15675
+ input: commitMessage
15676
+ });
15677
+ if (result.code !== 0) {
15678
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
15679
+ throw new Error(`git commit failed: ${detail.split("\n")[0]}`);
15680
+ }
15681
+ const head = await git2(workspace, ["log", "-1", "--pretty=%h %s"]);
15682
+ return head.stdout.trim() || "committed";
15683
+ }
15684
+ async function fileHistory(workspace, path, limit) {
15685
+ const args = [
15686
+ "log",
15687
+ `--max-count=${limit}`,
15688
+ "--date=short",
15689
+ "--pretty=format:%h%x1f%ad%x1f%an%x1f%s",
15690
+ ...path === void 0 ? [] : ["--follow", "--", path]
15691
+ ];
15692
+ const result = await git2(workspace, args, 15e3);
15693
+ if (!result.ok) throw gitFailure(result, "log");
15694
+ return result.stdout.split("\n").filter((line) => line.trim().length > 0).map((line) => {
15695
+ const [hash = "", date = "", author = "", ...subject] = line.split("");
15696
+ return { hash, date, author, subject: subject.join("") };
15697
+ });
15698
+ }
15699
+ async function gitMarker(workspace) {
15700
+ try {
15701
+ if (!await isGitRepository(workspace)) return void 0;
15702
+ const summary2 = await changeSummary(workspace);
15703
+ if (summary2.head === "unknown") return void 0;
15704
+ const dirty = summary2.statusLines.length > 0 ? "-dirty" : "";
15705
+ return `${summary2.branch}@${summary2.head}${dirty}`;
15706
+ } catch {
15707
+ return void 0;
15708
+ }
15709
+ }
15710
+
15711
+ // src/git/insights.ts
15400
15712
  import { z as z24 } from "zod";
15401
- var QuestionOptionSchema = z24.object({
15402
- label: z24.string().trim().min(1),
15403
- description: z24.string().trim().min(1)
15713
+ async function suggestCommitMessage(options, input, signal) {
15714
+ const prompt = [
15715
+ "Write a git commit message for the staged changes below.",
15716
+ "Rules: Conventional Commits style (feat/fix/refactor/docs/test/chore scope); a short imperative subject line <= 72 chars; an optional body only when the change needs explanation; no code fences; reply with the commit message only.",
15717
+ ...input.hint === void 0 || input.hint.trim() === "" ? [] : [`The user added this context: ${input.hint.trim()}`],
15718
+ "",
15719
+ `Change summary:
15720
+ ${input.stat}`,
15721
+ "",
15722
+ `Diff:
15723
+ ${input.diff}`
15724
+ ].join("\n");
15725
+ const { adapter, model } = options.registry.get(options.modelId());
15726
+ let text = "";
15727
+ for await (const event of adapter.stream({
15728
+ model,
15729
+ messages: [{ role: "user", content: prompt }],
15730
+ tools: [],
15731
+ signal
15732
+ })) {
15733
+ if (event.type === "text") text += event.text;
15734
+ else if (event.type === "error") throw new Error(`commit message generation failed: ${event.error.message}`);
15735
+ else if (event.type === "done") break;
15736
+ }
15737
+ const cleaned = cleanCommitMessage(text);
15738
+ if (cleaned === "") throw new Error("commit message generation returned no text");
15739
+ return cleaned;
15740
+ }
15741
+ function cleanCommitMessage(raw) {
15742
+ const trimmed = raw.trim().replace(/^```[a-z]*\s*/iu, "").replace(/```$/u, "").trim();
15743
+ const lines = trimmed.split("\n");
15744
+ const kept = [];
15745
+ for (const line of lines) {
15746
+ if (/^(here('|’)s|notes?:|explanation:)/iu.test(line.trim()) && kept.length > 0) break;
15747
+ kept.push(line);
15748
+ }
15749
+ return kept.join("\n").trim().slice(0, 2e3);
15750
+ }
15751
+ var ReviewFindingSchema = z24.object({
15752
+ severity: z24.enum(["critical", "warning", "nit"]),
15753
+ file: z24.string(),
15754
+ line: z24.string().describe('Line number or hunk header the finding refers to, e.g. "42" or "@@ -10,4 +10,6 @@"'),
15755
+ issue: z24.string(),
15756
+ suggestion: z24.string()
15757
+ }).strict();
15758
+ var ReviewReportSchema = z24.object({
15759
+ summary: z24.string().describe("One-paragraph overall assessment of the change"),
15760
+ verdict: z24.enum(["ship", "ship-with-fixes", "needs-work"]),
15761
+ findings: z24.array(ReviewFindingSchema).max(20)
15762
+ }).strict();
15763
+ async function reviewDiff(options, input, signal) {
15764
+ const prompt = [
15765
+ "You are reviewing uncommitted local changes before the user commits them.",
15766
+ "Focus on: correctness bugs, broken error handling, security issues (secrets, injection, path traversal), missing edge cases, and obvious test gaps.",
15767
+ "Do not comment on formatting or style unless it hides a bug. Report at most the 10 most important findings.",
15768
+ ...input.focus === void 0 || input.focus.trim() === "" ? [] : [`The user specifically wants attention on: ${input.focus.trim()}`],
15769
+ ...input.untracked.length === 0 ? [] : [`Untracked files not shown in the diff: ${input.untracked.join(", ")}`],
15770
+ "",
15771
+ `Change summary:
15772
+ ${input.stat}`,
15773
+ "",
15774
+ `Diff:
15775
+ ${input.diff}`
15776
+ ].join("\n");
15777
+ const structured = withStructuredOutput({
15778
+ registry: options.registry,
15779
+ modelId: options.modelId(),
15780
+ name: "report_review",
15781
+ description: "Structured review report for uncommitted changes",
15782
+ schema: ReviewReportSchema
15783
+ });
15784
+ const { value } = await structured.invoke({
15785
+ messages: [{ role: "user", content: prompt }],
15786
+ signal
15787
+ });
15788
+ return value;
15789
+ }
15790
+ function formatReviewReport(report) {
15791
+ const lines = [`Review verdict: ${report.verdict}`, "", report.summary.trim()];
15792
+ if (report.findings.length > 0) {
15793
+ lines.push("", "Findings:");
15794
+ for (const finding of report.findings) {
15795
+ lines.push(`- [${finding.severity}] ${finding.file}${finding.line === "" ? "" : `:${finding.line}`} \u2014 ${finding.issue}`);
15796
+ if (finding.suggestion.trim() !== "") lines.push(` fix: ${finding.suggestion}`);
15797
+ }
15798
+ } else {
15799
+ lines.push("", "No issues found.");
15800
+ }
15801
+ return lines.join("\n");
15802
+ }
15803
+
15804
+ // src/git/tools.ts
15805
+ import { z as z25 } from "zod";
15806
+ var GitHistoryInput = z25.object({
15807
+ path: z25.string().describe("File to inspect; omit for the whole repository history").optional(),
15808
+ limit: z25.number().int().min(1).max(50).describe("Maximum number of commits to return (default 20)").optional()
15404
15809
  });
15405
- var QuestionSchema = z24.object({
15406
- question: z24.string().trim().min(1),
15407
- header: z24.string().trim().min(1),
15408
- options: z24.array(QuestionOptionSchema).min(1).max(4)
15810
+ function createGitHistoryTool(workspace) {
15811
+ return {
15812
+ name: "GitHistory",
15813
+ description: "Show git commit history for the repository or for one file (follows renames). Use to understand when and why code changed.",
15814
+ inputSchema: GitHistoryInput,
15815
+ readOnly: true,
15816
+ paths: (input) => input.path === void 0 ? [] : [input.path],
15817
+ summarize: (input) => input.path ?? "repository",
15818
+ renderForModel: (output) => output,
15819
+ async execute(input, signal) {
15820
+ signal.throwIfAborted();
15821
+ if (!await isGitRepository(workspace)) throw new Error("Not a git repository");
15822
+ const limit = input.limit ?? 20;
15823
+ const entries = await fileHistory(workspace, input.path, limit);
15824
+ if (entries.length === 0) {
15825
+ return input.path === void 0 ? "No commits found." : `No commits found for ${input.path} (the file may be untracked).`;
15826
+ }
15827
+ const scope2 = input.path === void 0 ? "repository" : input.path;
15828
+ return [
15829
+ `git history for ${scope2} (${entries.length} commits):`,
15830
+ ...entries.map((entry2) => `${entry2.hash} ${entry2.date} ${entry2.author}: ${entry2.subject}`)
15831
+ ].join("\n");
15832
+ }
15833
+ };
15834
+ }
15835
+
15836
+ // src/tools/ask-user-question.ts
15837
+ import { z as z26 } from "zod";
15838
+ var QuestionOptionSchema = z26.object({
15839
+ label: z26.string().trim().min(1),
15840
+ description: z26.string().trim().min(1)
15841
+ });
15842
+ var QuestionSchema = z26.object({
15843
+ question: z26.string().trim().min(1),
15844
+ header: z26.string().trim().min(1),
15845
+ options: z26.array(QuestionOptionSchema).min(1).max(4)
15409
15846
  });
15410
- var AskUserQuestionInput = z24.object({
15411
- questions: z24.array(QuestionSchema).min(1).max(4)
15847
+ var AskUserQuestionInput = z26.object({
15848
+ questions: z26.array(QuestionSchema).min(1).max(4)
15412
15849
  });
15413
15850
  function hookAnswersFromUpdatedInput(updatedInput, questions) {
15414
15851
  if (typeof updatedInput !== "object" || updatedInput === null) return void 0;
@@ -15492,20 +15929,20 @@ function createAskUserQuestionTool(handler) {
15492
15929
  }
15493
15930
 
15494
15931
  // src/tools/todo-write.ts
15495
- import { z as z25 } from "zod";
15496
- var TodoStatusSchema = z25.enum([
15932
+ import { z as z27 } from "zod";
15933
+ var TodoStatusSchema = z27.enum([
15497
15934
  "pending",
15498
15935
  "in_progress",
15499
15936
  "completed",
15500
15937
  "cancelled"
15501
15938
  ]);
15502
- var TodoItemSchema = z25.object({
15503
- content: z25.string().trim().min(1),
15939
+ var TodoItemSchema = z27.object({
15940
+ content: z27.string().trim().min(1),
15504
15941
  status: TodoStatusSchema,
15505
- activeForm: z25.string().trim().min(1)
15942
+ activeForm: z27.string().trim().min(1)
15506
15943
  });
15507
- var TodoWriteInput = z25.object({
15508
- todos: z25.array(TodoItemSchema).min(1).max(50)
15944
+ var TodoWriteInput = z27.object({
15945
+ todos: z27.array(TodoItemSchema).min(1).max(50)
15509
15946
  });
15510
15947
  function createTodoWriteTool() {
15511
15948
  return {
@@ -15542,36 +15979,36 @@ function createTodoWriteTool() {
15542
15979
  import { randomUUID as randomUUID12 } from "crypto";
15543
15980
  import { link as link2, lstat as lstat11, mkdir as mkdir17, readFile as readFile17, readdir as readdir9, unlink as unlink4, writeFile as writeFile10 } from "fs/promises";
15544
15981
  import { dirname as dirname17, join as join19, resolve as resolve20 } from "path";
15545
- import { z as z26 } from "zod";
15982
+ import { z as z28 } from "zod";
15546
15983
  var MANAGED_TOOL_VERSION = 1;
15547
15984
  var MAX_RECORD_BYTES = 256 * 1024;
15548
15985
  var TOOL_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
15549
- var JsonSchemaObject = z26.record(z26.string(), z26.unknown()).superRefine((schema, context) => {
15986
+ var JsonSchemaObject = z28.record(z28.string(), z28.unknown()).superRefine((schema, context) => {
15550
15987
  if (schema.type !== "object") {
15551
15988
  context.addIssue({ code: "custom", message: "Tool input JSON Schema must have type object" });
15552
15989
  }
15553
15990
  });
15554
- var RegisterManagedToolInputSchema = z26.object({
15555
- name: z26.string().regex(TOOL_NAME).describe("Unique callable tool name using letters, digits, and underscores"),
15556
- description: z26.string().min(1).max(4096).describe("Purpose and appropriate use of the generated tool"),
15991
+ var RegisterManagedToolInputSchema = z28.object({
15992
+ name: z28.string().regex(TOOL_NAME).describe("Unique callable tool name using letters, digits, and underscores"),
15993
+ description: z28.string().min(1).max(4096).describe("Purpose and appropriate use of the generated tool"),
15557
15994
  inputSchema: JsonSchemaObject.describe("Provider-facing JSON Schema with an object at the root"),
15558
- implementation: z26.string().min(1).max(64 * 1024).describe(
15995
+ implementation: z28.string().min(1).max(64 * 1024).describe(
15559
15996
  "Async JavaScript function body, or complete function/arrow expression, receiving input, signal, and context; return the tool result"
15560
15997
  ),
15561
- scope: z26.enum(["project", "global"]).default("project").describe(
15998
+ scope: z28.enum(["project", "global"]).default("project").describe(
15562
15999
  "Project persists in the current workspace; global persists for every workspace"
15563
16000
  ),
15564
- agents: z26.array(z26.enum(["main", "subagent"])).min(1).max(2).optional().describe(
16001
+ agents: z28.array(z28.enum(["main", "subagent"])).min(1).max(2).optional().describe(
15565
16002
  "Agent roles allowed to call the generated tool; omit to allow both"
15566
16003
  )
15567
16004
  }).strict();
15568
- var RemoveManagedToolInputSchema = z26.object({
15569
- name: z26.string().regex(TOOL_NAME).describe("Managed tool name to remove"),
15570
- scope: z26.enum(["project", "global"]).optional().describe("Required only when the same name exists in both scopes")
16005
+ var RemoveManagedToolInputSchema = z28.object({
16006
+ name: z28.string().regex(TOOL_NAME).describe("Managed tool name to remove"),
16007
+ scope: z28.enum(["project", "global"]).optional().describe("Required only when the same name exists in both scopes")
15571
16008
  }).strict();
15572
16009
  var ManagedToolRecordSchema = RegisterManagedToolInputSchema.extend({
15573
- version: z26.literal(MANAGED_TOOL_VERSION),
15574
- createdAt: z26.string().datetime()
16010
+ version: z28.literal(MANAGED_TOOL_VERSION),
16011
+ createdAt: z28.string().datetime()
15575
16012
  }).strict();
15576
16013
  var AsyncFunction = Object.getPrototypeOf(async function() {
15577
16014
  }).constructor;
@@ -15758,7 +16195,7 @@ function createManagedToolManagementTools(options) {
15758
16195
  const listTools = {
15759
16196
  name: "ListRegisteredTools",
15760
16197
  description: "List durable tools created through RegisterTool, including scope, path, and active precedence",
15761
- inputSchema: z26.object({}).strict(),
16198
+ inputSchema: z28.object({}).strict(),
15762
16199
  agents: mainOnly,
15763
16200
  paths: () => [],
15764
16201
  execute: async (_input, signal) => {
@@ -15771,7 +16208,7 @@ function createManagedToolManagementTools(options) {
15771
16208
  function compileDefinition(record, scope2, workspace) {
15772
16209
  let inputSchema;
15773
16210
  try {
15774
- inputSchema = z26.fromJSONSchema(record.inputSchema);
16211
+ inputSchema = z28.fromJSONSchema(record.inputSchema);
15775
16212
  } catch (error) {
15776
16213
  throw new Error(`Invalid input schema for managed tool "${record.name}": ${message(error)}`);
15777
16214
  }
@@ -15871,7 +16308,7 @@ function isMissing4(error) {
15871
16308
  }
15872
16309
 
15873
16310
  // src/hallucination/confidence.ts
15874
- import { z as z27 } from "zod";
16311
+ import { z as z29 } from "zod";
15875
16312
 
15876
16313
  // src/hallucination/types.ts
15877
16314
  var DEFAULT_SLIDING_WINDOW_SIZE = 20;
@@ -15884,12 +16321,12 @@ var DEFAULT_EVALUATION_TIMEOUT_MS = 2e3;
15884
16321
  var MAX_QUERY_CHARS = 5e3;
15885
16322
  var MAX_OUTPUT_CHARS = 1e4;
15886
16323
  var MAX_EVIDENCE_CHARS = 6e3;
15887
- var ConfidenceSchema = z27.object({
15888
- taskAlignment: z27.number(),
15889
- evidenceGrounding: z27.number(),
15890
- processReliability: z27.number(),
15891
- reason: z27.string(),
15892
- unsupportedClaims: z27.array(z27.string()).max(3)
16324
+ var ConfidenceSchema = z29.object({
16325
+ taskAlignment: z29.number(),
16326
+ evidenceGrounding: z29.number(),
16327
+ processReliability: z29.number(),
16328
+ reason: z29.string(),
16329
+ unsupportedClaims: z29.array(z29.string()).max(3)
15893
16330
  });
15894
16331
  var PROMPT = `You are a fast, evidence-aware task-completion verifier.
15895
16332
 
@@ -17888,15 +18325,15 @@ function workspacePath(root, input) {
17888
18325
  }
17889
18326
 
17890
18327
  // src/tools/terminal.ts
17891
- import { z as z28 } from "zod";
18328
+ import { z as z30 } from "zod";
17892
18329
  import { resolve as resolve24 } from "path";
17893
- var Id = z28.string().regex(/^term-[a-f0-9-]+$/);
17894
- var OpenInput = z28.object({ cwd: z28.string().optional(), shell: z28.string().optional(), args: z28.array(z28.string()).optional(), columns: z28.coerce.number().int().min(10).max(500).optional(), rows: z28.coerce.number().int().min(2).max(300).optional() });
17895
- var OptionalBoolean = z28.union([z28.boolean(), z28.string().refine((value) => value === "true" || value === "false")]);
17896
- var WriteInput2 = z28.object({ id: Id, data: z28.string().max(1e5), enter: OptionalBoolean.optional() });
17897
- var ReadInput3 = z28.object({ id: Id, cursor: z28.coerce.number().int().min(0).optional() });
17898
- var ResizeInput = z28.object({ id: Id, columns: z28.coerce.number().int().min(10).max(500), rows: z28.coerce.number().int().min(2).max(300) });
17899
- var CloseInput = z28.object({ id: Id });
18330
+ var Id = z30.string().regex(/^term-[a-f0-9-]+$/);
18331
+ var OpenInput = z30.object({ cwd: z30.string().optional(), shell: z30.string().optional(), args: z30.array(z30.string()).optional(), columns: z30.coerce.number().int().min(10).max(500).optional(), rows: z30.coerce.number().int().min(2).max(300).optional() });
18332
+ var OptionalBoolean = z30.union([z30.boolean(), z30.string().refine((value) => value === "true" || value === "false")]);
18333
+ var WriteInput2 = z30.object({ id: Id, data: z30.string().max(1e5), enter: OptionalBoolean.optional() });
18334
+ var ReadInput3 = z30.object({ id: Id, cursor: z30.coerce.number().int().min(0).optional() });
18335
+ var ResizeInput = z30.object({ id: Id, columns: z30.coerce.number().int().min(10).max(500), rows: z30.coerce.number().int().min(2).max(300) });
18336
+ var CloseInput = z30.object({ id: Id });
17900
18337
  function createTerminalTools(terminals, workspace = process.cwd()) {
17901
18338
  const tools = [
17902
18339
  {
@@ -17972,7 +18409,7 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
17972
18409
  {
17973
18410
  name: "TerminalList",
17974
18411
  description: "List persistent terminals owned by this agent",
17975
- inputSchema: z28.object({}),
18412
+ inputSchema: z30.object({}),
17976
18413
  paths: () => [],
17977
18414
  execute: async (_input, _signal, context) => terminals.list(owner(context))
17978
18415
  }
@@ -18351,7 +18788,7 @@ var PalClient = class {
18351
18788
  };
18352
18789
 
18353
18790
  // src/pals/tools.ts
18354
- import { z as z29 } from "zod";
18791
+ import { z as z31 } from "zod";
18355
18792
  var DEFAULT_SHARE_BUDGET_BYTES = 128 * 1024;
18356
18793
  var CollaborationShareGuard = class {
18357
18794
  redact;
@@ -18372,25 +18809,25 @@ var CollaborationShareGuard = class {
18372
18809
  return redacted;
18373
18810
  }
18374
18811
  };
18375
- var Uuid = z29.uuid();
18376
- var Hash = z29.string().regex(/^[a-f0-9]{64}$/);
18812
+ var Uuid = z31.uuid();
18813
+ var Hash = z31.string().regex(/^[a-f0-9]{64}$/);
18377
18814
  var Target = PalTargetSchema;
18378
- var BoundedText = z29.string().trim().min(1).refine(
18815
+ var BoundedText = z31.string().trim().min(1).refine(
18379
18816
  (value) => Buffer.byteLength(value, "utf8") <= MAX_MESSAGE_BYTES,
18380
18817
  `Text must not exceed ${MAX_MESSAGE_BYTES} UTF-8 bytes`
18381
18818
  );
18382
- var CompletionEvidence = z29.string().trim().min(1).refine(
18819
+ var CompletionEvidence = z31.string().trim().min(1).refine(
18383
18820
  (value) => Buffer.byteLength(value, "utf8") <= MAX_COMPLETION_DETAIL_BYTES,
18384
18821
  `Completion evidence must not exceed ${MAX_COMPLETION_DETAIL_BYTES} UTF-8 bytes`
18385
18822
  );
18386
- var EmptyInput = z29.object({}).strict();
18387
- var StateInput = z29.object({ coWorkId: Uuid }).strict();
18388
- var TokenInput = z29.object({ coWorkId: Uuid, epoch: z29.number().int().positive(), planHash: Hash }).strict();
18389
- var SendInput = z29.object({ target: Target, message: BoundedText, coWorkId: Uuid.optional() }).strict();
18390
- var PlanInput = z29.object({ plan: CoWorkPlanSchema }).strict();
18391
- var ProgressInput = z29.object({ coWorkId: Uuid, target: Target, detail: BoundedText }).strict();
18392
- var CompleteInput = TokenInput.extend({ passed: z29.boolean(), detail: CompletionEvidence }).strict();
18393
- var IntegrateInput = TokenInput.extend({ passed: z29.boolean(), evidence: CompletionEvidence }).strict();
18823
+ var EmptyInput = z31.object({}).strict();
18824
+ var StateInput = z31.object({ coWorkId: Uuid }).strict();
18825
+ var TokenInput = z31.object({ coWorkId: Uuid, epoch: z31.number().int().positive(), planHash: Hash }).strict();
18826
+ var SendInput = z31.object({ target: Target, message: BoundedText, coWorkId: Uuid.optional() }).strict();
18827
+ var PlanInput = z31.object({ plan: CoWorkPlanSchema }).strict();
18828
+ var ProgressInput = z31.object({ coWorkId: Uuid, target: Target, detail: BoundedText }).strict();
18829
+ var CompleteInput = TokenInput.extend({ passed: z31.boolean(), detail: CompletionEvidence }).strict();
18830
+ var IntegrateInput = TokenInput.extend({ passed: z31.boolean(), evidence: CompletionEvidence }).strict();
18394
18831
  var sharedBytes = (...values) => values.reduce((total, value) => total + Buffer.byteLength(value, "utf8"), 0);
18395
18832
  function resolvePresence(presences, target) {
18396
18833
  const normalized = normalizePalIdentity(target);
@@ -18743,6 +19180,7 @@ async function createProductionRuntime(options) {
18743
19180
  createApplyPatchTool(workspace, fileMutationOptions),
18744
19181
  createGlobTool(workspace),
18745
19182
  createGrepTool(workspace),
19183
+ createGitHistoryTool(workspace),
18746
19184
  createShellTool(workspace, {
18747
19185
  jobs,
18748
19186
  ...executionEnvironment === void 0 ? {} : { executionEnvironment }
@@ -19059,7 +19497,10 @@ async function createProductionRuntime(options) {
19059
19497
  states: { ...taskStates },
19060
19498
  results: { ...taskResults }
19061
19499
  },
19062
- models: { main: harness.mainModelId, subagent: harness.subagentModelId },
19500
+ // Persist the resolved decision, not the harness snapshot: after logout the
19501
+ // harness may still hold the old login model, and resuming from that stale
19502
+ // value is what made the welcome card show a previous service.
19503
+ models: { main: mainModel, subagent: childModel },
19063
19504
  permissionMode: harness.permissionMode,
19064
19505
  memory: memoryLifecycle,
19065
19506
  timeline: { version: 1, state: timelineState }
@@ -19119,7 +19560,7 @@ async function createProductionRuntime(options) {
19119
19560
  })) tools.push(tool);
19120
19561
  const taskTool = {
19121
19562
  name: "Task",
19122
- description: "Validate a task graph and execute its nodes with isolated child agents",
19563
+ description: "Validate a task graph and execute its nodes with isolated child agents. Declare each node's `files` (the workspace files it may create or modify) whenever a task writes files; nodes with overlapping files are serialized automatically to prevent concurrent write conflicts.",
19123
19564
  inputSchema: TaskGraphSchema,
19124
19565
  paths: () => [],
19125
19566
  execute: async (input, signal) => {
@@ -19700,8 +20141,8 @@ async function createProductionRuntime(options) {
19700
20141
  const services = {
19701
20142
  hooks,
19702
20143
  workspace,
19703
- mainModel: () => harness.mainModelId,
19704
- subagentModel: () => harness.subagentModelId,
20144
+ mainModel: () => mainModel,
20145
+ subagentModel: () => childModel,
19705
20146
  llmServiceName: () => effectiveLlm?.serviceName,
19706
20147
  permissionMode: () => harness.permissionMode,
19707
20148
  ...palServices === void 0 ? {} : { pals: palServices },
@@ -19775,6 +20216,8 @@ async function createProductionRuntime(options) {
19775
20216
  }
19776
20217
  }
19777
20218
  harness.setModel(role, id);
20219
+ if (role === "main") mainModel = id;
20220
+ else childModel = id;
19778
20221
  await persist();
19779
20222
  },
19780
20223
  setPermissionMode: async (mode) => {
@@ -19807,6 +20250,19 @@ async function createProductionRuntime(options) {
19807
20250
  hooksStatus: () => HOOK_EVENT_NAMES.map((name) => ({ name, pluginHandlers: pluginHooks.filter((item) => item === name).length })),
19808
20251
  tasks: () => ({ plan: taskPlan, graph: taskGraph, states: taskStates, results: taskResults }),
19809
20252
  evolve: (args) => evolveService.handleCommand(args),
20253
+ gitCommit: (hint, signal) => runGitCommit({
20254
+ workspace,
20255
+ registry,
20256
+ questions,
20257
+ modelId: () => harness.subagentModelId,
20258
+ notify: (message2) => emitOutput({ type: "notice", message: message2 })
20259
+ }, hint, signal),
20260
+ gitReview: (focus, signal) => runGitReview({
20261
+ workspace,
20262
+ registry,
20263
+ modelId: () => harness.subagentModelId,
20264
+ notify: (message2) => emitOutput({ type: "notice", message: message2 })
20265
+ }, focus, signal),
19810
20266
  audit: async (toolFilter) => {
19811
20267
  try {
19812
20268
  const raw = await readFile20(auditLogger.path, "utf8");
@@ -19894,7 +20350,11 @@ ${summary2}`;
19894
20350
  },
19895
20351
  ide: () => ide.status(),
19896
20352
  ideContext: () => ide.editorContext(),
19897
- checkpoint: (label) => sessionHistory.checkpoint(label, harness.main.context.snapshot()),
20353
+ checkpoint: async (label) => {
20354
+ const marker = await gitMarker(workspace);
20355
+ const tagged = marker === void 0 ? label : label === void 0 ? `git: ${marker}` : `${label} (git: ${marker})`;
20356
+ return sessionHistory.checkpoint(tagged, harness.main.context.snapshot());
20357
+ },
19898
20358
  tree: () => sessionHistory.tree(),
19899
20359
  rewind: async (nodeId) => {
19900
20360
  await sessionHistory.rewind(nodeId, harness.main.context.snapshot());
@@ -20004,12 +20464,48 @@ ${renderMemoryDocument(entries)}`;
20004
20464
  goalOrchestrator.setModels(mainModel, mainModel);
20005
20465
  delete selectedModels.mainError;
20006
20466
  delete selectedModels.childError;
20467
+ await retainOnlyCredentials(tokenStore, credentialId);
20007
20468
  if (!secrets.includes(next.accessToken)) secrets.push(next.accessToken);
20008
20469
  await persist();
20009
20470
  return `Authenticated to "${next.serviceName}". Main model ${mainModel}; subagent ${childModel}; gateway ${next.baseURL}. Configuration is active now.`;
20010
20471
  } catch (error) {
20011
20472
  return `Login failed: ${message(error)}`;
20012
20473
  }
20474
+ },
20475
+ async logout() {
20476
+ const tokenStore = createFileTokenStore(join22(home, ".flavor-code", "auth.json"));
20477
+ const tokens = await tokenStore.load();
20478
+ const hadCredentials = Object.keys(tokens).length > 0 || effectiveLlm !== void 0;
20479
+ if (!hadCredentials) {
20480
+ return "Not authenticated \u2014 no stored OAuth credentials to clear.";
20481
+ }
20482
+ for (let i = registeredProviders.length - 1; i >= 0; i -= 1) {
20483
+ const provider = registeredProviders[i];
20484
+ if (provider !== void 0 && provider.pkceManaged === true) {
20485
+ registry.unregister(provider.name);
20486
+ registeredProviders.splice(i, 1);
20487
+ }
20488
+ }
20489
+ if (effectiveLlm !== void 0) {
20490
+ registry.unregister(effectiveLlm.providerId);
20491
+ effectiveLlm = void 0;
20492
+ }
20493
+ await tokenStore.save({});
20494
+ const fallback = selectModels(config, registeredProviders, diagnostics);
20495
+ mainModel = fallback.main;
20496
+ childModel = fallback.child;
20497
+ if (fallback.mainError === void 0) delete selectedModels.mainError;
20498
+ else selectedModels.mainError = fallback.mainError;
20499
+ if (fallback.childError === void 0) delete selectedModels.childError;
20500
+ else selectedModels.childError = fallback.childError;
20501
+ if (fallback.mainError === void 0 && registry.has(safeProvider(mainModel)) && registry.has(safeProvider(childModel))) {
20502
+ harness.setModel("main", mainModel);
20503
+ harness.setModel("subagent", childModel);
20504
+ hallucinationGuard.setModel(childModel);
20505
+ goalOrchestrator.setModels(mainModel, mainModel);
20506
+ }
20507
+ await persist();
20508
+ return fallback.mainError === void 0 ? `Logged out. Cleared OAuth credentials. Main model ${mainModel}; subagent ${childModel}.` : `Logged out. Cleared OAuth credentials. ${fallback.mainError}`;
20013
20509
  }
20014
20510
  };
20015
20511
  if (config.sleep) {
@@ -20326,6 +20822,70 @@ async function* persistEach(source, persist) {
20326
20822
  await persist();
20327
20823
  }
20328
20824
  }
20825
+ async function runGitCommit(deps, hint, signal) {
20826
+ if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /commit needs git.";
20827
+ let summary2 = await changeSummary(deps.workspace);
20828
+ if (summary2.statusLines.length === 0) return "Working tree clean \u2014 nothing to commit.";
20829
+ if (summary2.stagedFiles.length === 0) {
20830
+ if (deps.questions === void 0) throw new Error("Nothing is staged. Run `git add` on the files you want to commit first.");
20831
+ const stageAnswers = await deps.questions.ask([{
20832
+ header: "Staging",
20833
+ question: `Nothing is staged, but ${summary2.statusLines.length} change(s) exist. Stage everything before committing?`,
20834
+ options: [
20835
+ { label: "Stage all", description: "Run git add -A, then generate the commit message." },
20836
+ { label: "Cancel", description: "Abort /commit and leave the working tree as-is." }
20837
+ ]
20838
+ }], signal);
20839
+ if (stageAnswers[0] !== "Stage all") return "/commit cancelled \u2014 nothing was staged.";
20840
+ await stageAll(deps.workspace);
20841
+ summary2 = await changeSummary(deps.workspace);
20842
+ if (summary2.stagedFiles.length === 0) return "Nothing ended up staged \u2014 /commit aborted.";
20843
+ }
20844
+ const staged = await stagedDiff(deps.workspace);
20845
+ signal.throwIfAborted();
20846
+ deps.notify?.("Generating commit message\u2026");
20847
+ let commitMessage;
20848
+ try {
20849
+ commitMessage = await suggestCommitMessage(
20850
+ { registry: deps.registry, modelId: deps.modelId },
20851
+ { stat: staged.stat, diff: staged.diff, ...hint === void 0 ? {} : { hint } },
20852
+ signal
20853
+ );
20854
+ } catch (error) {
20855
+ signal.throwIfAborted();
20856
+ const scope2 = summary2.stagedFiles.length === 1 ? summary2.stagedFiles[0] ?? "1 file" : `${summary2.stagedFiles.length} files`;
20857
+ commitMessage = `chore: update ${scope2}`;
20858
+ deps.notify?.(`Commit message generation failed (${message(error)}); using a fallback message.`);
20859
+ }
20860
+ deps.notify?.(`Proposed commit message:
20861
+ ${commitMessage}`);
20862
+ if (deps.questions !== void 0) {
20863
+ const confirmAnswers = await deps.questions.ask([{
20864
+ header: "Commit",
20865
+ question: `Commit ${summary2.stagedFiles.length} staged file(s) on ${summary2.branch} with the message above?`,
20866
+ options: [
20867
+ { label: "Commit", description: "Run git commit with the proposed message." },
20868
+ { label: "Cancel", description: "Abort; the staged changes stay as-is." }
20869
+ ]
20870
+ }], signal);
20871
+ if (confirmAnswers[0] !== "Commit") return "/commit cancelled \u2014 staged changes left untouched.";
20872
+ }
20873
+ const result = await commit(deps.workspace, commitMessage);
20874
+ return `Committed: ${result}`;
20875
+ }
20876
+ async function runGitReview(deps, focus, signal) {
20877
+ if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /review needs git.";
20878
+ const changes = await uncommittedDiff(deps.workspace);
20879
+ if (changes.diff.trim() === "" && changes.untracked.length === 0) return "No uncommitted changes to review.";
20880
+ signal.throwIfAborted();
20881
+ deps.notify?.("Reviewing uncommitted changes\u2026");
20882
+ const report = await reviewDiff(
20883
+ { registry: deps.registry, modelId: deps.modelId },
20884
+ { stat: changes.stat, diff: changes.diff, untracked: changes.untracked, ...focus === void 0 ? {} : { focus } },
20885
+ signal
20886
+ );
20887
+ return formatReviewReport(report);
20888
+ }
20329
20889
  async function runChild(harness, skills, task, attempt, signal, parentContext) {
20330
20890
  return harness.runSubagent(task, async (child, childSignal) => {
20331
20891
  const skill = await skills.match(task.description);
@@ -20338,6 +20898,7 @@ ${await skills.loadBody(skill)}`;
20338
20898
  `Complete task ${task.id}: ${task.description}`,
20339
20899
  `Expected outputs: ${task.expectedOutputs.join("; ")}`,
20340
20900
  `Verification: ${task.verification.join("; ")}`,
20901
+ ...task.files === void 0 || task.files.length === 0 ? [] : [`Owned files: ${task.files.join(", ")}. Restrict your file writes to these paths; concurrent tasks own everything else.`],
20341
20902
  `For completed work, finish by calling TaskOutput. Otherwise return only JSON matching these fields: ${Object.keys(SubagentResultSchema.shape).join(", ")}.${repair}`
20342
20903
  ].join("\n");
20343
20904
  for await (const event of child.loop.run({ prompt, signal: childSignal })) {