@yagni-app/code-staging 0.2.1-staging.1038.1 → 0.2.1-staging.1041.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/cli.d.ts +21 -0
- package/dist/cli.js +74 -1
- package/dist/extension/branding.d.ts +1 -1
- package/dist/extension/branding.js +6 -3
- package/dist/extension/footer.d.ts +9 -1
- package/dist/extension/footer.js +23 -5
- package/dist/extension/rerouteNotice.d.ts +2 -3
- package/dist/extension/rerouteNotice.js +19 -10
- package/dist/extension/subagents.js +48 -1
- package/dist/launch.js +5 -0
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/package.json +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,27 @@
|
|
|
12
12
|
* plus a configured pi spawn. Everything that makes this "YAGNI Code" lives in
|
|
13
13
|
* pi-extension-yagni and the YAGNI backend.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
17
|
+
* input is padded to match the chat/output area (`outputPad`) and the status
|
|
18
|
+
* bar. Fills the key in only when absent — an explicit user choice (including
|
|
19
|
+
* 0) is never overwritten. Best-effort: a missing/corrupt settings file or a
|
|
20
|
+
* write failure must never block the launch.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
24
|
+
* input aligns with the chat/output area (`outputPad`) and the status bar.
|
|
25
|
+
*
|
|
26
|
+
* This writes to a user-owned file, so it is deliberately conservative:
|
|
27
|
+
* - fills the key in ONLY when absent — an explicit user choice (including 0)
|
|
28
|
+
* is never overwritten;
|
|
29
|
+
* - backs off on a missing-dir, corrupt, non-object, or symlinked settings
|
|
30
|
+
* file rather than risk clobbering anything;
|
|
31
|
+
* - writes ATOMICALLY (temp file + rename) and preserves the existing file's
|
|
32
|
+
* permissions, so a crash mid-write can never truncate the user's settings.
|
|
33
|
+
* Any failure is swallowed: seeding must never block or break a launch.
|
|
34
|
+
*/
|
|
35
|
+
export declare function seedEditorPadding(piAgentDir: string): void;
|
|
15
36
|
export declare const HELP_TEXT: string;
|
|
16
37
|
/** Parse `use <name> [--base-url <url>]` argv into its parts. */
|
|
17
38
|
export declare function parseUseArgs(args: string[]): {
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* pi-extension-yagni and the YAGNI backend.
|
|
14
14
|
*/
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
-
import { mkdirSync, realpathSync } from "node:fs";
|
|
16
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
17
18
|
import { createInterface } from "node:readline/promises";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
import { PI_CONFIG_NAME } from "./branding.js";
|
|
@@ -27,6 +28,7 @@ import { runDoctor } from "./doctor.js";
|
|
|
27
28
|
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
28
29
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
29
30
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
31
|
+
import { PAD_X } from "./padding.js";
|
|
30
32
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
31
33
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
32
34
|
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
@@ -43,6 +45,72 @@ async function confirmOnTty(question) {
|
|
|
43
45
|
rl.close();
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
50
|
+
* input is padded to match the chat/output area (`outputPad`) and the status
|
|
51
|
+
* bar. Fills the key in only when absent — an explicit user choice (including
|
|
52
|
+
* 0) is never overwritten. Best-effort: a missing/corrupt settings file or a
|
|
53
|
+
* write failure must never block the launch.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
57
|
+
* input aligns with the chat/output area (`outputPad`) and the status bar.
|
|
58
|
+
*
|
|
59
|
+
* This writes to a user-owned file, so it is deliberately conservative:
|
|
60
|
+
* - fills the key in ONLY when absent — an explicit user choice (including 0)
|
|
61
|
+
* is never overwritten;
|
|
62
|
+
* - backs off on a missing-dir, corrupt, non-object, or symlinked settings
|
|
63
|
+
* file rather than risk clobbering anything;
|
|
64
|
+
* - writes ATOMICALLY (temp file + rename) and preserves the existing file's
|
|
65
|
+
* permissions, so a crash mid-write can never truncate the user's settings.
|
|
66
|
+
* Any failure is swallowed: seeding must never block or break a launch.
|
|
67
|
+
*/
|
|
68
|
+
export function seedEditorPadding(piAgentDir) {
|
|
69
|
+
try {
|
|
70
|
+
const settingsPath = join(piAgentDir, "settings.json");
|
|
71
|
+
let settings = {};
|
|
72
|
+
let existingMode;
|
|
73
|
+
if (existsSync(settingsPath)) {
|
|
74
|
+
// Refuse to follow a symlink: we must only ever write a regular, real
|
|
75
|
+
// settings file the user (or pi) owns.
|
|
76
|
+
if (lstatSync(settingsPath).isSymbolicLink())
|
|
77
|
+
return;
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return; // corrupt file: back off rather than clobber the user's settings
|
|
84
|
+
}
|
|
85
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
86
|
+
return; // non-object settings: leave it alone
|
|
87
|
+
}
|
|
88
|
+
settings = parsed;
|
|
89
|
+
existingMode = lstatSync(settingsPath).mode & 0o777;
|
|
90
|
+
}
|
|
91
|
+
if (settings.editorPaddingX !== undefined)
|
|
92
|
+
return; // user already chose
|
|
93
|
+
settings.editorPaddingX = PAD_X;
|
|
94
|
+
// Atomic write: serialize to a temp file in the same directory, then
|
|
95
|
+
// rename over the target. A crash leaves either the old file or the temp
|
|
96
|
+
// file, never a half-written settings.json.
|
|
97
|
+
const tmpPath = join(piAgentDir, `.settings.json.yagni-${process.pid}-${Date.now()}.tmp`);
|
|
98
|
+
try {
|
|
99
|
+
writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
100
|
+
renameSync(tmpPath, settingsPath);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
rmSync(tmpPath, { force: true }); // no-op once the rename succeeded
|
|
104
|
+
}
|
|
105
|
+
// Restore the original permissions if the file already had them (rename
|
|
106
|
+
// replaces the inode, which would otherwise reset to 0600).
|
|
107
|
+
if (existingMode !== undefined)
|
|
108
|
+
chmodSync(settingsPath, existingMode);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// never block launch on a settings-seed failure
|
|
112
|
+
}
|
|
113
|
+
}
|
|
46
114
|
async function runDefault(passthroughArgs) {
|
|
47
115
|
// Cache-backed update nudge (never a network wait), then a background cache
|
|
48
116
|
// refresh that completes while the session runs. Both fail soft.
|
|
@@ -72,6 +140,11 @@ async function runDefault(passthroughArgs) {
|
|
|
72
140
|
// instead of ~/.pi/agent, and prod state never bleeds into staging.
|
|
73
141
|
const piAgentDir = agentDir(profile.name);
|
|
74
142
|
mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
|
|
143
|
+
// Seed the editor's horizontal padding so the prompt input aligns with the
|
|
144
|
+
// chat/output area and the status bar. Only fills in when the user has not
|
|
145
|
+
// set it themselves — an explicit choice is never clobbered. Best-effort: a
|
|
146
|
+
// corrupt or unreadable settings.json must never block the launch.
|
|
147
|
+
seedEditorPadding(piAgentDir);
|
|
75
148
|
// Generate the shadow pi package so the terminal title/process name read
|
|
76
149
|
// "YAGNI Code" instead of "pi"/"π". Best-effort: if it can't be built we
|
|
77
150
|
// still launch (un-rebranded but hermetic), never blocking the agent.
|
|
@@ -37,7 +37,7 @@ export declare const DRIVER_DELEGATION_PARAGRAPH: string;
|
|
|
37
37
|
* effective `x-yagni-caller` attribution (config.ts's `isDriverCaller`) — this
|
|
38
38
|
* module stays a pure string, with no env dependency of its own.
|
|
39
39
|
*/
|
|
40
|
-
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). 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.";
|
|
40
|
+
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. 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.";
|
|
41
41
|
export declare const PI_IDENTITY_RE: RegExp;
|
|
42
42
|
/**
|
|
43
43
|
* Env switch that bypasses the system-prompt rewrite entirely, so pi's
|
|
@@ -40,9 +40,12 @@ export const YAGNI_IDENTITY = "You are YAGNI Code, an autonomous terminal coding
|
|
|
40
40
|
* exist.
|
|
41
41
|
*/
|
|
42
42
|
export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wide searches, and mechanical multi-file " +
|
|
43
|
-
"work out to subagents (they run on cheaper tiers).
|
|
44
|
-
"
|
|
45
|
-
"for
|
|
43
|
+
"work out to subagents (they run on cheaper tiers). Reach for the stock " +
|
|
44
|
+
"agents by name: `searcher` for read-only reconnaissance and summarizing, " +
|
|
45
|
+
"`implementer` for executing a change you have already fully specified. " +
|
|
46
|
+
"Keep judgment, synthesis, and the conversation with the user in this " +
|
|
47
|
+
"session. Do not spawn a subagent for work you can finish in a couple of " +
|
|
48
|
+
"tool calls.";
|
|
46
49
|
/**
|
|
47
50
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
48
51
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -40,6 +40,14 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
42
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
45
|
+
* aligns with the editor input and the chat/output area on one shared column.
|
|
46
|
+
* The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
|
|
47
|
+
* read pi settings, so the value crosses over env. Falls back to the output
|
|
48
|
+
* area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveFooterPadX(raw?: string | undefined): number;
|
|
43
51
|
/** Format token counts for compact footer display (mirrors pi's formatTokens). */
|
|
44
52
|
export declare function formatTokens(count: number): string;
|
|
45
53
|
/** Shorten cwd relative to home, like pi's built-in footer. */
|
|
@@ -85,7 +93,7 @@ export declare function renderFooterLines(input: {
|
|
|
85
93
|
usage: UsageTotals;
|
|
86
94
|
contextPercent: number | null;
|
|
87
95
|
statuses: string[];
|
|
88
|
-
}, theme: Pick<Theme, "fg">, width: number): string[];
|
|
96
|
+
}, theme: Pick<Theme, "fg">, width: number, padX?: number): string[];
|
|
89
97
|
/**
|
|
90
98
|
* Create a footer factory that captures the session `ctx` (for session data)
|
|
91
99
|
* and returns the component `setFooter` expects. Called from the
|
package/dist/extension/footer.js
CHANGED
|
@@ -46,6 +46,19 @@ export const BRANCH_MAX_WIDTH = 60;
|
|
|
46
46
|
const WORKTREE_MAX_WIDTH = 30;
|
|
47
47
|
/** Section separator: single space + middle dot + single space. */
|
|
48
48
|
const SEP = " · ";
|
|
49
|
+
/** Default horizontal pad when the launcher didn't forward one (matches outputPad=1). */
|
|
50
|
+
const DEFAULT_PAD_X = 1;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the status bar's left pad from the launcher's `YAGNI_PAD_X` env so it
|
|
53
|
+
* aligns with the editor input and the chat/output area on one shared column.
|
|
54
|
+
* The launcher seeds the same value as pi's `editorPaddingX`; this footer can't
|
|
55
|
+
* read pi settings, so the value crosses over env. Falls back to the output
|
|
56
|
+
* area's default (1) when unset/invalid. Clamped to pi's 0–3 editor range.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveFooterPadX(raw = process.env.YAGNI_PAD_X) {
|
|
59
|
+
const n = raw === undefined ? NaN : Number.parseInt(raw, 10);
|
|
60
|
+
return Number.isFinite(n) && n >= 0 && n <= 3 ? n : DEFAULT_PAD_X;
|
|
61
|
+
}
|
|
49
62
|
/** Format token counts for compact footer display (mirrors pi's formatTokens). */
|
|
50
63
|
export function formatTokens(count) {
|
|
51
64
|
if (count < 1000)
|
|
@@ -191,9 +204,14 @@ function contextColor(percent) {
|
|
|
191
204
|
return "dim";
|
|
192
205
|
}
|
|
193
206
|
/** Pure line-builder, exported for tests. All data injected; colors via theme. */
|
|
194
|
-
export function renderFooterLines(input, theme, width) {
|
|
207
|
+
export function renderFooterLines(input, theme, width, padX = 0) {
|
|
195
208
|
const dim = (s) => theme.fg("dim", s);
|
|
196
209
|
const sep = dim(SEP);
|
|
210
|
+
// Reserve the left pad so the status bar's text starts on the same column as
|
|
211
|
+
// the (padded) editor input and the chat/output area, instead of hugging the
|
|
212
|
+
// terminal edge. Truncation runs against the reduced content width.
|
|
213
|
+
const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
|
|
214
|
+
const contentWidth = Math.max(1, width - pad.length);
|
|
197
215
|
// Line 1: folder · [worktree] · branch
|
|
198
216
|
const line1Parts = [theme.fg("accent", input.git.folder)];
|
|
199
217
|
if (input.git.inRepo) {
|
|
@@ -202,7 +220,7 @@ export function renderFooterLines(input, theme, width) {
|
|
|
202
220
|
if (input.git.branch)
|
|
203
221
|
line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH)));
|
|
204
222
|
}
|
|
205
|
-
const line1 = truncateToWidth(line1Parts.join(sep),
|
|
223
|
+
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
206
224
|
// Line 2: model · ↑in ↓out $cost · ctx%
|
|
207
225
|
const statParts = [];
|
|
208
226
|
if (input.usage.input)
|
|
@@ -217,12 +235,12 @@ export function renderFooterLines(input, theme, width) {
|
|
|
217
235
|
if (stats)
|
|
218
236
|
line2Parts.push(dim(stats));
|
|
219
237
|
line2Parts.push(theme.fg(contextColor(input.contextPercent), percentText));
|
|
220
|
-
const line2 = truncateToWidth(line2Parts.join(sep),
|
|
238
|
+
const line2 = pad + truncateToWidth(line2Parts.join(sep), contentWidth, dim("…"));
|
|
221
239
|
// Line 3: extension statuses (branding, todo counter, mode), joined by " · ".
|
|
222
240
|
const statuses = input.statuses.map((s) => s.replace(/[\r\n\t]/g, " ").trim()).filter(Boolean);
|
|
223
241
|
const lines = [line1, line2];
|
|
224
242
|
if (statuses.length > 0) {
|
|
225
|
-
lines.push(truncateToWidth(dim(statuses.join(SEP)),
|
|
243
|
+
lines.push(pad + truncateToWidth(dim(statuses.join(SEP)), contentWidth, dim("…")));
|
|
226
244
|
}
|
|
227
245
|
return lines;
|
|
228
246
|
}
|
|
@@ -258,7 +276,7 @@ export function createYagniFooterFactory(ctx) {
|
|
|
258
276
|
usage: collectUsage(ctx.sessionManager),
|
|
259
277
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
260
278
|
statuses,
|
|
261
|
-
}, theme, width);
|
|
279
|
+
}, theme, width, resolveFooterPadX());
|
|
262
280
|
},
|
|
263
281
|
invalidate() {
|
|
264
282
|
gitCache = undefined;
|
|
@@ -22,9 +22,8 @@ export interface Reroute {
|
|
|
22
22
|
export type HeaderMap = Record<string, string | undefined>;
|
|
23
23
|
/**
|
|
24
24
|
* Parse the `x-yagni-model-reroute` header. Returns null when the header is
|
|
25
|
-
* absent, does not match `<from>-><to>:<reason>`, or the reason is not
|
|
26
|
-
*
|
|
27
|
-
* can honestly claim).
|
|
25
|
+
* absent, does not match `<from>-><to>:<reason>`, or the reason is not a
|
|
26
|
+
* known reason (vision or policy; see KNOWN_REASONS).
|
|
28
27
|
*/
|
|
29
28
|
export declare function parseReroute(headers: HeaderMap): Reroute | null;
|
|
30
29
|
export declare class RerouteNotifier {
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
const REROUTE_HEADER = "x-yagni-model-reroute";
|
|
16
16
|
const REROUTE_PATTERN = /^(.+?)->(.+?):(\w+)$/;
|
|
17
|
+
/** Reroute reasons the client understands. `policy` (caller→tier routing,
|
|
18
|
+
* 2026-08-11 spec) is parsed but deliberately produces NO user notice: the
|
|
19
|
+
* spec keeps per-request routing quiet — /cost and the savings receipts are
|
|
20
|
+
* the user-facing surface. `vision` keeps its one-time notice ("Your image…"
|
|
21
|
+
* is copy only a vision reroute can honestly claim). */
|
|
22
|
+
const KNOWN_REASONS = new Set(["vision", "policy"]);
|
|
17
23
|
/**
|
|
18
24
|
* Parse the `x-yagni-model-reroute` header. Returns null when the header is
|
|
19
|
-
* absent, does not match `<from>-><to>:<reason>`, or the reason is not
|
|
20
|
-
*
|
|
21
|
-
* can honestly claim).
|
|
25
|
+
* absent, does not match `<from>-><to>:<reason>`, or the reason is not a
|
|
26
|
+
* known reason (vision or policy; see KNOWN_REASONS).
|
|
22
27
|
*/
|
|
23
28
|
export function parseReroute(headers) {
|
|
24
29
|
const value = headers[REROUTE_HEADER];
|
|
@@ -28,17 +33,19 @@ export function parseReroute(headers) {
|
|
|
28
33
|
if (!match)
|
|
29
34
|
return null;
|
|
30
35
|
const [, from, to, reason] = match;
|
|
31
|
-
if (reason
|
|
36
|
+
if (!KNOWN_REASONS.has(reason))
|
|
32
37
|
return null;
|
|
33
38
|
return { from, to, reason };
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
36
41
|
* Dedupe wrapper around {@link parseReroute}: `observe` returns the notice
|
|
37
|
-
* message at most once per distinct from->to
|
|
38
|
-
* instance (i.e. per session), and null otherwise (absent, malformed,
|
|
39
|
-
* already-seen
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* message at most once per distinct from->to->reason triple for the life of
|
|
43
|
+
* the instance (i.e. per session), and null otherwise (absent, malformed,
|
|
44
|
+
* already-seen, or a non-vision reason — `policy` reroutes are parsed but
|
|
45
|
+
* deliberately silent; see KNOWN_REASONS). Returning the message rather than
|
|
46
|
+
* taking a notify callback keeps the caller in charge of the ctx it has on
|
|
47
|
+
* hand at call time, instead of this class holding a stale reference across
|
|
48
|
+
* calls.
|
|
42
49
|
*/
|
|
43
50
|
/**
|
|
44
51
|
* The concrete tier ladder cheapest-first, mirroring the proxy's
|
|
@@ -53,7 +60,9 @@ export class RerouteNotifier {
|
|
|
53
60
|
const reroute = parseReroute(headers);
|
|
54
61
|
if (!reroute)
|
|
55
62
|
return null;
|
|
56
|
-
|
|
63
|
+
if (reroute.reason !== "vision")
|
|
64
|
+
return null;
|
|
65
|
+
const key = `${reroute.from}->${reroute.to}:${reroute.reason}`;
|
|
57
66
|
if (this.seen.has(key))
|
|
58
67
|
return null;
|
|
59
68
|
this.seen.add(key);
|
|
@@ -75,6 +75,53 @@ const GENERAL_AGENT = {
|
|
|
75
75
|
body: GENERAL_BODY,
|
|
76
76
|
source: "builtin",
|
|
77
77
|
};
|
|
78
|
+
const SEARCHER_BODY = `You are a repo scout. Your job is wide, mechanical reconnaissance:
|
|
79
|
+
find files, map structure, trace usages, and summarize what is there. You do
|
|
80
|
+
not write code and you do not run commands; you read and report.
|
|
81
|
+
|
|
82
|
+
You are grounded in how THIS company works: call ask_yagni before inferring a
|
|
83
|
+
convention, an ownership rule, or anything organization-specific.
|
|
84
|
+
|
|
85
|
+
Your final message is your report back to the driving agent, which has NOT
|
|
86
|
+
seen what you read. Make it compressed and complete: exact file paths, the
|
|
87
|
+
key excerpts, and a one-paragraph map of how the pieces relate. Say what you
|
|
88
|
+
did NOT find as plainly as what you found.`;
|
|
89
|
+
/** Wide search and repo mapping on the cheapest tier: read-only by
|
|
90
|
+
* construction, so a wrong answer costs a re-ask, never a bad edit. */
|
|
91
|
+
const SEARCHER_AGENT = {
|
|
92
|
+
name: "searcher",
|
|
93
|
+
description: "Fast repo reconnaissance: wide searches, structure mapping, usage tracing, " +
|
|
94
|
+
"summarizing files. Read-only. Use for any broad look-around you would " +
|
|
95
|
+
"otherwise do with a chain of grep/read calls.",
|
|
96
|
+
model: "efficient",
|
|
97
|
+
tools: ["read", "grep", "find", "ls", "ask_yagni"],
|
|
98
|
+
body: SEARCHER_BODY,
|
|
99
|
+
source: "builtin",
|
|
100
|
+
};
|
|
101
|
+
const IMPLEMENTER_BODY = `You are a mechanical implementer. You execute a
|
|
102
|
+
well-specified change: apply an edit across files, fix a failing test, rename
|
|
103
|
+
carefully, wire a defined seam. The judgment calls were made before you were
|
|
104
|
+
spawned; if the task turns out to require one, STOP and report the fork in
|
|
105
|
+
your final message instead of guessing.
|
|
106
|
+
|
|
107
|
+
You are grounded in how THIS company works: call ask_yagni before inferring a
|
|
108
|
+
convention, an ownership rule, or anything organization-specific.
|
|
109
|
+
|
|
110
|
+
Your final message is your report back to the driving agent, which has NOT
|
|
111
|
+
seen what you did. List every file you touched, what changed in each, the
|
|
112
|
+
commands you ran with their outcomes, and anything you deliberately left
|
|
113
|
+
undone.`;
|
|
114
|
+
/** Mechanical multi-file execution on the mid tier: the task arrives fully
|
|
115
|
+
* specified, so the premium tiers' judgment is not being paid for. */
|
|
116
|
+
const IMPLEMENTER_AGENT = {
|
|
117
|
+
name: "implementer",
|
|
118
|
+
description: "Mechanical execution of a fully-specified change: multi-file edits, " +
|
|
119
|
+
"test-fix grinds, careful renames. Spawn it with the decision already " +
|
|
120
|
+
"made; it stops and reports rather than improvising.",
|
|
121
|
+
model: "standard",
|
|
122
|
+
body: IMPLEMENTER_BODY,
|
|
123
|
+
source: "builtin",
|
|
124
|
+
};
|
|
78
125
|
// Concrete tiers a subagent can actually run on. `balanced` is deliberately NOT
|
|
79
126
|
// a member here even though it is a member of `ModelTier`: a subagent needs
|
|
80
127
|
// ONE model for its whole run, and balanced is a session-level routing policy,
|
|
@@ -162,7 +209,7 @@ function loadAgentsFromDir(dir, source) {
|
|
|
162
209
|
export function discoverSubagents(deps) {
|
|
163
210
|
const home = deps.homeDir ?? homedir();
|
|
164
211
|
const layers = [
|
|
165
|
-
[GENERAL_AGENT],
|
|
212
|
+
[GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT],
|
|
166
213
|
...pluginAgentDirs(deps.env ?? process.env).map((dir) => loadAgentsFromDir(dir, "plugin")),
|
|
167
214
|
loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
|
|
168
215
|
loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
|
package/dist/launch.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { agentDirEnvVar } from "./branding.js";
|
|
10
|
+
import { PAD_X_ENV, resolvePadX } from "./padding.js";
|
|
10
11
|
/**
|
|
11
12
|
* How close to expiry the token can be before launch warns. A coding session
|
|
12
13
|
* easily outlives a token, so warn early enough that re-logging in before a long
|
|
@@ -95,6 +96,10 @@ export function buildLaunch(creds, passthroughArgs, opts) {
|
|
|
95
96
|
// Match the e2b harness defaults: skip pi's update check + telemetry.
|
|
96
97
|
PI_SKIP_VERSION_CHECK: "1",
|
|
97
98
|
PI_TELEMETRY: "0",
|
|
99
|
+
// Forward the chosen horizontal pad so the extension's footer aligns with
|
|
100
|
+
// the editor (which the launcher pads by seeding editorPaddingX). The
|
|
101
|
+
// footer can't read pi's settings, so the value crosses over env.
|
|
102
|
+
[PAD_X_ENV]: String(resolvePadX()),
|
|
98
103
|
};
|
|
99
104
|
// Always load our extension. Default the provider to `yagni` unless the user
|
|
100
105
|
// explicitly chose one (so power users can still point pi elsewhere).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the CLI's horizontal padding, shared by the
|
|
3
|
+
* launcher (which seeds the editor setting) and — via the `YAGNI_PAD_X` env the
|
|
4
|
+
* launcher forwards — pi-extension-yagni's footer.
|
|
5
|
+
*
|
|
6
|
+
* The value must match pi's `outputPad` (the chat/output area's left+right
|
|
7
|
+
* padding, which defaults to 1) so the input editor, the output stream, and
|
|
8
|
+
* the status bar all align on the same column. Keeping it in one module makes
|
|
9
|
+
* the coherence structural rather than eyeballed: change this one number and
|
|
10
|
+
* the editor, footer, and seeded default move together.
|
|
11
|
+
*
|
|
12
|
+
* Separators (the ─ rules above/below the input) intentionally stay full-bleed
|
|
13
|
+
* and are NOT padded — matching Claude Code's frame.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PAD_X = 1;
|
|
16
|
+
/** Left (and right) padding string applied to the editor and footer. */
|
|
17
|
+
export declare const PAD_STR: string;
|
|
18
|
+
/** Env var the launcher sets so the extension's footer can match the editor pad. */
|
|
19
|
+
export declare const PAD_X_ENV = "YAGNI_PAD_X";
|
|
20
|
+
/** Parse a pad width from the env override, falling back to {@link PAD_X}. */
|
|
21
|
+
export declare function resolvePadX(raw?: string | undefined): number;
|
|
22
|
+
//# sourceMappingURL=padding.d.ts.map
|
package/dist/padding.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the CLI's horizontal padding, shared by the
|
|
3
|
+
* launcher (which seeds the editor setting) and — via the `YAGNI_PAD_X` env the
|
|
4
|
+
* launcher forwards — pi-extension-yagni's footer.
|
|
5
|
+
*
|
|
6
|
+
* The value must match pi's `outputPad` (the chat/output area's left+right
|
|
7
|
+
* padding, which defaults to 1) so the input editor, the output stream, and
|
|
8
|
+
* the status bar all align on the same column. Keeping it in one module makes
|
|
9
|
+
* the coherence structural rather than eyeballed: change this one number and
|
|
10
|
+
* the editor, footer, and seeded default move together.
|
|
11
|
+
*
|
|
12
|
+
* Separators (the ─ rules above/below the input) intentionally stay full-bleed
|
|
13
|
+
* and are NOT padded — matching Claude Code's frame.
|
|
14
|
+
*/
|
|
15
|
+
export const PAD_X = 1;
|
|
16
|
+
/** Left (and right) padding string applied to the editor and footer. */
|
|
17
|
+
export const PAD_STR = " ".repeat(PAD_X);
|
|
18
|
+
/** Env var the launcher sets so the extension's footer can match the editor pad. */
|
|
19
|
+
export const PAD_X_ENV = "YAGNI_PAD_X";
|
|
20
|
+
/** Parse a pad width from the env override, falling back to {@link PAD_X}. */
|
|
21
|
+
export function resolvePadX(raw = process.env[PAD_X_ENV]) {
|
|
22
|
+
const n = raw === undefined ? NaN : Number.parseInt(raw, 10);
|
|
23
|
+
return Number.isFinite(n) && n >= 0 && n <= 3 ? n : PAD_X;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=padding.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.2.1-staging.
|
|
3
|
+
"version": "0.2.1-staging.1041.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.83.0",
|
|
39
39
|
"typebox": "^1.1.38"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "c0f29e93789c8996bced9dcc24583a9ab426efc0"
|
|
42
42
|
}
|