pi-ui-extend 0.1.13 → 0.1.17

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
@@ -1,16 +1,45 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadPiToolsSuiteConfig } from "../config.js";
2
3
  import { autoClearCompletedTodos } from "./state/auto-clear.js";
3
4
  import { loadPersistedPlan, syncPersistedPlan } from "./state/persistence.js";
4
5
  import { replayFromBranch } from "./state/replay.js";
5
6
  import { ACTIVE_STATUSES, isTaskBlocked, selectVisibleTasks } from "./state/selectors.js";
6
7
  import { getState, replaceState } from "./state/store.js";
7
- import { publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
8
+ import { DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
9
+ import type { TaskMutationParams } from "./tool/types.js";
8
10
 
9
11
  const TODO_NUDGE_LIMIT = 8;
10
12
  const TODO_NUDGE_INITIAL_DELAY_MS = 5_000;
11
13
  const TODO_NUDGE_IDLE_RETRY_DELAY_MS = 100;
12
14
  const TODO_NUDGE_MAX_IDLE_ATTEMPTS = 40;
13
15
  const ASK_USER_TOOL_NAMES = new Set(["ask_user", "ask_user_question", "question"]);
16
+ const TODO_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
17
+
18
+ type TodoThinkingLevel = (typeof TODO_THINKING_LEVELS)[number];
19
+ type ModelLike = { reasoning?: boolean; thinkingLevelMap?: Partial<Record<TodoThinkingLevel, unknown | null>> };
20
+
21
+ function isTodoThinkingLevel(value: unknown): value is TodoThinkingLevel {
22
+ return TODO_THINKING_LEVELS.includes(value as TodoThinkingLevel);
23
+ }
24
+
25
+ function getAvailableTodoThinkingLevels(model: unknown): TodoThinkingLevel[] {
26
+ const m = model as ModelLike | undefined;
27
+ if (!m?.reasoning) return ["off"];
28
+ const map = m.thinkingLevelMap;
29
+ return TODO_THINKING_LEVELS.filter((level) => level === "off" || map?.[level] !== null);
30
+ }
31
+
32
+ function buildThinkingPromptParts(model: unknown): { promptSnippet?: string; promptGuidelines?: string[] } {
33
+ const levels = getAvailableTodoThinkingLevels(model);
34
+ if (levels.length <= 1) return {};
35
+ return {
36
+ promptSnippet: `${DEFAULT_PROMPT_SNIPPET} Optional per-item thinking: ${levels.join("|")}.`.trim(),
37
+ promptGuidelines: [
38
+ ...DEFAULT_PROMPT_GUIDELINES,
39
+ `If todoThinking is enabled, assign task \`thinking\` during create/batch_create or update whenever planned items differ in complexity; choose from ${levels.join(", ")}. Use higher thinking for investigation, hard debugging, risky edits, or review; use lower/off for mechanical steps and the final user-facing report. Do not leave all thinking unset for a non-trivial mixed-complexity plan.`,
40
+ ],
41
+ };
42
+ }
14
43
 
15
44
  function isAskUserToolName(toolName: string): boolean {
16
45
  return ASK_USER_TOOL_NAMES.has(toolName);
@@ -45,7 +74,7 @@ function getUnfinishedTodoNudge(): { signature: string; message: string } | unde
45
74
  message: [
46
75
  "Todo auto-nudge: unfinished todo items remain after your last response.",
47
76
  "Continue working on them now. Pick exactly one pending/in_progress item, mark it in_progress if needed, make concrete progress, and update or complete todos immediately as work changes.",
48
- "If the user added/removed/canceled requirements or changed goal/scope/priority/approach, or if discovered facts make the current plan stale/incomplete/impossible, synchronize todos first: update still-relevant items, defer/delete obsolete ones, add new tasks, and adjust blockers/order.",
77
+ "If the user added/removed/canceled requirements or changed goal/scope/approach, or if discovered facts make the current plan stale/incomplete/impossible, synchronize todos first: update still-relevant items, defer/delete obsolete ones, add new tasks, and adjust blockers/order.",
49
78
  "If progress is waiting on user-supplied data, clarification, or a decision, defer the affected plan/todos before your final response instead of leaving them pending/in_progress, so auto-nudge stops until the user replies.",
50
79
  "For non-user blockers, leave the current item in_progress and create/update a blocker task instead of stopping.",
51
80
  "",
@@ -59,9 +88,8 @@ function getPersistedPlanPrompt(path: string): string | undefined {
59
88
  if (unfinished.length === 0) return undefined;
60
89
  const lines = unfinished.slice(0, TODO_NUDGE_LIMIT).map((task) => {
61
90
  const activeForm = task.activeForm ? ` — ${task.activeForm}` : "";
62
- const priority = task.priority ? ` (${task.priority})` : "";
63
91
  const blockedBy = task.blockedBy?.length ? ` (blocked by #${task.blockedBy.join(", #")})` : "";
64
- return `- #${task.id} [${task.status}]${priority} ${task.subject}${activeForm}${blockedBy}`;
92
+ return `- #${task.id} [${task.status}] ${task.subject}${activeForm}${blockedBy}`;
65
93
  });
66
94
  if (unfinished.length > TODO_NUDGE_LIMIT) lines.push(`- …and ${unfinished.length - TODO_NUDGE_LIMIT} more unfinished todo item(s).`);
67
95
  return [
@@ -84,9 +112,86 @@ function emitPersistedPlanPrompt(pi: ExtensionAPI, ctx: ExtensionContext, prompt
84
112
  }
85
113
 
86
114
  export default function (pi: ExtensionAPI) {
115
+ let currentModel: unknown;
116
+ const todoThinkingEnabled = loadPiToolsSuiteConfig(["todo"]).todoThinking;
117
+ const rememberedThinkingByTaskId = new Map<number, TodoThinkingLevel>();
87
118
  let lastNudgedSignature: string | undefined;
88
119
  let nudgeTimer: ReturnType<typeof setTimeout> | undefined;
89
120
  const pendingAskUserToolCallIds = new Set<string>();
121
+ let suppressNextNudgeForThinkingSwitch = false;
122
+
123
+ function registerTodoToolWithCurrentPrompt(): void {
124
+ const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
125
+ registerTodoTool(pi, {
126
+ ...thinkingPrompt,
127
+ afterCommit: async (state, ctx, info) => {
128
+ if (todoThinkingEnabled) applyTodoThinkingAfterCommit(state, info);
129
+ try {
130
+ const sync = syncPersistedPlan(ctx.cwd, state);
131
+ if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
132
+ } catch (err) {
133
+ console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
134
+ }
135
+ },
136
+ });
137
+ }
138
+
139
+ function applyTodoThinkingAfterCommit(state: ReturnType<typeof getState>, info: { action: string; params: TaskMutationParams }): void {
140
+ const mutations = getTodoThinkingMutations(info.action, info.params);
141
+ for (const mutation of mutations) {
142
+ if (mutation.id === undefined || mutation.status === "in_progress") continue;
143
+ restoreTaskThinking(mutation.id);
144
+ }
145
+ restoreInactiveTodoThinking(state);
146
+ for (const mutation of mutations) {
147
+ if (mutation.id === undefined) continue;
148
+ const task = state.tasks.find((item) => item.id === mutation.id);
149
+ if (!task || task.status !== "in_progress" || !task.thinking) continue;
150
+ if (mutation.status !== "in_progress" && mutation.thinking === undefined) continue;
151
+ switchToTaskThinking(task.id, task.thinking);
152
+ }
153
+ }
154
+
155
+ function getTodoThinkingMutations(action: string, params: TaskMutationParams): TaskMutationParams[] {
156
+ if (action === "update") return [params];
157
+ if (action === "batch_update") return params.items ?? [];
158
+ return [];
159
+ }
160
+
161
+ function getCurrentThinkingLevel(): TodoThinkingLevel | undefined {
162
+ const level = (pi as { getThinkingLevel?: () => unknown }).getThinkingLevel?.();
163
+ return isTodoThinkingLevel(level) ? level : undefined;
164
+ }
165
+
166
+ function switchToTaskThinking(taskId: number, level: TodoThinkingLevel): void {
167
+ if (!getAvailableTodoThinkingLevels(currentModel).includes(level)) return;
168
+ const current = getCurrentThinkingLevel();
169
+ if (!current) return;
170
+ if (!rememberedThinkingByTaskId.has(taskId)) rememberedThinkingByTaskId.set(taskId, current);
171
+ if (current !== level) setTodoThinkingLevel(level);
172
+ }
173
+
174
+ function restoreTaskThinking(taskId: number): void {
175
+ const previous = rememberedThinkingByTaskId.get(taskId);
176
+ if (!previous) return;
177
+ rememberedThinkingByTaskId.delete(taskId);
178
+ if (getCurrentThinkingLevel() !== previous) setTodoThinkingLevel(previous);
179
+ }
180
+
181
+ function restoreInactiveTodoThinking(state: ReturnType<typeof getState>): void {
182
+ for (const taskId of [...rememberedThinkingByTaskId.keys()]) {
183
+ const task = state.tasks.find((item) => item.id === taskId);
184
+ if (task?.status === "in_progress") continue;
185
+ restoreTaskThinking(taskId);
186
+ }
187
+ }
188
+
189
+ function setTodoThinkingLevel(level: TodoThinkingLevel): void {
190
+ const setter = (pi as { setThinkingLevel?: (level: TodoThinkingLevel) => void }).setThinkingLevel;
191
+ if (!setter) return;
192
+ suppressNextNudgeForThinkingSwitch = true;
193
+ setter.call(pi, level);
194
+ }
90
195
 
91
196
  function clearNudgeTimer(): void {
92
197
  if (!nudgeTimer) return;
@@ -127,19 +232,12 @@ export default function (pi: ExtensionAPI) {
127
232
  }, delayMs);
128
233
  }
129
234
 
130
- registerTodoTool(pi, {
131
- afterCommit: (state, ctx) => {
132
- try {
133
- const sync = syncPersistedPlan(ctx.cwd, state);
134
- if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
135
- } catch (err) {
136
- console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
137
- }
138
- },
139
- });
235
+ registerTodoToolWithCurrentPrompt();
140
236
  registerTodosCommand(pi);
141
237
 
142
238
  pi.on("session_start", async (_event, ctx) => {
239
+ currentModel = ctx.model;
240
+ registerTodoToolWithCurrentPrompt();
143
241
  const persisted = loadPersistedPlan(ctx.cwd);
144
242
  const loaded = autoClearCompletedTodos(persisted?.state ?? replayFromBranch(ctx));
145
243
  replaceState(loaded.state);
@@ -171,6 +269,11 @@ export default function (pi: ExtensionAPI) {
171
269
  clearNudgeTimer();
172
270
  });
173
271
 
272
+ pi.on("model_select", async (event) => {
273
+ currentModel = event.model;
274
+ if (todoThinkingEnabled) registerTodoToolWithCurrentPrompt();
275
+ });
276
+
174
277
  // Reads getTodos() at render time; do NOT call replayFromBranch here
175
278
  // (branch is stale — message_end runs after tool_execution_end).
176
279
  pi.on("tool_execution_start", async (event) => {
@@ -188,6 +291,12 @@ export default function (pi: ExtensionAPI) {
188
291
  });
189
292
 
190
293
  pi.on("agent_end", async (_event, ctx) => {
294
+ if (suppressNextNudgeForThinkingSwitch) {
295
+ suppressNextNudgeForThinkingSwitch = false;
296
+ clearNudgeTimer();
297
+ return;
298
+ }
299
+
191
300
  if (pendingAskUserToolCallIds.size > 0) {
192
301
  clearNudgeTimer();
193
302
  return;
@@ -22,7 +22,6 @@ function cloneTask(task: Task): Task {
22
22
  return {
23
23
  ...task,
24
24
  blockedBy: task.blockedBy ? [...task.blockedBy] : undefined,
25
- tags: task.tags ? [...task.tags] : undefined,
26
25
  metadata: task.metadata ? { ...task.metadata } : undefined,
27
26
  };
28
27
  }
@@ -1,18 +1,18 @@
1
- import type { Task, TaskAction, TaskMutationParams, TaskPriority, TaskStatus } from "../tool/types.js";
1
+ import type { Task, TaskAction, TaskMutationParams, TaskStatus, TodoThinkingLevel } from "../tool/types.js";
2
2
  import { isTransitionValid } from "./invariants.js";
3
3
  import type { TaskState } from "./state.js";
4
4
  import { detectCycle } from "./task-graph.js";
5
5
 
6
6
  export type Op =
7
- | { kind: "create"; taskId: number }
7
+ | { kind: "create"; taskId: number; replacedCount?: number }
8
8
  | { kind: "update"; id: number; fromStatus: TaskStatus; toStatus: TaskStatus }
9
- | { kind: "batch_create"; ids: number[] }
9
+ | { kind: "batch_create"; ids: number[]; replacedCount?: number }
10
10
  | { kind: "batch_update"; ids: number[] }
11
11
  | { kind: "delete"; id: number; subject: string }
12
- | { kind: "list"; statusFilter?: TaskStatus; priorityFilter?: TaskPriority; tagFilter?: string; blockedOnly: boolean; includeDeleted: boolean }
12
+ | { kind: "list"; statusFilter?: TaskStatus; blockedOnly: boolean; includeDeleted: boolean }
13
13
  | { kind: "get"; task: Task }
14
14
  | { kind: "clear"; count: number }
15
- | { kind: "export"; format: "json" | "markdown"; statusFilter?: TaskStatus; priorityFilter?: TaskPriority; tagFilter?: string; blockedOnly: boolean; includeDeleted: boolean }
15
+ | { kind: "export"; format: "json" | "markdown"; statusFilter?: TaskStatus; blockedOnly: boolean; includeDeleted: boolean }
16
16
  | { kind: "import"; count: number; replaced: boolean }
17
17
  | { kind: "error"; message: string };
18
18
 
@@ -29,9 +29,8 @@ function uniqueNumbers(values: number[] | undefined): number[] {
29
29
  return [...new Set((values ?? []).filter((value) => Number.isFinite(value)))];
30
30
  }
31
31
 
32
- function normalizeTags(tags: string[] | undefined): string[] | undefined {
33
- const normalized = [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
34
- return normalized.length ? normalized : undefined;
32
+ function isTodoThinkingLevel(value: unknown): value is TodoThinkingLevel {
33
+ return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
35
34
  }
36
35
 
37
36
  function findTask(state: TaskState, id: number): Task | undefined {
@@ -87,12 +86,10 @@ function coerceTask(value: unknown, fallbackId: number): Task | undefined {
87
86
  const task: Task = { id: typeof v.id === "number" && Number.isFinite(v.id) ? v.id : fallbackId, subject, status };
88
87
  if (typeof v.description === "string" && v.description) task.description = v.description;
89
88
  if (typeof v.activeForm === "string" && v.activeForm) task.activeForm = v.activeForm;
90
- if (v.priority === "low" || v.priority === "medium" || v.priority === "high" || v.priority === "urgent") task.priority = v.priority;
89
+ if (isTodoThinkingLevel(v.thinking)) task.thinking = v.thinking;
91
90
  if (typeof v.parentId === "number" && Number.isFinite(v.parentId)) task.parentId = v.parentId;
92
91
  const blockedBy = Array.isArray(v.blockedBy) ? uniqueNumbers(v.blockedBy as number[]) : undefined;
93
92
  if (blockedBy?.length) task.blockedBy = blockedBy;
94
- const tags = Array.isArray(v.tags) ? normalizeTags(v.tags.filter((tag): tag is string => typeof tag === "string")) : undefined;
95
- if (tags) task.tags = tags;
96
93
  if (typeof v.owner === "string" && v.owner) task.owner = v.owner;
97
94
  if (v.metadata && typeof v.metadata === "object" && !Array.isArray(v.metadata)) task.metadata = { ...(v.metadata as Record<string, unknown>) };
98
95
  return task;
@@ -114,12 +111,11 @@ function parseMarkdownImport(content: string): Task[] {
114
111
  const tasks: Task[] = [];
115
112
  const stack: Array<{ indent: number; id: number }> = [];
116
113
  for (const line of content.split(/\r?\n/)) {
117
- const match = /^(\s*)- \[([ xX])\](?: #(\d+))?(?: \((low|medium|high|urgent)\))? (.+?)(?: \[(#[^\]]+)\])?$/.exec(line);
114
+ const match = /^(\s*)- \[([ xX])\](?: #(\d+))? (.+?)$/.exec(line);
118
115
  if (!match) continue;
119
116
  const indent = match[1].length;
120
117
  const id = match[3] ? Number(match[3]) : tasks.length + 1;
121
- const tags = match[6] ? normalizeTags(match[6].split(/\s+/).map((tag) => tag.replace(/^#/, ""))) : undefined;
122
- const subjectParts = match[5].split(/\s+⛓\s+/);
118
+ const subjectParts = match[4].split(/\s+⛓\s+/);
123
119
  const blockedBy = subjectParts[1] ? uniqueNumbers(subjectParts[1].split(/\s*,\s*/).map((ref) => Number(ref.replace(/^#/, "")))) : [];
124
120
  while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
125
121
  const rawSubject = subjectParts[0];
@@ -132,10 +128,8 @@ function parseMarkdownImport(content: string): Task[] {
132
128
  subject: rawSubject.replace(/\s+\{deferred\}$/, "").trim(),
133
129
  status,
134
130
  };
135
- if (match[4]) task.priority = match[4] as TaskPriority;
136
131
  if (stack.length) task.parentId = stack[stack.length - 1].id;
137
132
  if (blockedBy.length) task.blockedBy = blockedBy;
138
- if (tags) task.tags = tags;
139
133
  tasks.push(task);
140
134
  stack.push({ indent, id });
141
135
  }
@@ -147,7 +141,6 @@ function remapImportedTasks(imported: readonly Task[], state: TaskState, replace
147
141
  return imported.map((task) => ({
148
142
  ...task,
149
143
  blockedBy: task.blockedBy ? [...task.blockedBy] : undefined,
150
- tags: task.tags ? [...task.tags] : undefined,
151
144
  }));
152
145
  }
153
146
  const idMap = new Map<number, number>();
@@ -157,7 +150,6 @@ function remapImportedTasks(imported: readonly Task[], state: TaskState, replace
157
150
  id: idMap.get(task.id) ?? task.id,
158
151
  parentId: task.parentId !== undefined ? (idMap.get(task.parentId) ?? task.parentId) : undefined,
159
152
  blockedBy: task.blockedBy?.map((id) => idMap.get(id) ?? id),
160
- tags: task.tags ? [...task.tags] : undefined,
161
153
  }));
162
154
  }
163
155
 
@@ -165,26 +157,29 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
165
157
  switch (action) {
166
158
  case "create": {
167
159
  if (!params.subject?.trim()) return errorResult(state, "subject required for create");
160
+ const replacedCount = params.replace === true ? state.tasks.length : 0;
161
+ const baseState = params.replace === true ? { tasks: [], nextId: 1 } : state;
168
162
  if (params.parentId !== undefined && params.parentId !== null) {
169
- const err = validateLiveReference(state, "parentId", params.parentId);
163
+ const err = validateLiveReference(baseState, "parentId", params.parentId);
170
164
  if (err) return errorResult(state, err);
171
165
  }
172
166
  for (const dep of uniqueNumbers(params.blockedBy)) {
173
- const err = validateLiveReference(state, "blockedBy", dep);
167
+ const err = validateLiveReference(baseState, "blockedBy", dep);
174
168
  if (err) return errorResult(state, err);
175
169
  }
176
- const newTask: Task = { id: state.nextId, subject: params.subject.trim(), status: "pending" };
170
+ const newTask: Task = { id: baseState.nextId, subject: params.subject.trim(), status: "pending" };
177
171
  if (params.description) newTask.description = params.description;
178
172
  if (params.activeForm) newTask.activeForm = params.activeForm;
179
- if (params.priority) newTask.priority = params.priority;
173
+ if (params.thinking) newTask.thinking = params.thinking;
180
174
  if (params.parentId !== undefined && params.parentId !== null) newTask.parentId = params.parentId;
181
175
  const blockedBy = uniqueNumbers(params.blockedBy);
182
176
  if (blockedBy.length) newTask.blockedBy = blockedBy;
183
- const tags = normalizeTags(params.tags);
184
- if (tags) newTask.tags = tags;
185
177
  if (params.owner) newTask.owner = params.owner;
186
178
  if (params.metadata) newTask.metadata = { ...params.metadata };
187
- return { state: { tasks: [...state.tasks, newTask], nextId: state.nextId + 1 }, op: { kind: "create", taskId: newTask.id } };
179
+ return {
180
+ state: { tasks: [...baseState.tasks, newTask], nextId: baseState.nextId + 1 },
181
+ op: { kind: "create", taskId: newTask.id, ...(replacedCount > 0 ? { replacedCount } : {}) },
182
+ };
188
183
  }
189
184
 
190
185
  case "update": {
@@ -194,8 +189,8 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
194
189
  const current = state.tasks[idx];
195
190
  const hasMutation =
196
191
  params.subject !== undefined || params.description !== undefined || params.activeForm !== undefined || params.status !== undefined ||
197
- params.priority !== undefined || params.parentId !== undefined || params.clearParent === true || params.owner !== undefined ||
198
- params.metadata !== undefined || params.tags !== undefined || (params.addTags?.length ?? 0) > 0 || (params.removeTags?.length ?? 0) > 0 ||
192
+ params.thinking !== undefined || params.parentId !== undefined || params.clearParent === true || params.owner !== undefined ||
193
+ params.metadata !== undefined ||
199
194
  (params.addBlockedBy?.length ?? 0) > 0 || (params.removeBlockedBy?.length ?? 0) > 0;
200
195
  if (!hasMutation) return errorResult(state, "update requires at least one mutable field");
201
196
 
@@ -230,13 +225,6 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
230
225
  if (detectCycle(state.tasks, current.id, newBlockedBy)) return errorResult(state, "addBlockedBy would create a cycle in the blockedBy graph");
231
226
  }
232
227
 
233
- let newTags = params.tags !== undefined ? normalizeTags(params.tags) : current.tags ? [...current.tags] : undefined;
234
- if (params.addTags?.length) newTags = normalizeTags([...(newTags ?? []), ...params.addTags]);
235
- if (params.removeTags?.length && newTags) {
236
- const remove = new Set(params.removeTags.map((tag) => tag.trim()).filter(Boolean));
237
- newTags = normalizeTags(newTags.filter((tag) => !remove.has(tag)));
238
- }
239
-
240
228
  let newMetadata = current.metadata;
241
229
  if (params.metadata !== undefined) {
242
230
  const merged: Record<string, unknown> = { ...(current.metadata ?? {}) };
@@ -248,14 +236,12 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
248
236
  if (params.subject !== undefined) updated.subject = params.subject;
249
237
  if (params.description !== undefined) updated.description = params.description;
250
238
  if (params.activeForm !== undefined) updated.activeForm = params.activeForm;
251
- if (params.priority !== undefined) updated.priority = params.priority;
239
+ if (params.thinking !== undefined) updated.thinking = params.thinking;
252
240
  if (params.owner !== undefined) updated.owner = params.owner;
253
241
  if (newParentId === undefined) delete updated.parentId;
254
242
  else updated.parentId = newParentId;
255
243
  if (newBlockedBy.length) updated.blockedBy = newBlockedBy;
256
244
  else delete updated.blockedBy;
257
- if (newTags) updated.tags = newTags;
258
- else delete updated.tags;
259
245
  if (newMetadata === undefined) delete updated.metadata;
260
246
  else updated.metadata = newMetadata;
261
247
 
@@ -266,7 +252,8 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
266
252
 
267
253
  case "batch_create": {
268
254
  if (!params.items?.length) return errorResult(state, "items required for batch_create");
269
- let working = state;
255
+ const replacedCount = params.replace === true ? state.tasks.length : 0;
256
+ let working = params.replace === true ? { tasks: [], nextId: 1 } : state;
270
257
  const ids: number[] = [];
271
258
  for (let i = 0; i < params.items.length; i++) {
272
259
  const result = applyTaskMutation(working, "create", { ...params.items[i], action: "create" });
@@ -274,7 +261,7 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
274
261
  if (result.op.kind === "create") ids.push(result.op.taskId);
275
262
  working = result.state;
276
263
  }
277
- return { state: working, op: { kind: "batch_create", ids } };
264
+ return { state: working, op: { kind: "batch_create", ids, ...(replacedCount > 0 ? { replacedCount } : {}) } };
278
265
  }
279
266
 
280
267
  case "batch_update": {
@@ -298,8 +285,6 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
298
285
  includeDeleted: params.includeDeleted === true,
299
286
  blockedOnly: params.blockedOnly === true,
300
287
  ...(params.status ? { statusFilter: params.status } : {}),
301
- ...(params.priority ? { priorityFilter: params.priority } : {}),
302
- ...(params.tag ? { tagFilter: params.tag } : {}),
303
288
  },
304
289
  };
305
290
 
@@ -337,8 +322,6 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
337
322
  includeDeleted: params.includeDeleted === true,
338
323
  blockedOnly: params.blockedOnly === true,
339
324
  ...(params.status ? { statusFilter: params.status } : {}),
340
- ...(params.priority ? { priorityFilter: params.priority } : {}),
341
- ...(params.tag ? { tagFilter: params.tag } : {}),
342
325
  },
343
326
  };
344
327
 
@@ -34,7 +34,6 @@ import {
34
34
  type Task,
35
35
  type TaskAction,
36
36
  type TaskMutationParams,
37
- type TaskPriority,
38
37
  type TaskStatus,
39
38
  TOOL_LABEL,
40
39
  TOOL_NAME,
@@ -63,8 +62,6 @@ const TODOS_ARGUMENT_COMPLETIONS: CommandCompletion[] = [
63
62
  { value: "--ready", label: "--ready", description: "Show pending tasks whose blockers are completed" },
64
63
  { value: "--blocked", label: "--blocked", description: "Show tasks with blockers" },
65
64
  { value: "--tree", label: "--tree", description: "Show parent/subtask tree" },
66
- { value: "--tag ", label: "--tag <tag>", description: "Filter by tag" },
67
- { value: "--priority ", label: "--priority <level>", description: "Filter by priority" },
68
65
  { value: "--status ", label: "--status <status>", description: "Filter by status" },
69
66
  { value: "--export markdown", label: "--export markdown", description: "Export visible todos as Markdown" },
70
67
  { value: "--export json", label: "--export json", description: "Export visible todos as JSON" },
@@ -78,7 +75,12 @@ const PERSIST_ARGUMENT_COMPLETIONS: CommandCompletion[] = [
78
75
  ];
79
76
 
80
77
  interface TodoToolHooks {
81
- afterCommit?: (state: ReturnType<typeof getState>, ctx: ExtensionContext) => void | Promise<void>;
78
+ afterCommit?: (state: ReturnType<typeof getState>, ctx: ExtensionContext, info: { action: TaskAction; params: TaskMutationParams }) => void | Promise<void>;
79
+ }
80
+
81
+ interface TodoToolRegistrationOptions extends TodoToolHooks {
82
+ promptSnippet?: string;
83
+ promptGuidelines?: string[];
82
84
  }
83
85
 
84
86
  type TodoStateEventContext = { sessionManager?: { getSessionFile?: () => unknown } };
@@ -86,8 +88,6 @@ type TodoStateEventEmitter = { events?: { emit?: (channel: string, data: unknown
86
88
 
87
89
  interface TodosCommandOptions {
88
90
  status?: TaskStatus;
89
- priority?: TaskPriority;
90
- tag?: string;
91
91
  blockedOnly: boolean;
92
92
  readyOnly: boolean;
93
93
  activeOnly: boolean;
@@ -125,12 +125,6 @@ function parseTodosArgs(args: unknown): TodosCommandOptions {
125
125
  case "--include-deleted":
126
126
  options.includeDeleted = true;
127
127
  break;
128
- case "--tag":
129
- if (next) options.tag = tokens[++i].replace(/^#/, "");
130
- break;
131
- case "--priority":
132
- if (next === "low" || next === "medium" || next === "high" || next === "urgent") options.priority = tokens[++i] as TaskPriority;
133
- break;
134
128
  case "--status":
135
129
  if (next === "pending" || next === "in_progress" || next === "deferred" || next === "completed" || next === "deleted") options.status = tokens[++i] as TaskStatus;
136
130
  break;
@@ -290,9 +284,6 @@ function filterCommandTasks(tasks: readonly Task[], options: TodosCommandOptions
290
284
  view = view.filter((task) => task.status === "pending" && !isTaskBlocked(task, byId));
291
285
  }
292
286
  if (options.status) view = view.filter((task) => task.status === options.status);
293
- if (options.priority) view = view.filter((task) => task.priority === options.priority);
294
- const tag = options.tag;
295
- if (tag) view = view.filter((task) => task.tags?.includes(tag));
296
287
  if (options.blockedOnly) view = view.filter((task) => (task.blockedBy?.length ?? 0) > 0);
297
288
  return view;
298
289
  }
@@ -343,7 +334,7 @@ export { isTransitionValid } from "./state/invariants.js";
343
334
  export { applyTaskMutation } from "./state/state-reducer.js";
344
335
  export { __resetState, getNextId, getTodos } from "./state/store.js";
345
336
  export { deriveBlocks, detectCycle } from "./state/task-graph.js";
346
- export type { Task, TaskAction, TaskDetails, TaskPriority, TaskStatus } from "./tool/types.js";
337
+ export type { Task, TaskAction, TaskDetails, TaskStatus } from "./tool/types.js";
347
338
  export { TOOL_NAME } from "./tool/types.js";
348
339
 
349
340
  /**
@@ -362,13 +353,13 @@ export function reconstructTodoState(ctx: Parameters<typeof replayFromBranch>[0]
362
353
  export const DEFAULT_PROMPT_SNIPPET = TODO_TOOL_DESCRIPTION.promptSnippet ?? "";
363
354
  export const DEFAULT_PROMPT_GUIDELINES: string[] = TODO_TOOL_DESCRIPTION.promptGuidelines ?? [];
364
355
 
365
- export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolHooks = {}): void {
356
+ export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolRegistrationOptions = {}): void {
366
357
  pi.registerTool({
367
358
  ...TODO_TOOL_DESCRIPTION,
368
359
  name: TOOL_NAME,
369
360
  label: TOOL_LABEL,
370
- promptSnippet: DEFAULT_PROMPT_SNIPPET,
371
- promptGuidelines: DEFAULT_PROMPT_GUIDELINES,
361
+ promptSnippet: hooks.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,
362
+ promptGuidelines: hooks.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,
372
363
  parameters: TodoParamsSchema,
373
364
 
374
365
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
@@ -379,7 +370,7 @@ export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolHooks = {}): v
379
370
  const autoClear = autoClearCompletedTodos(result.state);
380
371
  commitState(autoClear.state);
381
372
  publishTodoState(pi as TodoStateEventEmitter, _ctx, params.action, params as Record<string, unknown>);
382
- await hooks.afterCommit?.(autoClear.state, _ctx as ExtensionContext);
373
+ await hooks.afterCommit?.(autoClear.state, _ctx as ExtensionContext, { action: params.action, params: params as TaskMutationParams });
383
374
  const toolResult = buildToolResult(params.action, params as TaskMutationParams, autoClear.state, result.op);
384
375
  if (!autoClear.cleared) return toolResult;
385
376
  return {
@@ -397,7 +388,7 @@ export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolHooks = {}): v
397
388
 
398
389
  export function registerTodosCommand(pi: ExtensionAPI): void {
399
390
  pi.registerCommand(COMMAND_NAME, {
400
- description: "Show todos on the current branch. Flags: --active, --ready, --blocked, --tree, --tag <tag>, --priority <level>, --status <status>, --export [json|markdown]. Commands: persist on|off|clear|status, scope <id...>",
391
+ description: "Show todos on the current branch. Flags: --active, --ready, --blocked, --tree, --status <status>, --export [json|markdown]. Commands: persist on|off|clear|status, scope <id...>",
401
392
  getArgumentCompletions: (prefix) => completeCommandArguments(String(prefix ?? ""), TODOS_ARGUMENT_COMPLETIONS),
402
393
  handler: async (args, ctx) => {
403
394
  if (handlePersistCommand(args, ctx)) return;
@@ -416,8 +407,6 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
416
407
  includeDeleted: options.includeDeleted,
417
408
  blockedOnly: options.blockedOnly,
418
409
  ...(options.status ? { statusFilter: options.status } : {}),
419
- ...(options.priority ? { priorityFilter: options.priority } : {}),
420
- ...(options.tag ? { tagFilter: options.tag } : {}),
421
410
  };
422
411
  ctx.ui.notify(formatContent(op, exportState), "info");
423
412
  return;
@@ -10,10 +10,9 @@ import type { Task, TaskAction, TaskDetails, TaskMutationParams } from "./types.
10
10
  function formatListLine(t: Task): string {
11
11
  const block = t.blockedBy?.length ? ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}` : "";
12
12
  const parent = t.parentId !== undefined ? ` ↳ #${t.parentId}` : "";
13
- const priority = t.priority ? ` (${t.priority})` : "";
14
- const tags = t.tags?.length ? ` ${t.tags.map((tag) => `#${tag}`).join(" ")}` : "";
13
+ const thinking = t.thinking ? ` {thinking:${t.thinking}}` : "";
15
14
  const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
16
- return `[${t.status}] #${t.id} ${t.subject}${priority}${form}${parent}${block}${tags}`;
15
+ return `[${t.status}] #${t.id} ${t.subject}${thinking}${form}${parent}${block}`;
17
16
  }
18
17
 
19
18
  /**
@@ -25,7 +24,7 @@ function formatGetLines(task: Task, state: TaskState): string {
25
24
  const lines = [`#${task.id} [${task.status}] ${task.subject}`];
26
25
  if (task.description) lines.push(` description: ${task.description}`);
27
26
  if (task.activeForm) lines.push(` activeForm: ${task.activeForm}`);
28
- if (task.priority) lines.push(` priority: ${task.priority}`);
27
+ if (task.thinking) lines.push(` thinking: ${task.thinking}`);
29
28
  if (task.parentId !== undefined) lines.push(` parentId: #${task.parentId}`);
30
29
  if (task.blockedBy?.length) {
31
30
  lines.push(` blockedBy: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`);
@@ -33,7 +32,6 @@ function formatGetLines(task: Task, state: TaskState): string {
33
32
  if (blocks.length) {
34
33
  lines.push(` blocks: ${blocks.map((id) => `#${id}`).join(", ")}`);
35
34
  }
36
- if (task.tags?.length) lines.push(` tags: ${task.tags.map((tag) => `#${tag}`).join(" ")}`);
37
35
  if (task.owner) lines.push(` owner: ${task.owner}`);
38
36
  return lines.join("\n");
39
37
  }
@@ -42,13 +40,15 @@ function filterTasks(op: Extract<Op, { kind: "list" | "export" }>, state: TaskSt
42
40
  let view = state.tasks;
43
41
  if (!op.includeDeleted) view = view.filter((t) => t.status !== "deleted");
44
42
  if (op.statusFilter) view = view.filter((t) => t.status === op.statusFilter);
45
- if (op.priorityFilter) view = view.filter((t) => t.priority === op.priorityFilter);
46
- const tagFilter = op.tagFilter;
47
- if (tagFilter) view = view.filter((t) => t.tags?.includes(tagFilter));
48
43
  if (op.blockedOnly) view = view.filter((t) => (t.blockedBy?.length ?? 0) > 0);
49
44
  return view;
50
45
  }
51
46
 
47
+ function formatReplacePrefix(replacedCount: number | undefined): string {
48
+ if (!replacedCount) return "";
49
+ return `Replaced ${replacedCount} existing todo item${replacedCount === 1 ? "" : "s"}; `;
50
+ }
51
+
52
52
  function formatMarkdownExport(tasks: readonly Task[]): string {
53
53
  const byParent = new Map<number | undefined, Task[]>();
54
54
  for (const task of tasks) {
@@ -62,11 +62,9 @@ function formatMarkdownExport(tasks: readonly Task[]): string {
62
62
  if (seen.has(task.id)) return;
63
63
  seen.add(task.id);
64
64
  const checked = task.status === "completed" ? "x" : " ";
65
- const priority = task.priority ? ` (${task.priority})` : "";
66
65
  const status = task.status === "deferred" ? " {deferred}" : "";
67
- const tags = task.tags?.length ? ` [${task.tags.map((tag) => `#${tag}`).join(" ")}]` : "";
68
66
  const blocked = task.blockedBy?.length ? ` ⛓ ${task.blockedBy.map((id) => `#${id}`).join(",")}` : "";
69
- lines.push(`${" ".repeat(depth)}- [${checked}] #${task.id}${priority} ${task.subject}${status}${blocked}${tags}`);
67
+ lines.push(`${" ".repeat(depth)}- [${checked}] #${task.id} ${task.subject}${status}${blocked}`);
70
68
  for (const child of byParent.get(task.id) ?? []) visit(child, depth + 1);
71
69
  };
72
70
  for (const root of byParent.get(undefined) ?? []) visit(root, 0);
@@ -85,15 +83,15 @@ export function formatContent(op: Op, state: TaskState): string {
85
83
  case "create": {
86
84
  const t = state.tasks.find((x) => x.id === op.taskId);
87
85
  // Defensive — `op.taskId` always resolves on success path.
88
- if (!t) return `Created #${op.taskId}`;
89
- return `Created #${t.id}: ${t.subject} (pending)`;
86
+ if (!t) return `${formatReplacePrefix(op.replacedCount)}Created #${op.taskId}`;
87
+ return `${formatReplacePrefix(op.replacedCount)}Created #${t.id}: ${t.subject} (pending)`;
90
88
  }
91
89
  case "update": {
92
90
  const transition = op.fromStatus !== op.toStatus ? ` (${op.fromStatus} → ${op.toStatus})` : "";
93
91
  return `Updated #${op.id}${transition}`;
94
92
  }
95
93
  case "batch_create":
96
- return `Created ${op.ids.length} tasks: ${op.ids.map((id) => `#${id}`).join(", ")}`;
94
+ return `${formatReplacePrefix(op.replacedCount)}Created ${op.ids.length} tasks: ${op.ids.map((id) => `#${id}`).join(", ")}`;
97
95
  case "batch_update":
98
96
  return `Updated ${op.ids.length} tasks: ${op.ids.map((id) => `#${id}`).join(", ")}`;
99
97
  case "delete":
@@ -144,8 +142,28 @@ export function buildToolResult(
144
142
 
145
143
  function appendWorkflowReminder(text: string, op: Op, state: TaskState): string {
146
144
  if (op.kind === "error" || op.kind === "export") return text;
145
+ const lines = [text];
146
+ if (op.kind === "create" || op.kind === "batch_create") {
147
+ lines.push(
148
+ "Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note.",
149
+ );
150
+ const createdIds = new Set(op.kind === "create" ? [op.taskId] : op.ids);
151
+ const hasOlderUnfinished = !op.replacedCount && state.tasks.some((task) => {
152
+ if (createdIds.has(task.id)) return false;
153
+ return task.status !== "completed" && task.status !== "deleted";
154
+ });
155
+ if (hasOlderUnfinished) {
156
+ lines.push(
157
+ "Reminder: existing unfinished todos are still present. If this is a new plan that supersedes them, use batch_create with replace:true or explicitly update/defer/delete obsolete tasks.",
158
+ );
159
+ }
160
+ }
147
161
  const hasPending = state.tasks.some((task) => task.status === "pending");
148
162
  const hasInProgress = state.tasks.some((task) => task.status === "in_progress");
149
- if (!hasPending || hasInProgress) return text;
150
- return `${text}\n\nReminder: pending todos exist but none is in_progress. Before starting work, call todo update on exactly one task with status in_progress and activeForm.`;
163
+ if (hasPending && !hasInProgress) {
164
+ lines.push(
165
+ "Reminder: pending todos exist but none is in_progress. Before starting work, call todo update on exactly one task with status in_progress and activeForm.",
166
+ );
167
+ }
168
+ return lines.join("\n\n");
151
169
  }