@yagni-app/code-staging 1.1.1-staging.1347.1 → 1.1.1-staging.1352.1

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.
@@ -99,7 +99,7 @@ export declare const COMMUNICATION_CONTRACT: string;
99
99
  */
100
100
  export declare const WRITE_FINDINGS_DOWN: string;
101
101
  /** The driver identity while /ultra is on: base identity + the diamond directive. */
102
- export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your todo_write checklist, so keep it current yourself: update it when you split the job and again as each fanned-out piece lands, not only at the end.";
102
+ export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your TodoWrite checklist, so keep it current yourself: update it when you split the job and again as each fanned-out piece lands, not only at the end.";
103
103
  export declare const PI_IDENTITY_RE: RegExp;
104
104
  /**
105
105
  * Env switch that bypasses the system-prompt rewrite entirely, so pi's
@@ -65,7 +65,7 @@ export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has
65
65
  "surface what they could not verify. Delegate by default and reserve this " +
66
66
  "session for splitting, judging, and synthesis; only trivial work you can " +
67
67
  "finish in a couple of tool calls skips the diamond. Subagents cannot touch " +
68
- "your todo_write checklist, so keep it current yourself: update it when you " +
68
+ "your TodoWrite checklist, so keep it current yourself: update it when you " +
69
69
  "split the job and again as each fanned-out piece lands, not only at the end.";
70
70
  /**
71
71
  * The identity used for the interactive DRIVER session ONLY: {@link
@@ -208,11 +208,18 @@ export async function registerYagni(pi, deps = {}) {
208
208
  // no-position record suggestions reach the model) and record_decision
209
209
  // (flywheel-attributed records send dedupe: true). Run 7.
210
210
  const flywheelState = makeFlywheelState();
211
+ // The session todo checklist: TodoWrite tool (Claude-parity surface), the
212
+ // above-editor widget, and /todos. Branch-replayed, so forks and resumes
213
+ // show the list as it stood at that point. Registered BEFORE the working
214
+ // line so the board's in_progress activeForm can drive the spinner verb.
215
+ const todosHandle = registerTodos(pi);
211
216
  // The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
212
217
  // Registered before the subagent/advisor tools: they publish their live
213
218
  // progress through this handle so the elapsed/token suffix survives their
214
- // overrides. TUI-gated internally (agent_start checks ctx.mode).
215
- const workingLine = registerWorkingLine(pi);
219
+ // overrides. The verb mirrors the todo board's live step when one is
220
+ // in_progress (Claude's currentTodo?.activeForm pattern). TUI-gated
221
+ // internally (agent_start checks ctx.mode).
222
+ const workingLine = registerWorkingLine(pi, { todoVerb: todosHandle.activeVerb });
216
223
  pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
217
224
  // WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
218
225
  // standard-tier extraction, replacing the bash + curl + python dance.
@@ -253,10 +260,6 @@ export async function registerYagni(pi, deps = {}) {
253
260
  // ask_yagni answers the same question next time instead of interrupting a human.
254
261
  pi.registerTool(makeRecordDecisionTool({ ...toolOpts, flywheel: flywheelState }));
255
262
  }
256
- // The visible checklist for multi-step work: the todo_write tool, its
257
- // above-editor widget, and /todos. Branch-replayed, so forks and resumes
258
- // show the list as it stood at that point.
259
- registerTodos(pi);
260
263
  // YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
261
264
  // process has no direct user to answer, so it is never nudged (same gating
262
265
  // as the delegation identity in branding.ts); eval mode is untouched so its
@@ -1480,7 +1483,7 @@ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatCompariso
1480
1483
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
1481
1484
  // The general subagent tool: Claude Code-format agent discovery + fan-out.
1482
1485
  export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
1483
- // The session todo checklist: todo_write tool, widget renderer, /todos.
1486
+ // The session todo checklist: TodoWrite tool, widget renderer, /todos.
1484
1487
  export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
1485
1488
  // P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
1486
1489
  // the session bless-with-remember capture hook.
@@ -309,7 +309,7 @@ export interface RegisterPermissionDeps {
309
309
  export declare const MODE_CONTEXT_TYPE = "yagni-mode-context";
310
310
  /** Legacy alias — the original plan-mode tag, kept for backward compat. */
311
311
  export declare const PLAN_CONTEXT_TYPE = "yagni-mode-context";
312
- export declare const PLAN_CONTEXT_MESSAGE = "[PLAN MODE ACTIVE]\nYou are in plan mode: explore and design, change nothing.\n- Read-only bash commands (ls, grep, git status, gh pr view, etc.) run freely to help you explore.\n- Ambiguous bash commands are reviewed by the Guardian; if non-mutating they run, if potentially mutating you will be asked.\n- write, edit, file_ticket, and update_ticket_status are held by the permission gate; do not attempt them.\n- Read, search, and ask_yagni freely to ground the plan in how this company works.\n- Produce a concrete numbered plan of the steps you would take, with the files involved.\n- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.\n- Once executing, track the plan's steps with todo_write.";
312
+ export declare const PLAN_CONTEXT_MESSAGE = "[PLAN MODE ACTIVE]\nYou are in plan mode: explore and design, change nothing.\n- Read-only bash commands (ls, grep, git status, gh pr view, etc.) run freely to help you explore.\n- Ambiguous bash commands are reviewed by the Guardian; if non-mutating they run, if potentially mutating you will be asked.\n- write, edit, file_ticket, and update_ticket_status are held by the permission gate; do not attempt them.\n- Read, search, and ask_yagni freely to ground the plan in how this company works.\n- Produce a concrete numbered plan of the steps you would take, with the files involved.\n- End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.\n- Once executing, track the plan's steps with TodoWrite.";
313
313
  /** Build the mode-awareness context message for the current permission mode. */
314
314
  export declare function buildModeContextMessage(mode: PermissionMode): string;
315
315
  /**
@@ -215,7 +215,7 @@ You are in plan mode: explore and design, change nothing.
215
215
  - Read, search, and ask_yagni freely to ground the plan in how this company works.
216
216
  - Produce a concrete numbered plan of the steps you would take, with the files involved.
217
217
  - End by asking the user to review the plan; they run /mode auto (or /mode review) to execute it.
218
- - Once executing, track the plan's steps with todo_write.`;
218
+ - Once executing, track the plan's steps with TodoWrite.`;
219
219
  const AUTO_CONTEXT_MESSAGE = `${AUTO_MARKER}
220
220
  You are in auto mode. Coding commands run directly.
221
221
  - Proactively verify your work: run tests, lint, and typecheck after changes.
@@ -43,6 +43,7 @@ export const MIN_SILENT_TURNS = 2;
43
43
  * utterance even when the assistant emitted no visible text block.
44
44
  */
