@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.
Files changed (98) hide show
  1. package/dist/cli.d.ts +30 -0
  2. package/dist/cli.js +147 -3
  3. package/dist/connectClaudeCode.d.ts +77 -0
  4. package/dist/connectClaudeCode.js +228 -0
  5. package/dist/connectCodex.d.ts +75 -0
  6. package/dist/connectCodex.js +201 -0
  7. package/dist/doctor.d.ts +1 -1
  8. package/dist/doctor.js +1 -1
  9. package/dist/extension/advisor.d.ts +4 -4
  10. package/dist/extension/advisor.js +6 -7
  11. package/dist/extension/approvedPrefixes.d.ts +103 -0
  12. package/dist/extension/approvedPrefixes.js +282 -0
  13. package/dist/extension/askAdvisorTool.d.ts +20 -5
  14. package/dist/extension/askAdvisorTool.js +126 -20
  15. package/dist/extension/askYagniTool.d.ts +23 -0
  16. package/dist/extension/askYagniTool.js +91 -2
  17. package/dist/extension/branding.d.ts +34 -3
  18. package/dist/extension/branding.js +114 -13
  19. package/dist/extension/chipEditor.d.ts +30 -9
  20. package/dist/extension/chipEditor.js +173 -59
  21. package/dist/extension/claudeRules.d.ts +0 -2
  22. package/dist/extension/claudeRules.js +0 -8
  23. package/dist/extension/cmux/dispatcher.d.ts +25 -0
  24. package/dist/extension/cmux/dispatcher.js +266 -0
  25. package/dist/extension/cmux/hooks.d.ts +12 -0
  26. package/dist/extension/cmux/hooks.js +192 -0
  27. package/dist/extension/cmux/index.d.ts +3 -0
  28. package/dist/extension/cmux/index.js +155 -0
  29. package/dist/extension/cmux/naming.d.ts +5 -0
  30. package/dist/extension/cmux/naming.js +23 -0
  31. package/dist/extension/cmux/state.d.ts +33 -0
  32. package/dist/extension/cmux/state.js +142 -0
  33. package/dist/extension/config.d.ts +44 -1
  34. package/dist/extension/config.js +37 -4
  35. package/dist/extension/costHud.d.ts +16 -22
  36. package/dist/extension/costHud.js +8 -47
  37. package/dist/extension/crashReport.js +1 -3
  38. package/dist/extension/execPolicy.d.ts +135 -0
  39. package/dist/extension/execPolicy.js +936 -0
  40. package/dist/extension/flywheel.d.ts +44 -0
  41. package/dist/extension/flywheel.js +53 -0
  42. package/dist/extension/footer.d.ts +118 -0
  43. package/dist/extension/footer.js +308 -0
  44. package/dist/extension/guardian.d.ts +139 -0
  45. package/dist/extension/guardian.js +237 -0
  46. package/dist/extension/index.d.ts +35 -7
  47. package/dist/extension/index.js +338 -33
  48. package/dist/extension/mineBeat.d.ts +95 -0
  49. package/dist/extension/mineBeat.js +193 -0
  50. package/dist/extension/permission.d.ts +124 -10
  51. package/dist/extension/permission.js +603 -52
  52. package/dist/extension/pipeline/childRegistry.d.ts +41 -0
  53. package/dist/extension/pipeline/childRegistry.js +118 -0
  54. package/dist/extension/pipeline/finish.js +5 -1
  55. package/dist/extension/pipeline/goCommand.d.ts +1 -1
  56. package/dist/extension/pipeline/goCommand.js +40 -9
  57. package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
  58. package/dist/extension/pipeline/goStatusCommands.js +61 -1
  59. package/dist/extension/pipeline/personas.js +26 -1
  60. package/dist/extension/pipeline/resilience.d.ts +2 -1
  61. package/dist/extension/pipeline/resilience.js +21 -2
  62. package/dist/extension/pipeline/runRegistry.d.ts +23 -1
  63. package/dist/extension/pipeline/runRegistry.js +57 -1
  64. package/dist/extension/pipeline/runner.js +4 -0
  65. package/dist/extension/pipeline/verify.d.ts +4 -0
  66. package/dist/extension/pipeline/verify.js +48 -26
  67. package/dist/extension/recordDecisionTool.d.ts +8 -0
  68. package/dist/extension/recordDecisionTool.js +24 -0
  69. package/dist/extension/redact.d.ts +20 -0
  70. package/dist/extension/redact.js +64 -0
  71. package/dist/extension/rerouteNotice.d.ts +3 -4
  72. package/dist/extension/rerouteNotice.js +20 -11
  73. package/dist/extension/subagentRender.d.ts +129 -0
  74. package/dist/extension/subagentRender.js +441 -0
  75. package/dist/extension/subagents.d.ts +11 -8
  76. package/dist/extension/subagents.js +162 -37
  77. package/dist/extension/ticketTools.d.ts +37 -0
  78. package/dist/extension/ticketTools.js +117 -0
  79. package/dist/extension/todos.d.ts +28 -1
  80. package/dist/extension/todos.js +76 -1
  81. package/dist/extension/tokenProvider.js +46 -5
  82. package/dist/extension/ultra.d.ts +27 -0
  83. package/dist/extension/ultra.js +76 -0
  84. package/dist/launch.d.ts +7 -0
  85. package/dist/launch.js +24 -12
  86. package/dist/login.d.ts +4 -2
  87. package/dist/login.js +19 -4
  88. package/dist/padding.d.ts +22 -0
  89. package/dist/padding.js +25 -0
  90. package/dist/promptEnrichment.d.ts +40 -0
  91. package/dist/promptEnrichment.js +85 -0
  92. package/dist/signalForward.d.ts +60 -0
  93. package/dist/signalForward.js +130 -0
  94. package/dist/token.d.ts +25 -0
  95. package/dist/token.js +45 -0
  96. package/package.json +6 -5
  97. package/dist/extension/boostCommand.d.ts +0 -144
  98. package/dist/extension/boostCommand.js +0 -263
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `ask_advisor` — the peak-tier escalation available to Balanced sessions (YAG-380).
2
+ * `ask_advisor` — the peak-tier escalation available to Advanced sessions (YAG-380).
3
3
  *
4
- * Cheap driver, expensive consultant: a Balanced session drives on `advanced` and
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 { 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 {};
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `ask_advisor` — the peak-tier escalation available to Balanced sessions (YAG-380).
2
+ * `ask_advisor` — the peak-tier escalation available to Advanced sessions (YAG-380).
3
3
  *
4
- * Cheap driver, expensive consultant: a Balanced session drives on `advanced` and
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
- 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",
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 Balanced tier, and capped per session, so use it for " +
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 (Balanced sessions, capped).",
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
- 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,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 (Balanced sessions only, capped per session).",
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, 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,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: data.answer }],
58
- details: { citations: data.citations ?? [] },
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. Idempotent: re-branding keeps a single identity.
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