@yagni-app/code-staging 0.3.0-staging.1090.1 → 0.3.0-staging.1093.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/askAdvisorTool.d.ts +1 -1
- package/dist/extension/askAdvisorTool.js +9 -1
- package/dist/extension/branding.d.ts +1 -1
- package/dist/extension/branding.js +3 -1
- package/dist/extension/guardian.js +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +1 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/ultra.d.ts +8 -4
- package/dist/extension/ultra.js +20 -5
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/package.json +2 -2
|
@@ -30,7 +30,7 @@ import { type Component } from "@earendil-works/pi-tui";
|
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
32
32
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
33
|
-
import type
|
|
33
|
+
import { type PipelineStage } from "./pipeline/types.js";
|
|
34
34
|
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
35
35
|
/**
|
|
36
36
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
@@ -29,7 +29,9 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
29
29
|
import { Type } from "typebox";
|
|
30
30
|
import { ADVISOR_MODEL_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatConsultCost, } from "./advisor.js";
|
|
31
31
|
import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
|
|
32
|
+
import { withResilience } from "./pipeline/resilience.js";
|
|
32
33
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
34
|
+
import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
33
35
|
import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, receiptLine, renderSubagentResult, runningLines, } from "./subagentRender.js";
|
|
34
36
|
/**
|
|
35
37
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
@@ -94,7 +96,13 @@ export function renderAdvisorCall(args, theme, _context) {
|
|
|
94
96
|
}
|
|
95
97
|
export function makeAskAdvisorTool(opts) {
|
|
96
98
|
const limits = opts.limits ?? DEFAULT_ADVISOR_LIMITS;
|
|
97
|
-
|
|
99
|
+
// The default runner rides the /go pipeline's resilience wrapper, exactly
|
|
100
|
+
// like the subagent tool: idle + wall-clock ceilings and transient-only
|
|
101
|
+
// retry, so a stalled consult child aborts honestly instead of hanging the
|
|
102
|
+
// driver's tool call until the user presses Esc. The synthetic stage id is
|
|
103
|
+
// "plan" (read-only tools, no bash), so the wrapper's write-gate never
|
|
104
|
+
// blocks a retry — re-running a consult cannot double-apply anything.
|
|
105
|
+
const runStage = opts.runStage ?? withResilience(defaultRunStage, DEFAULT_RESILIENCE_POLICY);
|
|
98
106
|
return {
|
|
99
107
|
name: "ask_advisor",
|
|
100
108
|
label: "Ask the advisor",
|
|
@@ -47,7 +47,7 @@ export declare const ULTRA_DELEGATION_PARAGRAPH: string;
|
|
|
47
47
|
*/
|
|
48
48
|
export declare const YAGNI_IDENTITY_DRIVER = "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: fan codebase mapping, wide searches, and mechanical multi-file work out to subagents (they run on cheaper tiers). Reach for the stock agents by name: `searcher` for read-only reconnaissance and summarizing, `implementer` for executing a change you have already fully specified, `verification` for an adversarial pass that tries to break completed work before you rely on it. Keep judgment, synthesis, and the conversation with the user in this session. Do not spawn a subagent for work you can finish in a couple of tool calls.";
|
|
49
49
|
/** The driver identity while /ultra is on: base identity + the diamond directive. */
|
|
50
|
-
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.";
|
|
50
|
+
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.";
|
|
51
51
|
export declare const PI_IDENTITY_RE: RegExp;
|
|
52
52
|
/**
|
|
53
53
|
* Env switch that bypasses the system-prompt rewrite entirely, so pi's
|
|
@@ -64,7 +64,9 @@ export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has
|
|
|
64
64
|
"agreement between checkers — not a single pass — as confirmation, and " +
|
|
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
|
-
"finish in a couple of tool calls skips the diamond."
|
|
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 " +
|
|
69
|
+
"split the job and again as each fanned-out piece lands, not only at the end.";
|
|
68
70
|
/**
|
|
69
71
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
70
72
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -143,7 +143,7 @@ export type { ComparisonReport, LaneFit, LaneOutcome } from "./pipeline/eval.js"
|
|
|
143
143
|
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
144
144
|
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";
|
|
145
145
|
export type { SubagentDef, SubagentSource } from "./subagents.js";
|
|
146
|
-
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
146
|
+
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
147
147
|
export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
|
|
148
148
|
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
149
149
|
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -814,7 +814,7 @@ export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
|
814
814
|
// The general subagent tool: Claude Code-format agent discovery + fan-out.
|
|
815
815
|
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";
|
|
816
816
|
// The session todo checklist: todo_write tool, widget renderer, /todos.
|
|
817
|
-
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
817
|
+
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
818
818
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
819
819
|
// the session bless-with-remember capture hook.
|
|
820
820
|
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `withResilience(runStage, policy)` is a composable higher-order function that
|
|
5
5
|
* wraps the per-stage child spawn (`runner.ts#runStage`) with the one axis the
|
|
6
6
|
* roadmap calls the whole competitive gap: a per-stage IDLE timeout (no NDJSON
|
|
7
|
-
* event for N ms
|
|
7
|
+
* event for N ms, deferred while a tool is in flight — see the stall note at
|
|
8
|
+
* the timer wiring) and a total WALL-CLOCK timeout, both firing the runner's
|
|
8
9
|
* existing SIGTERM -> SIGKILL abort; bounded exponential backoff with jitter; and
|
|
9
10
|
* retry of CLASSIFIED-TRANSIENT outcomes only. One structured telemetry record is
|
|
10
11
|
* emitted per attempt.
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `withResilience(runStage, policy)` is a composable higher-order function that
|
|
5
5
|
* wraps the per-stage child spawn (`runner.ts#runStage`) with the one axis the
|
|
6
6
|
* roadmap calls the whole competitive gap: a per-stage IDLE timeout (no NDJSON
|
|
7
|
-
* event for N ms
|
|
7
|
+
* event for N ms, deferred while a tool is in flight — see the stall note at
|
|
8
|
+
* the timer wiring) and a total WALL-CLOCK timeout, both firing the runner's
|
|
8
9
|
* existing SIGTERM -> SIGKILL abort; bounded exponential backoff with jitter; and
|
|
9
10
|
* retry of CLASSIFIED-TRANSIENT outcomes only. One structured telemetry record is
|
|
10
11
|
* emitted per attempt.
|
|
@@ -100,19 +101,37 @@ export function withResilience(base, policy, opts = {}) {
|
|
|
100
101
|
if (!timeoutController.signal.aborted)
|
|
101
102
|
timeoutController.abort();
|
|
102
103
|
};
|
|
104
|
+
// Tools the child has started but not finished. The idle window measures
|
|
105
|
+
// STALL, not silence: a long quiet tool (a 6-minute test suite, a slow
|
|
106
|
+
// build) emits no NDJSON between its start and end events, and that is
|
|
107
|
+
// progress, not a hang. While a tool is in flight the idle expiry defers
|
|
108
|
+
// and re-arms instead of aborting; the wall-clock timer stays the
|
|
109
|
+
// backstop for a tool that is genuinely hung.
|
|
110
|
+
let inFlightTools = 0;
|
|
103
111
|
let idleTimer;
|
|
104
112
|
const armIdle = () => {
|
|
105
113
|
if (idleTimer)
|
|
106
114
|
clearTimeout(idleTimer);
|
|
107
|
-
idleTimer = setTimeout(
|
|
115
|
+
idleTimer = setTimeout(fireIdle, policy.idleTimeoutMs);
|
|
108
116
|
idleTimer.unref?.();
|
|
109
117
|
};
|
|
118
|
+
const fireIdle = () => {
|
|
119
|
+
if (inFlightTools > 0) {
|
|
120
|
+
armIdle();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
fireTimeout();
|
|
124
|
+
};
|
|
110
125
|
const wallTimer = setTimeout(fireTimeout, policy.wallTimeoutMs);
|
|
111
126
|
wallTimer.unref?.();
|
|
112
127
|
armIdle();
|
|
113
128
|
const originalOnEvent = deps.onEvent;
|
|
114
129
|
const onEvent = (ev) => {
|
|
115
130
|
sawAnyEvent = true;
|
|
131
|
+
if (ev.type === "tool_execution_start")
|
|
132
|
+
inFlightTools += 1;
|
|
133
|
+
else if (ev.type === "tool_execution_end")
|
|
134
|
+
inFlightTools = Math.max(0, inFlightTools - 1);
|
|
116
135
|
armIdle(); // reset the idle window on every live event
|
|
117
136
|
originalOnEvent?.(ev);
|
|
118
137
|
};
|
|
@@ -17,6 +17,16 @@ import { Type } from "typebox";
|
|
|
17
17
|
export declare const TODO_TOOL_NAME = "todo_write";
|
|
18
18
|
export declare const MAX_TODOS = 50;
|
|
19
19
|
export declare const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* Staleness-reminder throttle (both counters must trip): a reminder is
|
|
22
|
+
* eligible only after this many assistant turns since the last todo_write AND
|
|
23
|
+
* this many since the last reminder. The two-counter shape (staleness gate +
|
|
24
|
+
* anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
|
|
25
|
+
* driver model routinely stops updating the board mid-grind (the frozen
|
|
26
|
+
* "Todos 0/8" report), and a bare description-level instruction does not
|
|
27
|
+
* survive a long run.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TODO_REMINDER_TURNS = 10;
|
|
20
30
|
/**
|
|
21
31
|
* The desktop's structured state record rides its own widget key, like the
|
|
22
32
|
* `/go` run state: one JSON line the app parses and renders itself, never
|
|
@@ -61,6 +71,23 @@ export interface TodoTheme {
|
|
|
61
71
|
export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme): string[];
|
|
62
72
|
/** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
|
|
63
73
|
export declare function todoStateLine(todos: TodoItem[]): string;
|
|
74
|
+
/**
|
|
75
|
+
* PURE: is a staleness reminder due? Only when the board has open work (an
|
|
76
|
+
* empty or fully-completed list never nags) and BOTH throttle counters have
|
|
77
|
+
* reached {@link TODO_REMINDER_TURNS}.
|
|
78
|
+
*/
|
|
79
|
+
export declare function shouldRemindTodos(input: {
|
|
80
|
+
todos: TodoItem[];
|
|
81
|
+
turnsSinceWrite: number;
|
|
82
|
+
turnsSinceReminder: number;
|
|
83
|
+
}): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* PURE: the hedged reminder block appended to a tool result when the board has
|
|
86
|
+
* gone stale. Carries the CURRENT list so the model can reconcile without a
|
|
87
|
+
* read, and explicitly licenses ignoring it, so an accurate board costs one
|
|
88
|
+
* glance rather than a spurious todo_write.
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatTodoReminder(todos: TodoItem[]): string;
|
|
64
91
|
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
65
92
|
export declare function reconstructTodos(entries: unknown[]): TodoItem[];
|
|
66
93
|
type TodoParams = {
|
|
@@ -104,7 +131,7 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
|
|
|
104
131
|
isError?: undefined;
|
|
105
132
|
}>;
|
|
106
133
|
};
|
|
107
|
-
/** Wire the tool, the branch-replay events, and
|
|
134
|
+
/** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
|
|
108
135
|
export declare function registerTodos(pi: ExtensionAPI): void;
|
|
109
136
|
export {};
|
|
110
137
|
//# sourceMappingURL=todos.d.ts.map
|
package/dist/extension/todos.js
CHANGED
|
@@ -17,6 +17,16 @@ import { isDesktopSurface } from "./surface.js";
|
|
|
17
17
|
export const TODO_TOOL_NAME = "todo_write";
|
|
18
18
|
export const MAX_TODOS = 50;
|
|
19
19
|
export const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* Staleness-reminder throttle (both counters must trip): a reminder is
|
|
22
|
+
* eligible only after this many assistant turns since the last todo_write AND
|
|
23
|
+
* this many since the last reminder. The two-counter shape (staleness gate +
|
|
24
|
+
* anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
|
|
25
|
+
* driver model routinely stops updating the board mid-grind (the frozen
|
|
26
|
+
* "Todos 0/8" report), and a bare description-level instruction does not
|
|
27
|
+
* survive a long run.
|
|
28
|
+
*/
|
|
29
|
+
export const TODO_REMINDER_TURNS = 10;
|
|
20
30
|
const WIDGET_KEY = "yagni-todos";
|
|
21
31
|
/**
|
|
22
32
|
* The desktop's structured state record rides its own widget key, like the
|
|
@@ -101,6 +111,33 @@ export function renderTodoWidget(todos, theme) {
|
|
|
101
111
|
export function todoStateLine(todos) {
|
|
102
112
|
return JSON.stringify({ v: 1, todos });
|
|
103
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* PURE: is a staleness reminder due? Only when the board has open work (an
|
|
116
|
+
* empty or fully-completed list never nags) and BOTH throttle counters have
|
|
117
|
+
* reached {@link TODO_REMINDER_TURNS}.
|
|
118
|
+
*/
|
|
119
|
+
export function shouldRemindTodos(input) {
|
|
120
|
+
const { todos, turnsSinceWrite, turnsSinceReminder } = input;
|
|
121
|
+
if (todos.length === 0)
|
|
122
|
+
return false;
|
|
123
|
+
const { done, total } = todoSummary(todos);
|
|
124
|
+
if (done === total)
|
|
125
|
+
return false;
|
|
126
|
+
return turnsSinceWrite >= TODO_REMINDER_TURNS && turnsSinceReminder >= TODO_REMINDER_TURNS;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* PURE: the hedged reminder block appended to a tool result when the board has
|
|
130
|
+
* gone stale. Carries the CURRENT list so the model can reconcile without a
|
|
131
|
+
* read, and explicitly licenses ignoring it, so an accurate board costs one
|
|
132
|
+
* glance rather than a spurious todo_write.
|
|
133
|
+
*/
|
|
134
|
+
export function formatTodoReminder(todos) {
|
|
135
|
+
return ("⟦YAGNI todos⟧ The todo_write checklist has not been updated for a while. " +
|
|
136
|
+
"If the work has moved on, bring it current now: mark finished steps completed, " +
|
|
137
|
+
"set the step you are on to in_progress, and add newly discovered steps. " +
|
|
138
|
+
"If the list is already accurate, ignore this.\n" +
|
|
139
|
+
formatTodoList(todos));
|
|
140
|
+
}
|
|
104
141
|
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
105
142
|
export function reconstructTodos(entries) {
|
|
106
143
|
let todos = [];
|
|
@@ -186,9 +223,14 @@ export function makeTodoTool(get, set) {
|
|
|
186
223
|
},
|
|
187
224
|
};
|
|
188
225
|
}
|
|
189
|
-
/** Wire the tool, the branch-replay events, and
|
|
226
|
+
/** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
|
|
190
227
|
export function registerTodos(pi) {
|
|
191
228
|
let todos = [];
|
|
229
|
+
// Staleness-reminder counters (see TODO_REMINDER_TURNS). Session-local like
|
|
230
|
+
// the list cache itself; branch replay resets them so a resume/fork never
|
|
231
|
+
// opens with an instantly-due reminder.
|
|
232
|
+
let turnsSinceWrite = 0;
|
|
233
|
+
let turnsSinceReminder = 0;
|
|
192
234
|
const reconstruct = (ctx) => {
|
|
193
235
|
try {
|
|
194
236
|
todos = reconstructTodos(ctx.sessionManager.getBranch());
|
|
@@ -196,12 +238,45 @@ export function registerTodos(pi) {
|
|
|
196
238
|
catch {
|
|
197
239
|
todos = [];
|
|
198
240
|
}
|
|
241
|
+
turnsSinceWrite = 0;
|
|
242
|
+
turnsSinceReminder = 0;
|
|
199
243
|
paintWidget(ctx, todos);
|
|
200
244
|
};
|
|
201
245
|
pi.on("session_start", async (_event, ctx) => reconstruct(ctx));
|
|
202
246
|
pi.on("session_tree", async (_event, ctx) => reconstruct(ctx));
|
|
247
|
+
// Turn counting: one tick per finalized assistant message, the same "turn"
|
|
248
|
+
// the model experiences between opportunities to call todo_write.
|
|
249
|
+
pi.on("message_end", async (event) => {
|
|
250
|
+
if (event.message?.role === "assistant") {
|
|
251
|
+
turnsSinceWrite += 1;
|
|
252
|
+
turnsSinceReminder += 1;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// The reminder rides an existing tool result (the same result-modification
|
|
256
|
+
// seam ambient recall uses), so it reaches the model mid-run without
|
|
257
|
+
// spending a turn. Never appended to todo_write's own result, and fail-soft:
|
|
258
|
+
// a reminder must never break a tool call.
|
|
259
|
+
pi.on("tool_result", async (event) => {
|
|
260
|
+
try {
|
|
261
|
+
if (event.toolName === TODO_TOOL_NAME)
|
|
262
|
+
return;
|
|
263
|
+
if (!shouldRemindTodos({ todos, turnsSinceWrite, turnsSinceReminder }))
|
|
264
|
+
return;
|
|
265
|
+
turnsSinceReminder = 0;
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
...event.content,
|
|
269
|
+
{ type: "text", text: `\n\n${formatTodoReminder(todos)}` },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
203
277
|
pi.registerTool(makeTodoTool(() => todos, (next) => {
|
|
204
278
|
todos = next;
|
|
279
|
+
turnsSinceWrite = 0;
|
|
205
280
|
}));
|
|
206
281
|
pi.registerCommand("todos", {
|
|
207
282
|
description: "Show the agent's current task list for this session.",
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
6
|
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
-
* YAGNI_IDENTITY_ULTRA
|
|
8
|
-
*
|
|
9
|
-
* ceiling
|
|
10
|
-
*
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA) and widens the subagent tool's per-call fan-out
|
|
8
|
+
* ceiling (subagents.ts). The two halves take effect at different moments:
|
|
9
|
+
* the fan-out ceiling is probed live on every subagent call, but the identity
|
|
10
|
+
* is read in index.ts's before_agent_start handler, which pi fires only when
|
|
11
|
+
* a NEW user prompt is submitted — a toggle mid-run leaves the running task on
|
|
12
|
+
* its existing instructions until the next message (the handler notifies when
|
|
13
|
+
* that is the case). Ultra is a prompt + ceiling change only: it never touches
|
|
14
|
+
* the permission mode, the model tier, or the /go pipeline.
|
|
11
15
|
*/
|
|
12
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
17
|
export interface UltraHolder {
|
package/dist/extension/ultra.js
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
6
|
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
-
* YAGNI_IDENTITY_ULTRA
|
|
8
|
-
*
|
|
9
|
-
* ceiling
|
|
10
|
-
*
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA) and widens the subagent tool's per-call fan-out
|
|
8
|
+
* ceiling (subagents.ts). The two halves take effect at different moments:
|
|
9
|
+
* the fan-out ceiling is probed live on every subagent call, but the identity
|
|
10
|
+
* is read in index.ts's before_agent_start handler, which pi fires only when
|
|
11
|
+
* a NEW user prompt is submitted — a toggle mid-run leaves the running task on
|
|
12
|
+
* its existing instructions until the next message (the handler notifies when
|
|
13
|
+
* that is the case). Ultra is a prompt + ceiling change only: it never touches
|
|
14
|
+
* the permission mode, the model tier, or the /go pipeline.
|
|
11
15
|
*/
|
|
12
16
|
export function createUltraHolder(initial = false) {
|
|
13
17
|
let on = initial;
|
|
@@ -24,6 +28,13 @@ const ULTRA_ON_COPY = "Ultra mode ON: meaningful work fans out to parallel subag
|
|
|
24
28
|
"verification agents try to break the result, then the agent synthesizes. " +
|
|
25
29
|
"Expect more subagent spend per task.";
|
|
26
30
|
const ULTRA_OFF_COPY = "Ultra mode OFF: back to delegate-when-useful.";
|
|
31
|
+
/**
|
|
32
|
+
* Appended when the toggle lands mid-run: the identity swap only applies when
|
|
33
|
+
* the next prompt is submitted (see the module docblock), so without this note
|
|
34
|
+
* the chip flips while the running task visibly keeps its old behavior — which
|
|
35
|
+
* reads as ultra mode being broken.
|
|
36
|
+
*/
|
|
37
|
+
const MID_RUN_NOTE = " The task currently running keeps its existing instructions; the change takes full effect on your next message.";
|
|
27
38
|
/**
|
|
28
39
|
* Wire the /ultra command onto a shared holder. No argument toggles; `on` /
|
|
29
40
|
* `off` set explicitly; `status` reports without changing anything.
|
|
@@ -54,7 +65,11 @@ export function registerUltraCommand(pi, holder) {
|
|
|
54
65
|
catch {
|
|
55
66
|
// The chip is chrome; never let it break /ultra.
|
|
56
67
|
}
|
|
57
|
-
|
|
68
|
+
// Guarded probe: test fakes (and any minimal harness ctx) may not carry
|
|
69
|
+
// isIdle, and its absence must read as idle, never as busy.
|
|
70
|
+
const midRun = typeof ctx.isIdle === "function" && !ctx.isIdle();
|
|
71
|
+
const copy = next ? ULTRA_ON_COPY : ULTRA_OFF_COPY;
|
|
72
|
+
notify(midRun ? copy + MID_RUN_NOTE : copy, "info");
|
|
58
73
|
},
|
|
59
74
|
});
|
|
60
75
|
}
|
package/dist/login.d.ts
CHANGED
|
@@ -34,8 +34,10 @@ type OpenRunner = (cmd: string, args: string[]) => Promise<void>;
|
|
|
34
34
|
*
|
|
35
35
|
* macOS: open <url>
|
|
36
36
|
* Linux: xdg-open <url>
|
|
37
|
-
* Windows:
|
|
38
|
-
* without
|
|
37
|
+
* Windows: rundll32 url.dll,FileProtocolHandler <url> (opens the default
|
|
38
|
+
* browser without going through cmd.exe — `cmd /c start <url>`
|
|
39
|
+
* re-parses its arguments as a shell line, so a hostile URL with
|
|
40
|
+
* `&`/`^` metacharacters could execute commands)
|
|
39
41
|
*/
|
|
40
42
|
export declare function resolveOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
41
43
|
cmd: string;
|
package/dist/login.js
CHANGED
|
@@ -31,14 +31,17 @@ function isTimeoutError(err) {
|
|
|
31
31
|
*
|
|
32
32
|
* macOS: open <url>
|
|
33
33
|
* Linux: xdg-open <url>
|
|
34
|
-
* Windows:
|
|
35
|
-
* without
|
|
34
|
+
* Windows: rundll32 url.dll,FileProtocolHandler <url> (opens the default
|
|
35
|
+
* browser without going through cmd.exe — `cmd /c start <url>`
|
|
36
|
+
* re-parses its arguments as a shell line, so a hostile URL with
|
|
37
|
+
* `&`/`^` metacharacters could execute commands)
|
|
36
38
|
*/
|
|
37
39
|
export function resolveOpenCommand(url, platform = process.platform) {
|
|
38
40
|
if (platform === "darwin")
|
|
39
41
|
return { cmd: "open", args: [url] };
|
|
40
|
-
if (platform === "win32")
|
|
41
|
-
return { cmd: "
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
|
|
44
|
+
}
|
|
42
45
|
return { cmd: "xdg-open", args: [url] };
|
|
43
46
|
}
|
|
44
47
|
const spawnRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
@@ -50,6 +53,18 @@ const spawnRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
|
50
53
|
* URL is still printed for manual copy. `runner` is injectable for tests.
|
|
51
54
|
*/
|
|
52
55
|
export const realOpenUrl = (url, runner = spawnRunner) => {
|
|
56
|
+
// Only ever hand http(s) URLs to the OS opener — refuse file:, javascript:,
|
|
57
|
+
// or custom schemes a compromised server response could try to smuggle in.
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(url);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
53
68
|
const { cmd, args } = resolveOpenCommand(url);
|
|
54
69
|
return runner(cmd, args).catch(() => {
|
|
55
70
|
// Silently fail — the user can still copy the URL manually.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.3.0-staging.
|
|
3
|
+
"version": "0.3.0-staging.1093.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)",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "0.84.1",
|
|
39
39
|
"typebox": "^1.3.11"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "189159f08a0b5f7ab5cf10221f8238782ff7d1f7"
|
|
42
42
|
}
|