@yagni-app/code 0.2.1 → 0.3.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 +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +44 -1
- package/dist/extension/config.js +37 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ask_advisor` — the peak-tier escalation available to
|
|
2
|
+
* `ask_advisor` — the peak-tier escalation available to Advanced sessions (YAG-380).
|
|
3
3
|
*
|
|
4
|
-
* Cheap driver, expensive consultant:
|
|
4
|
+
* Cheap driver, expensive consultant: an Advanced session drives on `advanced` and
|
|
5
5
|
* calls this when it hits a judgment call worth the strongest model. The consult
|
|
6
6
|
* spawns a child pi at `peak` through the same `runStage` seam `/go` uses, and
|
|
7
7
|
* returns the advice as plain TEXT the driver acts on — text we own, so
|
|
@@ -26,10 +26,12 @@
|
|
|
26
26
|
* this module is the I/O.
|
|
27
27
|
*/
|
|
28
28
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
29
30
|
import { Type } from "typebox";
|
|
30
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
31
32
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
32
|
-
import type
|
|
33
|
+
import { type PipelineStage } from "./pipeline/types.js";
|
|
34
|
+
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
33
35
|
/**
|
|
34
36
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
35
37
|
* reason documented there: enough tools to navigate instead of flailing on a
|
|
@@ -66,10 +68,18 @@ export declare function buildConsultBrief(params: {
|
|
|
66
68
|
* persona, and that is the advisor's own.
|
|
67
69
|
*/
|
|
68
70
|
export declare function advisorStage(): PipelineStage;
|
|
69
|
-
|
|
71
|
+
/** The ask_advisor result details: consult accounting plus the live progress
|
|
72
|
+
* record the subagent renderers paint (one task, the consult itself). */
|
|
73
|
+
export interface AdvisorToolDetails {
|
|
70
74
|
consults: number;
|
|
71
75
|
cost: number;
|
|
72
|
-
|
|
76
|
+
tasks?: SubagentTaskProgress[];
|
|
77
|
+
}
|
|
78
|
+
/** Title painted the moment the ask_advisor call streams in. */
|
|
79
|
+
export declare function renderAdvisorCall(args: {
|
|
80
|
+
question?: string;
|
|
81
|
+
} | undefined, theme: RenderTheme, _context: unknown): Component;
|
|
82
|
+
export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, AdvisorToolDetails>;
|
|
73
83
|
/**
|
|
74
84
|
* `/advise <question>` — the manual escalation lever.
|
|
75
85
|
*
|
|
@@ -77,6 +87,11 @@ export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): Too
|
|
|
77
87
|
* consult draws on the same cap rather than opening a side channel around it.
|
|
78
88
|
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
79
89
|
* driver sees it in context and can act on it.
|
|
90
|
+
*
|
|
91
|
+
* A command has no tool row for pi to render, so the live view is an
|
|
92
|
+
* aboveEditor widget painted from the tool's partial updates: the same
|
|
93
|
+
* two-line `agent — task` / `↳ current tool` status the subagent tool shows,
|
|
94
|
+
* driven by a spinner ticker so it visibly moves between child events.
|
|
80
95
|
*/
|
|
81
96
|
export declare function registerAdviseCommand(pi: ExtensionAPI, tool: ReturnType<typeof makeAskAdvisorTool>): void;
|
|
82
97
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ask_advisor` — the peak-tier escalation available to
|
|
2
|
+
* `ask_advisor` — the peak-tier escalation available to Advanced sessions (YAG-380).
|
|
3
3
|
*
|
|
4
|
-
* Cheap driver, expensive consultant:
|
|
4
|
+
* Cheap driver, expensive consultant: an Advanced session drives on `advanced` and
|
|
5
5
|
* calls this when it hits a judgment call worth the strongest model. The consult
|
|
6
6
|
* spawns a child pi at `peak` through the same `runStage` seam `/go` uses, and
|
|
7
7
|
* returns the advice as plain TEXT the driver acts on — text we own, so
|
|
@@ -25,9 +25,14 @@
|
|
|
25
25
|
* The gate, the cap and the spend ceiling live in `advisor.ts` and are pure;
|
|
26
26
|
* this module is the I/O.
|
|
27
27
|
*/
|
|
28
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
28
29
|
import { Type } from "typebox";
|
|
29
30
|
import { ADVISOR_MODEL_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatConsultCost, } from "./advisor.js";
|
|
31
|
+
import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
|
|
32
|
+
import { withResilience } from "./pipeline/resilience.js";
|
|
30
33
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
34
|
+
import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
35
|
+
import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, receiptLine, renderSubagentResult, runningLines, } from "./subagentRender.js";
|
|
31
36
|
/**
|
|
32
37
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
33
38
|
* reason documented there: enough tools to navigate instead of flailing on a
|
|
@@ -71,15 +76,39 @@ export function advisorStage() {
|
|
|
71
76
|
taskTemplate: "{ticket}",
|
|
72
77
|
};
|
|
73
78
|
}
|
|
79
|
+
/** Spinner cadence for the advisor's live views; matches subagentRender. */
|
|
80
|
+
const SPINNER_TICK_MS = 120;
|
|
81
|
+
/** Question preview width on the call title line. */
|
|
82
|
+
const QUESTION_PREVIEW_MAX = 128;
|
|
83
|
+
function clip(text, max) {
|
|
84
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
85
|
+
if (collapsed.length <= max)
|
|
86
|
+
return collapsed;
|
|
87
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
88
|
+
}
|
|
89
|
+
/** Title painted the moment the ask_advisor call streams in. */
|
|
90
|
+
export function renderAdvisorCall(args, theme, _context) {
|
|
91
|
+
const title = theme.fg("toolTitle", theme.bold("advisor"));
|
|
92
|
+
let text = `${title} ${theme.fg("accent", "peak-tier consult")}`;
|
|
93
|
+
if (args?.question)
|
|
94
|
+
text += `\n ${theme.fg("dim", clip(args.question, QUESTION_PREVIEW_MAX))}`;
|
|
95
|
+
return new Text(text, 0, 0);
|
|
96
|
+
}
|
|
74
97
|
export function makeAskAdvisorTool(opts) {
|
|
75
98
|
const limits = opts.limits ?? DEFAULT_ADVISOR_LIMITS;
|
|
76
|
-
|
|
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);
|
|
77
106
|
return {
|
|
78
107
|
name: "ask_advisor",
|
|
79
108
|
label: "Ask the advisor",
|
|
80
109
|
description: "Escalate ONE hard judgment call to the peak-tier advisor — the strongest " +
|
|
81
110
|
"model available, which reads the code itself and returns a recommendation. " +
|
|
82
|
-
"Available only on the
|
|
111
|
+
"Available only on the Advanced tier, and capped per session, so use it for " +
|
|
83
112
|
"calls that are genuinely worth it: an architectural fork with no obvious " +
|
|
84
113
|
"right answer, a subtle correctness question you cannot settle by reading, " +
|
|
85
114
|
"a change whose blast radius you are unsure of, or a second opinion before " +
|
|
@@ -87,7 +116,7 @@ export function makeAskAdvisorTool(opts) {
|
|
|
87
116
|
"lookups (use ask_yagni), for anything you can settle by reading the code, " +
|
|
88
117
|
"or to review work you have already finished. Pass a sharp question plus the " +
|
|
89
118
|
"relevant excerpts — never a conversation transcript.",
|
|
90
|
-
promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (
|
|
119
|
+
promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Advanced sessions, capped).",
|
|
91
120
|
promptGuidelines: [
|
|
92
121
|
"Call ask_advisor only for a genuine judgment fork — an architectural choice, a subtle correctness question, or a second opinion before an approach you would have to unwind. Reading the code is cheaper; do that first.",
|
|
93
122
|
"Ask ONE specific question per consult, and include the excerpts that matter. The advisor reads the repo itself, so point it at the right place rather than pasting everything.",
|
|
@@ -95,6 +124,8 @@ export function makeAskAdvisorTool(opts) {
|
|
|
95
124
|
"The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
|
|
96
125
|
],
|
|
97
126
|
parameters,
|
|
127
|
+
renderCall: renderAdvisorCall,
|
|
128
|
+
renderResult: renderSubagentResult,
|
|
98
129
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
99
130
|
// Read the LIVE session model: pi's picker can change it after this tool
|
|
100
131
|
// was registered, in both directions.
|
|
@@ -109,19 +140,51 @@ export function makeAskAdvisorTool(opts) {
|
|
|
109
140
|
details: { consults: opts.state.read().consults, cost: 0 },
|
|
110
141
|
};
|
|
111
142
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
// Live progress, exactly the subagent tool's shape: the folded record
|
|
144
|
+
// rides every partial update's `details.tasks` (painted by
|
|
145
|
+
// renderSubagentResult), the plain-text summary rides `content` for
|
|
146
|
+
// headless consumers, and the harness "Working…" line mirrors it.
|
|
147
|
+
const progress = newTaskProgress("advisor", params.question, Date.now());
|
|
148
|
+
const ui = ctx?.hasUI ? ctx.ui : undefined;
|
|
149
|
+
let lastWorking;
|
|
150
|
+
const emit = () => {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
onUpdate?.({
|
|
153
|
+
content: [{ type: "text", text: progressSummaryText([progress], now) }],
|
|
154
|
+
details: {
|
|
155
|
+
consults: opts.state.read().consults,
|
|
156
|
+
cost: progress.usage.cost,
|
|
157
|
+
tasks: [{ ...progress, actions: [...progress.actions], usage: { ...progress.usage } }],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const working = formatWorkingMessage([progress], now);
|
|
161
|
+
if (ui && working !== lastWorking) {
|
|
162
|
+
lastWorking = working;
|
|
163
|
+
ui.setWorkingMessage?.(working);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
emit();
|
|
167
|
+
let result;
|
|
168
|
+
try {
|
|
169
|
+
result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, {
|
|
170
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
171
|
+
...(signal ? { signal } : {}),
|
|
172
|
+
// YAG-471: attribute the consult's completions to the advisor, not
|
|
173
|
+
// the "plan" stage id advisorStage() borrows (see its docblock).
|
|
174
|
+
callerLabel: "advisor",
|
|
175
|
+
onEvent: (ev) => {
|
|
176
|
+
if (applyChildEvent(progress, ev))
|
|
177
|
+
emit();
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
// Restore the default "Working…" text whether we resolved or threw.
|
|
183
|
+
ui?.setWorkingMessage?.();
|
|
184
|
+
}
|
|
123
185
|
const cost = result.usage?.cost ?? 0;
|
|
124
186
|
const state = opts.state.record(cost);
|
|
187
|
+
finalizeTask(progress, result, Date.now());
|
|
125
188
|
if (result.exitCode !== 0 && !result.finalOutput.trim()) {
|
|
126
189
|
// Fail honestly rather than returning an empty recommendation. The
|
|
127
190
|
// consult still counts: it spawned, and it may well have spent.
|
|
@@ -134,7 +197,7 @@ export function makeAskAdvisorTool(opts) {
|
|
|
134
197
|
`Decide this one yourself.`,
|
|
135
198
|
},
|
|
136
199
|
],
|
|
137
|
-
details: { consults: state.consults, cost },
|
|
200
|
+
details: { consults: state.consults, cost, tasks: [progress] },
|
|
138
201
|
};
|
|
139
202
|
}
|
|
140
203
|
return {
|
|
@@ -144,11 +207,13 @@ export function makeAskAdvisorTool(opts) {
|
|
|
144
207
|
text: `${result.finalOutput.trim()}\n\n${formatConsultCost({ cost, consults: state.consults, limits })}`,
|
|
145
208
|
},
|
|
146
209
|
],
|
|
147
|
-
details: { consults: state.consults, cost },
|
|
210
|
+
details: { consults: state.consults, cost, tasks: [progress] },
|
|
148
211
|
};
|
|
149
212
|
},
|
|
150
213
|
};
|
|
151
214
|
}
|
|
215
|
+
/** The widget key the /advise live panel paints under. */
|
|
216
|
+
const ADVISE_WIDGET_KEY = "yagni-advise";
|
|
152
217
|
/**
|
|
153
218
|
* `/advise <question>` — the manual escalation lever.
|
|
154
219
|
*
|
|
@@ -156,10 +221,15 @@ export function makeAskAdvisorTool(opts) {
|
|
|
156
221
|
* consult draws on the same cap rather than opening a side channel around it.
|
|
157
222
|
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
158
223
|
* driver sees it in context and can act on it.
|
|
224
|
+
*
|
|
225
|
+
* A command has no tool row for pi to render, so the live view is an
|
|
226
|
+
* aboveEditor widget painted from the tool's partial updates: the same
|
|
227
|
+
* two-line `agent — task` / `↳ current tool` status the subagent tool shows,
|
|
228
|
+
* driven by a spinner ticker so it visibly moves between child events.
|
|
159
229
|
*/
|
|
160
230
|
export function registerAdviseCommand(pi, tool) {
|
|
161
231
|
pi.registerCommand("advise", {
|
|
162
|
-
description: "Escalate one hard call to the peak-tier advisor (
|
|
232
|
+
description: "Escalate one hard call to the peak-tier advisor (Advanced sessions only, capped per session).",
|
|
163
233
|
handler: async (args, ctx) => {
|
|
164
234
|
const notify = (message, type) => {
|
|
165
235
|
if (ctx.hasUI)
|
|
@@ -175,8 +245,41 @@ export function registerAdviseCommand(pi, tool) {
|
|
|
175
245
|
return;
|
|
176
246
|
}
|
|
177
247
|
notify("Consulting the peak-tier advisor. This reads the repo, so it takes a moment.", "info");
|
|
248
|
+
// Live panel state: the newest folded progress record from the tool's
|
|
249
|
+
// partial updates, repainted on a spinner tick until the consult ends.
|
|
250
|
+
let progress;
|
|
251
|
+
let paintTimer;
|
|
252
|
+
const theme = ctx.hasUI ? ctx.ui.theme : undefined;
|
|
253
|
+
const paint = () => {
|
|
254
|
+
if (!ctx.hasUI || !theme || !progress)
|
|
255
|
+
return;
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const lines = progress.status === "running"
|
|
258
|
+
? runningLines(progress, theme, now, SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length])
|
|
259
|
+
: [receiptLine(progress, theme)];
|
|
260
|
+
ctx.ui.setWidget?.(ADVISE_WIDGET_KEY, lines, { placement: "aboveEditor" });
|
|
261
|
+
};
|
|
262
|
+
const onUpdate = (update) => {
|
|
263
|
+
const task = update.details?.tasks?.[0];
|
|
264
|
+
if (!task)
|
|
265
|
+
return;
|
|
266
|
+
progress = task;
|
|
267
|
+
if (ctx.hasUI && theme && !paintTimer) {
|
|
268
|
+
paintTimer = setInterval(paint, SPINNER_TICK_MS);
|
|
269
|
+
paintTimer.unref?.();
|
|
270
|
+
}
|
|
271
|
+
paint();
|
|
272
|
+
};
|
|
273
|
+
const clearPanel = () => {
|
|
274
|
+
if (paintTimer) {
|
|
275
|
+
clearInterval(paintTimer);
|
|
276
|
+
paintTimer = undefined;
|
|
277
|
+
}
|
|
278
|
+
if (ctx.hasUI)
|
|
279
|
+
ctx.ui.setWidget?.(ADVISE_WIDGET_KEY, undefined);
|
|
280
|
+
};
|
|
178
281
|
try {
|
|
179
|
-
const out = await tool.execute("advise", { question }, ctx.signal,
|
|
282
|
+
const out = await tool.execute("advise", { question }, ctx.signal, onUpdate, ctx);
|
|
180
283
|
const text = out.content
|
|
181
284
|
.map((c) => c.text ?? "")
|
|
182
285
|
.join("\n")
|
|
@@ -188,6 +291,9 @@ export function registerAdviseCommand(pi, tool) {
|
|
|
188
291
|
notify(`/advise failed: ${message}`, "error");
|
|
189
292
|
await pi.sendUserMessage(`/advise failed: ${message}`);
|
|
190
293
|
}
|
|
294
|
+
finally {
|
|
295
|
+
clearPanel();
|
|
296
|
+
}
|
|
191
297
|
},
|
|
192
298
|
});
|
|
193
299
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { type FlywheelState } from "./flywheel.js";
|
|
3
4
|
import { type RepoDocSnippet } from "./repoDocs.js";
|
|
4
5
|
/** A single source citation returned by the YAGNI `ask` endpoint. */
|
|
5
6
|
export interface Citation {
|
|
6
7
|
title: string;
|
|
7
8
|
url: string;
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* How hard the answer may be leaned on (mirror of `@yagni/shared`'s
|
|
12
|
+
* AskStanding — mirrored locally by this extension's no-workspace-imports
|
|
13
|
+
* convention, see costHud.ts).
|
|
14
|
+
*/
|
|
15
|
+
export type AskStanding = "confirmed" | "asserted" | "inferred" | "no_position";
|
|
16
|
+
/** One line the TUI shows above the answer, per standing. */
|
|
17
|
+
export declare const STANDING_LINES: Record<AskStanding, string>;
|
|
9
18
|
/** Options for {@link makeAskYagniTool}. */
|
|
10
19
|
export interface MakeAskYagniToolOptions {
|
|
11
20
|
baseUrl: string;
|
|
@@ -19,6 +28,19 @@ export interface MakeAskYagniToolOptions {
|
|
|
19
28
|
* checkout's own era, since the docs are read from the tree being edited.
|
|
20
29
|
*/
|
|
21
30
|
collectDocs?: (cwd: string, query: string) => RepoDocSnippet[];
|
|
31
|
+
/**
|
|
32
|
+
* Shared flywheel session state (Run 7): caps how many no-position
|
|
33
|
+
* recordSuggestions reach the model per session and attributes the
|
|
34
|
+
* follow-up record_decision for dedupe. Absent → suggestions always
|
|
35
|
+
* surface, nothing is attributed (tests, older wiring).
|
|
36
|
+
*/
|
|
37
|
+
flywheel?: FlywheelState;
|
|
38
|
+
/**
|
|
39
|
+
* The session repo (`owner/name`), for the backend's soft-scoped decision
|
|
40
|
+
* read (workspace-level decisions plus this repo's own — never another
|
|
41
|
+
* repo's conventions). Absent keeps the workspace-wide read.
|
|
42
|
+
*/
|
|
43
|
+
getRepo?: () => string | undefined;
|
|
22
44
|
}
|
|
23
45
|
declare const parameters: Type.TObject<{
|
|
24
46
|
question: Type.TString;
|
|
@@ -34,6 +56,7 @@ declare const parameters: Type.TObject<{
|
|
|
34
56
|
*/
|
|
35
57
|
export declare function makeAskYagniTool(opts: MakeAskYagniToolOptions): ToolDefinition<typeof parameters, {
|
|
36
58
|
citations: Citation[];
|
|
59
|
+
standing?: AskStanding;
|
|
37
60
|
}>;
|
|
38
61
|
export {};
|
|
39
62
|
//# sourceMappingURL=askYagniTool.d.ts.map
|
|
@@ -1,10 +1,37 @@
|
|
|
1
|
+
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
1
2
|
import { Type } from "typebox";
|
|
3
|
+
import { canSurfaceSuggestion, noteSuggestionSurfaced } from "./flywheel.js";
|
|
2
4
|
import { collectRepoDocs } from "./repoDocs.js";
|
|
3
5
|
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
6
|
+
import { markdownOrPlain } from "./subagentRender.js";
|
|
7
|
+
/** One line the TUI shows above the answer, per standing. */
|
|
8
|
+
export const STANDING_LINES = {
|
|
9
|
+
confirmed: "Grounded in a confirmed decision",
|
|
10
|
+
asserted: "Grounded in a recorded assumption, not yet verified",
|
|
11
|
+
inferred: "Inferred from workspace context, not a recorded decision",
|
|
12
|
+
no_position: "No recorded position in this workspace",
|
|
13
|
+
};
|
|
14
|
+
function standingLine(value) {
|
|
15
|
+
return typeof value === "string" && value in STANDING_LINES
|
|
16
|
+
? STANDING_LINES[value]
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
4
19
|
const parameters = Type.Object({
|
|
5
20
|
question: Type.String(),
|
|
6
21
|
context: Type.Optional(Type.String()),
|
|
7
22
|
});
|
|
23
|
+
/** Collapsed answer preview length, in lines. */
|
|
24
|
+
const ANSWER_PREVIEW_LINES = 4;
|
|
25
|
+
/** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
|
|
26
|
+
function clipLine(text, max) {
|
|
27
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
28
|
+
if (collapsed.length <= max)
|
|
29
|
+
return collapsed;
|
|
30
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
function citationCount(n) {
|
|
33
|
+
return n === 1 ? "1 citation" : `${n} citations`;
|
|
34
|
+
}
|
|
8
35
|
/**
|
|
9
36
|
* Build the `ask_yagni` tool definition.
|
|
10
37
|
*
|
|
@@ -26,8 +53,49 @@ export function makeAskYagniTool(opts) {
|
|
|
26
53
|
"Call ask_yagni BEFORE guessing about anything organization- or codebase-specific (conventions, policies, architecture, ownership, product decisions).",
|
|
27
54
|
"Pass the user's actual question; add relevant local context (file paths, snippets) in the optional `context` field.",
|
|
28
55
|
"When you use an answer, quote or reference its citations so the user can verify the source.",
|
|
56
|
+
"Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
|
|
29
57
|
],
|
|
30
58
|
parameters,
|
|
59
|
+
renderCall(args, theme) {
|
|
60
|
+
const t = theme;
|
|
61
|
+
let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
|
|
62
|
+
if (args?.context)
|
|
63
|
+
text += t.fg("muted", " (+context)");
|
|
64
|
+
return new Text(text, 0, 0);
|
|
65
|
+
},
|
|
66
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
67
|
+
const t = theme;
|
|
68
|
+
const answer = result.content.find((c) => c.type === "text")?.text ?? "";
|
|
69
|
+
const citations = result.details?.citations ?? [];
|
|
70
|
+
if (isPartial)
|
|
71
|
+
return new Text(t.fg("muted", answer || "Asking YAGNI…"), 0, 0);
|
|
72
|
+
const standing = standingLine(result.details?.standing);
|
|
73
|
+
if (expanded) {
|
|
74
|
+
const container = new Container();
|
|
75
|
+
if (standing)
|
|
76
|
+
container.addChild(new Text(t.fg("muted", standing), 0, 0));
|
|
77
|
+
container.addChild(markdownOrPlain(answer || "(no answer)", t));
|
|
78
|
+
if (citations.length > 0) {
|
|
79
|
+
container.addChild(new Spacer(1));
|
|
80
|
+
for (const c of citations) {
|
|
81
|
+
container.addChild(new Text(` ${t.fg("muted", "•")} ${t.fg("accent", c.title)} ${t.fg("dim", c.url)}`, 0, 0));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return container;
|
|
85
|
+
}
|
|
86
|
+
const lines = answer.trim().split("\n");
|
|
87
|
+
const out = lines.slice(0, ANSWER_PREVIEW_LINES).map((l) => t.fg("toolOutput", l));
|
|
88
|
+
const meta = [];
|
|
89
|
+
if (standing)
|
|
90
|
+
meta.push(standing);
|
|
91
|
+
if (citations.length > 0)
|
|
92
|
+
meta.push(citationCount(citations.length));
|
|
93
|
+
if (lines.length > ANSWER_PREVIEW_LINES || citations.length > 0)
|
|
94
|
+
meta.push("(ctrl+o to expand)");
|
|
95
|
+
if (meta.length > 0)
|
|
96
|
+
out.push(t.fg("muted", ` ${meta.join(" · ")}`));
|
|
97
|
+
return new Text(out.join("\n"), 0, 0);
|
|
98
|
+
},
|
|
31
99
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
32
100
|
onUpdate?.({ content: [{ type: "text", text: "Asking YAGNI…" }], details: { citations: [] } });
|
|
33
101
|
// Era-correct repo grounding: gather the working tree's most relevant docs
|
|
@@ -46,6 +114,7 @@ export function makeAskYagniTool(opts) {
|
|
|
46
114
|
question: params.question,
|
|
47
115
|
context: params.context,
|
|
48
116
|
cwd: ctx?.cwd,
|
|
117
|
+
...(opts.getRepo?.() ? { repo: opts.getRepo() } : {}),
|
|
49
118
|
...(repoDocs.length > 0 ? { repoDocs } : {}),
|
|
50
119
|
}),
|
|
51
120
|
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
@@ -53,9 +122,29 @@ export function makeAskYagniTool(opts) {
|
|
|
53
122
|
throw new Error(await friendlyFetchError("ask_yagni", res));
|
|
54
123
|
}
|
|
55
124
|
const data = (await res.json());
|
|
125
|
+
// Run 7 flywheel: a no-position answer carries the record-the-assumption
|
|
126
|
+
// instruction. It reaches the model VERBATIM inside the tool result, but
|
|
127
|
+
// only up to the per-session cap — past it a busy run stops being told
|
|
128
|
+
// to bank more asserted rows.
|
|
129
|
+
let text = data.answer;
|
|
130
|
+
const suggestion = data.recordSuggestion?.instruction;
|
|
131
|
+
if (suggestion && data.standing === "no_position") {
|
|
132
|
+
const state = opts.flywheel;
|
|
133
|
+
if (!state || canSurfaceSuggestion(state)) {
|
|
134
|
+
// Attribution correlates to the QUESTION the suggestion asked
|
|
135
|
+
// about, so only the record_decision that answers it inherits the
|
|
136
|
+
// dedupe flag.
|
|
137
|
+
if (state)
|
|
138
|
+
noteSuggestionSurfaced(state, data.recordSuggestion?.question ?? params.question);
|
|
139
|
+
text = `${text}\n\n${suggestion}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
56
142
|
return {
|
|
57
|
-
content: [{ type: "text", text
|
|
58
|
-
details: {
|
|
143
|
+
content: [{ type: "text", text }],
|
|
144
|
+
details: {
|
|
145
|
+
citations: data.citations ?? [],
|
|
146
|
+
...(data.standing ? { standing: data.standing } : {}),
|
|
147
|
+
},
|
|
59
148
|
};
|
|
60
149
|
},
|
|
61
150
|
};
|
|
@@ -30,6 +30,14 @@ export declare const YAGNI_IDENTITY: string;
|
|
|
30
30
|
* exist.
|
|
31
31
|
*/
|
|
32
32
|
export declare const DRIVER_DELEGATION_PARAGRAPH: string;
|
|
33
|
+
/**
|
|
34
|
+
* The ultra-mode delegation directive (/ultra): the user has opted into
|
|
35
|
+
* aggressive multi-agent orchestration, so the driver is told to structure
|
|
36
|
+
* meaningful work as a diamond — split, fan out workers, fan out refuting
|
|
37
|
+
* checkers, synthesize — instead of delegating only when convenient.
|
|
38
|
+
* DRIVER-ONLY for the same reason as {@link DRIVER_DELEGATION_PARAGRAPH}.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ULTRA_DELEGATION_PARAGRAPH: string;
|
|
33
41
|
/**
|
|
34
42
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
35
43
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -37,7 +45,9 @@ export declare const DRIVER_DELEGATION_PARAGRAPH: string;
|
|
|
37
45
|
* effective `x-yagni-caller` attribution (config.ts's `isDriverCaller`) — this
|
|
38
46
|
* module stays a pure string, with no env dependency of its own.
|
|
39
47
|
*/
|
|
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.";
|
|
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
|
+
/** 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. 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.";
|
|
41
51
|
export declare const PI_IDENTITY_RE: RegExp;
|
|
42
52
|
/**
|
|
43
53
|
* Env switch that bypasses the system-prompt rewrite entirely, so pi's
|
|
@@ -53,6 +63,15 @@ export interface BrandSystemPromptOptions {
|
|
|
53
63
|
identity?: string;
|
|
54
64
|
/** Live company brief to inject so the agent boots grounded (Phase C). */
|
|
55
65
|
contextBrief?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Repository rules section (`.claude/rules`, built by claudeRules.ts).
|
|
68
|
+
* Placed after the brief and BEFORE the closing reminder: rules are user
|
|
69
|
+
* content that may legitimately name other coding agents — exactly the text
|
|
70
|
+
* the reminder inoculates against — so the reminder must stay the most
|
|
71
|
+
* recent instruction. Appended after the scrub step, so rules content is
|
|
72
|
+
* never brand-rewritten (same exemption as <project_context>).
|
|
73
|
+
*/
|
|
74
|
+
rulesSection?: string | null;
|
|
56
75
|
}
|
|
57
76
|
/**
|
|
58
77
|
* Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
|
|
@@ -61,7 +80,9 @@ export interface BrandSystemPromptOptions {
|
|
|
61
80
|
* Steps: (1) drop pi's self-referential documentation block, (2) replace the
|
|
62
81
|
* identity sentence (with a loose fallback, else prepend), (3) scrub residual
|
|
63
82
|
* brand tokens outside user content, (4) append the company brief + a proactive
|
|
64
|
-
* ask_yagni directive
|
|
83
|
+
* ask_yagni directive, (5) append the repository-rules section verbatim, (6)
|
|
84
|
+
* close with the identity reminder, which must stay last (recency). Idempotent:
|
|
85
|
+
* re-branding keeps a single identity, brief, rules section, and reminder.
|
|
65
86
|
*/
|
|
66
87
|
export declare function brandSystemPrompt(original: string, opts?: BrandSystemPromptOptions): string;
|
|
67
88
|
/** The minimal theme surface the masthead needs (a subset of pi's Theme). */
|
|
@@ -69,11 +90,21 @@ export interface MastheadTheme {
|
|
|
69
90
|
bold(s: string): string;
|
|
70
91
|
fg(color: string, s: string): string;
|
|
71
92
|
}
|
|
93
|
+
export interface MastheadOptions {
|
|
94
|
+
/** CLI version (e.g. "0.2.1") shown next to the title; omitted when absent. */
|
|
95
|
+
version?: string;
|
|
96
|
+
/** Working directory shown on the third line (already home-collapsed). */
|
|
97
|
+
cwd?: string;
|
|
98
|
+
}
|
|
72
99
|
/**
|
|
73
100
|
* Build the YAGNI Code startup masthead string, rendered into a header that
|
|
74
101
|
* REPLACES pi's built-in startup banner (which otherwise shows "pi v<version>"
|
|
75
102
|
* and a "Pi can explain its own features…" onboarding line). Kept here as a
|
|
76
103
|
* pure string builder so its content is unit-testable without a terminal.
|
|
104
|
+
*
|
|
105
|
+
* Layout: the YAGNI art on the left, and to its right a three-line block —
|
|
106
|
+
* bold white "YAGNI Code" + version, the tagline, and the cwd. Art rows are
|
|
107
|
+
* padded to a uniform width so the right column stays straight.
|
|
77
108
|
*/
|
|
78
|
-
export declare function buildMastheadString(theme: MastheadTheme): string;
|
|
109
|
+
export declare function buildMastheadString(theme: MastheadTheme, opts?: MastheadOptions): string;
|
|
79
110
|
//# sourceMappingURL=branding.d.ts.map
|