@yagni-app/code-staging 1.1.1-staging.1347.1 → 1.1.1-staging.1355.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/README.md CHANGED
@@ -23,6 +23,9 @@ correct, autonomous work than a coding agent that starts blank.
23
23
  npm install -g @yagni-app/code
24
24
  ```
25
25
 
26
+ Requires Node.js 22.19 or newer (`node --version`); an older Node stops at
27
+ launch with an upgrade message instead of crashing mid-session.
28
+
26
29
  The command it installs is `yagni`:
27
30
 
28
31
  ```bash
package/dist/bin.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * yagni — the published bin entry.
4
+ *
5
+ * Deliberately tiny: the only static imports are the dependency-free Node
6
+ * version gate and the (equally dependency-free) distribution record that
7
+ * names the installed channel. Everything else (`cli.js` and the module graph
8
+ * behind it) is loaded dynamically AFTER the check passes, so an old Node
9
+ * prints one clear message and exits instead of blowing up inside a
10
+ * dependency — or failing to parse one. `cli.js` stays directly runnable
11
+ * (`node dist/cli.js`) for the e2e lanes; its own entrypoint guard is false
12
+ * when this shim is argv[1], so the shim calls `runAsEntrypoint()` explicitly.
13
+ *
14
+ * Both failure paths set `process.exitCode` and return instead of calling
15
+ * `process.exit()`: stderr on a pipe is asynchronous on Windows, and an
16
+ * immediate exit can truncate the very diagnostic this shim exists to print.
17
+ * Letting the event loop drain flushes it.
18
+ *
19
+ * Behavioral coverage lives in `test/bin.test.ts`, which spawns this file
20
+ * under a faked-old `process.versions.node` and against a stub launcher that
21
+ * fails to load.
22
+ */
23
+ export {};
24
+ //# sourceMappingURL=bin.d.ts.map
package/dist/bin.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * yagni — the published bin entry.
4
+ *
5
+ * Deliberately tiny: the only static imports are the dependency-free Node
6
+ * version gate and the (equally dependency-free) distribution record that
7
+ * names the installed channel. Everything else (`cli.js` and the module graph
8
+ * behind it) is loaded dynamically AFTER the check passes, so an old Node
9
+ * prints one clear message and exits instead of blowing up inside a
10
+ * dependency — or failing to parse one. `cli.js` stays directly runnable
11
+ * (`node dist/cli.js`) for the e2e lanes; its own entrypoint guard is false
12
+ * when this shim is argv[1], so the shim calls `runAsEntrypoint()` explicitly.
13
+ *
14
+ * Both failure paths set `process.exitCode` and return instead of calling
15
+ * `process.exit()`: stderr on a pipe is asynchronous on Windows, and an
16
+ * immediate exit can truncate the very diagnostic this shim exists to print.
17
+ * Letting the event loop drain flushes it.
18
+ *
19
+ * Behavioral coverage lives in `test/bin.test.ts`, which spawns this file
20
+ * under a faked-old `process.versions.node` and against a stub launcher that
21
+ * fails to load.
22
+ */
23
+ import { DISTRIBUTION } from "./distribution.js";
24
+ import { nodeCheckSkipped, nodeVersionProblem } from "./nodeVersion.js";
25
+ function debugEnabled() {
26
+ const value = process.env.YAGNI_DEBUG;
27
+ return value !== undefined && value !== "" && value !== "0";
28
+ }
29
+ async function main() {
30
+ const problem = nodeCheckSkipped() ? null : nodeVersionProblem(process.versions.node, DISTRIBUTION);
31
+ if (problem !== null) {
32
+ process.stderr.write(`${problem}\n`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ // A launcher that fails to LOAD (corrupt or partial install, a dependency
37
+ // missing from node_modules) would otherwise die as a raw unhandled
38
+ // rejection before the launcher's crash handlers exist — the same opaque
39
+ // failure this shim is here to prevent. No crash report is possible at this
40
+ // point (the reporter is part of what failed to load), so say what to do
41
+ // instead. The message carries the error class and message; YAGNI_DEBUG=1
42
+ // adds the full stack (resolution chain, parse location) for a support thread.
43
+ let launcher;
44
+ try {
45
+ launcher = await import("./cli.js");
46
+ }
47
+ catch (err) {
48
+ const name = err instanceof Error && err.name ? err.name : "Error";
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ const lines = [
51
+ `${DISTRIBUTION.displayName} failed to load: ${name}: ${message}`,
52
+ "",
53
+ "The install looks incomplete or corrupt. Reinstall it:",
54
+ "",
55
+ ` npm install -g ${DISTRIBUTION.packageName}`,
56
+ "",
57
+ ];
58
+ if (debugEnabled()) {
59
+ const stack = err instanceof Error && err.stack ? err.stack : "(no stack)";
60
+ const cause = err instanceof Error && err.cause !== undefined ? `\ncause: ${String(err.cause)}` : "";
61
+ lines.push(`${stack}${cause}`, "");
62
+ }
63
+ else {
64
+ lines.push("(YAGNI_DEBUG=1 prints the full stack.)", "");
65
+ }
66
+ process.stderr.write(lines.join("\n"));
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ launcher.runAsEntrypoint();
71
+ }
72
+ await main();
73
+ //# sourceMappingURL=bin.js.map
package/dist/cli.d.ts CHANGED
@@ -166,5 +166,11 @@ export declare function main(argv: string[]): Promise<number>;
166
166
  * test import (argv[1] points at the test runner) does not.
167
167
  */
168
168
  export declare function isEntrypoint(argv1: string | undefined, moduleUrl: string): boolean;
169
+ /**
170
+ * Run the launcher as the process entrypoint. Called by the published bin
171
+ * shim (`bin.js`, after the Node version gate) and by the guard below when
172
+ * this module is executed directly (`node dist/cli.js`, the e2e lanes).
173
+ */
174
+ export declare function runAsEntrypoint(): void;
169
175
  export {};
170
176
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -889,9 +889,12 @@ export function isEntrypoint(argv1, moduleUrl) {
889
889
  };
890
890
  return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
891
891
  }
892
- // Only auto-run when invoked as the CLI entry, so tests can import this module
893
- // (e.g. to exercise wantsHelp) without spawning the agent.
894
- if (isEntrypoint(process.argv[1], import.meta.url)) {
892
+ /**
893
+ * Run the launcher as the process entrypoint. Called by the published bin
894
+ * shim (`bin.js`, after the Node version gate) and by the guard below when
895
+ * this module is executed directly (`node dist/cli.js`, the e2e lanes).
896
+ */
897
+ export function runAsEntrypoint() {
895
898
  // Crash reporting for the LAUNCHER process only (pi runs as a child and the
896
899
  // extension covers the session side). Fire-and-forget, sanitized, bounded;
897
900
  // YAGNI_DISABLE_CRASH_REPORTS=1 turns it off. Registered before main() so a
@@ -904,4 +907,9 @@ if (isEntrypoint(process.argv[1], import.meta.url)) {
904
907
  process.exit(1);
905
908
  });
906
909
  }
910
+ // Only auto-run when invoked as the CLI entry, so tests can import this module
911
+ // (e.g. to exercise wantsHelp) without spawning the agent.
912
+ if (isEntrypoint(process.argv[1], import.meta.url)) {
913
+ runAsEntrypoint();
914
+ }
907
915
  //# sourceMappingURL=cli.js.map
@@ -77,6 +77,13 @@ export interface SanitizedCrash {
77
77
  * payloads…) are never touched.
78
78
  */
79
79
  export declare function sanitizeCrashError(err: unknown, opts?: SanitizeCrashOptions): SanitizedCrash;
80
+ /**
81
+ * OS, arch AND the Node version. The runtime is a first-class crash cause
82
+ * (an old Node dies inside undici on `zlib.createZstdDecompress`), and it
83
+ * rides the existing `platform` field so the backend and its Sentry tag need
84
+ * no change. Mirrored in `pi-extension-yagni/src/crashReport.ts`.
85
+ */
86
+ export declare function platformLabel(): string;
80
87
  export type CrashClient = "cli" | "desktop" | "desktop-driver";
81
88
  export interface CrashReportInput {
82
89
  client: CrashClient;
@@ -177,6 +177,15 @@ export function sanitizeCrashError(err, opts = {}) {
177
177
  ...(cappedStack !== undefined ? { stack: cappedStack } : {}),
178
178
  };
179
179
  }
180
+ /**
181
+ * OS, arch AND the Node version. The runtime is a first-class crash cause
182
+ * (an old Node dies inside undici on `zlib.createZstdDecompress`), and it
183
+ * rides the existing `platform` field so the backend and its Sentry tag need
184
+ * no change. Mirrored in `pi-extension-yagni/src/crashReport.ts`.
185
+ */
186
+ export function platformLabel() {
187
+ return `${process.platform} ${process.arch} node ${process.version}`;
188
+ }
180
189
  /**
181
190
  * Sanitize + POST one crash report from pre-extracted fields. Resolves on
182
191
  * every outcome — timeout, network error, non-2xx, disabled — and never
@@ -199,7 +208,7 @@ export async function sendCrashReport(input) {
199
208
  const payload = {
200
209
  client: input.client,
201
210
  clientVersion: input.clientVersion,
202
- platform: `${process.platform} ${process.arch}`,
211
+ platform: platformLabel(),
203
212
  errorClass: sanitizeCrashText(input.errorClass, opts).slice(0, MAX_CRASH_ERROR_CLASS),
204
213
  message: sanitizeCrashText(input.message, opts).slice(0, MAX_CRASH_MESSAGE),
205
214
  ...(stack !== undefined ? { stack } : {}),
package/dist/doctor.d.ts CHANGED
@@ -50,6 +50,13 @@ export type BackendProbe = {
50
50
  } | {
51
51
  kind: "network";
52
52
  };
53
+ /**
54
+ * The Node floor, first in the list because every other check is moot
55
+ * without it: an old Node dies inside pi's HTTP client mid-request (Sentry
56
+ * YAGNI-BACKEND-4S: `zlib.createZstdDecompress is not a function`). The bin
57
+ * shim refuses to launch below the floor; doctor explains it in the same terms.
58
+ */
59
+ export declare function checkNodeVersion(version: string): CheckResult;
53
60
  export declare function checkPiEngine(probe: PiEngineProbe): CheckResult;
54
61
  export declare function checkExtension(probe: ExtensionProbe): CheckResult;
55
62
  export declare function checkProfileToken(profile: Pick<Profile, "name" | "token">): CheckResult;
@@ -115,6 +122,8 @@ export declare function buildDoctorReport(checks: CheckResult[]): DoctorReport;
115
122
  export declare function formatDoctorReport(report: DoctorReport): string;
116
123
  export interface DoctorDeps {
117
124
  now?: () => number;
125
+ /** Running Node version (defaults to process.versions.node). */
126
+ nodeVersion?: string;
118
127
  probePiEngine?: () => PiEngineProbe;
119
128
  probeExtension?: () => ExtensionProbe;
120
129
  readActiveProfile?: () => Promise<Profile>;
package/dist/doctor.js CHANGED
@@ -21,7 +21,27 @@ import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
21
21
  import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveTelemetryProbePath } from "./paths.js";
22
22
  import { readActiveProfile } from "./profiles.js";
23
23
  import { resolveMcpConfigPath } from "./mcpCommand.js";
24
+ import { MIN_NODE_VERSION, nodeVersionSatisfies } from "./nodeVersion.js";
24
25
  // ── Pure check builders ─────────────────────────────────────────────────────
26
+ /**
27
+ * The Node floor, first in the list because every other check is moot
28
+ * without it: an old Node dies inside pi's HTTP client mid-request (Sentry
29
+ * YAGNI-BACKEND-4S: `zlib.createZstdDecompress is not a function`). The bin
30
+ * shim refuses to launch below the floor; doctor explains it in the same terms.
31
+ */
32
+ export function checkNodeVersion(version) {
33
+ const shown = version.startsWith("v") ? version : `v${version}`;
34
+ if (!nodeVersionSatisfies(version, MIN_NODE_VERSION)) {
35
+ return {
36
+ name: "node",
37
+ status: "fail",
38
+ detail: `${shown} is older than the ${MIN_NODE_VERSION} floor`,
39
+ hint: "upgrade Node.js (https://nodejs.org or `nvm install 22`); older Node crashes mid-request on missing zlib APIs",
40
+ required: true,
41
+ };
42
+ }
43
+ return { name: "node", status: "ok", detail: `${shown} (needs ${MIN_NODE_VERSION}+)`, required: true };
44
+ }
25
45
  export function checkPiEngine(probe) {
26
46
  if (!probe.binPath || !probe.binExists) {
27
47
  return {
@@ -512,6 +532,7 @@ export async function gatherChecks(deps = {}) {
512
532
  const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
513
533
  const probeLatestVersion = deps.probeLatestVersion ?? (() => fetchLatestVersion());
514
534
  const checks = [];
535
+ checks.push(checkNodeVersion(deps.nodeVersion ?? process.versions.node));
515
536
  checks.push(checkPiEngine(probePiEngine()));
516
537
  checks.push(checkExtension(probeExtension()));
517
538
  // win32 only, and skipped means NOT SHOWN: on macOS/Linux there is nothing
@@ -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
@@ -72,6 +72,13 @@ export interface CrashReporterOpts {
72
72
  timeoutMs?: number;
73
73
  }
74
74
  export type CrashReporter = (error: unknown, context?: string, repoRoot?: string) => Promise<void>;
75
+ /**
76
+ * OS, arch AND the Node version: the runtime is a first-class crash cause (an
77
+ * old Node dies inside undici on `zlib.createZstdDecompress`), and it rides
78
+ * the existing `platform` field so the backend and its Sentry tag need no
79
+ * change. Mirrored in `yagni-code-cli/src/crashReport.ts`.
80
+ */
81
+ export declare function platformLabel(): string;
75
82
  /**
76
83
  * Build the fail-soft reporter. The extension runs inside pi's process, so
77
84
  * the client label follows the surface: `desktop` under the desktop shell
@@ -145,6 +145,15 @@ export function sanitizeCrashError(err, opts = {}) {
145
145
  ...(cappedStack !== undefined ? { stack: cappedStack } : {}),
146
146
  };
147
147
  }
148
+ /**
149
+ * OS, arch AND the Node version: the runtime is a first-class crash cause (an
150
+ * old Node dies inside undici on `zlib.createZstdDecompress`), and it rides
151
+ * the existing `platform` field so the backend and its Sentry tag need no
152
+ * change. Mirrored in `yagni-code-cli/src/crashReport.ts`.
153
+ */
154
+ export function platformLabel() {
155
+ return `${process.platform} ${process.arch} node ${process.version}`;
156
+ }
148
157
  /**
149
158
  * Build the fail-soft reporter. The extension runs inside pi's process, so
150
159
  * the client label follows the surface: `desktop` under the desktop shell
@@ -163,7 +172,7 @@ export function makeCrashReporter(opts) {
163
172
  const payload = {
164
173
  client: isDesktopSurface() ? "desktop" : "cli",
165
174
  clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
166
- platform: `${process.platform} ${process.arch}`,
175
+ platform: platformLabel(),
167
176
  ...sanitized,
168
177
  ...(context !== undefined ? { context } : {}),
169
178
  timestamp: new Date().toISOString(),
@@ -234,7 +243,7 @@ export function reportFatalCrash(error, opts, context) {
234
243
  const payload = {
235
244
  client: isDesktopSurface() ? "desktop" : "cli",
236
245
  clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
237
- platform: `${process.platform} ${process.arch}`,
246
+ platform: platformLabel(),
238
247
  ...sanitized,
239
248
  ...(context !== undefined ? { context } : {}),
240
249
  timestamp: new Date().toISOString(),
@@ -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