@yagni-app/code-staging 1.1.1-staging.1358.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
@@ -15,7 +15,7 @@ import { registerCmuxBridge } from "./cmux/index.js";
15
15
  import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
16
16
  import { makeRecordDecisionTool } from "./recordDecisionTool.js";
17
17
  import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
18
- import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
18
+ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE } from "./branding.js";
19
19
  import { claudeRulesSection } from "./claudeRules.js";
20
20
  import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadSection } from "./scratchpad.js";
21
21
  import { registerCostCommand } from "./costHud.js";
@@ -47,6 +47,7 @@ import { createUltraHolder, registerUltraCommand } from "./ultra.js";
47
47
  import { registerTodos } from "./todos.js";
48
48
  import { registerDecisionCommands } from "./decisions.js";
49
49
  import { makeDecisionCapture } from "./decisionCapture.js";
50
+ import { loadGroundingEnabled } from "./grounding.js";
50
51
  import { registerAmbientRecall } from "./recall.js";
51
52
  import { resilientFetch } from "./resilientFetch.js";
52
53
  import { installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest } from "./crashReport.js";
@@ -146,6 +147,27 @@ export async function registerYagni(pi, deps = {}) {
146
147
  const fetchCatalog = deps.fetchCatalog ?? defaultFetchCatalog;
147
148
  const env = deps.env ?? process.env;
148
149
  const evalMode = isEvalMode(env);
150
+ // The grounding settings switch (the judgment-loop kill switch): resolved
151
+ // ONCE at boot from the USER-tier config (default on; a malformed config
152
+ // fails soft to on with a sink warn). Eval mode is its own carve-out and
153
+ // stays orthogonal — a scoping/eval session keeps its measured behavior.
154
+ const groundingLoad = deps.grounding !== undefined
155
+ ? { enabled: deps.grounding }
156
+ : loadGroundingEnabled(codeStateHome(null, env), env);
157
+ const grounding = groundingLoad.enabled;
158
+ if (!grounding) {
159
+ logEvent({
160
+ source: "grounding",
161
+ level: "info",
162
+ event: "grounding_disabled",
163
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
164
+ fields: {},
165
+ });
166
+ }
167
+ // The fail-soft warn half (a malformed config silently ignored the user's
168
+ // switch): keep it for the session_start surface below — the local sink
169
+ // alone is invisible to the developer who asked for the switch.
170
+ const groundingConfigWarning = groundingLoad.warning;
149
171
  // The refreshing TokenProvider replaces the old boot-time token snapshot:
150
172
  // tools read the CURRENT token per call, a proactive unref'd timer rotates
151
173
  // it near expiry, and `authedFetch` gives every tool's 401 path a single
@@ -220,7 +242,12 @@ export async function registerYagni(pi, deps = {}) {
220
242
  // in_progress (Claude's currentTodo?.activeForm pattern). TUI-gated
221
243
  // internally (agent_start checks ctx.mode).
222
244
  const workingLine = registerWorkingLine(pi, { todoVerb: todosHandle.activeVerb });
223
- pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
245
+ // The grounding read gate: with `grounding: false`, the judgment loop is
246
+ // severed in both directions — ask_yagni (the consultation read) is never
247
+ // registered, so no recorded decision reaches the model through it.
248
+ if (grounding) {
249
+ pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
250
+ }
224
251
  // WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
225
252
  // standard-tier extraction, replacing the bash + curl + python dance.
226
253
  pi.registerTool(makeWebFetchTool(toolOpts));
@@ -241,20 +268,27 @@ export async function registerYagni(pi, deps = {}) {
241
268
  // tasks, advisor consults, Guardian reviews, /go runs) lands here so the
242
269
  // footer's session totals include what the per-child receipts print.
243
270
  const childUsage = makeChildUsageState();
244
- const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine, childUsage });
271
+ // The advisor consult itself stays available with grounding off (it is
272
+ // model escalation, not a judgment read); the consult just runs BLIND —
273
+ // the existing grounding-free advisor persona + the ask_yagni-free tool
274
+ // list — so no recorded decision reaches the consult either.
275
+ const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine, childUsage, grounded: grounding });
245
276
  pi.registerTool(askAdvisorTool);
