@yagni-app/code 0.3.0 → 0.3.2

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 (56) 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/crashReport.d.ts +12 -0
  7. package/dist/crashReport.js +28 -1
  8. package/dist/extension/approvedPrefixes.d.ts +11 -0
  9. package/dist/extension/approvedPrefixes.js +30 -0
  10. package/dist/extension/askAdvisorTool.d.ts +18 -3
  11. package/dist/extension/askAdvisorTool.js +121 -15
  12. package/dist/extension/askYagniTool.d.ts +23 -0
  13. package/dist/extension/askYagniTool.js +42 -2
  14. package/dist/extension/branding.d.ts +11 -1
  15. package/dist/extension/branding.js +47 -7
  16. package/dist/extension/config.d.ts +12 -0
  17. package/dist/extension/config.js +2 -1
  18. package/dist/extension/crashReport.d.ts +18 -0
  19. package/dist/extension/crashReport.js +35 -2
  20. package/dist/extension/execPolicy.d.ts +17 -1
  21. package/dist/extension/execPolicy.js +227 -33
  22. package/dist/extension/flywheel.d.ts +44 -0
  23. package/dist/extension/flywheel.js +53 -0
  24. package/dist/extension/footer.d.ts +8 -1
  25. package/dist/extension/footer.js +33 -19
  26. package/dist/extension/guardian.d.ts +14 -4
  27. package/dist/extension/guardian.js +35 -11
  28. package/dist/extension/index.d.ts +20 -3
  29. package/dist/extension/index.js +92 -13
  30. package/dist/extension/mineBeat.d.ts +95 -0
  31. package/dist/extension/mineBeat.js +193 -0
  32. package/dist/extension/permission.d.ts +2 -1
  33. package/dist/extension/permission.js +75 -22
  34. package/dist/extension/pipeline/goCommand.js +6 -4
  35. package/dist/extension/pipeline/invocation.d.ts +24 -2
  36. package/dist/extension/pipeline/invocation.js +30 -2
  37. package/dist/extension/pipeline/personas.js +2 -2
  38. package/dist/extension/pipeline/resilience.d.ts +2 -1
  39. package/dist/extension/pipeline/resilience.js +21 -2
  40. package/dist/extension/pipeline/runRegistry.d.ts +9 -1
  41. package/dist/extension/pipeline/runRegistry.js +22 -1
  42. package/dist/extension/recordDecisionTool.d.ts +8 -0
  43. package/dist/extension/recordDecisionTool.js +24 -0
  44. package/dist/extension/subagents.d.ts +7 -1
  45. package/dist/extension/subagents.js +73 -5
  46. package/dist/extension/todos.d.ts +28 -1
  47. package/dist/extension/todos.js +76 -1
  48. package/dist/extension/ultra.d.ts +27 -0
  49. package/dist/extension/ultra.js +76 -0
  50. package/dist/login.d.ts +4 -2
  51. package/dist/login.js +19 -4
  52. package/dist/promptEnrichment.d.ts +1 -1
  53. package/dist/promptEnrichment.js +1 -1
  54. package/dist/token.d.ts +25 -0
  55. package/dist/token.js +45 -0
  56. package/package.json +3 -2
@@ -41,11 +41,20 @@
41
41
  import { spawnSync } from "node:child_process";
42
42
  import { statSync } from "node:fs";
43
43
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
44
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
44
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
45
45
  export const BRANCH_MAX_WIDTH = 60;
46
46
  const WORKTREE_MAX_WIDTH = 30;
47
47
  /** Section separator: single space + middle dot + single space. */
48
48
  const SEP = " · ";
49
+ const MODE_CYCLE = ["auto", "review", "plan"];
50
+ export function cyclePermissionMode(current) {
51
+ const idx = MODE_CYCLE.indexOf(current);
52
+ return MODE_CYCLE[(idx + 1) % MODE_CYCLE.length];
53
+ }
54
+ export function isShiftTab(data) {
55
+ return matchesKey(data, "shift+tab");
56
+ }
57
+ export const GIT_MUTATING_PATTERN = /\bgit\s+(?:checkout|switch|branch|worktree|reset|restore|rebase|merge|cherry-pick|bisect)\b/;
49
58
  /** Default horizontal pad when the launcher didn't forward one (matches outputPad=1). */
50
59
  const DEFAULT_PAD_X = 1;
