@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.
Files changed (48) hide show
  1. package/dist/cli.js +12 -0
  2. package/dist/connectClaudeCode.d.ts +77 -0
  3. package/dist/connectClaudeCode.js +228 -0
  4. package/dist/connectCodex.d.ts +75 -0
  5. package/dist/connectCodex.js +201 -0
  6. package/dist/extension/approvedPrefixes.d.ts +11 -0
  7. package/dist/extension/approvedPrefixes.js +30 -0
  8. package/dist/extension/askAdvisorTool.d.ts +18 -3
  9. package/dist/extension/askAdvisorTool.js +121 -15
  10. package/dist/extension/askYagniTool.d.ts +23 -0
  11. package/dist/extension/askYagniTool.js +42 -2
  12. package/dist/extension/branding.d.ts +11 -1
  13. package/dist/extension/branding.js +47 -7
  14. package/dist/extension/config.d.ts +12 -0
  15. package/dist/extension/config.js +2 -1
  16. package/dist/extension/execPolicy.d.ts +17 -1
  17. package/dist/extension/execPolicy.js +164 -33
  18. package/dist/extension/flywheel.d.ts +44 -0
  19. package/dist/extension/flywheel.js +53 -0
  20. package/dist/extension/footer.d.ts +8 -1
  21. package/dist/extension/footer.js +33 -19
  22. package/dist/extension/guardian.d.ts +14 -4
  23. package/dist/extension/guardian.js +35 -11
  24. package/dist/extension/index.d.ts +20 -3
  25. package/dist/extension/index.js +92 -13
  26. package/dist/extension/mineBeat.d.ts +95 -0
  27. package/dist/extension/mineBeat.js +193 -0
  28. package/dist/extension/permission.d.ts +1 -0
  29. package/dist/extension/permission.js +23 -18
  30. package/dist/extension/pipeline/goCommand.js +6 -4
  31. package/dist/extension/pipeline/personas.js +1 -1
  32. package/dist/extension/pipeline/resilience.d.ts +2 -1
  33. package/dist/extension/pipeline/resilience.js +21 -2
  34. package/dist/extension/pipeline/runRegistry.d.ts +9 -1
  35. package/dist/extension/pipeline/runRegistry.js +22 -1
  36. package/dist/extension/recordDecisionTool.d.ts +8 -0
  37. package/dist/extension/recordDecisionTool.js +24 -0
  38. package/dist/extension/subagents.d.ts +7 -1
  39. package/dist/extension/subagents.js +60 -5
  40. package/dist/extension/todos.d.ts +28 -1
  41. package/dist/extension/todos.js +76 -1
  42. package/dist/extension/ultra.d.ts +27 -0
  43. package/dist/extension/ultra.js +76 -0
  44. package/dist/login.d.ts +4 -2
  45. package/dist/login.js +19 -4
  46. package/dist/token.d.ts +25 -0
  47. package/dist/token.js +45 -0
  48. 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 { PipelineStage } from "./pipeline/types.js";
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
- export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, {
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
- const runStage = opts.runStage ?? defaultRunStage;
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
- onUpdate?.({
113
- content: [{ type: "text", text: "Consulting the advisor…" }],
114
- details: { consults: opts.state.read().consults, cost: 0 },
115
- });
116
- const result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, {
117
- cwd: ctx?.cwd ?? process.cwd(),
118
- ...(signal ? { signal } : {}),
119
- // YAG-471: attribute the consult's completions to the advisor, not
120
- // the "plan" stage id advisorStage() borrows (see its docblock).
121
- callerLabel: "advisor",
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, undefined, ctx);
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: data.answer }],
107
- details: { citations: data.citations ?? [] },
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
- "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.";
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 if (!s.trimStart().startsWith(identity)) {
123
- s = `${identity}\n\n${s}`;
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.
@@ -208,7 +208,8 @@ export function sessionTelemetryHeaders(env = process.env) {
208
208
  */
209
209
  export async function fetchContextBrief(opts) {
210
210
  try {
211
- const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/context`, {
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" | "redirect" | "substitution";
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