@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.
- package/dist/extension/branding.d.ts +1 -1
- package/dist/extension/branding.js +1 -1
- package/dist/extension/index.js +10 -7
- package/dist/extension/permission/gate.d.ts +1 -1
- package/dist/extension/permission/gate.js +1 -1
- package/dist/extension/silentTurnReminder.js +1 -0
- package/dist/extension/todos.d.ts +97 -15
- package/dist/extension/todos.js +240 -57
- package/dist/extension/workingLine.d.ts +7 -0
- package/dist/extension/workingLine.js +7 -1
- package/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
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
|
package/dist/extension/index.js
CHANGED
|
@@ -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.
|
|
215
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session todo list — the visible checklist for multi-step work.
|
|
3
3
|
*
|
|
4
|
-
* A `
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
150
|
+
* glance rather than a spurious TodoWrite.
|
|
89
151
|
*/
|
|
90
152
|
export declare function formatTodoReminder(todos: TodoItem[]): string;
|
|
91
|
-
/** Replay the branch: the last
|
|
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
|
-
|
|
157
|
+
content: string;
|
|
158
|
+
activeForm: string;
|
|
96
159
|
status: string;
|
|
97
160
|
}>;
|
|
98
161
|
};
|
|
99
162
|
/**
|
|
100
|
-
* Build the
|
|
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
|
-
|
|
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
|
-
/**
|
|
136
|
-
|
|
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
|
package/dist/extension/todos.js
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session todo list — the visible checklist for multi-step work.
|
|
3
3
|
*
|
|
4
|
-
* A `
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
|
56
|
-
if (
|
|
57
|
-
return { ok: false, error:
|
|
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")?.
|
|
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.
|
|
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.
|
|
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 {
|
|
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",
|
|
92
|
-
const
|
|
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.
|
|
96
|
-
lines.push(`${theme.fg("success", "
|
|
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
|
-
|
|
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", "
|
|
211
|
+
lines.push(`${theme.fg("dim", "◻ ")}${theme.fg("muted", todo.content)}`);
|
|
103
212
|
}
|
|
104
213
|
}
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
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
|
|
241
|
+
* glance rather than a spurious TodoWrite.
|
|
133
242
|
*/
|
|
134
243
|
export function formatTodoReminder(todos) {
|
|
135
|
-
return ("⟦YAGNI todos⟧ The
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
promptSnippet: "
|
|
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
|
|
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
|
-
"
|
|
219
|
-
"
|
|
220
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/promptEnrichment.js
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
61
|
+
"yagniSourceSha": "1a93f050375804c9206e4660c812aebbb1ec338a"
|
|
62
62
|
}
|