@yagni-app/code-staging 0.3.0-staging.1088.1 → 0.3.0-staging.1091.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/approvedPrefixes.d.ts +11 -0
- package/dist/extension/approvedPrefixes.js +30 -0
- 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.d.ts +14 -4
- package/dist/extension/guardian.js +34 -10
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +4 -2
- package/dist/extension/permission.js +10 -4
- package/dist/extension/pipeline/goCommand.js +6 -4
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +9 -1
- package/dist/extension/pipeline/runRegistry.js +22 -1
- 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/package.json +2 -2
|
@@ -73,6 +73,17 @@ export declare function matchesGrant(command: string, grants: readonly ApprovedP
|
|
|
73
73
|
export declare function validateGrant(command: string, policy: ExecPolicy, repoKey: string): ApprovedPrefixGrant | null;
|
|
74
74
|
/** Human label for the remember option: "git push …". */
|
|
75
75
|
export declare function describePrefix(pattern: string[]): string;
|
|
76
|
+
/**
|
|
77
|
+
* Could {@link derivePrefix} ever have produced this pattern? The persisted
|
|
78
|
+
* file is plain JSON on disk, so a row that derivation could not have written
|
|
79
|
+
* (a banned interpreter/destruction/egress prefix, a bare multi-subcommand
|
|
80
|
+
* tool, a path-prefixed word, or an over-long pattern) is treated as
|
|
81
|
+
* tampered/corrupt and dropped at load time rather than honored (PR #1698
|
|
82
|
+
* review). This is defense in depth, not the trust boundary itself — the
|
|
83
|
+
* boundary is that grants only enter the live gate at startup or through the
|
|
84
|
+
* gate's own ask flow.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isDerivablePattern(pattern: readonly string[]): boolean;
|
|
76
87
|
export declare function rulesFilePath(homeOverride?: string | null): string;
|
|
77
88
|
/**
|
|
78
89
|
* Resolve the grant scope key for a session cwd: the git remote origin URL,
|
|
@@ -179,6 +179,35 @@ export function validateGrant(command, policy, repoKey) {
|
|
|
179
179
|
export function describePrefix(pattern) {
|
|
180
180
|
return `${pattern.join(" ")} …`;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Could {@link derivePrefix} ever have produced this pattern? The persisted
|
|
184
|
+
* file is plain JSON on disk, so a row that derivation could not have written
|
|
185
|
+
* (a banned interpreter/destruction/egress prefix, a bare multi-subcommand
|
|
186
|
+
* tool, a path-prefixed word, or an over-long pattern) is treated as
|
|
187
|
+
* tampered/corrupt and dropped at load time rather than honored (PR #1698
|
|
188
|
+
* review). This is defense in depth, not the trust boundary itself — the
|
|
189
|
+
* boundary is that grants only enter the live gate at startup or through the
|
|
190
|
+
* gate's own ask flow.
|
|
191
|
+
*/
|
|
192
|
+
export function isDerivablePattern(pattern) {
|
|
193
|
+
if (pattern.length < 1 || pattern.length > 2)
|
|
194
|
+
return false;
|
|
195
|
+
const first = pattern[0];
|
|
196
|
+
if (first.includes("/") || first.startsWith("\\"))
|
|
197
|
+
return false;
|
|
198
|
+
if (BANNED_PREFIXES.has(first))
|
|
199
|
+
return false;
|
|
200
|
+
if (pattern.length === 2) {
|
|
201
|
+
const second = pattern[1];
|
|
202
|
+
if (!MULTI_SUBCOMMAND_TOOLS.has(first))
|
|
203
|
+
return false;
|
|
204
|
+
if (second.startsWith("-") || !SAFE_SUBCOMMAND_RE.test(second))
|
|
205
|
+
return false;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// Derivation never emits a bare multi-subcommand tool ("git" alone).
|
|
209
|
+
return !MULTI_SUBCOMMAND_TOOLS.has(first);
|
|
210
|
+
}
|
|
182
211
|
// --- I/O half ---
|
|
183
212
|
export function rulesFilePath(homeOverride = null) {
|
|
184
213
|
return join(codeStateHome(homeOverride), "rules.json");
|
|
@@ -220,6 +249,7 @@ export function loadGrants(homeOverride = null) {
|
|
|
220
249
|
return parsed.grants.filter((g) => Array.isArray(g?.pattern) &&
|
|
221
250
|
g.pattern.length > 0 &&
|
|
222
251
|
g.pattern.every((t) => typeof t === "string") &&
|
|
252
|
+
isDerivablePattern(g.pattern) &&
|
|
223
253
|
typeof g.repoKey === "string" &&
|
|
224
254
|
typeof g.addedAt === "string" &&
|
|
225
255
|
typeof g.cwd === "string");
|
|
@@ -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)
|
|
@@ -37,7 +37,7 @@ export interface GuardianVerdict {
|
|
|
37
37
|
rationale: string;
|
|
38
38
|
}
|
|
39
39
|
export interface GuardianLimits {
|
|
40
|
-
/**
|
|
40
|
+
/** Cap on Guardian reviews within the sliding window ({@link GUARDIAN_REVIEW_WINDOW_MS}). */
|
|
41
41
|
maxReviews: number;
|
|
42
42
|
/** Consecutive denials per turn before the circuit breaker trips. */
|
|
43
43
|
maxConsecutiveDenials: number;
|
|
@@ -47,15 +47,25 @@ export interface GuardianLimits {
|
|
|
47
47
|
export declare const DEFAULT_GUARDIAN_LIMITS: GuardianLimits;
|
|
48
48
|
/**
|
|
49
49
|
* Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
|
|
50
|
-
* overrides the
|
|
51
|
-
* the default (a bad value must never zero out the cap and lock the
|
|
50
|
+
* overrides the sliding-window review cap; anything non-numeric or < 1 falls
|
|
51
|
+
* back to the default (a bad value must never zero out the cap and lock the
|
|
52
|
+
* session).
|
|
52
53
|
*/
|
|
53
54
|
export declare function resolveGuardianLimits(env?: Record<string, string | undefined>): GuardianLimits;
|
|
54
55
|
/** The model tier the Guardian runs on. Configurable via YAGNI_GUARDIAN_TIER. */
|
|
55
56
|
export declare const GUARDIAN_MODEL_TIER = "efficient";
|
|
56
57
|
/** Read-only tools — the Guardian can read files for context but cannot write or execute. */
|
|
57
58
|
export declare const GUARDIAN_TOOLS: string[];
|
|
59
|
+
/**
|
|
60
|
+
* The review-cap window. `reviews` counts consults inside a SLIDING window
|
|
61
|
+
* rather than for the session's lifetime: a 24/7 session (a fleet operator's
|
|
62
|
+
* always-on terminal) must regain review capacity as old consults age out,
|
|
63
|
+
* not hard-block forever after the first N. The cap is a cost/runaway bound,
|
|
64
|
+
* not a safety bound — safety is the verdicts themselves.
|
|
65
|
+
*/
|
|
66
|
+
export declare const GUARDIAN_REVIEW_WINDOW_MS: number;
|
|
58
67
|
export interface GuardianState {
|
|
68
|
+
/** Guardian consults within the last {@link GUARDIAN_REVIEW_WINDOW_MS}. */
|
|
59
69
|
reviews: number;
|
|
60
70
|
consecutiveDenials: number;
|
|
61
71
|
}
|
|
@@ -64,7 +74,7 @@ export interface GuardianStateHandle {
|
|
|
64
74
|
recordReview(outcome: GuardianOutcome): GuardianState;
|
|
65
75
|
resetTurn(): void;
|
|
66
76
|
}
|
|
67
|
-
export declare function makeGuardianState(): GuardianStateHandle;
|
|
77
|
+
export declare function makeGuardianState(now?: () => number): GuardianStateHandle;
|
|
68
78
|
export interface CircuitBreakerResult {
|
|
69
79
|
tripped: boolean;
|
|
70
80
|
reason?: string;
|
|
@@ -35,8 +35,9 @@ export const DEFAULT_GUARDIAN_LIMITS = {
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
|
|
38
|
-
* overrides the
|
|
39
|
-
* the default (a bad value must never zero out the cap and lock the
|
|
38
|
+
* overrides the sliding-window review cap; anything non-numeric or < 1 falls
|
|
39
|
+
* back to the default (a bad value must never zero out the cap and lock the
|
|
40
|
+
* session).
|
|
40
41
|
*/
|
|
41
42
|
export function resolveGuardianLimits(env = process.env) {
|
|
42
43
|
const raw = env.YAGNI_GUARDIAN_MAX_REVIEWS?.trim();
|
|
@@ -48,25 +49,48 @@ export function resolveGuardianLimits(env = process.env) {
|
|
|
48
49
|
export const GUARDIAN_MODEL_TIER = "efficient";
|
|
49
50
|
/** Read-only tools — the Guardian can read files for context but cannot write or execute. */
|
|
50
51
|
export const GUARDIAN_TOOLS = ["read"];
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// --- State ---
|
|
53
|
+
/**
|
|
54
|
+
* The review-cap window. `reviews` counts consults inside a SLIDING window
|
|
55
|
+
* rather than for the session's lifetime: a 24/7 session (a fleet operator's
|
|
56
|
+
* always-on terminal) must regain review capacity as old consults age out,
|
|
57
|
+
* not hard-block forever after the first N. The cap is a cost/runaway bound,
|
|
58
|
+
* not a safety bound — safety is the verdicts themselves.
|
|
59
|
+
*/
|
|
60
|
+
export const GUARDIAN_REVIEW_WINDOW_MS = 60 * 60_000;
|
|
61
|
+
export function makeGuardianState(now = Date.now) {
|
|
62
|
+
const reviewTimes = [];
|
|
63
|
+
let consecutiveDenials = 0;
|
|
64
|
+
const prune = () => {
|
|
65
|
+
const cutoff = now() - GUARDIAN_REVIEW_WINDOW_MS;
|
|
66
|
+
while (reviewTimes.length > 0 && reviewTimes[0] <= cutoff)
|
|
67
|
+
reviewTimes.shift();
|
|
68
|
+
};
|
|
69
|
+
const snapshot = () => ({
|
|
70
|
+
reviews: reviewTimes.length,
|
|
71
|
+
consecutiveDenials,
|
|
72
|
+
});
|
|
53
73
|
return {
|
|
54
|
-
read: () =>
|
|
74
|
+
read: () => {
|
|
75
|
+
prune();
|
|
76
|
+
return snapshot();
|
|
77
|
+
},
|
|
55
78
|
recordReview(outcome) {
|
|
56
|
-
|
|
79
|
+
prune();
|
|
80
|
+
reviewTimes.push(now());
|
|
57
81
|
if (outcome === "deny") {
|
|
58
|
-
|
|
82
|
+
consecutiveDenials += 1;
|
|
59
83
|
}
|
|
60
84
|
else if (outcome === "allow") {
|
|
61
|
-
|
|
85
|
+
consecutiveDenials = 0;
|
|
62
86
|
}
|
|
63
87
|
// "ask" leaves the denial streak UNCHANGED: it is neither a denial nor
|
|
64
88
|
// an exoneration. If it reset the streak, deny/ask/deny/ask would never
|
|
65
89
|
// trip the breaker (round-2 review blocker).
|
|
66
|
-
return
|
|
90
|
+
return snapshot();
|
|
67
91
|
},
|
|
68
92
|
resetTurn() {
|
|
69
|
-
|
|
93
|
+
consecutiveDenials = 0;
|
|
70
94
|
},
|
|
71
95
|
};
|
|
72
96
|
}
|
|
@@ -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
|
@@ -268,7 +268,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
268
268
|
catch { /* logging must never break the session */ }
|
|
269
269
|
};
|
|
270
270
|
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
271
|
-
// at startup (grants added by other concurrent sessions appear next launch
|
|
271
|
+
// at startup (grants added by other concurrent sessions appear next launch —
|
|
272
|
+
// the startup load is the trust boundary; live reload was reviewed and
|
|
273
|
+
// rejected as a same-session self-authorization path, PR #1698).
|
|
272
274
|
const sessionGrants = evalMode ? [] : loadGrants();
|
|
273
275
|
const GUARDIAN_EVENT_TIMEOUT_MS = 5_000;
|
|
274
276
|
registerPermissionGate(pi, {
|
|
@@ -812,7 +814,7 @@ export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
|
812
814
|
// The general subagent tool: Claude Code-format agent discovery + fan-out.
|
|
813
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";
|
|
814
816
|
// The session todo checklist: todo_write tool, widget renderer, /todos.
|
|
815
|
-
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";
|
|
816
818
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
817
819
|
// the session bless-with-remember capture hook.
|
|
818
820
|
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
@@ -279,6 +279,11 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
279
279
|
const guardianTier = deps.guardianTier;
|
|
280
280
|
// --- YAG-510 gate state ---
|
|
281
281
|
// Grants: in-memory list seeded from deps, appended on "don't ask again".
|
|
282
|
+
// Deliberately NOT live-reloaded from disk: auto mode can write files, so a
|
|
283
|
+
// mid-session re-read of rules.json would let the agent (or a prompt
|
|
284
|
+
// injection) author its own grants and self-authorize within the same
|
|
285
|
+
// session. New grants from concurrent sessions apply at next launch — the
|
|
286
|
+
// startup load is the trust boundary (PR #1698 review).
|
|
282
287
|
const grants = [...(deps.grants ?? [])];
|
|
283
288
|
// Keyed by cwd: a session can change working directory (cd, /go worktrees),
|
|
284
289
|
// and a repoKey memoized from the first cwd would let repo-A grants match
|
|
@@ -410,12 +415,13 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
410
415
|
const guardianAvailable = Boolean(guardianState && !guardianDisabled && guardianReview);
|
|
411
416
|
const limits = guardianLimits ?? DEFAULT_GUARDIAN_LIMITS;
|
|
412
417
|
if (guardianAvailable && guardianState.read().reviews >= limits.maxReviews) {
|
|
413
|
-
//
|
|
414
|
-
//
|
|
418
|
+
// Sliding-window consult cap (capacity recovers as old reviews age
|
|
419
|
+
// out — a long-lived session is never bricked). Review mode falls
|
|
420
|
+
// through to its ordinary confirm (no LLM cost); auto blocks.
|
|
415
421
|
if (modeAtEntry === "auto") {
|
|
416
422
|
if (ctx?.hasUI)
|
|
417
|
-
ctx.ui.notify(`Guardian review cap reached (${limits.maxReviews}
|
|
418
|
-
return { block: true, reason: `Guardian review cap reached (${limits.maxReviews}
|
|
423
|
+
ctx.ui.notify(`Guardian review cap reached (${limits.maxReviews} in the last hour).`, "warning");
|
|
424
|
+
return { block: true, reason: `Guardian review cap reached (${limits.maxReviews} in the last hour). Capacity recovers as older reviews age out; switch to /mode review to approve manually, or retry this step later.` };
|
|
419
425
|
}
|
|
420
426
|
// fall through to decision.confirm below
|
|
421
427
|
}
|
|
@@ -77,7 +77,7 @@ import { registerGoStatusCommands } from "./goStatusCommands.js";
|
|
|
77
77
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
78
78
|
import { composeAbortSignal } from "./resilience.js";
|
|
79
79
|
import { planResume } from "./resume.js";
|
|
80
|
-
import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows,
|
|
80
|
+
import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, resolveMaxConcurrentRuns, settleRun, trackRunAbort, trackRunPromise, worktreesDir, } from "./runRegistry.js";
|
|
81
81
|
import { makeRunSession as defaultMakeRunSession } from "./runSession.js";
|
|
82
82
|
import { recordSessionRun } from "../sessionRuns.js";
|
|
83
83
|
import { resolveTicketBrief as defaultResolveTicketBrief } from "./ticketResolution.js";
|
|
@@ -460,13 +460,15 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
462
|
// In-flight guards: the same ticket never runs twice at once in this
|
|
463
|
-
// process, and at most
|
|
463
|
+
// process, and at most resolveMaxConcurrentRuns() runs are in flight
|
|
464
|
+
// (default 3; fleet operators raise it via YAGNI_MAX_CONCURRENT_RUNS).
|
|
464
465
|
if (findActiveRunByTicket(ticket)) {
|
|
465
466
|
notify(`/go ${ticket} is already running - see /go-status.`, "warning");
|
|
466
467
|
return;
|
|
467
468
|
}
|
|
468
|
-
|
|
469
|
-
|
|
469
|
+
const maxConcurrentRuns = resolveMaxConcurrentRuns();
|
|
470
|
+
if (activeRunCount() >= maxConcurrentRuns) {
|
|
471
|
+
notify(`${maxConcurrentRuns} /go runs are already in flight; wait for one to finish (see /go-status) or raise YAGNI_MAX_CONCURRENT_RUNS.`, "warning");
|
|
470
472
|
return;
|
|
471
473
|
}
|
|
472
474
|
// --- Run tree resolution: worktree by default; --here = legacy in-place.
|
|
@@ -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
|
};
|
|
@@ -22,8 +22,16 @@
|
|
|
22
22
|
* candidate).
|
|
23
23
|
*/
|
|
24
24
|
import type { CheckpointRecord, StopReason } from "./types.js";
|
|
25
|
-
/**
|
|
25
|
+
/** Default bound on simultaneously in-flight /go runs in one process (spec §3b). */
|
|
26
26
|
export declare const MAX_CONCURRENT_RUNS = 3;
|
|
27
|
+
/** Hard ceiling for the env override — a typo must not launch hundreds of runs. */
|
|
28
|
+
export declare const MAX_CONCURRENT_RUNS_CEILING = 32;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the in-flight /go cap from the environment. `YAGNI_MAX_CONCURRENT_RUNS`
|
|
31
|
+
* raises (or lowers) the default for fleet-scale operators; non-numeric or < 1
|
|
32
|
+
* falls back to the default, and anything above the ceiling clamps to it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveMaxConcurrentRuns(env?: Record<string, string | undefined>): number;
|
|
27
35
|
/**
|
|
28
36
|
* A non-terminal row whose journal has been quiet this long is treated as
|
|
29
37
|
* INTERRUPTED (its process died) rather than still running elsewhere. Sits
|
|
@@ -24,8 +24,22 @@
|
|
|
24
24
|
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { codeStateHome } from "../stateHome.js";
|
|
27
|
-
/**
|
|
27
|
+
/** Default bound on simultaneously in-flight /go runs in one process (spec §3b). */
|
|
28
28
|
export const MAX_CONCURRENT_RUNS = 3;
|
|
29
|
+
/** Hard ceiling for the env override — a typo must not launch hundreds of runs. */
|
|
30
|
+
export const MAX_CONCURRENT_RUNS_CEILING = 32;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the in-flight /go cap from the environment. `YAGNI_MAX_CONCURRENT_RUNS`
|
|
33
|
+
* raises (or lowers) the default for fleet-scale operators; non-numeric or < 1
|
|
34
|
+
* falls back to the default, and anything above the ceiling clamps to it.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveMaxConcurrentRuns(env = process.env) {
|
|
37
|
+
const raw = env.YAGNI_MAX_CONCURRENT_RUNS?.trim();
|
|
38
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
39
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
40
|
+
return MAX_CONCURRENT_RUNS;
|
|
41
|
+
return Math.min(parsed, MAX_CONCURRENT_RUNS_CEILING);
|
|
42
|
+
}
|
|
29
43
|
/**
|
|
30
44
|
* A non-terminal row whose journal has been quiet this long is treated as
|
|
31
45
|
* INTERRUPTED (its process died) rather than still running elsewhere. Sits
|
|
@@ -106,6 +120,13 @@ const active = new Map();
|
|
|
106
120
|
export function _resetRunRegistryForTest() {
|
|
107
121
|
active.clear();
|
|
108
122
|
}
|
|
123
|
+
// NOTE on growth: the mirror is append-only and grows without bound on a
|
|
124
|
+
// long-lived install. In-place compaction was reviewed and REMOVED (PR #1698):
|
|
125
|
+
// a fold+rewrite without cross-process exclusion can permanently erase another
|
|
126
|
+
// process's terminal settle (nothing ever re-appends a final row), which would
|
|
127
|
+
// resurrect a finished run as "interrupted" and invite duplicate worktree
|
|
128
|
+
// adoption. Compaction needs an inter-process lock + unique temp files —
|
|
129
|
+
// tracked separately; until then, growth is the safe failure mode.
|
|
109
130
|
/** Fail-soft append of one full row to the mirror (self-heals a torn previous write). */
|
|
110
131
|
function appendRow(row) {
|
|
111
132
|
try {
|
|
@@ -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/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.1091.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": "50ccbb69838d6ee20a0b0d7a7b7259105158f215"
|
|
42
42
|
}
|