45
45
  export const USER_FACING_TOOLS = new Set([
46
+ "TodoWrite",
46
47
  "todo_write",
47
48
  "file_ticket",
48
49
  "update_ticket_status",
@@ -1,25 +1,48 @@
1
1
  /**
2
2
  * The session todo list — the visible checklist for multi-step work.
3
3
  *
4
- * A `todo_write` tool the model calls with the FULL list every time (replace,
4
+ * A `TodoWrite` tool the model calls with the FULL list every time (replace,
5
5
  * not patch: replacement is idempotent under retries and always renders a
6
6
  * coherent board), a persistent above-editor widget while steps remain open,
7
7
  * and a `/todos` command to pull the list on demand.
8
8
  *
9
+ * The model-facing surface (name, item schema, description, result echo)
10
+ * matches Claude Code's shipped TodoWrite v1 exactly: the tick-immediately /
11
+ * don't-batch habits are trained behavior attached to that shape, and the
12
+ * custom `todo_write` surface was getting weak attachment (lists created at
13
+ * round start, entire rounds ground through unticked, one batch
14
+ * complete-at-end — the frozen-board report). Claude's TodoV2 per-task tools
15
+ * are a feature-flagged experiment there and are deliberately NOT adopted.
16
+ *
17
+ * The UI mirrors Claude Code too: TodoWrite leaves ZERO transcript rows
18
+ * (their renderToolUseMessage() => null; here, zero-line renderCall /
19
+ * renderResult renderers under renderShell "self" — pi removes the row
20
+ * entirely), and the board lives in the above-editor widget. The widget
21
+ * borrows Claude's TaskList details: the "N tasks (X done, Y in progress,
22
+ * Z open)" header, ✔/◼/◻ glyphs with a bold active row, activeForm as the
23
+ * live verb, and prioritized truncation (recently-completed tasks linger
24
+ * ~30s so a fresh tick is still visible, then older completions fall behind
25
+ * open work) with a "… +N in progress, M completed" overflow summary. One
26
+ * deliberate deviation: the list is always pinned above the editor, not
27
+ * behind Claude's ctrl+t toggle.
28
+ *
9
29
  * State follows pi's branching model the same way the session does: the
10
- * canonical list is the LAST `todo_write` tool result on the current branch,
30
+ * canonical list is the LAST todo tool result on the current branch,
11
31
  * reconstructed on session_start/session_tree, so forking or rewinding a
12
32
  * session automatically shows the list as it stood at that point. The
13
33
  * in-memory copy is just a cache of that.
14
34
  */
15
35
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
36
+ import { type Component } from "@earendil-works/pi-tui";
16
37
  import { Type } from "typebox";
17
- export declare const TODO_TOOL_NAME = "todo_write";
38
+ export declare const TODO_TOOL_NAME = "TodoWrite";
39
+ /** The pre-parity tool name. Recorded sessions carry it in tool results. */
40
+ export declare const TODO_LEGACY_TOOL_NAME = "todo_write";
18
41
  export declare const MAX_TODOS = 50;
19
42
  export declare const MAX_TODO_TEXT = 300;
20
43
  /**
21
44
  * Staleness-reminder throttle (both counters must trip): a reminder is
22
- * eligible only after this many assistant turns since the last todo_write AND
45
+ * eligible only after this many assistant turns since the last TodoWrite AND
23
46
  * this many since the last reminder. The two-counter shape (staleness gate +
24
47
  * anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
25
48
  * driver model routinely stops updating the board mid-grind (the frozen
@@ -27,6 +50,11 @@ export declare const MAX_TODO_TEXT = 300;
27
50
  * survive a long run.
28
51
  */
29
52
  export declare const TODO_REMINDER_TURNS = 10;
53
+ /**
54
+ * How long a freshly-completed task lingers at the top of the widget before
55
+ * dropping behind open work — Claude Code's TaskList RECENT_COMPLETED_TTL.
56
+ */
57
+ export declare const TODO_COMPLETED_LINGER_MS = 30000;
30
58
  /**
31
59
  * The desktop's structured state record rides its own widget key, like the
32
60
  * `/go` run state: one JSON line the app parses and renders itself, never
@@ -37,12 +65,17 @@ export declare const TODO_REMINDER_TURNS = 10;
37
65
  export declare const TODO_STATE_KEY = "yagni-todos:state";
38
66
  export type TodoStatus = "pending" | "in_progress" | "completed";
39
67
  export interface TodoItem {
40
- text: string;
68
+ /** Imperative form: what needs to be done ("Run tests"). */
69
+ content: string;
70
+ /** Present continuous form, shown while in_progress ("Running tests"). */
71
+ activeForm: string;
41
72
  status: TodoStatus;
42
73
  }
43
74
  /**
44
75
  * Validate a full replacement list. Strict: this is model input rendered
45
76
  * straight into the terminal. An empty list is valid (it clears the board).
77
+ * Accepts both the current shape ({content, activeForm}) and the legacy
78
+ * {text} shape so old sessions replay cleanly.
46
79
  */
47
80
  export declare function normalizeTodos(raw: unknown): {
48
81
  ok: true;
@@ -58,17 +91,46 @@ export declare function todoSummary(todos: TodoItem[]): {
58
91
  };
59
92
  /** Plain-text checklist (tool results, /todos in headless contexts). */
60
93
  export declare function formatTodoList(todos: TodoItem[]): string;
94
+ /** One count for the whole board — header and overflow always agree. */
95
+ export declare function todoCounts(todos: TodoItem[]): {
96
+ total: number;
97
+ done: number;
98
+ inProgress: number;
99
+ open: number;
100
+ };
101
+ /** Claude Code's widget header: `3 tasks (1 done, 1 in progress, 6 open)`. */
102
+ export declare function formatTodoHeader(todos: TodoItem[]): string;
103
+ /**
104
+ * The every-write result echo — Claude Code's exact reinforcer. The tick
105
+ * habit decays over a long run; this lands on every TodoWrite result
106
+ * (including the create-at-start write, right before the model enters its
107
+ * grind) so the board stays current in the model's attention.
108
+ */
109
+ export declare const TODO_RESULT_ECHO: string;
61
110
  /** The slice of pi's Theme the widget styles with (matches the feed's pattern). */
62
111
  export interface TodoTheme {
63
112
  fg(color: string, s: string): string;
113
+ bold?(s: string): string;
64
114
  strikethrough?(s: string): string;
65
115
  }
116
+ /**
117
+ * Claude Code's task truncation priority: recently-completed first (a fresh
118
+ * tick lingers {@link TODO_COMPLETED_LINGER_MS} so the user sees it land),
119
+ * then in_progress, then pending, then older completed — everything else
120
+ * falls behind the cap and is summarized.
121
+ */
122
+ export declare function prioritizeTodos(todos: TodoItem[], completedAt: (content: string) => number | undefined, nowMs: number): TodoItem[];
123
+ /** Claude Code's overflow summary: `… +2 in progress, 3 completed`. */
124
+ export declare function formatTodoOverflow(hidden: TodoItem[]): string | null;
66
125
  /**
67
126
  * The above-editor checklist. Empty (paint nothing) when there is no list or
68
- * every step is completed — a finished board should leave the screen. Capped
127
+ * every step is completed — a finished board should leave the screen. Rows
128
+ * past the cap are truncated with Claude's prioritization + overflow summary
69
129
  * so header + items + overflow stays under pi's 10-line widget truncation.
130
+ * The in-progress row shows the active form in bold (the live "what am I
131
+ * doing" signal); pending and completed rows show the imperative content.
70
132
  */
71
- export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme): string[];
133
+ export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme, completedAtCache?: Map<string, number>, nowMs?: number): string[];
72
134
  /** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
73
135
  export declare function todoStateLine(todos: TodoItem[]): string;
74
136
  /**
@@ -85,22 +147,23 @@ export declare function shouldRemindTodos(input: {
85
147
  * PURE: the hedged reminder block appended to a tool result when the board has
86
148
  * gone stale. Carries the CURRENT list so the model can reconcile without a
87
149
  * read, and explicitly licenses ignoring it, so an accurate board costs one
88
- * glance rather than a spurious todo_write.
150
+ * glance rather than a spurious TodoWrite.
89
151
  */