51
60
  /**
@@ -107,10 +116,10 @@ export function collectUsage(sessionManager) {
107
116
  return totals;
108
117
  }
109
118
  /** End-cut ellipsis truncation (ANSI-aware) so the branch prefix stays readable. */
110
- function truncateEnd(text, maxWidth) {
119
+ function truncateEnd(text, maxWidth, ellipsis = "…") {
111
120
  if (visibleWidth(text) <= maxWidth)
112
121
  return text;
113
- return truncateToWidth(text, maxWidth, "…");
122
+ return truncateToWidth(text, maxWidth, ellipsis);
114
123
  }
115
124
  function runGit(args, cwd) {
116
125
  try {
@@ -193,15 +202,23 @@ export function detectGitInfo(cwd, home) {
193
202
  const worktree = resolveWorktreeLabel(root, branch);
194
203
  return { folder: basename(gitMainRepoRoot(root)), inRepo: true, branch, worktree };
195
204
  }
196
- /** Context color: dim below 70, warning 70-90, error above 90. */
205
+ const MODE_DISPLAY = {
206
+ auto: { text: "⏵⏵ auto mode", color: "accent" },
207
+ review: { text: "✓ review mode", color: "warning" },
208
+ plan: { text: "⏸ plan mode", color: "success" },
209
+ };
210
+ function modeDisplay(mode) {
211
+ return MODE_DISPLAY[mode];
212
+ }
213
+ /** Context color: success below 70, warning 70-90, error above 90. */
197
214
  function contextColor(percent) {
198
215
  if (percent === null)
199
- return "dim";
216
+ return "success";
200
217
  if (percent > 90)
201
218
  return "error";
202
219
  if (percent > 70)
203
220
  return "warning";
204
- return "dim";
221
+ return "success";
205
222
  }
206
223
  /** Pure line-builder, exported for tests. All data injected; colors via theme. */
207
224
  export function renderFooterLines(input, theme, width, padX = 0) {
@@ -218,7 +235,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
218
235
  if (input.git.worktree)
219
236
  line1Parts.push(theme.fg("warning", `[${input.git.worktree}]`));
220
237
  if (input.git.branch)
221
- line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH)));
238
+ line1Parts.push(theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…"))));
222
239
  }
223
240
  const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
224
241
  // Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
@@ -232,8 +249,10 @@ export function renderFooterLines(input, theme, width, padX = 0) {
232
249
  const stats = statParts.join(" ");
233
250
  const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
234
251
  const line2Parts = [];
235
- if (input.mode)
236
- line2Parts.push(dim(`${input.mode} mode`));
252
+ if (input.mode) {
253
+ const modeLabel = modeDisplay(input.mode);
254
+ line2Parts.push(theme.fg(modeLabel.color, modeLabel.text) + dim(" (shift+tab to change)"));
255
+ }
237
256
  line2Parts.push(dim(input.model));
238
257
  if (stats)
239
258
  line2Parts.push(dim(stats));
@@ -247,17 +266,8 @@ export function renderFooterLines(input, theme, width, padX = 0) {
247
266
  }
248
267
  return lines;
249
268
  }
250
- /**
251
- * Create a footer factory that captures the session `ctx` (for session data)
252
- * and returns the component `setFooter` expects. Called from the
253
- * `session_start` handler in index.ts.
254
- */
255
- export function createYagniFooterFactory(ctx, modeHolder) {
269
+ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
256
270
  return (_tui, theme, footerData) => {
257
- // Recompute git/worktree info only when the branch actually changes. Optional-
258
- // chained: onBranchChange is typed on ReadonlyFooterDataProvider, but the runtime
259
- // provider is whatever pi version is installed — guard so a mismatch can't break
260
- // footer construction (worst case, git info just doesn't auto-invalidate).
261
271
  let gitCache;
262
272
  const unsubscribeBranch = footerData.onBranchChange?.(() => {
263
273
  gitCache = undefined;
@@ -268,6 +278,10 @@ export function createYagniFooterFactory(ctx, modeHolder) {
268
278
  }
269
279
  return gitCache;
270
280
  };
281
+ if (invalidateHandle) {
282
+ invalidateHandle.invalidateGit = () => { gitCache = undefined; };
283
+ invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
284
+ }
271
285
  return {
272
286
  render(width) {
273
287
  const statuses = [...footerData.getExtensionStatuses().entries()]
@@ -37,7 +37,7 @@ export interface GuardianVerdict {
37
37
  rationale: string;
38
38
  }
39
39
  export interface GuardianLimits {
40
- /** Session cap on total Guardian reviews. */
40
+ /** Cap on Guardian reviews within the sliding window ({@link GUARDIAN_REVIEW_WINDOW_MS}). */
41
41
  maxReviews: number;
42
42
  /** Consecutive denials per turn before the circuit breaker trips. */
43
43
  maxConsecutiveDenials: number;
@@ -47,15 +47,25 @@ export interface GuardianLimits {
47
47
  export declare const DEFAULT_GUARDIAN_LIMITS: GuardianLimits;
48
48
  /**
49
49
  * Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
50
- * overrides the session review cap; anything non-numeric or < 1 falls back to
51
- * the default (a bad value must never zero out the cap and lock the session).
50
+ * overrides the sliding-window review cap; anything non-numeric or < 1 falls
51
+ * back to the default (a bad value must never zero out the cap and lock the
52
+ * session).
52
53
  */
53
54
  export declare function resolveGuardianLimits(env?: Record<string, string | undefined>): GuardianLimits;
54
55
  /** The model tier the Guardian runs on. Configurable via YAGNI_GUARDIAN_TIER. */
55
56
  export declare const GUARDIAN_MODEL_TIER = "efficient";
56
57
  /** Read-only tools — the Guardian can read files for context but cannot write or execute. */
57
58
  export declare const GUARDIAN_TOOLS: string[];
59
+ /**
60
+ * The review-cap window. `reviews` counts consults inside a SLIDING window
61
+ * rather than for the session's lifetime: a 24/7 session (a fleet operator's
62
+ * always-on terminal) must regain review capacity as old consults age out,
63
+ * not hard-block forever after the first N. The cap is a cost/runaway bound,
64
+ * not a safety bound — safety is the verdicts themselves.
65
+ */
66
+ export declare const GUARDIAN_REVIEW_WINDOW_MS: number;
58
67
  export interface GuardianState {
68
+ /** Guardian consults within the last {@link GUARDIAN_REVIEW_WINDOW_MS}. */
59
69
  reviews: number;
60
70
  consecutiveDenials: number;
61
71
  }
@@ -64,7 +74,7 @@ export interface GuardianStateHandle {
64
74
  recordReview(outcome: GuardianOutcome): GuardianState;
65
75
  resetTurn(): void;
66
76
  }
67
- export declare function makeGuardianState(): GuardianStateHandle;
77
+ export declare function makeGuardianState(now?: () => number): GuardianStateHandle;
68
78
  export interface CircuitBreakerResult {
69
79
  tripped: boolean;
70
80
  reason?: string;
@@ -29,14 +29,15 @@
29
29
  */
30
30
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
31
31
  export const DEFAULT_GUARDIAN_LIMITS = {
32
- maxReviews: 30,
32
+ maxReviews: 120,
33
33
  maxConsecutiveDenials: 3,
34
34
  timeoutMs: 15_000,
35
35
  };
36
36
  /**
37
37
  * Resolve Guardian limits from the environment. `YAGNI_GUARDIAN_MAX_REVIEWS`
38
- * overrides the session review cap; anything non-numeric or < 1 falls back to
39
- * the default (a bad value must never zero out the cap and lock the session).
38
+ * overrides the sliding-window review cap; anything non-numeric or < 1 falls
39
+ * back to the default (a bad value must never zero out the cap and lock the
40
+ * session).
40
41
  */
41
42
  export function resolveGuardianLimits(env = process.env) {
42
43
  const raw = env.YAGNI_GUARDIAN_MAX_REVIEWS?.trim();
@@ -48,25 +49,48 @@ export function resolveGuardianLimits(env = process.env) {
48
49
  export const GUARDIAN_MODEL_TIER = "efficient";
49
50
  /** Read-only tools — the Guardian can read files for context but cannot write or execute. */
50
51
  export const GUARDIAN_TOOLS = ["read"];
51
- export function makeGuardianState() {
52
- const state = { reviews: 0, consecutiveDenials: 0 };
52
+ // --- State ---
53
+ /**
54
+ * The review-cap window. `reviews` counts consults inside a SLIDING window
55
+ * rather than for the session's lifetime: a 24/7 session (a fleet operator's
56
+ * always-on terminal) must regain review capacity as old consults age out,
57
+ * not hard-block forever after the first N. The cap is a cost/runaway bound,
58
+ * not a safety bound — safety is the verdicts themselves.
59
+ */
60
+ export const GUARDIAN_REVIEW_WINDOW_MS = 60 * 60_000;
61
+ export function makeGuardianState(now = Date.now) {
62
+ const reviewTimes = [];
63
+ let consecutiveDenials = 0;
64
+ const prune = () => {
65
+ const cutoff = now() - GUARDIAN_REVIEW_WINDOW_MS;
66
+ while (reviewTimes.length > 0 && reviewTimes[0] <= cutoff)
67
+ reviewTimes.shift();
68
+ };
69
+ const snapshot = () => ({
70
+ reviews: reviewTimes.length,
71
+ consecutiveDenials,
72
+ });
53
73
  return {
54
- read: () => ({ ...state }),
74
+ read: () => {
75
+ prune();
76
+ return snapshot();
77
+ },
55
78
  recordReview(outcome) {
56
- state.reviews += 1;
79
+ prune();
80
+ reviewTimes.push(now());
57
81
  if (outcome === "deny") {
58
- state.consecutiveDenials += 1;
82
+ consecutiveDenials += 1;
59
83
  }
60
84
  else if (outcome === "allow") {
61
- state.consecutiveDenials = 0;
85
+ consecutiveDenials = 0;
62
86
  }
63
87
  // "ask" leaves the denial streak UNCHANGED: it is neither a denial nor
64
88
  // an exoneration. If it reset the streak, deny/ask/deny/ask would never
65
89
  // trip the breaker (round-2 review blocker).
66
- return { ...state };
90
+ return snapshot();
67
91
  },
68
92
  resetTurn() {
69
- state.consecutiveDenials = 0;
93
+ consecutiveDenials = 0;
70
94
  },
71
95
  };
72
96
  }
@@ -5,6 +5,7 @@ import { fetchMcpServers as defaultFetchMcpServers } from "./mcpTools.js";
5
5
  import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
6
6
  import { type TokenProvider } from "./tokenProvider.js";
7
7
  import { type CatalogResult, type ContextBrief } from "./config.js";
8
+ import { maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, type MineBeatMarkers } from "./mineBeat.js";
8
9
  /**
9
10
  * YAGNI Code extension entry point.
10
11
  *
@@ -36,7 +37,21 @@ export interface RegisterYagniDeps {
36
37
  baseUrl: string;
37
38
  getToken: () => string | undefined;
38
39
  fetchImpl?: typeof fetch;
40
+ repo?: string;
39
41
  }) => Promise<ContextBrief | null>;
42
+ /**
43
+ * The session repo (`owner/name`), resolved from the origin remote before
44
+ * the boot brief fetch so `/context` can carry `repoDecisionCount` for the
45
+ * mining beat. Injectable (no git in tests); undefined outside a repo.
46
+ */
47
+ resolveRepoFullName?: (cwd: string) => string | undefined;
48
+ /**
49
+ * The opt-in seeding beat itself (Run 7). Injectable so tests assert it
50
+ * fires only on a startup with an explicitly-zero repo ledger.
51
+ */
52
+ offerMiningBeat?: typeof defaultMaybeOfferMiningBeat;
53
+ /** The beat's once-per-repo marker store. Injectable (no disk in tests). */
54
+ mineBeatMarkers?: MineBeatMarkers;
40
55
  /**
41
56
  * Workspace MCP server list seam (YAG-446). The default hits
42
57
  * `/api/yagni-code/mcp/servers` and is fail-soft: older backends and
@@ -122,7 +137,9 @@ export type { RecordDecisionParams } from "./recordDecisionTool.js";
122
137
  export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
123
138
  export type { RunInitPassDeps, InitPassOutcome, RunTeamSetupDeps, TeamSetupOutcome, RepoIntake, RepoIntakeFs, EngineeringDraft, DraftTeam, } from "./initPass.js";
124
139
  export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
125
- export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
140
+ export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
141
+ export { createUltraHolder, registerUltraCommand } from "./ultra.js";
142
+ export type { UltraHolder } from "./ultra.js";
126
143
  export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
127
144
  export type { CatalogResult, FetchCatalogOptions, ModelEntry } from "./config.js";
128
145
  export { buildYagniProvider } from "./provider.js";
@@ -139,9 +156,9 @@ export type { VerifyCommand, VerifyOutcome } from "./pipeline/verify.js";
139
156
  export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
140
157
  export type { ComparisonReport, LaneFit, LaneOutcome } from "./pipeline/eval.js";
141
158
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
142
- export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
159
+ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
143
160
  export type { SubagentDef, SubagentSource } from "./subagents.js";
144
- export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
161
+ export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
145
162
  export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
146
163
  export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
147
164
  export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
@@ -14,13 +14,13 @@ import { registerCmuxBridge } from "./cmux/index.js";
14
14
  import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
15
15
  import { makeRecordDecisionTool } from "./recordDecisionTool.js";
16
16
  import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
17
- import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER } from "./branding.js";
17
+ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
18
18
  import { claudeRulesSection } from "./claudeRules.js";
19
19
  import { registerCostCommand } from "./costHud.js";
20
20
  import { isDebug } from "./diagnostics.js";
21
21
  import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
22
22
  import { codeStateHome } from "./stateHome.js";
23
- import { createYagniFooterFactory, formatCwd } from "./footer.js";
23
+ import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
24
24
  import { RerouteNotifier } from "./rerouteNotice.js";
25
25
  import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
26
26
  import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
@@ -29,6 +29,7 @@ import { registerGoCommand } from "./pipeline/goCommand.js";
29
29
  import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
30
30
  import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
31
31
  import { registerSubagents } from "./subagents.js";
32
+ import { createUltraHolder, registerUltraCommand } from "./ultra.js";
32
33
  import { registerTodos } from "./todos.js";
33
34
  import { registerDecisionCommands } from "./decisions.js";
34
35
  import { makeDecisionCapture } from "./decisionCapture.js";
@@ -40,6 +41,8 @@ import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
40
41
  import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
41
42
  import { buildYagniProvider } from "./provider.js";
42
43
  import { registerChipEditor } from "./chipEditor.js";
44
+ import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
45
+ import { makeFlywheelState } from "./flywheel.js";
43
46
  function isEvalMode(env = process.env) {
44
47
  return env.YAGNI_CODE_EVAL_MODE === "1";
45
48
  }
@@ -143,7 +146,11 @@ export async function registerYagni(pi, deps = {}) {
143
146
  // child, a subagent, or an advisor consult).
144
147
  pi.registerProvider("yagni", buildYagniProvider(catalog, baseUrl, attributionHeaders(deps.env)));
145
148
  const toolOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
146
- pi.registerTool(makeAskYagniTool(toolOpts));
149
+ // One flywheel state per session, shared by ask_yagni (caps how many
150
+ // no-position record suggestions reach the model) and record_decision
151
+ // (flywheel-attributed records send dedupe: true). Run 7.
152
+ const flywheelState = makeFlywheelState();
153
+ pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
147
154
  // Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
148
155
  // workspace tracker, attributed to the developer via per-user credentials.
149
156
  if (!evalMode) {
@@ -171,7 +178,7 @@ export async function registerYagni(pi, deps = {}) {
171
178
  pi.registerTool(makeRecordEngineeringContextTool(toolOpts));
172
179
  // The capture half of the judgment loop: bank a product-intent decision so
173
180
  // ask_yagni answers the same question next time instead of interrupting a human.
174
- pi.registerTool(makeRecordDecisionTool(toolOpts));
181
+ pi.registerTool(makeRecordDecisionTool({ ...toolOpts, flywheel: flywheelState }));
175
182
  }
176
183
  // The visible checklist for multi-step work: the todo_write tool, its
177
184
  // above-editor widget, and /todos. Branch-replayed, so forks and resumes
@@ -185,7 +192,12 @@ export async function registerYagni(pi, deps = {}) {
185
192
  // The general subagent tool: delegate self-contained tasks (optionally in
186
193
  // parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
187
194
  // riding the /go pipeline's child runner. /agents lists what's available.
188
- registerSubagents(pi);
195
+ // Ultra mode (/ultra) is an explicit orchestration dial: it swaps the
196
+ // driver's delegation identity for the diamond directive (see the
197
+ // before_agent_start handler below) and widens the tool's fan-out ceiling.
198
+ const ultraHolder = createUltraHolder();
199
+ registerSubagents(pi, { isUltra: () => ultraHolder.get() });
200
+ registerUltraCommand(pi, ultraHolder);
189
201
  // Shared fetch timeout for the small, interactive display-path reads below
190
202
  // (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
191
203
  // 5s, not the 10s other boot/grounding fetches use, because the user is
@@ -238,6 +250,7 @@ export async function registerYagni(pi, deps = {}) {
238
250
  // Mutating MCP tools join write/edit/bash in the gate policy: plan mode
239
251
  // holds them, review mode confirms them.
240
252
  const modeHolder = createModeHolder();
253
+ const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
241
254
  const guardianState = makeGuardianState();
242
255
  // Disabled by the local env override OR the workspace kill switch
243
256
  // (yagni_code.guardian, read from the catalog response at launch). The env
@@ -262,7 +275,9 @@ export async function registerYagni(pi, deps = {}) {
262
275
  catch { /* logging must never break the session */ }
263
276
  };
264
277
  // YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
265
- // at startup (grants added by other concurrent sessions appear next launch).
278
+ // at startup (grants added by other concurrent sessions appear next launch
279
+ // the startup load is the trust boundary; live reload was reviewed and
280
+ // rejected as a same-session self-authorization path, PR #1698).
266
281
  const sessionGrants = evalMode ? [] : loadGrants();
267
282
  const GUARDIAN_EVENT_TIMEOUT_MS = 5_000;
268
283
  registerPermissionGate(pi, {
@@ -452,11 +467,27 @@ export async function registerYagni(pi, deps = {}) {
452
467
  // must never block the agent), then inject it into the system prompt so the
453
468
  // very first turn already knows how this company and codebase work.
454
469
  const fetchBrief = deps.fetchContextBrief ?? defaultFetchContextBrief;
470
+ // Resolved BEFORE the brief fetch so /context can answer repoDecisionCount
471
+ // in the same round-trip (the mining beat's signal). Best-effort: undefined
472
+ // outside a git repo or without an origin remote.
473
+ const resolveRepoFn = deps.resolveRepoFullName ?? ((cwd) => defaultMineBeatGit.repoFullName(cwd));
474
+ let sessionRepo;
475
+ try {
476
+ sessionRepo = evalMode ? undefined : resolveRepoFn(process.cwd());
477
+ }
478
+ catch {
479
+ sessionRepo = undefined;
480
+ }
455
481
  let contextBrief;
456
482
  let briefResult = null;
457
483
  if (!evalMode) {
458
484
  try {
459
- briefResult = await fetchBrief({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
485
+ briefResult = await fetchBrief({
486
+ baseUrl,
487
+ getToken: getTokenFn,
488
+ fetchImpl: authedFetch,
489
+ repo: sessionRepo,
490
+ });
460
491
  contextBrief = briefResult?.brief?.trim() ? briefResult.brief : undefined;
461
492
  }
462
493
  catch {
@@ -517,13 +548,19 @@ export async function registerYagni(pi, deps = {}) {
517
548
  // assembled prompt passes through byte-exact (no rewrite, no brief
518
549
  // injection, no rules section, no delegation identity).
519
550
  const noBranding = brandingDisabled(env);
520
- const identity = isDriverCaller(env) ? YAGNI_IDENTITY_DRIVER : undefined;
551
+ // Resolved per turn (not once at activation): /ultra can flip the driver
552
+ // between the delegation and diamond identities mid-session.
553
+ const isDriver = isDriverCaller(env);
521
554
  pi.on("before_agent_start", (event) => noBranding
522
555
  ? undefined
523
556
  : {
524
557
  systemPrompt: brandSystemPrompt(event.systemPrompt, {
525
558
  contextBrief,
526
- identity,
559
+ identity: isDriver
560
+ ? ultraHolder.get()
561
+ ? YAGNI_IDENTITY_ULTRA
562
+ : YAGNI_IDENTITY_DRIVER
563
+ : undefined,
527
564
  rulesSection,
528
565
  }),
529
566
  });
@@ -681,7 +718,15 @@ export async function registerYagni(pi, deps = {}) {
681
718
  // context % on line 2, and extension statuses (brand, todos, mode) on
682
719
  // line 3. The factory captures ctx so the footer can read session data
683
720
  // (token stats, context usage) that isn't on the footerData provider.
684
- ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder)(tui, theme, footerData));
721
+ ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle)(tui, theme, footerData));
722
+ ctx.ui?.onTerminalInput?.((data) => {
723
+ if (isShiftTab(data)) {
724
+ modeHolder.set(cyclePermissionMode(modeHolder.get()));
725
+ footerInvalidateHandle.requestRender();
726
+ return { consume: true };
727
+ }
728
+ return undefined;
729
+ });
685
730
  // Label the collapsed chain-of-thought line so users know it is reasoning
686
731
  // and how to reveal the full trace. Harmless when reasoning is expanded
687
732
  // (the label only shows on hidden thinking blocks). Fails closed to pi's
@@ -755,6 +800,38 @@ export async function registerYagni(pi, deps = {}) {
755
800
  // The init pass is best-effort; a failure never blocks the session.
756
801
  }
757
802
  }
803
+ // Run 7: the opt-in repo seeding beat. Fires only on a genuine startup
804
+ // when /context reported an explicitly-empty repo-scoped ledger, once per
805
+ // repo (marker store). Fully fail-soft — a beat failure never blocks the
806
+ // session.
807
+ if (event.reason === "startup" && !evalMode) {
808
+ try {
809
+ const offerBeat = deps.offerMiningBeat ?? defaultMaybeOfferMiningBeat;
810
+ const markers = deps.mineBeatMarkers ?? fileMineBeatMarkers(codeStateHome(null, env));
811
+ await offerBeat(ctx, {
812
+ repoFullName: sessionRepo,
813
+ repoDecisionCount: briefResult?.repoDecisionCount,
814
+ cwd: process.cwd(),
815
+ baseUrl,
816
+ getToken: getTokenFn,
817
+ fetchImpl: authedFetch,
818
+ markers,
819
+ });
820
+ }
821
+ catch {
822
+ // Seeding is best-effort; a failure never blocks the session.
823
+ }
824
+ }
825
+ });
826
+ pi.on("tool_result", (event) => {
827
+ if (event.toolName !== "bash")
828
+ return;
829
+ const command = typeof event.input?.command === "string"
830
+ ? event.input.command
831
+ : "";
832
+ if (GIT_MUTATING_PATTERN.test(command)) {
833
+ footerInvalidateHandle.invalidateGit();
834
+ }
758
835
  });
759
836
  }
760
837
  export default async function (pi) {
@@ -781,7 +858,9 @@ export { recordDecision } from "./recordDecisionTool.js";
781
858
  export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
782
859
  // Onramp Door B (F2a): the one-time init-pass idempotency marker.
783
860
  export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
784
- export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
861
+ export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
862
+ // Ultra mode (/ultra): the aggressive fan-out/verify/synthesize dial.
863
+ export { createUltraHolder, registerUltraCommand } from "./ultra.js";
785
864
  export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
786
865
  export { buildYagniProvider } from "./provider.js";
787
866
  export { registerGoCommand } from "./pipeline/goCommand.js";
@@ -796,9 +875,9 @@ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures
796
875
  export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
797
876
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
798
877
  // The general subagent tool: Claude Code-format agent discovery + fan-out.
799
- export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
878
+ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
800
879
  // The session todo checklist: todo_write tool, widget renderer, /todos.
801
- export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
880
+ export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
802
881
  // P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
803
882
  // the session bless-with-remember capture hook.
804
883
  export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The opt-in repo seeding beat (org plane, Run 7).
3
+ *
4
+ * On the first interactive session in a repo whose repo-scoped decision
5
+ * ledger is empty (`/context?repo=` reports `repoDecisionCount: 0`), YAGNI
6
+ * Code offers — once — to read the repo's own decision record and seed the
7
+ * ledger: "Read this repo's ADRs and docs and seed the decision ledger?".
8
+ * One keystroke, then the backend extracts, dedupes, and banks at most 12
9
+ * `repo_mined` asserted rows, and the beat prints the banked count plus the
10
+ * Library review deep link.
11
+ *
12
+ * Never automatic, never a nag: a decline writes a per-repo marker so the
13
+ * beat does not recur; an accepted pass marks the repo only after the server
14
+ * confirms (a transport failure leaves the offer available next session).
15
+ * The corpus is collected at git HEAD (same echo-chamber guard as
16
+ * repoDocs.ts) — ADR directories, agent-instruction root docs, top-level
17
+ * docs/*.md, and recent merge subjects — bounded client-side to the same
18
+ * limits the backend enforces.
19
+ */
20
+ /**
21
+ * Client-side bounds on the mining corpus — mirror of the backend's
22
+ * MINE_INPUT_LIMITS so nothing is silently truncated server-side.
23
+ */
24
+ export declare const MINE_BEAT_LIMITS: {
25
+ readonly maxDocs: 40;
26
+ readonly maxPathChars: 300;
27
+ readonly maxExcerptChars: 8000;
28
+ readonly maxCommitSubjects: 200;
29
+ readonly maxSubjectChars: 200;
30
+ };
31
+ /** One document offered to the mining pass. */
32
+ export interface MineBeatDoc {
33
+ path: string;
34
+ excerpt: string;
35
+ }
36
+ /** Injectable git seam (default: execFileSync; every call fail-soft). */
37
+ export interface MineBeatGit {
38
+ /** `owner/name` from the origin remote, or undefined outside a repo. */
39
+ repoFullName: (cwd: string) => string | undefined;
40
+ /** Tracked paths at HEAD, or null when cwd is not a usable git tree. */
41
+ listAtHead: (cwd: string) => string[] | null;
42
+ /** One file's HEAD content, or null when unreadable. */
43
+ readAtHead: (cwd: string, rel: string) => string | null;
44
+ /** Recent merge-commit subjects (fallback: plain subjects on squash repos). */
45
+ mergeSubjects: (cwd: string, max: number) => string[];
46
+ }
47
+ /** Parse `owner/name` from an origin remote URL (ssh or https). */
48
+ export declare function parseRepoFullName(remote: string | null | undefined): string | undefined;
49
+ export declare const defaultMineBeatGit: MineBeatGit;
50
+ /** Is this HEAD-tracked path part of the repo's decision record? */
51
+ export declare function isMineCandidate(rel: string): boolean;
52
+ /** Collect the HEAD-era mining corpus, bounded to MINE_BEAT_LIMITS. */
53
+ export declare function collectMineCorpus(cwd: string, gitImpl?: MineBeatGit): {
54
+ docs: MineBeatDoc[];
55
+ commitSubjects: string[];
56
+ };
57
+ /** Injectable marker-store seam (default: a JSON file per repo on disk). */
58
+ export interface MineBeatMarkers {
59
+ has: (repoFullName: string) => boolean;
60
+ write: (repoFullName: string, status: "declined" | "seeded") => void;
61
+ }
62
+ /** Filesystem path of one repo's beat marker under the state home. */
63
+ export declare function mineMarkerPath(stateHome: string, repoFullName: string): string;
64
+ export declare function fileMineBeatMarkers(stateHome: string): MineBeatMarkers;
65
+ /** The subset of the session-start ctx the beat needs. */
66
+ export interface MineBeatUi {
67
+ hasUI: boolean;
68
+ ui: {
69
+ confirm?: (title: string, message: string) => Promise<boolean>;
70
+ notify: (message: string, level: "info" | "error") => void;
71
+ };
72
+ }
73
+ export interface OfferMiningBeatOptions {
74
+ repoFullName: string | undefined;
75
+ /** From `/context?repo=`; the beat fires ONLY on an explicit 0. */
76
+ repoDecisionCount: number | undefined;
77
+ cwd: string;
78
+ baseUrl: string;
79
+ getToken: () => string | undefined;
80
+ fetchImpl?: typeof fetch;
81
+ markers: MineBeatMarkers;
82
+ gitImpl?: MineBeatGit;
83
+ collect?: typeof collectMineCorpus;
84
+ }
85
+ export interface MiningBeatOutcome {
86
+ offered: boolean;
87
+ accepted?: boolean;
88
+ banked?: number;
89
+ }
90
+ /**
91
+ * Offer (once per repo) and, on yes, run the seeding pass. Fail-soft
92
+ * throughout: any error surfaces as a notify at most, never a broken session.
93
+ */
94
+ export declare function maybeOfferMiningBeat(ctx: MineBeatUi, opts: OfferMiningBeatOptions): Promise<MiningBeatOutcome>;
95
+ //# sourceMappingURL=mineBeat.d.ts.map