246
277
  // /advise runs the SAME tool, sharing the state handle, so a manual consult
247
278
  // draws on the same cap rather than opening a side channel around it.
248
279
  registerAdviseCommand(pi, askAdvisorTool);
249
280
  // Structured human questions: the model poses a closed 2-4-option question
250
281
  // and gets a clean machine-readable answer via ctx.ui.custom.
251
- pi.registerTool(makeAskUserQuestionTool());
282
+ pi.registerTool(makeAskUserQuestionTool({ grounded: grounding }));
252
283
  // The differentiated business-grounded tools (loop bricks): review a change
253
284
  // for business fit, rank the next work by business priority, and record the
254
- // engineering rationale back onto the work-item.
255
- pi.registerTool(makeReviewBusinessMatchTool(toolOpts));
256
- pi.registerTool(makeSuggestNextWorkTool(toolOpts));
257
- if (!evalMode) {
285
+ // engineering rationale back onto the work-item. All three read the recorded
286
+ // judgment, so they join the grounding gate.
287
+ if (grounding) {
288
+ pi.registerTool(makeReviewBusinessMatchTool(toolOpts));
289
+ pi.registerTool(makeSuggestNextWorkTool(toolOpts));
290
+ }
291
+ if (!evalMode && grounding) {
258
292
  pi.registerTool(makeRecordEngineeringContextTool(toolOpts));
259
293
  // The capture half of the judgment loop: bank a product-intent decision so
260
294
  // ask_yagni answers the same question next time instead of interrupting a human.
@@ -282,7 +316,7 @@ export async function registerYagni(pi, deps = {}) {
282
316
  // driver's delegation identity for the diamond directive (see the
283
317
  // before_agent_start handler below) and widens the tool's fan-out ceiling.
284
318
  const ultraHolder = createUltraHolder();
285
- registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage });
319
+ registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage, grounded: grounding });
286
320
  // /plugin + /reload-plugins: manage Claude Code marketplace plugins from the
287
321
  // session, driving the launcher's `yagni plugin …` (YAGNI_CLI_PATH).
288
322
  registerPluginPanel(pi, { env: deps.env, runCli: deps.runPluginCli });