90
152
  export declare function formatTodoReminder(todos: TodoItem[]): string;
91
- /** Replay the branch: the last todo_write result is the canonical list. */
153
+ /** Replay the branch: the last todo-tool result is the canonical list. */
92
154
  export declare function reconstructTodos(entries: unknown[]): TodoItem[];
93
155
  type TodoParams = {
94
156
  todos: Array<{
95
- text: string;
157
+ content: string;
158
+ activeForm: string;
96
159
  status: string;
97
160
  }>;
98
161
  };
99
162
  /**
100
- * Build the todo_write tool definition around a shared store. Separated from
163
+ * Build the TodoWrite tool definition around a shared store. Separated from
101
164
  * registration so tests can drive execute directly.
102
165
  */
103
- export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoItem[]) => void): {
166
+ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoItem[]) => void, completedAt?: Map<string, number>): {
104
167
  name: string;
105
168
  label: string;
106
169
  description: string;
@@ -108,10 +171,21 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
108
171
  promptGuidelines: string[];
109
172
  parameters: Type.TObject<{
110
173
  todos: Type.TArray<Type.TObject<{
111
- text: Type.TString;
174
+ content: Type.TString;
175
+ activeForm: Type.TString;
112
176
  status: Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">]>;
113
177
  }>>;
114
178
  }>;
179
+ renderShell: "self";
180
+ renderCall: () => Component;
181
+ renderResult: (result: {
182
+ content?: Array<{
183
+ type: string;
184
+ text?: string;
185
+ }>;
186
+ }, options: {
187
+ isError?: boolean;
188
+ }, theme: TodoTheme) => Component;
115
189
  execute(_toolCallId: string, params: TodoParams, _signal?: AbortSignal, _onUpdate?: unknown, ctx?: ExtensionContext): Promise<{
116
190
  content: {
117
191
  type: "text";
@@ -132,7 +206,15 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
132
206
  isError?: undefined;
133
207
  }>;
134
208
  };
135
- /** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
136
- export declare function registerTodos(pi: ExtensionAPI): void;
209
+ /**
210
+ * Wire the tool, the branch-replay events, the staleness reminder, the
211
+ * widget refresh loop, and /todos.
212
+ */
213
+ /** What registerTodos hands back: the live spinner-verb override. */
214
+ export interface TodosHandle {
215
+ /** The in_progress item's activeForm, or undefined with no active step. */
216
+ activeVerb(): string | undefined;
217
+ }
218
+ export declare function registerTodos(pi: ExtensionAPI): TodosHandle;
137
219
  export {};
138
220
  //# sourceMappingURL=todos.d.ts.map
@@ -1,25 +1,49 @@
1
1
  /**
2
2
  * The session todo list — the visible checklist for multi-step work.
3
3
  *
4
- * A `todo_write` tool the model calls with the FULL list every time (replace,
4
+ * A `TodoWrite` tool the model calls with the FULL list every time (replace,
5
5
  * not patch: replacement is idempotent under retries and always renders a
6
6
  * coherent board), a persistent above-editor widget while steps remain open,
7
7
  * and a `/todos` command to pull the list on demand.
8
8
  *
9
+ * The model-facing surface (name, item schema, description, result echo)
10
+ * matches Claude Code's shipped TodoWrite v1 exactly: the tick-immediately /
11
+ * don't-batch habits are trained behavior attached to that shape, and the
12
+ * custom `todo_write` surface was getting weak attachment (lists created at
13
+ * round start, entire rounds ground through unticked, one batch
14
+ * complete-at-end — the frozen-board report). Claude's TodoV2 per-task tools
15
+ * are a feature-flagged experiment there and are deliberately NOT adopted.
16
+ *
17
+ * The UI mirrors Claude Code too: TodoWrite leaves ZERO transcript rows
18
+ * (their renderToolUseMessage() => null; here, zero-line renderCall /
19
+ * renderResult renderers under renderShell "self" — pi removes the row
20
+ * entirely), and the board lives in the above-editor widget. The widget
21
+ * borrows Claude's TaskList details: the "N tasks (X done, Y in progress,
22
+ * Z open)" header, ✔/◼/◻ glyphs with a bold active row, activeForm as the
23
+ * live verb, and prioritized truncation (recently-completed tasks linger
24
+ * ~30s so a fresh tick is still visible, then older completions fall behind
25
+ * open work) with a "… +N in progress, M completed" overflow summary. One
26
+ * deliberate deviation: the list is always pinned above the editor, not
27
+ * behind Claude's ctrl+t toggle.
28
+ *
9
29
  * State follows pi's branching model the same way the session does: the
10
- * canonical list is the LAST `todo_write` tool result on the current branch,
30
+ * canonical list is the LAST todo tool result on the current branch,
11
31
  * reconstructed on session_start/session_tree, so forking or rewinding a
12
32
  * session automatically shows the list as it stood at that point. The
13
33
  * in-memory copy is just a cache of that.
14
34
  */
35
+ import { Container, Text } from "@earendil-works/pi-tui";
15
36
  import { Type } from "typebox";
37
+ import { logEvent } from "./errorSink.js";
16
38
  import { isDesktopSurface } from "./surface.js";
17
- export const TODO_TOOL_NAME = "todo_write";
39
+ export const TODO_TOOL_NAME = "TodoWrite";
40
+ /** The pre-parity tool name. Recorded sessions carry it in tool results. */
41
+ export const TODO_LEGACY_TOOL_NAME = "todo_write";
18
42
  export const MAX_TODOS = 50;
19
43
  export const MAX_TODO_TEXT = 300;
20
44
  /**
21
45
  * Staleness-reminder throttle (both counters must trip): a reminder is
22
- * eligible only after this many assistant turns since the last todo_write AND
46
+ * eligible only after this many assistant turns since the last TodoWrite AND
23
47
  * this many since the last reminder. The two-counter shape (staleness gate +
24
48
  * anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
25
49
  * driver model routinely stops updating the board mid-grind (the frozen
@@ -27,6 +51,11 @@ export const MAX_TODO_TEXT = 300;
27
51
  * survive a long run.
28
52
  */
29
53
  export const TODO_REMINDER_TURNS = 10;
54
+ /**
55
+ * How long a freshly-completed task lingers at the top of the widget before
56
+ * dropping behind open work — Claude Code's TaskList RECENT_COMPLETED_TTL.
57
+ */
58
+ export const TODO_COMPLETED_LINGER_MS = 30_000;
30
59
  const WIDGET_KEY = "yagni-todos";
31
60
  /**
32
61
  * The desktop's structured state record rides its own widget key, like the
@@ -38,10 +67,32 @@ const WIDGET_KEY = "yagni-todos";
38
67
  export const TODO_STATE_KEY = "yagni-todos:state";
39
68
  /** Header + items + overflow line must stay under pi's 10-line widget cap. */
40
69
  const MAX_WIDGET_ITEMS = 7;
70
+ /** The current desktop record version ({@link todoStateLine}). */
71
+ const TODO_STATE_VERSION = 2;
41
72
  const STATUSES = ["pending", "in_progress", "completed"];
73
+ function coerceItem(raw) {
74
+ const { content, activeForm, text, status } = raw;
75
+ // Legacy `text` items (pre-parity tool calls and recorded sessions) map
76
+ // onto the new shape; the active form falls back to the content.
77
+ const contentText = typeof content === "string" && content.trim().length > 0 ? content : text;
78
+ if (typeof contentText !== "string" || contentText.trim().length === 0) {
79
+ return { error: "each todo needs non-empty content" };
80
+ }
81
+ if (typeof status !== "string" || !STATUSES.includes(status)) {
82
+ return { error: `status must be one of ${STATUSES.join(", ")}` };
83
+ }
84
+ const active = typeof activeForm === "string" && activeForm.trim().length > 0 ? activeForm : contentText;
85
+ return {
86
+ content: contentText.trim().slice(0, MAX_TODO_TEXT),
87
+ activeForm: active.trim().slice(0, MAX_TODO_TEXT),
88
+ status: status,
89
+ };
90
+ }
42
91
  /**
43
92
  * Validate a full replacement list. Strict: this is model input rendered
44
93
  * straight into the terminal. An empty list is valid (it clears the board).
94
+ * Accepts both the current shape ({content, activeForm}) and the legacy
95
+ * {text} shape so old sessions replay cleanly.
45
96
  */
46
97
  export function normalizeTodos(raw) {
47
98
  if (!Array.isArray(raw))
@@ -52,14 +103,10 @@ export function normalizeTodos(raw) {
52
103
  for (const item of raw) {
53
104
  if (!item || typeof item !== "object")
54
105
  return { ok: false, error: "each todo must be an object" };
55
- const { text, status } = item;
56
- if (typeof text !== "string" || text.trim().length === 0) {
57
- return { ok: false, error: "each todo needs non-empty text" };
58
- }
59
- if (typeof status !== "string" || !STATUSES.includes(status)) {
60
- return { ok: false, error: `status must be one of ${STATUSES.join(", ")}` };
61
- }
62
- todos.push({ text: text.trim().slice(0, MAX_TODO_TEXT), status: status });
106
+ const coerced = coerceItem(item);
107
+ if ("error" in coerced)
108
+ return { ok: false, error: coerced.error };
109
+ todos.push(coerced);
63
110
  }
64
111
  return { ok: true, todos };
65
112
  }
@@ -67,7 +114,7 @@ export function todoSummary(todos) {
67
114
  return {
68
115
  done: todos.filter((t) => t.status === "completed").length,
69
116
  total: todos.length,
70
- active: todos.find((t) => t.status === "in_progress")?.text ?? null,
117
+ active: todos.find((t) => t.status === "in_progress")?.activeForm ?? null,
71
118
  };
72
119
  }
73
120
  const GLYPH = { pending: "[ ]", in_progress: "[>]", completed: "[x]" };
@@ -76,40 +123,102 @@ export function formatTodoList(todos) {
76
123
  if (todos.length === 0)
77
124
  return "No todos.";
78
125
  const { done, total } = todoSummary(todos);
79
- const lines = todos.map((t) => `${GLYPH[t.status]} ${t.text}`);
126
+ const lines = todos.map((t) => `${GLYPH[t.status]} ${t.content}`);
80
127
  return [`${done}/${total} done`, ...lines].join("\n");
81
128
  }
129
+ /** One count for the whole board — header and overflow always agree. */
130
+ export function todoCounts(todos) {
131
+ const done = todos.filter((t) => t.status === "completed").length;
132
+ const inProgress = todos.filter((t) => t.status === "in_progress").length;
133
+ return { total: todos.length, done, inProgress, open: todos.length - done - inProgress };
134
+ }
135
+ /** Claude Code's widget header: `3 tasks (1 done, 1 in progress, 6 open)`. */
136
+ export function formatTodoHeader(todos) {
137
+ const { total, done, inProgress, open } = todoCounts(todos);
138
+ const parts = [`${done} done`, ...(inProgress > 0 ? [`${inProgress} in progress`] : []), `${open} open`];
139
+ return `${total} tasks (${parts.join(", ")})`;
140
+ }
141
+ /**
142
+ * The every-write result echo — Claude Code's exact reinforcer. The tick
143
+ * habit decays over a long run; this lands on every TodoWrite result
144
+ * (including the create-at-start write, right before the model enters its
145
+ * grind) so the board stays current in the model's attention.
146
+ */
147
+ export const TODO_RESULT_ECHO = "Todos have been modified successfully. Ensure that you continue to use the " +
148
+ "todo list to track your progress. Please proceed with the current tasks if applicable";
149
+ /**
150
+ * Claude Code's task truncation priority: recently-completed first (a fresh
151
+ * tick lingers {@link TODO_COMPLETED_LINGER_MS} so the user sees it land),
152
+ * then in_progress, then pending, then older completed — everything else
153
+ * falls behind the cap and is summarized.
154
+ */
155
+ export function prioritizeTodos(todos, completedAt, nowMs) {
156
+ const recent = [];
157
+ const older = [];
158
+ for (const t of todos) {
159
+ if (t.status !== "completed")
160
+ continue;
161
+ const at = completedAt(t.content);
162
+ if (at !== undefined && nowMs - at < TODO_COMPLETED_LINGER_MS)
163
+ recent.push(t);
164
+ else
165
+ older.push(t);
166
+ }
167
+ return [
168
+ ...recent,
169
+ ...todos.filter((t) => t.status === "in_progress"),
170
+ ...todos.filter((t) => t.status === "pending"),
171
+ ...older,
172
+ ];
173
+ }
174
+ /** Claude Code's overflow summary: `… +2 in progress, 3 completed`. */
175
+ export function formatTodoOverflow(hidden) {
176
+ if (hidden.length === 0)
177
+ return null;
178
+ const { inProgress, open, done } = todoCounts(hidden);
179
+ const parts = [
180
+ ...(inProgress > 0 ? [`${inProgress} in progress`] : []),
181
+ ...(open > 0 ? [`${open} open`] : []),
182
+ ...(done > 0 ? [`${done} completed`] : []),
183
+ ];
184
+ return parts.length > 0 ? `… +${parts.join(", ")}` : null;
185
+ }
82
186
  /**
83
187
  * The above-editor checklist. Empty (paint nothing) when there is no list or
84
- * every step is completed — a finished board should leave the screen. Capped
188
+ * every step is completed — a finished board should leave the screen. Rows
189
+ * past the cap are truncated with Claude's prioritization + overflow summary
85
190
  * so header + items + overflow stays under pi's 10-line widget truncation.
191
+ * The in-progress row shows the active form in bold (the live "what am I
192
+ * doing" signal); pending and completed rows show the imperative content.
86
193
  */
87
- export function renderTodoWidget(todos, theme) {
88
- const { done, total } = todoSummary(todos);
194
+ export function renderTodoWidget(todos, theme, completedAtCache = new Map(), nowMs = Date.now()) {
195
+ const { total, done } = todoCounts(todos);
89
196
  if (total === 0 || done === total)
90
197
  return [];
91
- const lines = [theme.fg("muted", `Todos ${done}/${total}`)];
92
- const visible = todos.slice(0, MAX_WIDGET_ITEMS);
198
+ const lines = [theme.fg("muted", formatTodoHeader(todos))];
199
+ const ordered = prioritizeTodos(todos, (content) => completedAtCache.get(content), nowMs);
200
+ const visible = ordered.slice(0, MAX_WIDGET_ITEMS);
93
201
  for (const todo of visible) {
94
202
  if (todo.status === "completed") {
95
- const text = theme.strikethrough ? theme.strikethrough(todo.text) : todo.text;
96
- lines.push(`${theme.fg("success", "☑ ")}${theme.fg("dim", text)}`);
203
+ const text = theme.strikethrough ? theme.strikethrough(todo.content) : todo.content;
204
+ lines.push(`${theme.fg("success", "✔ ")}${theme.fg("dim", text)}`);
97
205
  }
98
206
  else if (todo.status === "in_progress") {
99
- lines.push(`${theme.fg("accent", "▶ ")}${theme.fg("text", todo.text)}`);
207
+ const active = theme.bold ? theme.bold(`${todo.activeForm}…`) : `${todo.activeForm}…`;
208
+ lines.push(`${theme.fg("accent", "◼ ")}${theme.fg("text", active)}`);
100
209
  }
101
210
  else {
102
- lines.push(`${theme.fg("dim", "☐ ")}${theme.fg("muted", todo.text)}`);
211
+ lines.push(`${theme.fg("dim", "◻ ")}${theme.fg("muted", todo.content)}`);
103
212
  }
104
213
  }
105
- if (todos.length > visible.length) {
106
- lines.push(theme.fg("dim", `…and ${todos.length - visible.length} more`));
107
- }
214
+ const overflow = formatTodoOverflow(ordered.slice(MAX_WIDGET_ITEMS));
215
+ if (overflow)
216
+ lines.push(theme.fg("dim", overflow));
108
217
  return lines;
109
218
  }
110
219
  /** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
111
220
  export function todoStateLine(todos) {
112
- return JSON.stringify({ v: 1, todos });
221
+ return JSON.stringify({ v: TODO_STATE_VERSION, todos });
113
222
  }
114
223
  /**
115
224
  * PURE: is a staleness reminder due? Only when the board has open work (an
@@ -129,16 +238,16 @@ export function shouldRemindTodos(input) {
129
238
  * PURE: the hedged reminder block appended to a tool result when the board has
130
239
  * gone stale. Carries the CURRENT list so the model can reconcile without a
131
240
  * read, and explicitly licenses ignoring it, so an accurate board costs one
132
- * glance rather than a spurious todo_write.
241
+ * glance rather than a spurious TodoWrite.
133
242
  */
134
243
  export function formatTodoReminder(todos) {
135
- return ("⟦YAGNI todos⟧ The todo_write checklist has not been updated for a while. " +
244
+ return ("⟦YAGNI todos⟧ The TodoWrite checklist has not been updated for a while. " +
136
245
  "If the work has moved on, bring it current now: mark finished steps completed, " +
137
246
  "set the step you are on to in_progress, and add newly discovered steps. " +
138
247
  "If the list is already accurate, ignore this.\n" +
139
248
  formatTodoList(todos));
140
249
  }
141
- /** Replay the branch: the last todo_write result is the canonical list. */
250
+ /** Replay the branch: the last todo-tool result is the canonical list. */
142
251
  export function reconstructTodos(entries) {
143
252
  let todos = [];
144
253
  for (const entry of entries) {
@@ -146,7 +255,9 @@ export function reconstructTodos(entries) {
146
255
  if (e?.type !== "message")
147
256
  continue;
148
257
  const msg = e.message;
149
- if (msg?.role !== "toolResult" || msg.toolName !== TODO_TOOL_NAME)
258
+ if (msg?.role !== "toolResult")
259
+ continue;
260
+ if (msg.toolName !== TODO_TOOL_NAME && msg.toolName !== TODO_LEGACY_TOOL_NAME)
150
261
  continue;
151
262
  const details = msg.details;
152
263
  const normalized = normalizeTodos(details?.todos);
@@ -157,7 +268,10 @@ export function reconstructTodos(entries) {
157
268
  }
158
269
  const parameters = Type.Object({
159
270
  todos: Type.Array(Type.Object({
160
- text: Type.String({ description: "The step, short and imperative." }),
271
+ content: Type.String({ description: "The step, short and imperative (e.g. \"Run tests\")." }),
272
+ activeForm: Type.String({
273
+ description: "Present continuous form shown while in progress (e.g. \"Running tests\").",
274
+ }),
161
275
  status: Type.Union([
162
276
  Type.Literal("pending"),
163
277
  Type.Literal("in_progress"),
@@ -165,7 +279,31 @@ const parameters = Type.Object({
165
279
  ]),
166
280
  }), { description: "The FULL list; it replaces the previous one." }),
167
281
  });
168
- function paintWidget(ctx, todos) {
282
+ /**
283
+ * The zero-line renderer pair that removes TodoWrite's transcript row
284
+ * entirely (collapsed AND ctrl+o expanded) — Claude Code's
285
+ * renderToolUseMessage() => null. The tool still executes and its result
286
+ * still reaches the model; only the visible row is gone.
287
+ */
288
+ function todoRenderers() {
289
+ return {
290
+ renderCall: () => new Container(),
291
+ // Success stays invisible (Claude's renderToolUseMessage() => null); a
292
+ // REJECTED write paints exactly one dim line so the user sees why the
293
+ // board did not change — the model still gets the full error result.
294
+ renderResult: (result, options, theme) => {
295
+ if (!options.isError)
296
+ return new Container();
297
+ const message = result.content
298
+ ?.find((c) => c.type === "text" && typeof c.text === "string")?.text
299
+ ?.split("\n")
300
+ .find((l) => l.length > 0);
301
+ const line = message ? `TodoWrite rejected: ${message}` : "TodoWrite rejected";
302
+ return new Text(theme.fg("error", line), 0, 0);
303
+ },
304
+ };
305
+ }
306
+ function paintWidget(ctx, todos, completedAt) {
169
307
  if (!ctx?.hasUI)
170
308
  return;
171
309
  try {
@@ -180,11 +318,11 @@ function paintWidget(ctx, todos) {
180
318
  return;
181
319
  }
182
320
  const theme = ctx.ui.theme;
183
- const lines = renderTodoWidget(todos, theme);
321
+ const lines = renderTodoWidget(todos, theme, completedAt);
184
322
  ctx.ui.setWidget?.(WIDGET_KEY, lines.length > 0 ? lines : undefined, {
185
323
  placement: "aboveEditor",
186
324
  });
187
- const { done, total } = todoSummary(todos);
325
+ const { total, done } = todoCounts(todos);
188
326
  ctx.ui.setStatus?.(WIDGET_KEY, total > 0 && done < total ? `todos ${done}/${total}` : undefined);
189
327
  }
190
328
  catch {
@@ -192,34 +330,40 @@ function paintWidget(ctx, todos) {
192
330
  }
193
331
  }
194
332
  /**
195
- * Build the todo_write tool definition around a shared store. Separated from
333
+ * Build the TodoWrite tool definition around a shared store. Separated from
196
334
  * registration so tests can drive execute directly.
197
335
  */
198
- export function makeTodoTool(get, set) {
336
+ export function makeTodoTool(get, set, completedAt) {
337
+ const { renderCall, renderResult } = todoRenderers();
199
338
  return {
200
339
  name: TODO_TOOL_NAME,
201
340
  label: "Todos",
202
- description: "Track your plan for multi-step work as a checklist the user can see. " +
203
- "Pass the FULL list every time — it replaces the previous list. Keep exactly one item " +
204
- "in_progress at a time, mark items completed the moment they are done, and add newly " +
205
- "discovered steps as pending. Use it for any task with three or more steps, updating as you go.",
206
- promptSnippet: "todo_write: keep a user-visible checklist for multi-step work (full-list replacement).",
341
+ // Claude Code's shipped description, verbatim.
342
+ description: "Update the todo list for the current session. To be used proactively and often to track " +
343
+ "progress and pending tasks. Make sure that at least one task is in_progress at all times. " +
344
+ "Always provide both content (imperative) and activeForm (present continuous) for each task.",
345
+ promptSnippet: "TodoWrite: keep a user-visible checklist for multi-step work (full-list replacement).",
207
346
  promptGuidelines: [
208
- "Use todo_write proactively when a task needs 3 or more distinct steps, requires careful " +
347
+ "Use TodoWrite proactively when a task needs 3 or more distinct steps, requires careful " +
209
348
  "planning, or the user gives you a list of things (numbered or comma-separated).",
210
349
  "Capture new instructions as todos the moment you receive them, and mark a step in_progress " +
211
- "BEFORE you start working on it.",
350
+ "BEFORE you start working on it. Ideally only one task is in_progress at a time.",
212
351
  "When in doubt, use it — a visible checklist answers \"is it stuck?\" without the user having " +
213
352
  "to interrupt.",
214
353
  "Skip it when there is only one straightforward task, the work is trivial, or the request is " +
215
354
  "purely conversational or informational — in those cases just do the task directly.",
216
355
  "Pass the FULL list every call; it replaces the previous one. Keep exactly ONE item in_progress " +
217
- "at a time.",
218
- "Mark a step completed the moment it is done (do not batch completions), and add newly " +
219
- "discovered steps as pending. Only mark a step completed when it is fully done — if tests " +
220
- "fail or work is partial, leave it in_progress and add a new step for the blocker.",
356
+ "at a time, and remove tasks that are no longer relevant from the list entirely.",
357
+ "Update task status in real time as you work: mark a step completed the MOMENT it is done " +
358
+ "(do not batch completions), and add newly discovered follow-up steps as pending.",
359
+ "Only mark a step completed when it is FULLY accomplished — if tests fail, work is partial, or " +
360
+ "you hit a blocker, leave it in_progress and add a new step for what must be resolved.",
221
361
  ],
222
362
  parameters,
363
+ // Claude Code's renderToolUseMessage() => null: no transcript row at all.
364
+ renderShell: "self",
365
+ renderCall,
366
+ renderResult,
223
367
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
224
368
  const normalized = normalizeTodos(params.todos);
225
369
  if (!normalized.ok) {
@@ -230,15 +374,14 @@ export function makeTodoTool(get, set) {
230
374
  };
231
375
  }
232
376
  set(normalized.todos);
233
- paintWidget(ctx, normalized.todos);
377
+ paintWidget(ctx, normalized.todos, completedAt);
234
378
  return {
235
- content: [{ type: "text", text: formatTodoList(normalized.todos) }],
379
+ content: [{ type: "text", text: `${formatTodoList(normalized.todos)}\n\n${TODO_RESULT_ECHO}` }],
236
380
  details: { todos: normalized.todos },
237
381
  };
238
382
  },
239
383
  };
240
384
  }
241
- /** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
242
385
  export function registerTodos(pi) {
243
386
  let todos = [];
244
387
  // Staleness-reminder counters (see TODO_REMINDER_TURNS). Session-local like
@@ -246,13 +389,49 @@ export function registerTodos(pi) {
246
389
  // opens with an instantly-due reminder.
247
390
  let turnsSinceWrite = 0;
248
391
  let turnsSinceReminder = 0;
392
+ // Completion timestamps for the widget's recently-completed priority
393
+ // (Claude's RECENT_COMPLETED_TTL linger): a fresh tick stays at the top of
394
+ // the board for ~30s, then drops behind open work. Entries only ever leave
395
+ // when their item leaves the board — an age-based eviction would re-stamp a
396
+ // still-completed item as fresh on the next observe loop (the !has guard
397
+ // passes), re-floating it every few minutes. prioritizeTodos already treats
398
+ // an aged-out stamp as "older"; the cache never needs a sweeper.
399
+ const completedAt = new Map();
400
+ const observe = (next, now = Date.now()) => {
401
+ const seen = new Set(next.map((t) => t.content));
402
+ for (const [content] of completedAt) {
403
+ if (!seen.has(content))
404
+ completedAt.delete(content);
405
+ }
406
+ for (const t of next) {
407
+ if (t.status === "completed" && !completedAt.has(t.content))
408
+ completedAt.set(t.content, now);
409
+ }
410
+ };
249
411
  const reconstruct = (ctx) => {
250
412
  try {
251
413
  todos = reconstructTodos(ctx.sessionManager.getBranch());
252
414
  }
253
- catch {
415
+ catch (err) {
416
+ // A replay failure must never break session start, but silently wiping
417
+ // the board hides a broken session file — leave a diagnostic trail.
418
+ logEvent({
419
+ source: "todos",
420
+ level: "warn",
421
+ event: "reconstruct_failed",
422
+ fields: { error: err instanceof Error ? err.message : String(err) },
423
+ });
254
424
  todos = [];
255
425
  }
426
+ // Seed the linger cache so a resumed/forked board orders deterministically:
427
+ // completed items from history rank as "older" (outside the linger
428
+ // window), exactly like a live item whose tick has aged out.
429
+ completedAt.clear();
430
+ for (const t of todos) {
431
+ if (t.status === "completed") {
432
+ completedAt.set(t.content, Date.now() - TODO_COMPLETED_LINGER_MS - 1);
433
+ }
434
+ }
256
435
  turnsSinceWrite = 0;
257
436
  turnsSinceReminder = 0;
258
437
  paintWidget(ctx, todos);
@@ -260,7 +439,7 @@ export function registerTodos(pi) {
260
439
  pi.on("session_start", async (_event, ctx) => reconstruct(ctx));
261
440
  pi.on("session_tree", async (_event, ctx) => reconstruct(ctx));
262
441
  // Turn counting: one tick per finalized assistant message, the same "turn"
263
- // the model experiences between opportunities to call todo_write.
442
+ // the model experiences between opportunities to call TodoWrite.
264
443
  pi.on("message_end", async (event) => {
265
444
  if (event.message?.role === "assistant") {
266
445
  turnsSinceWrite += 1;
@@ -269,11 +448,11 @@ export function registerTodos(pi) {
269
448
  });
270
449
  // The reminder rides an existing tool result (the same result-modification
271
450
  // seam ambient recall uses), so it reaches the model mid-run without
272
- // spending a turn. Never appended to todo_write's own result, and fail-soft:
273
- // a reminder must never break a tool call.
451
+ // spending a turn. Never appended to the todo tool's own result, and
452
+ // fail-soft: a reminder must never break a tool call.
274
453
  pi.on("tool_result", async (event) => {
275
454
  try {
276
- if (event.toolName === TODO_TOOL_NAME)
455
+ if (event.toolName === TODO_TOOL_NAME || event.toolName === TODO_LEGACY_TOOL_NAME)
277
456
  return;
278
457
  if (!shouldRemindTodos({ todos, turnsSinceWrite, turnsSinceReminder }))
279
458
  return;
@@ -291,8 +470,9 @@ export function registerTodos(pi) {
291
470
  });
292
471
  pi.registerTool(makeTodoTool(() => todos, (next) => {
293
472
  todos = next;
473
+ observe(next);
294
474
  turnsSinceWrite = 0;
295
- }));
475
+ }, completedAt));
296
476
  pi.registerCommand("todos", {
297
477
  description: "Show the agent's current task list for this session.",
298
478
  handler: async (_args, ctx) => {
@@ -303,5 +483,8 @@ export function registerTodos(pi) {
303
483
  : formatTodoList(todos), "info");
304
484
  },
305
485
  });
486
+ return {
487
+ activeVerb: () => todos.find((t) => t.status === "in_progress")?.activeForm,
488
+ };
306
489
  }
307
490
  //# sourceMappingURL=todos.js.map
@@ -44,6 +44,13 @@ export interface RegisterWorkingLineDeps {
44
44
  pickVerb?: (verbs: readonly string[]) => string;
45
45
  /** Refresh cadence for the elapsed clock. */
46
46
  tickMs?: number;
47
+ /**
48
+ * Live override for the verb when no subagent/advisor activity text is
49
+ * set — the session todo board's in_progress activeForm ("Running tests"),
50
+ * Claude Code's `currentTodo?.activeForm ?? randomVerb` pattern. Called on
51
+ * every refresh so a mid-loop tick changes the line without re-wiring.
52
+ */
53
+ todoVerb?: () => string | undefined;
47
54
  }
48
55
  export declare function registerWorkingLine(pi: ExtensionAPI, deps?: RegisterWorkingLineDeps): WorkingLineHandle;
49
56
  //# sourceMappingURL=workingLine.d.ts.map
@@ -65,7 +65,13 @@ export function registerWorkingLine(pi, deps = {}) {
65
65
  if (!ui || startedAt === undefined)
66
66
  return;
67
67
  try {
68
- ui.setWorkingMessage?.(composeWorkingMessage({ activity, verb, elapsedMs: now() - startedAt, outputTokens }));
68
+ const verbOverride = activity === undefined ? deps.todoVerb?.() : undefined;
69
+ ui.setWorkingMessage?.(composeWorkingMessage({
70
+ activity,
71
+ verb: verbOverride ?? verb,
72
+ elapsedMs: now() - startedAt,
73
+ outputTokens,
74
+ }));
69
75
  }
70
76
  catch {
71
77
  // The status line must never break a turn.
@@ -36,5 +36,5 @@ export declare function promptEnrichmentDisabled(env: NodeJS.ProcessEnv): boolea
36
36
  * model, and the load-bearing instructions (ask_yagni contract, delegation)
37
37
  * live elsewhere in the prompt.
38
38
  */
39
- export declare const ENGINEERING_PRACTICE_SECTION = "Engineering practice:\n\nAnswering vs acting: distinguish what the user is asking for before responding. When the user asks you to analyze, investigate, find a root cause, study how something works, explore an approach, or asks a strategic or advisory question (\"should we...\", \"what's your read on...\", \"go/no-go on...\"), answer in prose \u2014 do not start coding or editing files. When the user asks you to implement, fix, or change something, use your tools to make the actual edits and run the actual commands \u2014 do not answer with a description of what you would do, or with code for the user to apply themselves.\n\nConventions:\n- Never assume a library is available, however well known. Before using one, confirm the project already depends on it (its package manifest, or imports in neighboring files).\n- When editing, read the surrounding code and its imports first; match the file's existing style, naming, and patterns rather than introducing your own.\n- When creating a new file or component, study an existing sibling first and follow its structure.\n- Never write code that logs or exposes secrets, keys, or credentials.\n\nVerification:\n- Consider what the code you are changing is supposed to do (from its name, location, and callers) before you change it.\n- Verify changes with the project's own tests when possible. Never assume a test framework or command \u2014 check the README, package scripts, or neighboring tests for the real one.\n- After completing a task, run the project's lint and typecheck commands if you know them; if you cannot find them, ask the user and suggest recording them in AGENTS.md for next time.\n\nVersion control:\n- No unsolicited commits: commit only when the user asked for one or the task at hand clearly calls for it.\n\nGit safety:\n- You may be in a dirty git worktree. Never revert existing changes you did not make unless explicitly asked \u2014 these were made by the user.\n- If there are unrelated changes in files you are touching, read and work with them rather than reverting.\n- If changes appear in unrelated files, ignore them and do not revert.\n- Do not amend a commit unless explicitly asked.\n- If you notice unexpected changes you did not make while working, stop immediately and ask the user.\n- Never use destructive git commands (git reset --hard, git checkout --) unless the user explicitly requests or approves them.\n\nTodo discipline:\n- Track multi-step work with todo_write: keep exactly one item in_progress at a time, mark items completed the moment they are done, and add newly discovered steps as pending.\n- Do not batch-complete items or create single-step plans. Skip planning for trivially small work (~25% of tasks).\n\nMode awareness:\n- In auto mode, proactively run tests, lint, and typecheck after your changes.\n- In review mode, propose verification steps but wait for approval before running them.\n- In plan mode, explore and design only \u2014 the gate holds all writes.\n\nCommunication:\n- Answer directly, without preamble or postamble (\"Here is what I will do next...\", \"Based on the information provided...\"). Match the length of your answer to the question.\n- After making edits, report the outcome briefly; do not restate the diff or explain the code you just wrote unless asked.\n- Do not add code comments that narrate what you changed or why the change is correct; comments are for future readers of the code.\n- Reference code as file_path:line_number so the user can jump to it.\n- Before running a non-trivial command that changes state, say in one line what it does and why.\n- Never guess or fabricate URLs. Only use URLs the user provided or that appear in local files.\n- No emojis unless the user asks for them.";
39
+ export declare const ENGINEERING_PRACTICE_SECTION = "Engineering practice:\n\nAnswering vs acting: distinguish what the user is asking for before responding. When the user asks you to analyze, investigate, find a root cause, study how something works, explore an approach, or asks a strategic or advisory question (\"should we...\", \"what's your read on...\", \"go/no-go on...\"), answer in prose \u2014 do not start coding or editing files. When the user asks you to implement, fix, or change something, use your tools to make the actual edits and run the actual commands \u2014 do not answer with a description of what you would do, or with code for the user to apply themselves.\n\nConventions:\n- Never assume a library is available, however well known. Before using one, confirm the project already depends on it (its package manifest, or imports in neighboring files).\n- When editing, read the surrounding code and its imports first; match the file's existing style, naming, and patterns rather than introducing your own.\n- When creating a new file or component, study an existing sibling first and follow its structure.\n- Never write code that logs or exposes secrets, keys, or credentials.\n\nVerification:\n- Consider what the code you are changing is supposed to do (from its name, location, and callers) before you change it.\n- Verify changes with the project's own tests when possible. Never assume a test framework or command \u2014 check the README, package scripts, or neighboring tests for the real one.\n- After completing a task, run the project's lint and typecheck commands if you know them; if you cannot find them, ask the user and suggest recording them in AGENTS.md for next time.\n\nVersion control:\n- No unsolicited commits: commit only when the user asked for one or the task at hand clearly calls for it.\n\nGit safety:\n- You may be in a dirty git worktree. Never revert existing changes you did not make unless explicitly asked \u2014 these were made by the user.\n- If there are unrelated changes in files you are touching, read and work with them rather than reverting.\n- If changes appear in unrelated files, ignore them and do not revert.\n- Do not amend a commit unless explicitly asked.\n- If you notice unexpected changes you did not make while working, stop immediately and ask the user.\n- Never use destructive git commands (git reset --hard, git checkout --) unless the user explicitly requests or approves them.\n\nTodo discipline:\n- Track multi-step work with TodoWrite: keep exactly one item in_progress at a time, mark items completed the moment they are done, and add newly discovered steps as pending.\n- Do not batch-complete items or create single-step plans. Skip planning for trivially small work (~25% of tasks).\n\nMode awareness:\n- In auto mode, proactively run tests, lint, and typecheck after your changes.\n- In review mode, propose verification steps but wait for approval before running them.\n- In plan mode, explore and design only \u2014 the gate holds all writes.\n\nCommunication:\n- Answer directly, without preamble or postamble (\"Here is what I will do next...\", \"Based on the information provided...\"). Match the length of your answer to the question.\n- After making edits, report the outcome briefly; do not restate the diff or explain the code you just wrote unless asked.\n- Do not add code comments that narrate what you changed or why the change is correct; comments are for future readers of the code.\n- Reference code as file_path:line_number so the user can jump to it.\n- Before running a non-trivial command that changes state, say in one line what it does and why.\n- Never guess or fabricate URLs. Only use URLs the user provided or that appear in local files.\n- No emojis unless the user asks for them.";
40
40
  //# sourceMappingURL=promptEnrichment.d.ts.map
@@ -66,7 +66,7 @@ Git safety:
66
66
  - Never use destructive git commands (git reset --hard, git checkout --) unless the user explicitly requests or approves them.
67
67
 
68
68
  Todo discipline:
69
- - Track multi-step work with todo_write: keep exactly one item in_progress at a time, mark items completed the moment they are done, and add newly discovered steps as pending.
69
+ - Track multi-step work with TodoWrite: keep exactly one item in_progress at a time, mark items completed the moment they are done, and add newly discovered steps as pending.
70
70
  - Do not batch-complete items or create single-step plans. Skip planning for trivially small work (~25% of tasks).
71
71
 
72
72
  Mode awareness:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.1.1-staging.1347.1",
3
+ "version": "1.1.1-staging.1352.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "54aa68c2232922e7b45ec4563c18fd2995dec236"
61
+ "yagniSourceSha": "1a93f050375804c9206e4660c812aebbb1ec338a"
62
62
  }