@cydm/pie 1.0.48 → 1.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/dist/builtin/extensions/ask-user/index.js +2 -9
  3. package/dist/builtin/extensions/ask-user/package.json +0 -7
  4. package/dist/builtin/extensions/changelog/index.js +15 -20
  5. package/dist/builtin/extensions/document-attachments/index.js +0 -1
  6. package/dist/builtin/extensions/document-attachments/package.json +1 -1
  7. package/dist/builtin/extensions/init/index.js +0 -13
  8. package/dist/builtin/extensions/plan-mode/index.js +29 -17
  9. package/dist/builtin/extensions/subagent/index.js +15 -31
  10. package/dist/builtin/extensions/todo/index.js +4 -8
  11. package/dist/builtin/skills/skill-creator/SKILL.md +15 -43
  12. package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +1 -1
  13. package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +2 -2
  14. package/dist/builtin/skills/skill-creator/references/schemas.md +1 -1
  15. package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +1 -1
  16. package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +3 -3
  17. package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +32 -4
  18. package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +1 -1
  19. package/dist/chunks/{chunk-IJMP3JTX.js → chunk-DZS34ACX.js} +230 -46
  20. package/dist/chunks/{chunk-U4B6RJ6Z.js → chunk-XBWFDIAU.js} +1 -1
  21. package/dist/cli.js +127 -80
  22. package/package.json +3 -3
  23. package/dist/builtin/extensions/deploy/index.js +0 -11
  24. package/dist/builtin/extensions/deploy/package.json +0 -11
  25. package/dist/builtin/extensions/files/index.js +0 -10
  26. package/dist/builtin/extensions/files/package.json +0 -14
  27. package/dist/builtin/extensions/kimi-attachments/index.js +0 -47
  28. package/dist/builtin/extensions/kimi-attachments/package.json +0 -11
  29. package/dist/builtin/skills/skill-creator/scripts/claude_cli.mjs +0 -115
@@ -545,7 +545,7 @@
545
545
  <div class="header">
546
546
  <div>
547
547
  <h1>Eval Review: <span id="skill-name"></span></h1>
548
- <div class="instructions">Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste into Claude Code.</div>
548
+ <div class="instructions">Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste it back into your Pie session.</div>
549
549
  </div>
550
550
  <div class="progress" id="progress"></div>
551
551
  </div>
@@ -634,7 +634,7 @@
634
634
  <div class="done-overlay" id="done-overlay">
635
635
  <div class="done-card">
636
636
  <h2>Review Complete</h2>
637
- <p>Your feedback has been saved. Go back to your Claude Code session and tell Claude you're done reviewing.</p>
637
+ <p>Your feedback has been saved. Go back to your Pie session and say you're done reviewing.</p>
638
638
  <div class="btn-row">
639
639
  <button onclick="closeDoneDialog()">OK</button>
640
640
  </div>