@@ -296,6 +330,11 @@ export async function registerYagni(pi, deps = {}) {
296
330
  // map → plan → implement → review → fix, each child grounded by inheritance.
297
331
  registerGoCommand(pi, {
298
332
  childUsage,
333
+ // The grounding switch threads into every /go run: `grounded: false`
334
+ // selects the existing blind personas + blind lens clauses (the M6
335
+ // eval lane's machinery — see pipeline/personas.ts) and strips the
336
+ // grounding tools from the stage whitelists via the orchestrator.
337
+ grounded: grounding,
299
338
  // /ultra is one dial for the whole session: the same holder the subagent
300
339
  // tool reads widens the implement diamond's parallel ceiling (4 -> 8) for
301
340
  // the fan and its fix turns. Read per run, so a toggle lands on the next /go.
@@ -320,7 +359,10 @@ export async function registerYagni(pi, deps = {}) {
320
359
  });
321
360
  // M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
322
361
  // the business-fit delta. Never wired to routing.
323
- registerGoCompareCommand(pi);
362
+ // /go-compare measures grounded-vs-blind; with grounding off both lanes
363
+ // would be blind and its delta would lie. Don't register it.
364
+ if (grounding)
365
+ registerGoCompareCommand(pi);
324
366
  // YAG-580: /feedback (alias /bug) + /diagnostics. The capture/upload is the
325
367
  // whole point, so it is gated to non-eval mode like every external side
326
368
  // effect; the command's deps (doctor report, git state, child transcripts)
@@ -338,8 +380,8 @@ export async function registerYagni(pi, deps = {}) {
338
380
  // bless-with-remember capture are the same product-intent write as the record
339
381
  // tools, so both are gated together (skipped in eval mode).
340
382
  const decisionClientOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
341
- const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
342
- if (!evalMode)
383
+ const decisionCapture = !evalMode && grounding ? makeDecisionCapture(decisionClientOpts) : undefined;
384
+ if (!evalMode && grounding)
343
385
  registerDecisionCommands(pi, decisionClientOpts);
344
386
  // Local MCP servers (standard client, replaces the backend-proxy /mcp of
345
387
  // the YAG-446 era): load user/project/local config, approval-gate project
@@ -973,18 +1015,25 @@ export async function registerYagni(pi, deps = {}) {
973
1015
  // M1 ambient judgment recall: after a `read`, append the recorded judgment for
974
1016
  // that path to the tool result (result modification is supported on pi's
975
1017
  // `tool_result` event — verified against 0.80.2). No-op when the corpus is thin
976
- // (<3 active decisions from /context.counts) or in eval mode.
977
- registerAmbientRecall(pi, {
978
- baseUrl,
979
- getToken: getTokenFn,
980
- fetchImpl: authedFetch,
981
- decisionsCount: briefResult?.counts?.decisions ?? 0,
982
- evalMode,
983
- });
1018
+ // (<3 active decisions from /context.counts) or in eval mode — and never
1019
+ // registered at all under `grounding: false` (recorded decisions must not
1020
+ // reach the model through any seam).
1021
+ if (grounding) {
1022
+ registerAmbientRecall(pi, {
1023
+ baseUrl,
1024
+ getToken: getTokenFn,
1025
+ fetchImpl: authedFetch,
1026
+ decisionsCount: briefResult?.counts?.decisions ?? 0,
1027
+ evalMode,
1028
+ });
1029
+ }
984
1030
  // ADR-0033: the Team-drafting flow is an explicit command, never a first-run
985
1031
  // ceremony. /setup-team drafts the Engineering Team + engineering brief from
986
- // the repo and banks the brief as one decision on approval.
987
- registerTeamSetupCommand(pi, { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
1032
+ // the repo and banks the brief as one decision on approval — a judgment WRITE,
1033
+ // so it joins the grounding gate.
1034
+ if (grounding) {
1035
+ registerTeamSetupCommand(pi, { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
1036
+ }
988
1037
  // Onramp Door B (spec §5B/§7, as amended by ADR-0033): on a FRESH workspace
989
1038
  // (empty/thin grounding corpus), the first run shows a short welcome + one
990
1039
  // next-work offer, with free text first-class — instead of a bare prompt. It
@@ -1088,12 +1137,21 @@ export async function registerYagni(pi, deps = {}) {
1088
1137
  ? undefined
1089
1138
  : {
1090
1139
  systemPrompt: brandSystemPrompt(event.systemPrompt, {
1091
- contextBrief,
1092
- identity: isDriver
1093
- ? ultraHolder.get()
1094
- ? YAGNI_IDENTITY_ULTRA
1095
- : YAGNI_IDENTITY_DRIVER
1096
- : undefined,
1140
+ // Grounding gate: the boot fetch still ran (fresh-workspace
1141
+ // detection), but with the switch off its text never reaches the
1142
+ // model — no brief block, no ask_yagni nudge.
1143
+ contextBrief: grounding ? contextBrief : undefined,
1144
+ identity: !grounding
1145
+ ? isDriver
1146
+ ? ultraHolder.get()
1147
+ ? YAGNI_IDENTITY_ULTRA_GROUNDING_FREE
1148
+ : YAGNI_IDENTITY_DRIVER_GROUNDING_FREE
1149
+ : YAGNI_IDENTITY_GROUNDING_FREE
1150
+ : isDriver
1151
+ ? ultraHolder.get()
1152
+ ? YAGNI_IDENTITY_ULTRA
1153
+ : YAGNI_IDENTITY_DRIVER
1154
+ : undefined,
1097
1155
  rulesSection,
1098
1156
  scratchpadSection: scratchpadSectionText,
1099
1157
  }),
@@ -1336,12 +1394,33 @@ export async function registerYagni(pi, deps = {}) {
1336
1394
  catch {
1337
1395
  // A notice must never disrupt session start.
1338
1396
  }
1397
+ // A malformed user config silently forced the grounding default — the
1398
+ // one surface where the user's kill switch was ignored. Same one-shot
1399
+ // notify posture as the expiry notice; never breaks session start.
1400
+ if (groundingConfigWarning) {
1401
+ try {
1402
+ ctx.ui.notify(groundingConfigWarning, "warning");
1403
+ }
1404
+ catch {
1405
+ // A notice must never disrupt session start — but the sink line is
1406
+ // then the ONLY surviving trace of the ignored switch, so record
1407
+ // that the notify itself failed (content-free: the event name).
1408
+ logEvent({
1409
+ source: "grounding",
1410
+ level: "warn",
1411
+ event: "config_warning_notify_failed",
1412
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
1413
+ fields: {},
1414
+ });
1415
+ }
1416
+ }
1339
1417
  }
1340
1418
  // R4 write half: drain any judgment writes spooled by a previous session
1341
1419
  // (transport/5xx failures of record_decision / record_engineering_context).
1342
1420
  // Fire-and-forget and fail-soft — a flush must never delay or break session
1343
1421
  // start; honest notices (replays, expiry drops) surface when it finishes.
1344
- if (!evalMode) {
1422
+ // A replay IS a capture write, so the grounding switch skips it entirely.
1423
+ if (!evalMode && grounding) {
1345
1424
  void flushSpoolFn({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch })
1346
1425
  .then((outcome) => {
1347
1426
  if (!ctx.hasUI)
@@ -1361,6 +1440,7 @@ export async function registerYagni(pi, deps = {}) {
1361
1440
  getToken: getTokenFn,
1362
1441
  fetchImpl: authedFetch,
1363
1442
  brief: briefResult,
1443
+ grounding,
1364
1444
  });
1365
1445
  // F2a: mark the workspace init-done on any real run (ran === true — thin
1366
1446
  // or not), so it never re-seeds. A skip (not_fresh / non_interactive) is
@@ -1375,8 +1455,9 @@ export async function registerYagni(pi, deps = {}) {
1375
1455
  // Run 7: the opt-in repo seeding beat. Fires only on a genuine startup
1376
1456
  // when /context reported an explicitly-empty repo-scoped ledger, once per
1377
1457
  // repo (marker store). Fully fail-soft — a beat failure never blocks the
1378
- // session.
1379
- if (event.reason === "startup" && !evalMode) {
1458
+ // session. Never offered under `grounding: false` — mining banks decisions
1459
+ // (a judgment write).
1460
+ if (event.reason === "startup" && !evalMode && grounding) {
1380
1461
  try {
1381
1462
  const offerBeat = deps.offerMiningBeat ?? defaultMaybeOfferMiningBeat;
1382
1463
  const markers = deps.mineBeatMarkers ?? fileMineBeatMarkers(codeStateHome(null, env));
@@ -96,6 +96,13 @@ export interface RunInitPassDeps {
96
96
  baseUrl: string;
97
97
  getToken: () => string | undefined;
98
98
  fetchImpl?: typeof fetch;
99
+ /**
100
+ * The boot-time grounding switch: with grounding off the welcome stays (it
101
+ * records nothing) but skips the next-work offer — `suggest_next_work` is
102
+ * unregistered, so the offer would steer to a dead surface — and shows a
103
+ * grounding-free notice. Defaults grounded.
104
+ */
105
+ grounding?: boolean;
99
106
  /**
100
107
  * A pre-fetched context brief (the caller usually already fetched it to boot
101
108
  * grounded). When `undefined`, the pass fetches it itself. Pass `null`
@@ -231,6 +231,10 @@ const WELCOME_NOTICE = "First run in this repo. Just type to start working. Ask
231
231
  "context (tickets, docs, decisions), or run /go <ticket> for a ticket-to-PR run.";
232
232
  /** The free-text choice: an untouched editor, ready for whatever they want to build. */
233
233
  const FREE_TEXT_LABEL = "Just start typing";
234
+ /** The grounding-free welcome: no company-context advertising (the switch
235
+ * turned that surface off), just the doors that still exist. */
236
+ const WELCOME_NOTICE_GROUNDING_FREE = "First run in this repo. Just type to start working, or run /go <ticket> " +
237
+ "for a ticket-to-PR run.";
234
238
  /**
235
239
  * Run the first-run welcome (ADR-0033). Guards on fresh-workspace detection
236
240
  * (defensive — the caller also guards), shows a short how-to-work-with-YAGNI
@@ -256,8 +260,14 @@ export async function runInitPass(pi, ctx, deps) {
256
260
  const ui = (ctx.hasUI ? ctx.ui : undefined);
257
261
  if (!ui)
258
262
  return { ran: false, reason: "non_interactive" };
263
+ const grounding = deps.grounding !== false;
259
264
  const notify = safeNotify(ui);
260
- notify(WELCOME_NOTICE, "info");
265
+ notify(grounding ? WELCOME_NOTICE : WELCOME_NOTICE_GROUNDING_FREE, "info");
266
+ if (!grounding) {
267
+ // The offer's primary action is `suggest_next_work` — unregistered under
268
+ // the switch. Skip the offer; the editor stays ready for free text.
269
+ return { ran: true, chosenAction: "free_text" };
270
+ }
261
271
  // 2. Offer the ONE default next action (spec §7.3: defaults over choices),
262
272
  // with free text as a first-class door. Everything here is best-effort:
263
273
  // whatever happens, the user ends at a prompt they can just type into.
@@ -136,6 +136,14 @@ export interface RegisterGoDeps {
136
136
  * test) simply means "not ultra", which is today's behavior.
137
137
  */
138
138
  isUltra?: () => boolean;
139
+ /**
140
+ * The boot-time `grounding` settings switch, threaded into the orchestrator's
141
+ * `grounded` flag: false runs the whole pipeline on the existing blind
142
+ * personas + blind lens clauses (the M6 eval lane's machinery) with the
143
+ * grounding tools stripped from the stage whitelists. Defaults true — an
144
+ * unwired embedder or test keeps today's grounded behavior.
145
+ */
146
+ grounded?: boolean;
139
147
  baseUrl?: string;
140
148
  getToken?: () => string | undefined;
141
149
  /**
@@ -432,6 +432,9 @@ export function registerGoCommand(pi, deps = {}) {
432
432
  // The dial is held, never its value: /ultra can be toggled between runs, so
433
433
  // the pipeline calls it when it needs the ceiling rather than reading it here.
434
434
  const isUltra = deps.isUltra;
435
+ // The boot-time grounding switch (defaults grounded): false runs the
436
+ // pipeline on the blind personas via the orchestrator's `grounded` flag.
437
+ const goGrounded = deps.grounded !== false;
435
438
  // Task 8: no default — see the RegisterGoDeps doc comment for why (only
436
439
  // index.ts's real wiring makes sense here; absent, `resolveCostText` falls
437
440
  // back to the local client estimate).
@@ -448,7 +451,9 @@ export function registerGoCommand(pi, deps = {}) {
448
451
  getToken: deps.getToken ?? defaultGetToken,
449
452
  });
450
453
  pi.registerCommand("go", {
451
- description: "Run the grounded multi-agent pipeline (map → plan → implement → review → fix) on a ticket and produce a reviewed change.",
454
+ description: goGrounded
455
+ ? "Run the grounded multi-agent pipeline (map → plan → implement → review → fix) on a ticket and produce a reviewed change."
456
+ : "Run the multi-agent pipeline (map → plan → implement → review → fix) on a ticket and produce a reviewed change.",
452
457
  handler: async (args, ctx) => {
453
458
  const notify = (message, type) => {
454
459
  if (ctx.hasUI)
@@ -910,6 +915,7 @@ export function registerGoCommand(pi, deps = {}) {
910
915
  cwd: runCwd,
911
916
  signal: runSignal,
912
917
  fanout: fanoutMode,
918
+ grounded: goGrounded,
913
919
  ...(isUltra ? { isUltra } : {}),
914
920
  ...(ticketBrief ? { ticketBrief } : {}),
915
921
  onProgress: (p) => {
@@ -52,6 +52,8 @@ import { resolveFanoutMode } from "./fanout.js";
52
52
  import { makeFanoutBeats } from "./fanoutBeats.js";
53
53
  import { normalizeMission } from "./mission.js";
54
54
  import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
55
+ import { loadGroundingEnabled } from "../grounding.js";
56
+ import { codeStateHome } from "../stateHome.js";
55
57
  import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
56
58
  /** One-line usage copy, shared by every argument error. */
57
59
  export const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
@@ -202,6 +204,11 @@ export async function runHeadlessGo(argv, deps = {}) {
202
204
  const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf8"));
203
205
  const runPipeline = deps.runPipeline ?? defaultRunPipeline;
204
206
  const env = deps.env ?? process.env;
207
+ // The grounding switch's default for a headless run: the user-tier config
208
+ // (the same key the interactive extension reads at boot). An explicit
209
+ // deps.grounded (an eval lane pinning its lane) always wins — the eval's
210
+ // measurement determinism is untouched.
211
+ const grounding = deps.grounded ?? loadGroundingEnabled(codeStateHome(null, env), env).enabled;
205
212
  const args = parseHeadlessGoArgs(argv);
206
213
  const problem = validateHeadlessGoArgs(args);
207
214
  if (problem) {
@@ -278,7 +285,7 @@ export async function runHeadlessGo(argv, deps = {}) {
278
285
  ...(deps.signal ? { signal: deps.signal } : {}),
279
286
  ...(deps.budget ? { budget: deps.budget } : {}),
280
287
  ...(deps.stages ? { stages: deps.stages } : {}),
281
- ...(deps.grounded !== undefined ? { grounded: deps.grounded } : {}),
288
+ grounded: deps.grounded ?? grounding,
282
289
  ...(mission ? { mission } : {}),
283
290
  onEvent: (ev, tag) => {
284
291
  deps.onEvent?.(ev, tag);
@@ -72,12 +72,9 @@ export interface DiscoverDeps {
72
72
  */
73
73
  export declare function discoverSubagents(deps: DiscoverDeps): SubagentDef[];
74
74
  export declare function formatAgentList(agents: SubagentDef[]): string;
75
- /**
76
- * A subagent invocation is a synthetic pipeline stage: the task rides in as
77
- * `{ticket}` verbatim, and the agent's body replaces the /go persona through
78
- * the runner's personaBody seam.
79
- */
80
- export declare function buildSubagentStage(def: SubagentDef, task: string): {
75
+ export declare function buildSubagentStage(def: SubagentDef, task: string, opts?: {
76
+ grounded?: boolean;
77
+ }): {
81
78
  stage: PipelineStage;
82
79
  ctx: {
83
80
  ticket: string;
@@ -98,6 +95,13 @@ export interface MakeSubagentToolDeps {
98
95
  homeDir?: string;
99
96
  /** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
100
97
  isUltra?: () => boolean;
98
+ /**
99
+ * The boot-time grounding switch (defaults grounded): false spawns every
100
+ * subagent with the grounding read stripped from its tool list and the
101
+ * grounding-free twin body for BUILTIN agents (user/project bodies ride
102
+ * verbatim — they are the user's own content).
103
+ */
104
+ grounded?: boolean;
101
105
  /**
102
106
  * The session working-line manager (workingLine.ts). When present, live
103
107
  * progress goes through it (so the elapsed/token suffix survives); absent,
@@ -157,6 +161,8 @@ export interface RegisterSubagentsDeps {
157
161
  homeDir?: string;
158
162
  /** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
159
163
  isUltra?: () => boolean;
164
+ /** The boot-time grounding switch; false spawns blind subagents. */
165
+ grounded?: boolean;
160
166
  /** Session working-line manager; see MakeSubagentToolDeps.workingLine. */
161
167
  workingLine?: WorkingLineHandle;
162
168
  /** Session child-usage accumulator; see MakeSubagentToolDeps.childUsage. */
@@ -318,16 +318,98 @@ export function formatAgentList(agents) {
318
318
  * `{ticket}` verbatim, and the agent's body replaces the /go persona through
319
319
  * the runner's personaBody seam.
320
320
  */
321
- export function buildSubagentStage(def, task) {
321
+ /**
322
+ * Grounding-free twins of the four builtin bodies: the same capable roles
323
+ * with the "You are grounded in how THIS company works: call ask_yagni…"
324
+ * clause removed (the blind-lane pattern — separate constants, so the
325
+ * grounded prompt text stays byte-identical). Only BUILTIN agents have
326
+ * twins; user/project-defined bodies are the user's own content and ride
327
+ * verbatim.
328
+ */
329
+ const GENERAL_BODY_GROUNDING_FREE = `You are a capable software-engineering subagent with a fresh context window. Complete the task you are given end to end, autonomously.
330
+
331
+ Never fabricate file paths, contents, or findings. If you cannot find something, say so.
332
+
333
+ Complete the task fully — do not gold-plate, but do not leave it half-done. Your final message is your report back to the driving agent, which has NOT seen what you read or did: make it a concise report of what was done and the key findings, since the caller relays it to the user and it only needs the essentials. Cover what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
334
+ const SEARCHER_BODY_GROUNDING_FREE = `You are a repo scout. Your job is wide, mechanical reconnaissance:
335
+ find files, map structure, trace usages, and summarize what is there. You do
336
+ not write code and you do not run commands; you read and report.
337
+
338
+ Never fabricate file paths, contents, or code. Every path you cite must be
339
+ one you actually read with a tool. If you cannot find something, say "not
340
+ found" — a plausible-sounding invention is worse than no answer because the
341
+ driving agent trusts your report.
342
+
343
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
344
+
345
+ Your final message is your report back to the driving agent, which has NOT
346
+ seen what you read. Make it compressed and complete: exact file paths, the
347
+ key excerpts, and a one-paragraph map of how the pieces relate. Say what you
348
+ did NOT find as plainly as what you found.`;
349
+ const IMPLEMENTER_BODY_GROUNDING_FREE = `You are a mechanical implementer. You execute a
350
+ well-specified change: apply an edit across files, fix a failing test, rename
351
+ carefully, wire a defined seam. The judgment calls were made before you were
352
+ spawned; if the task turns out to require one, STOP and report the fork in
353
+ your final message instead of guessing.
354
+
355
+ Never fabricate file paths or results. Report what you actually did and what
356
+ you actually found.
357
+
358
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
359
+
360
+ Your final message is your report back to the driving agent, which has NOT
361
+ seen what you did. List every file you touched, what changed in each, the
362
+ commands you ran with their outcomes, and anything you deliberately left
363
+ undone.`;
364
+ const VERIFICATION_BODY_GROUNDING_FREE = `You are an adversarial verifier. Another agent
365
+ produced work — a change, a plan, or a claim — and your job is to try to
366
+ BREAK it, not to summarize it. Default to skepticism: hunt for the concrete
367
+ failure scenario (the inputs, state, or sequence that makes it wrong). Bash
368
+ is read-only here (\`git diff\`, \`git log\`, \`git show\`); do NOT modify files
369
+ or run builds.
370
+
371
+ If your task names a lens (correctness, edge cases, codebase fit, security,
372
+ …), judge ONLY through that lens and leave the rest to your sibling
373
+ verifiers.
374
+
375
+ Never fabricate file paths or findings. If you could not verify something,
376
+ say exactly what you tried and why you could not.
377
+
378
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
379
+
380
+ Your final message is your verdict back to the driving agent, which has NOT
381
+ seen what you read. Format:
382
+ ## Verdict
383
+ BROKEN or HOLDS, with one sentence why.
384
+ ## Findings
385
+ Each real problem: file:line, the concrete failure scenario, severity. No
386
+ style nits.
387
+ ## Not verified
388
+ What you could not check, and why.
389
+
390
+ A HOLDS after real digging is valuable; a rubber stamp is not. If you found
391
+ nothing, say exactly what you tried to break and how.`;
392
+ /** Builtin-body twins keyed by agent name; absent = not a builtin. */
393
+ const BUILTIN_BLIND_BODIES = {
394
+ [GENERAL_AGENT_NAME]: GENERAL_BODY_GROUNDING_FREE,
395
+ searcher: SEARCHER_BODY_GROUNDING_FREE,
396
+ implementer: IMPLEMENTER_BODY_GROUNDING_FREE,
397
+ verification: VERIFICATION_BODY_GROUNDING_FREE,
398
+ };
399
+ /** The canonical grounding-strip filter lives in grounding.ts; one home,
400
+ * shared with the advisor consults. */
401
+ import { stripGroundingTool } from "./grounding.js";
402
+ export function buildSubagentStage(def, task, opts = {}) {
403
+ const grounded = opts.grounded !== false;
322
404
  return {
323
405
  stage: {
324
406
  id: "implement",
325
407
  agent: def.name,
326
408
  model: def.model,
327
- tools: def.tools ?? DEFAULT_SUBAGENT_TOOLS,
409
+ tools: grounded ? def.tools ?? DEFAULT_SUBAGENT_TOOLS : stripGroundingTool(def.tools ?? DEFAULT_SUBAGENT_TOOLS),
328
410
  taskTemplate: "{ticket}",
329
411
  },
330
- ctx: { ticket: task, grounded: true },
412
+ ctx: { ticket: task, grounded },
331
413
  };
332
414
  }
333
415
  const parameters = Type.Object({
@@ -351,6 +433,15 @@ export function makeSubagentTool(deps = {}) {
351
433
  // landed a partial edit.
352
434
  const run = deps.runStageImpl ?? withResilience(runStage, DEFAULT_RESILIENCE_POLICY);
353
435
  const discover = deps.discover ?? discoverSubagents;
436
+ const grounded = deps.grounded !== false;
437
+ // Resolve the effective body for a spawned subagent: the grounding-free
438
+ // twin for builtins under `grounding: false`; the definition's own body
439
+ // everywhere else (user/project content rides verbatim).
440
+ const bodyFor = (def) => {
441
+ if (grounded || def.source !== "builtin")
442
+ return def.body;
443
+ return BUILTIN_BLIND_BODIES[def.name] ?? def.body;
444
+ };
354
445
  return {
355
446
  name: SUBAGENT_TOOL_NAME,
356
447
  label: "Subagent",
@@ -423,11 +514,11 @@ export function makeSubagentTool(deps = {}) {
423
514
  try {
424
515
  outcomes = await Promise.all(resolved.map(async ({ def, task }, index) => {
425
516
  const progress = progresses[index];
426
- const { stage, ctx: stageCtx } = buildSubagentStage(def, task);
517
+ const { stage, ctx: stageCtx } = buildSubagentStage(def, task, { grounded });
427
518
  const result = await run(stage, stageCtx, {
428
519
  cwd,
429
520
  signal,
430
- personaBody: () => def.body,
521
+ personaBody: () => bodyFor(def),
431
522
  // YAG-471: attribute this child's completions to the specific
432
523
  // subagent, not the generic /go stage label the runner would
433
524
  // otherwise derive from stage.id ("implement", reused as the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.1.1-staging.1358.1",
3
+ "version": "1.1.1-staging.1359.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "d3f818e827670951d1a080060578f482cbdd9fbc"
61
+ "yagniSourceSha": "beb4c9e3abfde8628b72e5b434d05ee2835f4bfc"
62
62
  }