@yagni-app/code 0.3.0 → 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.js +12 -0
- 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/extension/approvedPrefixes.d.ts +11 -0
- package/dist/extension/approvedPrefixes.js +30 -0
- package/dist/extension/askAdvisorTool.d.ts +18 -3
- package/dist/extension/askAdvisorTool.js +121 -15
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +42 -2
- package/dist/extension/branding.d.ts +11 -1
- package/dist/extension/branding.js +47 -7
- package/dist/extension/config.d.ts +12 -0
- package/dist/extension/config.js +2 -1
- package/dist/extension/execPolicy.d.ts +17 -1
- package/dist/extension/execPolicy.js +164 -33
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +8 -1
- package/dist/extension/footer.js +33 -19
- package/dist/extension/guardian.d.ts +14 -4
- package/dist/extension/guardian.js +35 -11
- package/dist/extension/index.d.ts +20 -3
- package/dist/extension/index.js +92 -13
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +1 -0
- package/dist/extension/permission.js +23 -18
- package/dist/extension/pipeline/goCommand.js +6 -4
- package/dist/extension/pipeline/personas.js +1 -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 +9 -1
- package/dist/extension/pipeline/runRegistry.js +22 -1
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/subagents.d.ts +7 -1
- package/dist/extension/subagents.js +60 -5
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +3 -2
|
@@ -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 {};
|
|
@@ -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,9 +76,33 @@ 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",
|
|
@@ -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,6 +221,11 @@ 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", {
|
|
@@ -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,8 +1,21 @@
|
|
|
1
1
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { canSurfaceSuggestion, noteSuggestionSurfaced } from "./flywheel.js";
|
|
3
4
|
import { collectRepoDocs } from "./repoDocs.js";
|
|
4
5
|
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
5
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
|
+
}
|
|
6
19
|
const parameters = Type.Object({
|
|
7
20
|
question: Type.String(),
|
|
8
21
|
context: Type.Optional(Type.String()),
|
|
@@ -40,6 +53,7 @@ export function makeAskYagniTool(opts) {
|
|
|
40
53
|
"Call ask_yagni BEFORE guessing about anything organization- or codebase-specific (conventions, policies, architecture, ownership, product decisions).",
|
|
41
54
|
"Pass the user's actual question; add relevant local context (file paths, snippets) in the optional `context` field.",
|
|
42
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.",
|
|
43
57
|
],
|
|
44
58
|
parameters,
|
|
45
59
|
renderCall(args, theme) {
|
|
@@ -55,8 +69,11 @@ export function makeAskYagniTool(opts) {
|
|
|
55
69
|
const citations = result.details?.citations ?? [];
|
|
56
70
|
if (isPartial)
|
|
57
71
|
return new Text(t.fg("muted", answer || "Asking YAGNI…"), 0, 0);
|
|
72
|
+
const standing = standingLine(result.details?.standing);
|
|
58
73
|
if (expanded) {
|
|
59
74
|
const container = new Container();
|
|
75
|
+
if (standing)
|
|
76
|
+
container.addChild(new Text(t.fg("muted", standing), 0, 0));
|
|
60
77
|
container.addChild(markdownOrPlain(answer || "(no answer)", t));
|
|
61
78
|
if (citations.length > 0) {
|
|
62
79
|
container.addChild(new Spacer(1));
|
|
@@ -69,6 +86,8 @@ export function makeAskYagniTool(opts) {
|
|
|
69
86
|
const lines = answer.trim().split("\n");
|
|
70
87
|
const out = lines.slice(0, ANSWER_PREVIEW_LINES).map((l) => t.fg("toolOutput", l));
|
|
71
88
|
const meta = [];
|
|
89
|
+
if (standing)
|
|
90
|
+
meta.push(standing);
|
|
72
91
|
if (citations.length > 0)
|
|
73
92
|
meta.push(citationCount(citations.length));
|
|
74
93
|
if (lines.length > ANSWER_PREVIEW_LINES || citations.length > 0)
|
|
@@ -95,6 +114,7 @@ export function makeAskYagniTool(opts) {
|
|
|
95
114
|
question: params.question,
|
|
96
115
|
context: params.context,
|
|
97
116
|
cwd: ctx?.cwd,
|
|
117
|
+
...(opts.getRepo?.() ? { repo: opts.getRepo() } : {}),
|
|
98
118
|
...(repoDocs.length > 0 ? { repoDocs } : {}),
|
|
99
119
|
}),
|
|
100
120
|
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
@@ -102,9 +122,29 @@ export function makeAskYagniTool(opts) {
|
|
|
102
122
|
throw new Error(await friendlyFetchError("ask_yagni", res));
|
|
103
123
|
}
|
|
104
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
|
+
}
|
|
105
142
|
return {
|
|
106
|
-
content: [{ type: "text", text
|
|
107
|
-
details: {
|
|
143
|
+
content: [{ type: "text", text }],
|
|
144
|
+
details: {
|
|
145
|
+
citations: data.citations ?? [],
|
|
146
|
+
...(data.standing ? { standing: data.standing } : {}),
|
|
147
|
+
},
|
|
108
148
|
};
|
|
109
149
|
},
|
|
110
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). 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.";
|
|
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
|
|
@@ -42,10 +42,31 @@ export const YAGNI_IDENTITY = "You are YAGNI Code, an autonomous terminal coding
|
|
|
42
42
|
export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wide searches, and mechanical multi-file " +
|
|
43
43
|
"work out to subagents (they run on cheaper tiers). Reach for the stock " +
|
|
44
44
|
"agents by name: `searcher` for read-only reconnaissance and summarizing, " +
|
|
45
|
-
"`implementer` for executing a change you have already fully specified
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"`implementer` for executing a change you have already fully specified, " +
|
|
46
|
+
"`verification` for an adversarial pass that tries to break completed work " +
|
|
47
|
+
"before you rely on it. Keep judgment, synthesis, and the conversation " +
|
|
48
|
+
"with the user in this session. Do not spawn a subagent for work you can " +
|
|
49
|
+
"finish in a couple of tool calls.";
|
|
50
|
+
/**
|
|
51
|
+
* The ultra-mode delegation directive (/ultra): the user has opted into
|
|
52
|
+
* aggressive multi-agent orchestration, so the driver is told to structure
|
|
53
|
+
* meaningful work as a diamond — split, fan out workers, fan out refuting
|
|
54
|
+
* checkers, synthesize — instead of delegating only when convenient.
|
|
55
|
+
* DRIVER-ONLY for the same reason as {@link DRIVER_DELEGATION_PARAGRAPH}.
|
|
56
|
+
*/
|
|
57
|
+
export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has switched this session to ultra mode " +
|
|
58
|
+
"— aggressive multi-agent orchestration. Structure any meaningful task as " +
|
|
59
|
+
"a diamond: SPLIT the job into independent pieces; FAN OUT parallel " +
|
|
60
|
+
"subagents on cheaper tiers (`searcher` to scout, `implementer` or " +
|
|
61
|
+
"`general` to execute); CHECK by fanning out `verification` subagents told " +
|
|
62
|
+
"to refute the work, each through a different lens (correctness, edge cases, " +
|
|
63
|
+
"fit with this codebase); then SYNTHESIZE the results yourself. Treat " +
|
|
64
|
+
"agreement between checkers — not a single pass — as confirmation, and " +
|
|
65
|
+
"surface what they could not verify. Delegate by default and reserve this " +
|
|
66
|
+
"session for splitting, judging, and synthesis; only trivial work you can " +
|
|
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.";
|
|
49
70
|
/**
|
|
50
71
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
51
72
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -54,6 +75,14 @@ export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wi
|
|
|
54
75
|
* module stays a pure string, with no env dependency of its own.
|
|
55
76
|
*/
|
|
56
77
|
export const YAGNI_IDENTITY_DRIVER = `${YAGNI_IDENTITY}\n\n${DRIVER_DELEGATION_PARAGRAPH}`;
|
|
78
|
+
/** The driver identity while /ultra is on: base identity + the diamond directive. */
|
|
79
|
+
export const YAGNI_IDENTITY_ULTRA = `${YAGNI_IDENTITY}\n\n${ULTRA_DELEGATION_PARAGRAPH}`;
|
|
80
|
+
/**
|
|
81
|
+
* Every identity variant this module can install, longest-composite-first, so
|
|
82
|
+
* the re-brand swap below never leaves a shorter variant's orphaned delegation
|
|
83
|
+
* paragraph behind (driver/ultra both start with the base identity).
|
|
84
|
+
*/
|
|
85
|
+
const KNOWN_IDENTITIES = [YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY];
|
|
57
86
|
// pi 0.84.1's exact identity sentence (dist/core/system-prompt.js). Exported as
|
|
58
87
|
// the identity anchor the CLI's pi-contract tripwire test reads back from pi's
|
|
59
88
|
// built system prompt, so a pi bump that reworded the opener (silently defeating
|
|
@@ -112,15 +141,26 @@ export function brandSystemPrompt(original, opts = {}) {
|
|
|
112
141
|
let s = original;
|
|
113
142
|
// 1. Drop pi's self-referential documentation block.
|
|
114
143
|
s = s.replace(PI_DOCS_BLOCK_RE, "");
|
|
115
|
-
// 2. Own the identity.
|
|
144
|
+
// 2. Own the identity. A previously-branded prompt may open with a DIFFERENT
|
|
145
|
+
// variant than the one now requested (/ultra toggles the driver between the
|
|
146
|
+
// delegation and diamond paragraphs mid-session) — swap it in place rather
|
|
147
|
+
// than prepending a second identity block.
|
|
116
148
|
if (PI_IDENTITY_RE.test(s)) {
|
|
117
149
|
s = s.replace(PI_IDENTITY_RE, identity);
|
|
118
150
|
}
|
|
119
151
|
else if (PI_IDENTITY_LOOSE_RE.test(s)) {
|
|
120
152
|
s = s.replace(PI_IDENTITY_LOOSE_RE, identity);
|
|
121
153
|
}
|
|
122
|
-
else
|
|
123
|
-
|
|
154
|
+
else {
|
|
155
|
+
const trimmed = s.trimStart();
|
|
156
|
+
const lead = s.slice(0, s.length - trimmed.length);
|
|
157
|
+
const existing = KNOWN_IDENTITIES.find((k) => trimmed.startsWith(k));
|
|
158
|
+
if (existing && existing !== identity) {
|
|
159
|
+
s = `${lead}${identity}${trimmed.slice(existing.length)}`;
|
|
160
|
+
}
|
|
161
|
+
else if (!existing && !trimmed.startsWith(identity)) {
|
|
162
|
+
s = `${identity}\n\n${s}`;
|
|
163
|
+
}
|
|
124
164
|
}
|
|
125
165
|
// 3. Safety net for residual brand tokens (never inside user content).
|
|
126
166
|
s = scrubOutsideProjectContext(s);
|
|
@@ -113,6 +113,12 @@ export interface ContextBrief {
|
|
|
113
113
|
decisions: number;
|
|
114
114
|
corrections: number;
|
|
115
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Active decisions scoped to the repo named on the request (additive, Run
|
|
118
|
+
* 7; absent without `?repo=`, on older backends, and on a count failure).
|
|
119
|
+
* The opt-in mining beat fires ONLY on an explicit 0.
|
|
120
|
+
*/
|
|
121
|
+
repoDecisionCount?: number;
|
|
116
122
|
}
|
|
117
123
|
/**
|
|
118
124
|
* Attribution headers for the model proxy (YAG-471). On the SERVER side these
|
|
@@ -192,6 +198,12 @@ export interface FetchContextBriefOptions {
|
|
|
192
198
|
fetchImpl?: typeof fetch;
|
|
193
199
|
/** Env seam for the telemetry headers; defaults to process.env. */
|
|
194
200
|
env?: NodeJS.ProcessEnv;
|
|
201
|
+
/**
|
|
202
|
+
* The session repo (`owner/name`), when resolvable. Rides as `?repo=` so
|
|
203
|
+
* the response can carry `repoDecisionCount` for the mining beat — no
|
|
204
|
+
* second round-trip at boot.
|
|
205
|
+
*/
|
|
206
|
+
repo?: string;
|
|
195
207
|
}
|
|
196
208
|
/**
|
|
197
209
|
* Fetch the workspace company brief at startup.
|
package/dist/extension/config.js
CHANGED
|
@@ -208,7 +208,8 @@ export function sessionTelemetryHeaders(env = process.env) {
|
|
|
208
208
|
*/
|
|
209
209
|
export async function fetchContextBrief(opts) {
|
|
210
210
|
try {
|
|
211
|
-
const
|
|
211
|
+
const query = opts.repo ? `?repo=${encodeURIComponent(opts.repo)}` : "";
|
|
212
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/context${query}`, {
|
|
212
213
|
method: "GET",
|
|
213
214
|
headers: {
|
|
214
215
|
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
@@ -35,7 +35,18 @@
|
|
|
35
35
|
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
36
36
|
*/
|
|
37
37
|
export type TokenEntry = string | {
|
|
38
|
-
op: "pipe" | "and" | "or" | "semi" | "
|
|
38
|
+
op: "pipe" | "and" | "or" | "semi" | "substitution";
|
|
39
|
+
} | {
|
|
40
|
+
op: "redirect";
|
|
41
|
+
direction: "out";
|
|
42
|
+
fd: "stdout" | "stderr";
|
|
43
|
+
target: string;
|
|
44
|
+
append: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
op: "redirect";
|
|
47
|
+
direction: "in";
|
|
48
|
+
} | {
|
|
49
|
+
op: "background";
|
|
39
50
|
};
|
|
40
51
|
/**
|
|
41
52
|
* Parse a shell command string into tokens and control operators.
|
|
@@ -48,6 +59,11 @@ export type TokenEntry = string | {
|
|
|
48
59
|
* - `#` comments (start-of-word to end-of-line, outside quotes)
|
|
49
60
|
* - Shell constructs we flag as unanalyzable: $(), backticks (INCLUDING
|
|
50
61
|
* inside double quotes — bash executes those), >, <, background &
|
|
62
|
+
* - Redirect metadata: stdout/stderr redirects carry fd + target so that
|
|
63
|
+
* safe redirects (2>/dev/null, 2>&1) can be distinguished from unsafe ones
|
|
64
|
+
* (> file.txt). Stdin redirects (<, <<) carry no metadata — they always
|
|
65
|
+
* floor. Background & emits a distinct `background` op (not `semi`) so
|
|
66
|
+
* hasUnhandledConstructs can always catch it.
|
|
51
67
|
*
|
|
52
68
|
* Does NOT handle: variable expansion, glob patterns, heredocs beyond the
|
|
53
69
|
* redirect flag, nested subshells beyond depth tracking. Commands using
|