@@ -225,7 +225,7 @@ Output from Benchmark mode. Located at `benchmarks/<timestamp>/benchmark.json`.
225
225
  "metadata": {
226
226
  "skill_name": "pdf",
227
227
  "skill_path": "/path/to/pdf",
228
- "executor_model": "claude-sonnet-4-20250514",
228
+ "executor_model": "pie-agent",
229
229
  "analyzer_model": "most-capable-model",
230
230
  "timestamp": "2026-01-15T10:30:00Z",
231
231
  "evals_run": [1, 2, 3],
@@ -75,7 +75,7 @@ ${refreshTag} <title>${titlePrefix}Skill Description Optimization</title>
75
75
  <body>
76
76
  <h1>${titlePrefix}Skill Description Optimization</h1>
77
77
  <div class="explainer">
78
- <strong>Optimizing your skill's description.</strong> This page updates automatically as Claude tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong.
78
+ <strong>Optimizing your skill's description.</strong> This page updates automatically as the model tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong.
79
79
  </div>
80
80
  <div class="summary">
81
81
  <p><strong>Original:</strong> ${escapeHtml(data?.original_description ?? "N/A")}</p>
@@ -25,9 +25,9 @@ export async function improveDescription({
25
25
  ? `Train: ${trainScore}, Test: ${testResults.summary.passed}/${testResults.summary.total}`
26
26
  : `Train: ${trainScore}`;
27
27
 
28
- let prompt = `You are optimizing a skill description for a Claude Code skill called "${skillName}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples.
28
+ let prompt = `You are optimizing a skill description for a Pie skill called "${skillName}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that the agent sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples.
29
29
 
30
- The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
30
+ The description appears in the agent's "available_skills" list. When a user sends a query, the agent decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
31
31
 
32
32
  Here's the current description:
33
33
  <current_description>
@@ -93,7 +93,7 @@ Concretely, your description should not be more than about 100-200 words, even i
93
93
  Here are some tips that we've found to work well in writing these descriptions:
94
94
  - The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does"
95
95
  - The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works.
96
- - The description competes with other skills for Claude's attention — make it distinctive and immediately recognizable.
96
+ - The description competes with other skills for the model's attention — make it distinctive and immediately recognizable.
97
97
  - If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings.
98
98
 
99
99
  I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end.
@@ -2,11 +2,11 @@
2
2
 
3
3
  import crypto from "node:crypto";
4
4
  import { spawn } from "node:child_process";
5
+ import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
8
9
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
9
- const REPO_ROOT = path.resolve(SCRIPT_DIR, "../../../../../../");
10
10
 
11
11
  export function findProjectRoot(start = process.cwd()) {
12
12
  return path.resolve(start);
@@ -29,13 +29,41 @@ export function openInBrowser(target) {
29
29
  spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
30
30
  }
31
31
 
32
- function getPieCliPath(cwd = process.cwd()) {
33
- return path.join(REPO_ROOT, "products/cli/dist/cli.js");
32
+ let cachedCliPath;
33
+
34
+ /**
35
+ * Locate the Pie CLI entry point across layouts:
36
+ * - installed package: <pkg>/dist/builtin/skills/skill-creator/scripts -> <pkg>/dist/cli.js
37
+ * - dev repository: <repo>/products/cli/builtin/skills/skill-creator/scripts -> <repo>/products/cli/dist/cli.js
38
+ * - explicit override via PIE_CLI_PATH
39
+ *
40
+ * Candidate order is layout-aware: an installed build always prefers the
41
+ * package root and a dev checkout always prefers products/cli, so an
42
+ * unrelated sibling artifact cannot win over the correct entry.
43
+ */
44
+ export function getPieCliPath() {
45
+ if (cachedCliPath) return cachedCliPath;
46
+ const installed = path.resolve(SCRIPT_DIR, "../../../../../dist/cli.js"); // installed package root
47
+ const dev = path.resolve(SCRIPT_DIR, "../../../../dist/cli.js"); // dev repo: products/cli
48
+ const inInstalledLayout = SCRIPT_DIR.includes(`${path.sep}dist${path.sep}builtin${path.sep}`);
49
+ const candidates = [
50
+ process.env.PIE_CLI_PATH,
51
+ ...(inInstalledLayout ? [installed, dev] : [dev, installed]),
52
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
53
+ for (const candidate of candidates) {
54
+ if (fs.existsSync(candidate)) {
55
+ cachedCliPath = candidate;
56
+ return cachedCliPath;
57
+ }
58
+ }
59
+ throw new Error(
60
+ `Pie CLI entry not found. Tried: ${candidates.join(", ")}. Set PIE_CLI_PATH to the cli.js entry point.`,
61
+ );
34
62
  }
35
63
 
36
64
  export async function runPieJsonPrompt(prompt, { cwd = process.cwd(), timeout = 300_000, sessionId = null } = {}) {
37
65
  return new Promise((resolve, reject) => {
38
- const args = [getPieCliPath(cwd), prompt, "--json-output"];
66
+ const args = [getPieCliPath(), prompt, "--json-output"];
39
67
  if (sessionId) {
40
68
  args.push("--session-id", sessionId);
41
69
  }
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { generateHtml } from "./generate_report.mjs";
8
8
  import { improveDescription } from "./improve_description.mjs";
9
- import { findProjectRoot, openInBrowser } from "./claude_cli.mjs";
9
+ import { findProjectRoot, openInBrowser } from "./pie_runner.mjs";
10
10
  import { runEval } from "./run_eval.mjs";
11
11
  import { parseSkillFile, resolveSkillPath } from "./skill_metadata.mjs";
12
12
 
@@ -848,6 +848,10 @@ var BUILTIN_TOOL_CAPABILITY_METADATA = {
848
848
  spawn_subagents_parallel: { riskClass: "read_only", concurrencySafe: false, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, maxOutputChars: 1e5 },
849
849
  ask_user_multi: { riskClass: "user_interaction", concurrencySafe: false, permissionScope: "user", asksUser: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
850
850
  manage_todo_list: { riskClass: "session_mutation", concurrencySafe: false, permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
851
+ // plan_scratch ships as the plan-mode extension's session-local draft tool;
852
+ // registration feeds /permissions display and the legacy plan-mode name list,
853
+ // while runtime decisions still honor the tool's inline risk metadata first.
854
+ plan_scratch: { riskClass: "session_mutation", concurrencySafe: false, permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
851
855
  unity_project_inspect: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
852
856
  unity_scene_query: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
853
857
  unity_scene_object_inspect: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
@@ -1409,8 +1413,9 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds) {
1409
1413
  const complexity = inferComplexity(task, priority);
1410
1414
  const modelClass = selectModelClass(complexity, priority);
1411
1415
  const modelResolution = deps.resolveModelClass?.(modelClass);
1412
- const model = modelResolution?.model ?? deps.model;
1413
- const apiKey = modelResolution?.apiKey ?? deps.apiKey;
1416
+ const resolvedPair = modelResolution?.model && modelResolution?.apiKey ? { model: modelResolution.model, apiKey: modelResolution.apiKey } : void 0;
1417
+ const model = resolvedPair?.model ?? deps.model;
1418
+ const apiKey = resolvedPair?.apiKey ?? deps.apiKey;
1414
1419
  const partialPlan = {
1415
1420
  task,
1416
1421
  name,
@@ -1508,6 +1513,9 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1508
1513
  if (!apiKey) {
1509
1514
  throw new Error("No API key available for subagent execution.");
1510
1515
  }
1516
+ if (!model) {
1517
+ throw new Error("No model configured for subagent execution.");
1518
+ }
1511
1519
  if (signal?.aborted) {
1512
1520
  throw new Error("Subagent aborted by parent");
1513
1521
  }
@@ -1845,6 +1853,12 @@ var ManageTodoListParamsSchema = Type.Object(
1845
1853
  },
1846
1854
  { additionalProperties: false }
1847
1855
  );
1856
+ var MANAGE_TODO_LIST_TOOL_DESCRIPTION = `Manage a session-local todo list. Use when the user explicitly asks to use the todo tool, create/manage todos, track tasks, follow a todo list, show tasks, or mark a task done.
1857
+ NEVER use for: internal planning, tracking your own progress, remembering subtasks, or creating checklists for yourself.
1858
+ Use action=create with items to create a list; provide todo titles and optional descriptions only. The tool assigns ids and starts item 1 automatically.
1859
+ Use action=complete_current after finishing the current item; the tool advances to the next item or clears the list after the final item. Do not call complete_current after a required install, build, test, or verification command just failed; fix/retry first or clear with a reason.
1860
+ When a todo list exists, keep executing from the current in-progress item until it is cleared. If the work cannot continue, explain the failure and use action=clear with a reason.
1861
+ Actions: create, read, complete_current, clear.`;
1848
1862
  function cloneTodos(todos) {
1849
1863
  return todos.map((todo) => ({ ...todo }));
1850
1864
  }
@@ -7869,6 +7883,193 @@ function fuzzyFindText(content, oldText) {
7869
7883
  function stripBom(content) {
7870
7884
  return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
7871
7885
  }
7886
+ function findEditMatch(content, oldText, newText) {
7887
+ const direct = fuzzyFindText(content, oldText);
7888
+ if (direct.found) {
7889
+ return { found: true, occurrences: direct.occurrences, matches: direct.matches, matchKind: direct.matchKind };
7890
+ }
7891
+ return findWhitespaceRelaxedMatch(content, oldText, newText);
7892
+ }
7893
+ function leadingWhitespace(line) {
7894
+ const match2 = line.match(/^[ \t]*/);
7895
+ return match2 ? match2[0] : "";
7896
+ }
7897
+ function findRelaxedWindows(fileLines, qLines) {
7898
+ const firstNonBlank = qLines.findIndex((line) => line.trim() !== "");
7899
+ if (firstNonBlank === -1) return [];
7900
+ const anchor = qLines[firstNonBlank].trim();
7901
+ if (!anchor) return [];
7902
+ const windows = [];
7903
+ for (let anchorFileLine = 0; anchorFileLine < fileLines.length; anchorFileLine++) {
7904
+ if (fileLines[anchorFileLine].trim() !== anchor) continue;
7905
+ const startLine = anchorFileLine - firstNonBlank;
7906
+ if (startLine < 0) continue;
7907
+ let fileLine = startLine;
7908
+ let ok = true;
7909
+ let lastPaired = -1;
7910
+ const qLineToFileLine = /* @__PURE__ */ new Map();
7911
+ for (let qLine = 0; qLine < qLines.length; qLine++) {
7912
+ if (fileLine >= fileLines.length) {
7913
+ ok = false;
7914
+ break;
7915
+ }
7916
+ const q = qLines[qLine];
7917
+ const f = fileLines[fileLine];
7918
+ if (q.trim() === "") {
7919
+ if (f.trim() !== "") {
7920
+ ok = false;
7921
+ break;
7922
+ }
7923
+ } else {
7924
+ if (f.trim() !== q.trim()) {
7925
+ ok = false;
7926
+ break;
7927
+ }
7928
+ qLineToFileLine.set(qLine, fileLine);
7929
+ lastPaired = fileLine;
7930
+ }
7931
+ fileLine++;
7932
+ }
7933
+ if (ok && lastPaired !== -1) windows.push({ startLine, endLine: lastPaired, qLineToFileLine });
7934
+ }
7935
+ return windows;
7936
+ }
7937
+ function lineSpanToCharSpan(fileLines, startLine, endLine) {
7938
+ let index = 0;
7939
+ for (let i = 0; i < startLine; i++) index += fileLines[i].length + 1;
7940
+ let matchLength = 0;
7941
+ for (let i = startLine; i <= endLine; i++) matchLength += fileLines[i].length + (i < endLine ? 1 : 0);
7942
+ return { index, matchLength };
7943
+ }
7944
+ function reindentNewText(newText, qLines, fileLines, window) {
7945
+ const newLines = newText.split("\n");
7946
+ const qIndent = (qLine) => {
7947
+ const fileLine = window.qLineToFileLine.get(qLine);
7948
+ return fileLine === void 0 ? "" : leadingWhitespace(fileLines[fileLine]);
7949
+ };
7950
+ const qIdx = [];
7951
+ for (let i = 0; i < qLines.length; i++) if (qLines[i].trim() !== "") qIdx.push(i);
7952
+ const nIdx = [];
7953
+ const nPosOfLine = /* @__PURE__ */ new Map();
7954
+ for (let i = 0; i < newLines.length; i++) {
7955
+ if (newLines[i].trim() === "") continue;
7956
+ nPosOfLine.set(i, nIdx.length);
7957
+ nIdx.push(i);
7958
+ }
7959
+ if (nIdx.length === 0) return newText;
7960
+ const counterpart = /* @__PURE__ */ new Map();
7961
+ let p = 0;
7962
+ while (p < qIdx.length && p < nIdx.length && qLines[qIdx[p]].trim() === newLines[nIdx[p]].trim()) {
7963
+ counterpart.set(p, p);
7964
+ p++;
7965
+ }
7966
+ let s = 0;
7967
+ while (s < qIdx.length - p && s < nIdx.length - p && qLines[qIdx[qIdx.length - 1 - s]].trim() === newLines[nIdx[nIdx.length - 1 - s]].trim()) {
7968
+ counterpart.set(nIdx.length - 1 - s, qIdx.length - 1 - s);
7969
+ s++;
7970
+ }
7971
+ const midQ = [];
7972
+ for (let pos = p; pos < qIdx.length - s; pos++) midQ.push(pos);
7973
+ const midN = [];
7974
+ for (let pos = p; pos < nIdx.length - s; pos++) midN.push(pos);
7975
+ const pairRegion = (qLo, qHi, nLo, nHi) => {
7976
+ const qCount = qHi - qLo;
7977
+ const nCount = nHi - nLo;
7978
+ const paired = Math.min(qCount, nCount);
7979
+ for (let k = 0; k < paired; k++) counterpart.set(nLo + k, qLo + k);
7980
+ const fallbackQ = qCount > 0 ? qHi - 1 : Math.max(qLo - 1, 0);
7981
+ for (let k = paired; k < nCount; k++) counterpart.set(nLo + k, fallbackQ);
7982
+ };
7983
+ const qTextCounts = /* @__PURE__ */ new Map();
7984
+ for (const qPos of midQ) {
7985
+ const t = qLines[qIdx[qPos]].trim();
7986
+ qTextCounts.set(t, (qTextCounts.get(t) ?? 0) + 1);
7987
+ }
7988
+ const qPosByText = /* @__PURE__ */ new Map();
7989
+ for (const qPos of midQ) {
7990
+ const t = qLines[qIdx[qPos]].trim();
7991
+ if (qTextCounts.get(t) === 1) qPosByText.set(t, qPos);
7992
+ }
7993
+ const nTextCounts = /* @__PURE__ */ new Map();
7994
+ for (const nPos of midN) {
7995
+ const t = newLines[nIdx[nPos]].trim();
7996
+ nTextCounts.set(t, (nTextCounts.get(t) ?? 0) + 1);
7997
+ }
7998
+ let cursorQ = p;
7999
+ let cursorN = p;
8000
+ for (const nPos of midN) {
8001
+ const t = newLines[nIdx[nPos]].trim();
8002
+ const qPos = qPosByText.get(t);
8003
+ if (nTextCounts.get(t) !== 1 || qPos === void 0 || qPos < cursorQ) continue;
8004
+ pairRegion(cursorQ, qPos, cursorN, nPos);
8005
+ counterpart.set(nPos, qPos);
8006
+ cursorQ = qPos + 1;
8007
+ cursorN = nPos + 1;
8008
+ }
8009
+ pairRegion(cursorQ, qIdx.length - s, cursorN, nIdx.length - s);
8010
+ const result = [];
8011
+ for (let i = 0; i < newLines.length; i++) {
8012
+ if (newLines[i].trim() === "") {
8013
+ result.push("");
8014
+ continue;
8015
+ }
8016
+ const nPos = nPosOfLine.get(i) ?? 0;
8017
+ const qPos = counterpart.get(nPos) ?? 0;
8018
+ result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + newLines[i].trim());
8019
+ }
8020
+ return result.join("\n");
8021
+ }
8022
+ function findWhitespaceRelaxedMatch(content, oldText, newText) {
8023
+ const fileLines = content.split("\n");
8024
+ const qLines = oldText.split("\n");
8025
+ const windows = findRelaxedWindows(fileLines, qLines);
8026
+ if (windows.length === 0) {
8027
+ return { found: false, occurrences: 0, matches: [], matchKind: "none" };
8028
+ }
8029
+ const matches = windows.map((window) => {
8030
+ const span = lineSpanToCharSpan(fileLines, window.startLine, window.endLine);
8031
+ return { ...span, replacementText: reindentNewText(newText, qLines, fileLines, window) };
8032
+ });
8033
+ return { found: true, occurrences: matches.length, matches, matchKind: "whitespace-relaxed" };
8034
+ }
8035
+ function describeClosestRegion(content, oldText) {
8036
+ const qLines = oldText.split("\n");
8037
+ const firstNonBlank = qLines.findIndex((line) => line.trim() !== "");
8038
+ if (firstNonBlank === -1) return void 0;
8039
+ const anchor = qLines[firstNonBlank].trim();
8040
+ const fileLines = content.split("\n");
8041
+ const clip = (text) => text.length > 90 ? `${text.slice(0, 90)}\u2026` : text;
8042
+ let best;
8043
+ for (let i = 0; i < fileLines.length; i++) {
8044
+ if (fileLines[i].trim() !== anchor) continue;
8045
+ let matched = 0;
8046
+ let fi = i;
8047
+ let firstDiff;
8048
+ for (let qi = firstNonBlank; qi < qLines.length; qi++) {
8049
+ const q = qLines[qi].trim();
8050
+ if (!q) continue;
8051
+ while (fi < fileLines.length && !fileLines[fi].trim()) fi++;
8052
+ if (fi >= fileLines.length || fileLines[fi].trim() !== q) {
8053
+ firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi] };
8054
+ break;
8055
+ }
8056
+ matched++;
8057
+ fi++;
8058
+ }
8059
+ if (!best || matched > best.matched) best = { start: i, matched, firstDiff };
8060
+ }
8061
+ const total = qLines.filter((line) => line.trim() !== "").length;
8062
+ if (!best) {
8063
+ return `No line matching ${JSON.stringify(clip(anchor))} exists in this file \u2014 the text may live in a different file, or the file changed since it was read.`;
8064
+ }
8065
+ const parts = [`Closest match: file lines ${best.start + 1}\u2013${best.start + total} (${best.matched}/${total} lines matched by trimmed content).`];
8066
+ if (best.firstDiff) {
8067
+ parts.push(`First difference at line ${best.firstDiff.fileNo}: file has ${JSON.stringify(clip(best.firstDiff.fileLine.trim()))}, edit expected ${JSON.stringify(clip(best.firstDiff.qLine.trim()))}.`);
8068
+ } else {
8069
+ parts.push("All non-empty lines match by trimmed content \u2014 the mismatch is leading whitespace or blank-line placement.");
8070
+ }
8071
+ return parts.join(" ");
8072
+ }
7872
8073
  function generateDiffString(oldContent, newContent, contextLines = 4) {
7873
8074
  const oldLines = normalizeToLF(stripBom(oldContent).text).split("\n");
7874
8075
  const newLines = normalizeToLF(stripBom(newContent).text).split("\n");
@@ -7982,7 +8183,8 @@ function createEditErrorResult(params) {
7982
8183
  const detailLines = [
7983
8184
  params.message,
7984
8185
  params.recoveryHint,
7985
- params.alreadyAppliedCandidate ? "Diagnostic: newText already appears in the current file, but edit_file did not treat that as success because it may be an unrelated occurrence." : ""
8186
+ params.alreadyAppliedCandidate ? "Diagnostic: newText already appears in the current file, but edit_file did not treat that as success because it may be an unrelated occurrence." : "",
8187
+ params.closestMatch ? `Diagnostic: ${params.closestMatch}` : ""
7986
8188
  ].filter(Boolean);
7987
8189
  return {
7988
8190
  content: [{ type: "text", text: detailLines.join("\n") }],
@@ -8034,7 +8236,7 @@ function createEditTool(cwd, options) {
8034
8236
  newText: Type.String({ description: "Replacement text", maxLength: MAX_EDIT_INPUT_CHARS }),
8035
8237
  replaceAll: Type.Optional(Type.Boolean({ description: "Replace every occurrence" }))
8036
8238
  }), {
8037
- description: "One or more independent replacements matched against the original file",
8239
+ description: "Replacements applied in order: each edit matches the file as left by the previous edit, so later edits can build on earlier ones. Whole-line indentation drift is tolerated \u2014 matches locate by trimmed content and the replacement adopts the file's indentation.",
8038
8240
  minItems: 1,
8039
8241
  maxItems: MAX_EDIT_BLOCKS
8040
8242
  })
@@ -8043,7 +8245,7 @@ function createEditTool(cwd, options) {
8043
8245
  return {
8044
8246
  name: "edit",
8045
8247
  label: "edit",
8046
- description: `Make precise exact-text replacements in one file. For multiple changes in the same file, put all independent replacements in one edits[] call. ${pathResolutionDescription}`,
8248
+ description: `Make precise exact-text replacements in one file. For multiple changes in the same file, put all replacements in one edits[] call \u2014 they apply in order, so later edits can target what earlier ones just changed. ${pathResolutionDescription}`,
8047
8249
  parameters: editSchema,
8048
8250
  execute: async (_toolCallId, params, signal) => {
8049
8251
  const { path: path4, root } = params;
@@ -8087,7 +8289,9 @@ function createEditTool(cwd, options) {
8087
8289
  const { bom, text: content } = stripBom(rawContent);
8088
8290
  const originalEnding = detectLineEnding(content);
8089
8291
  const normalizedContent = normalizeToLF(content);
8090
- const planned = [];
8292
+ let workingContent = normalizedContent;
8293
+ const plannedKinds = /* @__PURE__ */ new Set();
8294
+ let replacedOccurrences = 0;
8091
8295
  let totalOldOccurrences = 0;
8092
8296
  let totalNewOccurrences = 0;
8093
8297
  let anyReplaceAll = false;
@@ -8095,8 +8299,8 @@ function createEditTool(cwd, options) {
8095
8299
  throwIfAborted();
8096
8300
  const normalizedOldText = normalizeToLF(replacement.oldText);
8097
8301
  const normalizedNewText = normalizeToLF(replacement.newText);
8098
- const newTextOccurrences = countOccurrences(normalizedContent, normalizedNewText);
8099
- const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
8302
+ const newTextOccurrences = countOccurrences(workingContent, normalizedNewText);
8303
+ const matchResult = findEditMatch(workingContent, normalizedOldText, normalizedNewText);
8100
8304
  totalOldOccurrences += matchResult.occurrences;
8101
8305
  totalNewOccurrences += newTextOccurrences;
8102
8306
  anyReplaceAll ||= replacement.replaceAll;
@@ -8111,7 +8315,8 @@ function createEditTool(cwd, options) {
8111
8315
  replaceAll: replacement.replaceAll,
8112
8316
  staleBaseLikely: newTextOccurrences > 0,
8113
8317
  alreadyAppliedCandidate: newTextOccurrences > 0,
8114
- recoveryHint: newTextOccurrences > 0 ? "The replacement may already be present. Read the current file before retrying." : "Read the latest file and retry with exact text."
8318
+ recoveryHint: newTextOccurrences > 0 ? "The replacement may already be present. Read the current file before retrying." : "Read the latest file and retry with exact text.",
8319
+ closestMatch: describeClosestRegion(workingContent, normalizedOldText)
8115
8320
  }));
8116
8321
  return;
8117
8322
  }
@@ -8128,9 +8333,9 @@ function createEditTool(cwd, options) {
8128
8333
  }));
8129
8334
  return;
8130
8335
  }
8131
- const matches = replacement.replaceAll ? matchResult.matches : matchResult.matches.slice(0, 1);
8132
- const changingMatches = matches.filter(
8133
- (match2) => normalizedContent.slice(match2.index, match2.index + match2.matchLength) !== normalizedNewText
8336
+ const selected = replacement.replaceAll ? matchResult.matches : matchResult.matches.slice(0, 1);
8337
+ const changingMatches = selected.filter(
8338
+ (match2) => workingContent.slice(match2.index, match2.index + match2.matchLength) !== (match2.replacementText ?? normalizedNewText)
8134
8339
  );
8135
8340
  if (changingMatches.length === 0) {
8136
8341
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -8146,42 +8351,20 @@ function createEditTool(cwd, options) {
8146
8351
  }));
8147
8352
  return;
8148
8353
  }
8149
- for (const match2 of changingMatches) {
8150
- planned.push({ ...match2, newText: normalizedNewText, matchKind: matchResult.matchKind });
8151
- }
8152
- }
8153
- const ordered = [...planned].sort((left, right) => left.index - right.index);
8154
- for (let index = 1; index < ordered.length; index++) {
8155
- throwIfAborted();
8156
- const previous = ordered[index - 1];
8157
- const current = ordered[index];
8158
- if (current.index < previous.index + previous.matchLength) {
8159
- if (signal) signal.removeEventListener("abort", onAbort);
8160
- resolve3(createEditErrorResult({
8161
- message: `Edits overlap in ${path4}; no changes were made.`,
8162
- matchKind: "mixed",
8163
- errorReason: "overlapping_edits",
8164
- oldTextOccurrences: totalOldOccurrences,
8165
- newTextOccurrences: totalNewOccurrences,
8166
- replaceAll: anyReplaceAll,
8167
- recoveryHint: "Merge overlapping replacements into one exact edit."
8168
- }));
8169
- return;
8354
+ for (const match2 of [...changingMatches].sort((left, right) => right.index - left.index)) {
8355
+ throwIfAborted();
8356
+ workingContent = workingContent.substring(0, match2.index) + (match2.replacementText ?? normalizedNewText) + workingContent.substring(match2.index + match2.matchLength);
8357
+ replacedOccurrences++;
8358
+ if (matchResult.matchKind !== "none") plannedKinds.add(matchResult.matchKind);
8170
8359
  }
8171
8360
  }
8172
8361
  throwIfAborted();
8173
- const baseContent = normalizedContent;
8174
- let newContent = baseContent;
8175
- for (const match2 of [...planned].sort((left, right) => right.index - left.index)) {
8176
- throwIfAborted();
8177
- newContent = newContent.substring(0, match2.index) + match2.newText + newContent.substring(match2.index + match2.matchLength);
8178
- }
8179
- if (baseContent === newContent) {
8362
+ if (workingContent === normalizedContent) {
8180
8363
  if (signal) signal.removeEventListener("abort", onAbort);
8181
8364
  resolve3(
8182
8365
  createEditErrorResult({
8183
8366
  message: `No changes made to ${path4}. The replacement produced identical content.`,
8184
- matchKind: planned.every((match2) => match2.matchKind === planned[0]?.matchKind) ? planned[0]?.matchKind ?? "none" : "mixed",
8367
+ matchKind: plannedKinds.size === 1 ? [...plannedKinds][0] : "mixed",
8185
8368
  errorReason: "replacement_no_change",
8186
8369
  oldTextOccurrences: totalOldOccurrences,
8187
8370
  newTextOccurrences: totalNewOccurrences,
@@ -8192,6 +8375,7 @@ function createEditTool(cwd, options) {
8192
8375
  );
8193
8376
  return;
8194
8377
  }
8378
+ const newContent = workingContent;
8195
8379
  const finalContent = bom + restoreLineEndings(newContent, originalEnding);
8196
8380
  throwIfAborted();
8197
8381
  await fs2.writeFile(absolutePath, finalContent, "utf-8");
@@ -8201,13 +8385,12 @@ function createEditTool(cwd, options) {
8201
8385
  signal.removeEventListener("abort", onAbort);
8202
8386
  }
8203
8387
  const diffResult = generateDiffString(content, restoreLineEndings(newContent, originalEnding));
8204
- const matchKinds = new Set(planned.map((match2) => match2.matchKind));
8205
- const matchKind = matchKinds.size === 1 ? planned[0]?.matchKind ?? "none" : "mixed";
8388
+ const matchKind = plannedKinds.size === 1 ? [...plannedKinds][0] : "mixed";
8206
8389
  resolve3({
8207
8390
  content: [
8208
8391
  {
8209
8392
  type: "text",
8210
- text: `Changed ${path4}: ${replacements.length} edit block${replacements.length === 1 ? "" : "s"}, ${planned.length} replacement${planned.length === 1 ? "" : "s"}. First changed line: ${diffResult.firstChangedLine}.`
8393
+ text: `Changed ${path4}: ${replacements.length} edit block${replacements.length === 1 ? "" : "s"}, ${replacedOccurrences} replacement${replacedOccurrences === 1 ? "" : "s"}. First changed line: ${diffResult.firstChangedLine}.`
8211
8394
  },
8212
8395
  { type: "text", text: boundedDiff(diffResult.diff) }
8213
8396
  ],
@@ -8221,7 +8404,7 @@ function createEditTool(cwd, options) {
8221
8404
  oldTextOccurrences: totalOldOccurrences,
8222
8405
  newTextOccurrences: totalNewOccurrences,
8223
8406
  replaceAll: anyReplaceAll,
8224
- replacedOccurrences: planned.length,
8407
+ replacedOccurrences,
8225
8408
  replacedBlocks: replacements.length
8226
8409
  }
8227
8410
  });
@@ -12363,7 +12546,7 @@ function sanitizeTitle(title) {
12363
12546
  }
12364
12547
  function isTextCompletionStepTitle(title) {
12365
12548
  const normalized = sanitizeTitle(title).toLowerCase();
12366
- return /反馈|总结|结论|汇报|说明|解释|收尾|验证|确认|位置|结果/u.test(title) || /\b(summarize|summary|report|explain|explanation|wrap[\s-]?up|verify|verification|confirm|result|results|location|findings)\b/i.test(normalized);
12549
+ return /反馈|总结|结论|汇报|说明|解释|收尾|验证|确认|位置|结果/u.test(normalized) || /\b(summarize|summary|report|explain|explanation|wrap[\s-]?up|verify|verification|confirm|result|results|location|findings)\b/i.test(normalized);
12367
12550
  }
12368
12551
  function maybeAdvanceTodoExecutionState(state, signal) {
12369
12552
  if (state.mode !== "todo" || state.source === "user_todo" || state.lifecycle !== "active" || !state.currentStepId) {
@@ -14997,6 +15180,7 @@ export {
14997
15180
  interpretShellExit,
14998
15181
  createSubagentCapability,
14999
15182
  ManageTodoListParamsSchema,
15183
+ MANAGE_TODO_LIST_TOOL_DESCRIPTION,
15000
15184
  executeManageTodoList,
15001
15185
  maybeAdvanceTodoExecutionState,
15002
15186
  createAskUserCapability,
@@ -8,7 +8,7 @@ import {
8
8
  createSharedWebSearchTool,
9
9
  interpretShellExit,
10
10
  requestInteraction
11
- } from "./chunk-IJMP3JTX.js";
11
+ } from "./chunk-DZS34ACX.js";
12
12
  import {
13
13
  Type
14
14
  } from "./chunk-K6ZOCJ4V.js";