@yagni-app/code-staging 1.1.1-staging.1355.1 → 1.1.1-staging.1359.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.
@@ -40,6 +40,9 @@ import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js
40
40
  * single `read`. No `bash`.
41
41
  */
42
42
  export declare const ADVISOR_TOOLS: string[];
43
+ /** The consult's tool list under `grounding: false` — grounding read
44
+ * stripped via the canonical filter (grounding.ts). */
45
+ export declare const ADVISOR_TOOLS_GROUNDING_FREE: string[];
43
46
  declare const parameters: Type.TObject<{
44
47
  question: Type.TString;
45
48
  tried: Type.TOptional<Type.TString>;
@@ -49,6 +52,13 @@ export interface MakeAskAdvisorToolOptions {
49
52
  /** Per-session consult accounting. Created once per session in index.ts. */
50
53
  state: AdvisorStateHandle;
51
54
  limits?: AdvisorLimits;
55
+ /**
56
+ * The boot-time grounding switch: false runs consults on the BLIND advisor
57
+ * persona (the existing ADVISOR_BLIND body in pipeline/personas.ts — the
58
+ * M6 eval lane's machinery) with the grounding-free tool list and metadata
59
+ * that no longer nudges ask_yagni / record_decision. Defaults grounded.
60
+ */
61
+ grounded?: boolean;
52
62
  /** Injectable so tests never spawn a child. */
53
63
  runStage?: typeof defaultRunStage;
54
64
  /**
@@ -81,7 +91,7 @@ export declare function buildConsultBrief(params: {
81
91
  * feed/activity reducers that switch over it. `agent` is what selects the
82
92
  * persona, and that is the advisor's own.
83
93
  */
84
- export declare function advisorStage(): PipelineStage;
94
+ export declare function advisorStage(grounded?: boolean): PipelineStage;
85
95
  /** The ask_advisor result details: consult accounting plus the live progress
86
96
  * record the subagent renderers paint (one task, the consult itself). */
87
97
  export interface AdvisorToolDetails {
@@ -30,6 +30,7 @@ import { Type } from "typebox";
30
30
  import { ADVISOR_MODEL_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatConsultCost, } from "./advisor.js";
31
31
  import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
32
32
  import { withResilience } from "./pipeline/resilience.js";
33
+ import { stripGroundingTool } from "./grounding.js";
33
34
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
34
35
  import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
35
36
  import { logEvent } from "./errorSink.js";
@@ -40,6 +41,9 @@ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, p
40
41
  * single `read`. No `bash`.
41
42
  */
42
43
  export const ADVISOR_TOOLS = ["read", "grep", "find", "ls", "ask_yagni"];
44
+ /** The consult's tool list under `grounding: false` — grounding read
45
+ * stripped via the canonical filter (grounding.ts). */
46
+ export const ADVISOR_TOOLS_GROUNDING_FREE = stripGroundingTool(ADVISOR_TOOLS);
43
47
  const parameters = Type.Object({
44
48
  question: Type.String({
45
49
  description: "The single judgment call you want a second opinion on. Be specific.",
@@ -68,12 +72,12 @@ export function buildConsultBrief(params) {
68
72
  * feed/activity reducers that switch over it. `agent` is what selects the
69
73
  * persona, and that is the advisor's own.
70
74
  */
71
- export function advisorStage() {
75
+ export function advisorStage(grounded = true) {
72
76
  return {
73
77
  id: "plan",
74
78
  agent: "advisor",
75
79
  model: ADVISOR_MODEL_TIER,
76
- tools: ADVISOR_TOOLS,
80
+ tools: grounded ? ADVISOR_TOOLS : ADVISOR_TOOLS_GROUNDING_FREE,
77
81
  taskTemplate: "{ticket}",
78
82
  };
79
83
  }
@@ -97,17 +101,12 @@ export function renderAdvisorCall(args, theme, _context) {
97
101
  }
98
102
  export function makeAskAdvisorTool(opts) {
99
103
  const limits = opts.limits ?? DEFAULT_ADVISOR_LIMITS;
100
- // The default runner rides the /go pipeline's resilience wrapper, exactly
101
- // like the subagent tool: idle + wall-clock ceilings and transient-only
102
- // retry, so a stalled consult child aborts honestly instead of hanging the
103
- // driver's tool call until the user presses Esc. The synthetic stage id is
104
- // "plan" (read-only tools, no bash), so the wrapper's write-gate never
105
- // blocks a retry re-running a consult cannot double-apply anything.
106
- const runStage = opts.runStage ?? withResilience(defaultRunStage, DEFAULT_RESILIENCE_POLICY);
107
- return {
108
- name: "ask_advisor",
109
- label: "Ask the advisor",
110
- description: "Escalate ONE hard judgment call to the peak-tier advisor — the strongest " +
104
+ const grounded = opts.grounded !== false;
105
+ // Metadata variants: the description/promptGuidelines reach the model as
106
+ // tool metadata even when the consult runs blind with grounding off they
107
+ // must not nudge ask_yagni (unregistered) or record_decision (unregistered).
108
+ const description = grounded
109
+ ? "Escalate ONE hard judgment call to the peak-tier advisor the strongest " +
111
110
  "model available, which reads the code itself and returns a recommendation. " +
112
111
  "Available only on the Advanced tier, and capped per session, so use it for " +
113
112
  "calls that are genuinely worth it: an architectural fork with no obvious " +
@@ -115,15 +114,43 @@ export function makeAskAdvisorTool(opts) {
115
114
  "a change whose blast radius you are unsure of, or a second opinion before " +
116
115
  "committing to an approach you would have to unwind. Do NOT use it for " +
117
116
  "lookups (use ask_yagni), for anything you can settle by reading the code, " +
118
- "or to review work you have already finished. Pass a sharp question plus the " +
119
- "relevant excerpts — never a conversation transcript.",
120
- promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Advanced sessions, capped).",
121
- promptGuidelines: [
117
+ "or to review work you have already finished. Pass a sharp question plus " +
118
+ "the relevant excerpts — never a conversation transcript."
119
+ : "Escalate ONE hard judgment call to the peak-tier advisor the strongest " +
120
+ "model available, which reads the code itself and returns a recommendation. " +
121
+ "Available only on the Advanced tier, and capped per session, so use it for " +
122
+ "calls that are genuinely worth it: an architectural fork with no obvious " +
123
+ "right answer, a subtle correctness question you cannot settle by reading, " +
124
+ "a change whose blast radius you are unsure of, or a second opinion before " +
125
+ "committing to an approach you would have to unwind. Do NOT use it for " +
126
+ "anything you can settle by reading the code, or to review work you have " +
127
+ "already finished. Pass a sharp question plus the relevant excerpts — never " +
128
+ "a conversation transcript.";
129
+ const promptGuidelines = grounded
130
+ ? [
122
131
  "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.",
123
132
  "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.",
124
133
  "Consults are capped per session. Spend them on the calls you would otherwise get wrong.",
125
134
  "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.",
126
- ],
135
+ ]
136
+ : [
137
+ "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.",
138
+ "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.",
139
+ "Consults are capped per session. Spend them on the calls you would otherwise get wrong.",
140
+ ];
141
+ // The default runner rides the /go pipeline's resilience wrapper, exactly
142
+ // like the subagent tool: idle + wall-clock ceilings and transient-only
143
+ // retry, so a stalled consult child aborts honestly instead of hanging the
144
+ // driver's tool call until the user presses Esc. The synthetic stage id is
145
+ // "plan" (read-only tools, no bash), so the wrapper's write-gate never
146
+ // blocks a retry — re-running a consult cannot double-apply anything.
147
+ const runStage = opts.runStage ?? withResilience(defaultRunStage, DEFAULT_RESILIENCE_POLICY);
148
+ return {
149
+ name: "ask_advisor",
150
+ label: "Ask the advisor",
151
+ description,
152
+ promptSnippet: "ask_advisor: escalate one hard judgment call to the peak-tier advisor (Advanced sessions, capped).",
153
+ promptGuidelines,
127
154
  parameters,
128
155
  // Self-framed: the condensed transcript look has no tinted tool boxes.
129
156
  renderShell: "self",
@@ -172,7 +199,10 @@ export function makeAskAdvisorTool(opts) {
172
199
  emit();
173
200
  let result;
174
201
  try {
175
- result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, {
202
+ result = await runStage(advisorStage(grounded),
203
+ // `grounded` selects the persona body in the runner's personaBody
204
+ // seam: false = ADVISOR_BLIND (the M6 blind lane's body).
205
+ { ticket: buildConsultBrief(params), grounded }, {
176
206
  cwd: ctx?.cwd ?? process.cwd(),
177
207
  ...(signal ? { signal } : {}),
178
208
  // YAG-471: attribute the consult's completions to the advisor, not
@@ -48,7 +48,14 @@ type AskDetails = {
48
48
  questions: Question[];
49
49
  answers: Record<string, string>;
50
50
  };
51
- /** Build the ask_user_question tool. Register it unconditionally (like ask_advisor). */
52
- export declare function makeAskUserQuestionTool(): ToolDefinition<typeof parameters, AskDetails>;
51
+ /**
52
+ * Build the ask_user_question tool. Registered unconditionally (like
53
+ * ask_advisor): the tool itself is not a grounding surface, but its metadata
54
+ * references the recorded-answer lookup, so `grounded: false` swaps in the
55
+ * reference-free variants.
56
+ */
57
+ export declare function makeAskUserQuestionTool(opts?: {
58
+ grounded?: boolean;
59
+ }): ToolDefinition<typeof parameters, AskDetails>;
53
60
  export {};
54
61
  //# sourceMappingURL=askUserQuestionTool.d.ts.map
@@ -70,6 +70,9 @@ const PROMPT_GUIDELINES = [
70
70
  "If you recommend a specific option, make it the first one and append '(Recommended)' to its label.",
71
71
  "Do not use this tool for feedback like 'does the plan look good?' — ask a real choice with real options.",
72
72
  ];
73
+ /** The grounding-free guideline list: the recorded-answer clause drops its
74
+ * ask_yagni reference (the tool is unregistered under `grounding: false`). */
75
+ const PROMPT_GUIDELINES_GROUNDING_FREE = PROMPT_GUIDELINES.map((g) => g.replace(" — and the answer is not already recorded (check ask_yagni first)", ""));
73
76
  /**
74
77
  * Whether any option in the question has a preview. Drives the two-column
75
78
  * layout (Claude switches to side-by-side only when previews exist).
@@ -566,15 +569,19 @@ async function askOne(ctx, q, qIndex) {
566
569
  logAskQuestion({ event: "resolved", status: "selected", detail: `values:${resolution.values.length}`, qIndex });
567
570
  return { answer: resolution.values.join(", "), cancelled: false };
568
571
  }
569
- function buildTool() {
572
+ function buildTool(grounded = true) {
570
573
  return {
571
574
  name: TOOL_NAME,
572
575
  label: "Ask user a question",
573
- description: "Pose a structured multiple-choice question to the user during execution and get back a clean machine-readable answer. " +
574
- "Use when you genuinely need a human decision (an architectural fork, a product-intent call with no recorded answer, or a choice between approaches)." +
575
- "Each question has 2-4 mutually exclusive options, each with a description of its tradeoff and an optional preview.",
576
+ description: grounded
577
+ ? "Pose a structured multiple-choice question to the user during execution and get back a clean machine-readable answer. " +
578
+ "Use when you genuinely need a human decision (an architectural fork, a product-intent call with no recorded answer, or a choice between approaches)." +
579
+ "Each question has 2-4 mutually exclusive options, each with a description of its tradeoff and an optional preview."
580
+ : "Pose a structured multiple-choice question to the user during execution and get back a clean machine-readable answer. " +
581
+ "Use when you genuinely need a human decision (an architectural fork, a product-intent call, or a choice between approaches)." +
582
+ "Each question has 2-4 mutually exclusive options, each with a description of its tradeoff and an optional preview.",
576
583
  promptSnippet: PROMPT_SNIPPET,
577
- promptGuidelines: PROMPT_GUIDELINES,
584
+ promptGuidelines: grounded ? PROMPT_GUIDELINES : PROMPT_GUIDELINES_GROUNDING_FREE,
578
585
  parameters,
579
586
  async execute(_toolCallId, params, _signal, onUpdate, ctx) {
580
587
  onUpdate?.({ content: [{ type: "text", text: "Asking you…" }], details: { questions: [], answers: {} } });
@@ -636,8 +643,13 @@ function buildTool() {
636
643
  },
637
644
  };
638
645
  }
639
- /** Build the ask_user_question tool. Register it unconditionally (like ask_advisor). */
640
- export function makeAskUserQuestionTool() {
641
- return buildTool();
646
+ /**
647
+ * Build the ask_user_question tool. Registered unconditionally (like
648
+ * ask_advisor): the tool itself is not a grounding surface, but its metadata
649
+ * references the recorded-answer lookup, so `grounded: false` swaps in the
650
+ * reference-free variants.
651
+ */
652
+ export function makeAskUserQuestionTool(opts = {}) {
653
+ return buildTool(opts.grounded !== false);
642
654
  }
643
655
  //# sourceMappingURL=askUserQuestionTool.js.map
@@ -100,6 +100,21 @@ export declare const COMMUNICATION_CONTRACT: string;
100
100
  export declare const WRITE_FINDINGS_DOWN: string;
101
101
  /** The driver identity while /ultra is on: base identity + the diamond directive. */
102
102
  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 TodoWrite 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.";
103
+ /**
104
+ * The grounding-FREE identity for `grounding: false` boots: the role and
105
+ * anti-confusion sentences of {@link YAGNI_IDENTITY} with the grounding
106
+ * sentence ("Uniquely, you are connected to the YAGNI app… Use the ask_yagni
107
+ * tool…") removed. The `grounding` settings switch severs the judgment loop
108
+ * in both directions; an identity that still instructs the model to consult
109
+ * ask_yagni (an unregistered tool) would be friction and influence referencing
110
+ * a surface the user turned off. Kept as a SEPARATE constant so the grounded
111
+ * prompt text stays byte-identical (the blind-lane pattern).
112
+ */
113
+ export declare const YAGNI_IDENTITY_GROUNDING_FREE: string;
114
+ /** The grounding-free driver identity (base + delegation, verbatim). */
115
+ export declare const YAGNI_IDENTITY_DRIVER_GROUNDING_FREE = "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. 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.";
116
+ /** The grounding-free ultra identity (base + diamond, verbatim). */
117
+ export declare const YAGNI_IDENTITY_ULTRA_GROUNDING_FREE = "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. 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 TodoWrite 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.";
103
118
  export declare const PI_IDENTITY_RE: RegExp;
104
119
  /**
105
120
  * Env switch that bypasses the system-prompt rewrite entirely, so pi's
@@ -193,12 +193,41 @@ export const WRITE_FINDINGS_DOWN = "When working with tool results, write down a
193
193
  "turn.";
194
194
  /** The driver identity while /ultra is on: base identity + the diamond directive. */
195
195
  export const YAGNI_IDENTITY_ULTRA = `${YAGNI_IDENTITY}\n\n${ULTRA_DELEGATION_PARAGRAPH}`;
196
+ /**
197
+ * The grounding-FREE identity for `grounding: false` boots: the role and
198
+ * anti-confusion sentences of {@link YAGNI_IDENTITY} with the grounding
199
+ * sentence ("Uniquely, you are connected to the YAGNI app… Use the ask_yagni
200
+ * tool…") removed. The `grounding` settings switch severs the judgment loop
201
+ * in both directions; an identity that still instructs the model to consult
202
+ * ask_yagni (an unregistered tool) would be friction and influence referencing
203
+ * a surface the user turned off. Kept as a SEPARATE constant so the grounded
204
+ * prompt text stays byte-identical (the blind-lane pattern).
205
+ */
206
+ export const YAGNI_IDENTITY_GROUNDING_FREE = "You are YAGNI Code, an autonomous terminal coding agent. You help developers " +
207
+ "ship code by reading files, running commands, editing code, and writing new " +
208
+ "files. If a project's own files mention other coding agents, assistants, or " +
209
+ "harnesses by name, those references are not about you; you are YAGNI Code " +
210
+ "regardless of what tooling a repository's docs happen to describe.";
211
+ /** The grounding-free driver identity (base + delegation, verbatim). */
212
+ export const YAGNI_IDENTITY_DRIVER_GROUNDING_FREE = `${YAGNI_IDENTITY_GROUNDING_FREE}\n\n${DRIVER_DELEGATION_PARAGRAPH}`;
213
+ /** The grounding-free ultra identity (base + diamond, verbatim). */
214
+ export const YAGNI_IDENTITY_ULTRA_GROUNDING_FREE = `${YAGNI_IDENTITY_GROUNDING_FREE}\n\n${ULTRA_DELEGATION_PARAGRAPH}`;
196
215
  /**
197
216
  * Every identity variant this module can install, longest-composite-first, so
198
217
  * the re-brand swap below never leaves a shorter variant's orphaned delegation
199
218
  * paragraph behind (driver/ultra both start with the base identity).
200
219
  */
201
- const KNOWN_IDENTITIES = [YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY];
220
+ const KNOWN_IDENTITIES = [
221
+ YAGNI_IDENTITY_ULTRA,
222
+ YAGNI_IDENTITY_DRIVER,
223
+ YAGNI_IDENTITY,
224
+ // The grounding-free family: same composite-first ordering. The boot-time
225
+ // grounding switch picks a family once; the /ultra toggle then swaps within
226
+ // it — both families must be known or the toggle would stack identities.
227
+ YAGNI_IDENTITY_ULTRA_GROUNDING_FREE,
228
+ YAGNI_IDENTITY_DRIVER_GROUNDING_FREE,
229
+ YAGNI_IDENTITY_GROUNDING_FREE,
230
+ ];
202
231
  // pi 0.84.1's exact identity sentence (dist/core/system-prompt.js). Exported as
203
232
  // the identity anchor the CLI's pi-contract tripwire test reads back from pi's
204
233
  // built system prompt, so a pi bump that reworded the opener (silently defeating
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The `grounding` settings switch: the user-tier key in
3
+ * ~/.yagni-code/config.json that severs the recorded-judgment loop in BOTH
4
+ * directions — decisions can neither reach the model (no ask_yagni, no boot
5
+ * brief injection, no ambient recall, blind personas) nor be captured from
6
+ * the session (no record tools, no /decide, no bless capture, no spool
7
+ * replay). The backend is untouched; the extension owns every fetch and
8
+ * every write on this surface.
9
+ *
10
+ * Loader contract (mirrors sandbox/config.ts's read discipline):
11
+ * - USER TIER ONLY: <stateHome>/config.json, top-level `grounding`. The
12
+ * project/local tiers are never consulted — a committed repo config
13
+ * cannot silently re-enable or disable a developer's judgment loop.
14
+ * - default ON: absent key, absent file, malformed JSON, or a non-boolean
15
+ * value all mean grounded (today's behavior). Fail-soft by design: a
16
+ * corrupt config must not wedge the agent's boot.
17
+ * - a malformed/invalid config warns to the error sink (source "grounding")
18
+ * with the error CLASS only — never the parse message, which embeds a
19
+ * snippet of the file's own content.
20
+ */
21
+ export interface GroundingLoad {
22
+ /** False = the judgment loop is severed for this boot. */
23
+ enabled: boolean;
24
+ /** Present only when a config problem forced the default (fail-soft). */
25
+ warning?: string;
26
+ }
27
+ /**
28
+ * Read the `grounding` key from the user-tier config. Pure-ish (one file
29
+ * read + an optional sink warn); returns the resolved switch plus any
30
+ * fail-soft warning so callers and tests can assert both halves.
31
+ */
32
+ /**
33
+ * The path a config warning names. The warn tier is upload-surfaceable
34
+ * (readSessionTrail keeps warn lines and /feedback binds them), and
35
+ * scrubSecrets redacts credentials, not paths — so the operator's personal
36
+ * default home collapses to ~. An OVERRIDDEN state home (YAGNI_CODE_HOME, an
37
+ * explicit env pin) is not personal — and the warning must name the file
38
+ * that is actually broken — so it stays literal there. PURE, exported for
39
+ * tests.
40
+ */
41
+ export declare function displayConfigPath(stateHome: string): string;
42
+ export declare function loadGroundingEnabled(stateHome: string, env?: NodeJS.ProcessEnv): GroundingLoad;
43
+ /**
44
+ * The canonical "strip the grounding read tool" filter — one home for the
45
+ * rule so subagent spawns and advisor consults can't drift apart.
46
+ */
47
+ export declare function stripGroundingTool(tools: readonly string[]): string[];
48
+ //# sourceMappingURL=grounding.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The `grounding` settings switch: the user-tier key in
3
+ * ~/.yagni-code/config.json that severs the recorded-judgment loop in BOTH
4
+ * directions — decisions can neither reach the model (no ask_yagni, no boot
5
+ * brief injection, no ambient recall, blind personas) nor be captured from
6
+ * the session (no record tools, no /decide, no bless capture, no spool
7
+ * replay). The backend is untouched; the extension owns every fetch and
8
+ * every write on this surface.
9
+ *
10
+ * Loader contract (mirrors sandbox/config.ts's read discipline):
11
+ * - USER TIER ONLY: <stateHome>/config.json, top-level `grounding`. The
12
+ * project/local tiers are never consulted — a committed repo config
13
+ * cannot silently re-enable or disable a developer's judgment loop.
14
+ * - default ON: absent key, absent file, malformed JSON, or a non-boolean
15
+ * value all mean grounded (today's behavior). Fail-soft by design: a
16
+ * corrupt config must not wedge the agent's boot.
17
+ * - a malformed/invalid config warns to the error sink (source "grounding")
18
+ * with the error CLASS only — never the parse message, which embeds a
19
+ * snippet of the file's own content.
20
+ */
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { logEvent } from "./errorSink.js";
25
+ /**
26
+ * Read the `grounding` key from the user-tier config. Pure-ish (one file
27
+ * read + an optional sink warn); returns the resolved switch plus any
28
+ * fail-soft warning so callers and tests can assert both halves.
29
+ */
30
+ /**
31
+ * The path a config warning names. The warn tier is upload-surfaceable
32
+ * (readSessionTrail keeps warn lines and /feedback binds them), and
33
+ * scrubSecrets redacts credentials, not paths — so the operator's personal
34
+ * default home collapses to ~. An OVERRIDDEN state home (YAGNI_CODE_HOME, an
35
+ * explicit env pin) is not personal — and the warning must name the file
36
+ * that is actually broken — so it stays literal there. PURE, exported for
37
+ * tests.
38
+ */
39
+ export function displayConfigPath(stateHome) {
40
+ const defaultHome = join(homedir(), ".yagni-code");
41
+ return stateHome === defaultHome ? "~/.yagni-code/config.json" : join(stateHome, "config.json");
42
+ }
43
+ export function loadGroundingEnabled(stateHome, env) {
44
+ const displayPath = displayConfigPath(stateHome);
45
+ const path = join(stateHome, "config.json");
46
+ if (!existsSync(path))
47
+ return { enabled: true };
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
51
+ }
52
+ catch (err) {
53
+ const warning = `grounding: could not parse ${displayPath}: ${err instanceof Error ? err.constructor.name : typeof err} — staying grounded`;
54
+ warnConfig(warning, env);
55
+ return { enabled: true, warning };
56
+ }
57
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
58
+ const warning = `grounding: config at ${displayPath} is not an object — staying grounded`;
59
+ warnConfig(warning, env);
60
+ return { enabled: true, warning };
61
+ }
62
+ const value = parsed.grounding;
63
+ if (value === undefined)
64
+ return { enabled: true };
65
+ if (typeof value !== "boolean") {
66
+ const warning = `grounding: "grounding" in ${displayPath} must be a boolean — staying grounded`;
67
+ warnConfig(warning, env);
68
+ return { enabled: true, warning };
69
+ }
70
+ return { enabled: value };
71
+ }
72
+ /**
73
+ * The canonical "strip the grounding read tool" filter — one home for the
74
+ * rule so subagent spawns and advisor consults can't drift apart.
75
+ */
76
+ export function stripGroundingTool(tools) {
77
+ return tools.filter((t) => t !== "ask_yagni");
78
+ }
79
+ function warnConfig(warning, env) {
80
+ // The warning carries only the file path + the error CLASS — never a parse
81
+ // message (those embed the file's own content). Same posture as the sandbox
82
+ // loader's config warnings.
83
+ logEvent({
84
+ source: "grounding",
85
+ level: "warn",
86
+ event: "config_warning",
87
+ sessionId: env?.YAGNI_SESSION_ID,
88
+ fields: { warning },
89
+ });
90
+ }
91
+ //# sourceMappingURL=grounding.js.map
@@ -88,6 +88,13 @@ export interface RegisterYagniDeps {
88
88
  tokenProvider?: TokenProvider;
89
89
  /** The spool flush (R4 write half), injectable so tests never touch disk. */
90
90
  flushSpool?: (opts: SpoolClientOpts) => Promise<FlushOutcome>;
91
+ /**
92
+ * The `grounding` settings switch seam. Defaults to the real user-config
93
+ * read (top-level `grounding` in ~/.yagni-code/config.json, default true).
94
+ * Inject `false` in tests to assert the gated wiring; inject nothing and
95
+ * the factory never touches disk beyond that one read.
96
+ */
97
+ grounding?: boolean;
91
98
  /**
92
99
  * Non-fatal auth-event reporter (YAG-500 Fix E). Defaults to
93
100
  * `makeCrashReporter` gated on `!evalMode`; inject a spy in tests to assert