@yagni-app/code-staging 1.2.13-staging.2171.1 → 1.2.13-staging.2176.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.
@@ -9,6 +9,15 @@
9
9
  * functions it runs on its own plan (`groundSessionPlan` in the backend's
10
10
  * builder/planQuestions.ts), so there is one normalizer, on the host.
11
11
  */
12
+ /**
13
+ * A plan's size (YAG-1083's first half, shadow mode): the plan turn's own
14
+ * judgment of how big the build will be. The vocabulary is local because this
15
+ * package cannot depend on `@yagni/shared`; the runtime copy the host reads
16
+ * and stores lives in `packages/backend/src/observability/planSize.ts`. A size
17
+ * outside this set is dropped, never the plan.
18
+ */
19
+ declare const PLAN_SIZES: readonly ["small", "standard", "complex"];
20
+ type PlanSize = (typeof PLAN_SIZES)[number];
12
21
  /** `plan.json`, version 1, as the host reads it. */
13
22
  export interface PlanFile {
14
23
  version: 1;
@@ -55,6 +64,15 @@ export interface PlanFile {
55
64
  why: string;
56
65
  repository?: string;
57
66
  }>;
67
+ /**
68
+ * The plan turn's own judgment of how big the build will be
69
+ * (`small | standard | complex`), absent when the plan wrote none or one
70
+ * outside the vocabulary. Recorded only (YAG-1083's first half): nothing
71
+ * reads it back yet.
72
+ */
73
+ size?: PlanSize;
74
+ /** One line on why the size, clipped; absent when the plan wrote none. */
75
+ sizeReason?: string;
58
76
  }
59
77
  /** The plan's `wrongRepository`, or null when it names no `owner/name`. Confidence is high only when said. */
60
78
  export declare function readWrongRepository(value: unknown): PlanFile["wrongRepository"] | null;
@@ -87,4 +105,5 @@ export declare function buildPlanFile(planText: string, fallbackPaths: () => str
87
105
  file: PlanFile;
88
106
  found: boolean;
89
107
  };
108
+ export {};
90
109
  //# sourceMappingURL=planOutput.d.ts.map
@@ -19,6 +19,14 @@ const MAX_TEXT = 500;
19
19
  * same number as `MAX_SPLIT_CHILDREN` in @yagni/shared, kept here because this package
20
20
  * does not depend on it. */
21
21
  const MAX_REPOSITORIES_NEEDED = 12;
22
+ /**
23
+ * A plan's size (YAG-1083's first half, shadow mode): the plan turn's own
24
+ * judgment of how big the build will be. The vocabulary is local because this
25
+ * package cannot depend on `@yagni/shared`; the runtime copy the host reads
26
+ * and stores lives in `packages/backend/src/observability/planSize.ts`. A size
27
+ * outside this set is dropped, never the plan.
28
+ */
29
+ const PLAN_SIZES = ["small", "standard", "complex"];
22
30
  /** The plan's `wrongRepository`, or null when it names no `owner/name`. Confidence is high only when said. */
23
31
  export function readWrongRepository(value) {
24
32
  if (!isRecord(value))
@@ -176,6 +184,8 @@ export function buildPlanFile(planText, fallbackPaths) {
176
184
  const paths = [...new Set(named.length > 0 ? named : fallbackPaths())].slice(0, MAX_LIST);
177
185
  const repositoriesNeeded = readRepositoriesNeeded(block?.repositoriesNeeded);
178
186
  const wrongRepository = readWrongRepository(block?.wrongRepository);
187
+ const size = PLAN_SIZES.find((known) => known === block?.size);
188
+ const sizeReason = size && typeof block?.sizeReason === "string" ? text(block.sizeReason, 200) : null;
179
189
  return {
180
190
  file: {
181
191
  version: 1,
@@ -187,6 +197,8 @@ export function buildPlanFile(planText, fallbackPaths) {
187
197
  ...(wrongRepository ? { wrongRepository } : {}),
188
198
  dependsOn: readDependsOn(block?.dependsOn),
189
199
  followUps: readFollowUps(block?.followUps),
200
+ ...(size ? { size } : {}),
201
+ ...(size && sizeReason ? { sizeReason } : {}),
190
202
  },
191
203
  found: block !== null,
192
204
  };
@@ -48,6 +48,12 @@ import type { WorktreeExec } from "./worktree.js";
48
48
  export declare const SESSION_TIER: ModelTier;
49
49
  /** The caller the model proxy attributes the session's own turns to. */
50
50
  export declare const SESSION_CALLER_LABEL = "go:session";
51
+ /**
52
+ * Whether this process is a session-lane turn: the pi child {@link runSessionTurn}
53
+ * starts, which the runner gives the session's caller label (`YAGNI_CALLER`).
54
+ * A subagent it spawns gets its own label, so only the turn itself answers yes.
55
+ */
56
+ export declare function isSessionLaneTurn(env?: NodeJS.ProcessEnv): boolean;
51
57
  /** The model's own account of a turn that builds, beside `result.json` in the output folder. */
52
58
  export declare const SESSION_REPORT_FILE_NAME = "session-report.json";
53
59
  /** What a plan turn may use: everything that reads or runs, nothing that edits. */
@@ -56,6 +56,14 @@ import { snapshotWorkspace } from "./workspace.js";
56
56
  export const SESSION_TIER = "peak";
57
57
  /** The caller the model proxy attributes the session's own turns to. */
58
58
  export const SESSION_CALLER_LABEL = "go:session";
59
+ /**
60
+ * Whether this process is a session-lane turn: the pi child {@link runSessionTurn}
61
+ * starts, which the runner gives the session's caller label (`YAGNI_CALLER`).
62
+ * A subagent it spawns gets its own label, so only the turn itself answers yes.
63
+ */
64
+ export function isSessionLaneTurn(env = process.env) {
65
+ return env.YAGNI_CALLER === SESSION_CALLER_LABEL;
66
+ }
59
67
  /** The model's own account of a turn that builds, beside `result.json` in the output folder. */
60
68
  export const SESSION_REPORT_FILE_NAME = "session-report.json";
61
69
  /** What a plan turn may use: everything that reads or runs, nothing that edits. */
@@ -196,6 +196,8 @@ export const PLAN_JSON_CONTRACT = "End your answer with a fenced ```plan-json bl
196
196
  ' "repositoriesNeeded": [{"repository": "owner/name", "summary": "one line on what changes there"}],\n' +
197
197
  ' "wrongRepository": {"repository": "owner/name", "why": "one line", "confidence": "high|low"},\n' +
198
198
  ' "dependsOn": [{"repository": "owner/name", "what": "one line on what it needs there"}],\n' +
199
+ ' "size": "small|standard|complex",\n' +
200
+ ' "sizeReason": "one line on why the size",\n' +
199
201
  ' "followUps": [{"title": "...", "why": "one line", "repository": "owner/name"}]\n' +
200
202
  "}\n" +
201
203
  "```\n" +
@@ -208,6 +210,7 @@ export const PLAN_JSON_CONTRACT = "End your answer with a fenced ```plan-json bl
208
210
  "- At most two alternatives per question. A planChange only adds or rewords; it never removes.\n" +
209
211
  '- "checks" are the commands that prove the change, each run from "cwd" (repo-relative, "." for the root). ' +
210
212
  '"paths" are the package folders the plan touches.\n' +
213
+ '- "size" is your own judgment of how big the build will be (small: a one-file or config fix; standard: a change across one or two packages; complex: a cross-package or schema/contract change), with one line as "sizeReason". A plan that omits them, or writes a size outside the vocabulary, still produces a plan: the value is dropped, never the plan.\n' +
211
214
  '- "decisionsCited" lists only decisions the read returned that the plan relies on. Omit what you do not use.\n' +
212
215
  '- "repositoriesNeeded" is for a change that needs writes in more than one repository: list every ' +
213
216
  "repository the change writes to, in the order they must be built and merged, this one included, each " +
@@ -69,6 +69,15 @@ export interface SubagentDef {
69
69
  * not a second build (on staging 2026-10-28, advanced-tier verifiers ran 34
70
70
  * minutes after an 11-minute build). */
71
71
  export declare const VERIFICATION_WALL_TIMEOUT_MS: number;
72
+ /**
73
+ * The tier the built-in verification agent asks for: `peak` in a session-lane
74
+ * turn (`pipeline/sessionLane.ts`), the one place the proxy serves a verifier
75
+ * above its advanced ceiling for subagents, and `advanced` everywhere else (the
76
+ * CLI, desktop, the attempt lane's stage graph), so a request never asks for a
77
+ * tier it would not be served. A repo's own `verification` agent keeps the tier
78
+ * it declares.
79
+ */
80
+ export declare function verificationTier(env?: NodeJS.ProcessEnv): ModelTier;
72
81
  /** Claude Code model names → YAGNI tiers; YAGNI tiers pass through. */
73
82
  export declare function mapModelTier(model: string | undefined): ModelTier;
74
83
  /**
@@ -80,7 +89,7 @@ export declare function parseAgentMarkdown(content: string, fallbackName: string
80
89
  export interface DiscoverDeps {
81
90
  cwd: string;
82
91
  homeDir?: string;
83
- /** Env seam for the plugin agent dirs (defaults to process.env). */
92
+ /** Env seam for the plugin agent dirs and the verification agent's tier (defaults to process.env). */
84
93
  env?: NodeJS.ProcessEnv;
85
94
  }
86
95
  /**
@@ -112,7 +121,7 @@ export interface MakeSubagentToolDeps {
112
121
  runStageImpl?: typeof runStage;
113
122
  discover?: (deps: DiscoverDeps) => SubagentDef[];
114
123
  homeDir?: string;
115
- /** Env seam for the telemetry sessionId read (defaults to process.env). */
124
+ /** Env seam for discovery and the telemetry sessionId read (defaults to process.env). */
116
125
  env?: NodeJS.ProcessEnv;
117
126
  /** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
118
127
  isUltra?: () => boolean;
@@ -20,13 +20,14 @@
20
20
  import * as fs from "node:fs";
21
21
  import { homedir } from "node:os";
22
22
  import { delimiter, join } from "node:path";
23
- import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
23
+ import { parseFrontmatter, } from "@earendil-works/pi-coding-agent";
24
24
  import { Type } from "typebox";
25
25
  import { sanitizeCallerSegment } from "./config.js";
26
26
  import { logEvent } from "./errorSink.js";
27
27
  import { withResilience } from "./pipeline/resilience.js";
28
28
  import { runStage } from "./pipeline/runner.js";
29
- import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
29
+ import { isSessionLaneTurn } from "./pipeline/sessionLane.js";
30
+ import { DEFAULT_RESILIENCE_POLICY, } from "./pipeline/types.js";
30
31
  import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, renderSubagentCall, renderSubagentResult, } from "./subagentRender.js";
31
32
  /**
32
33
  * pi's tool contract signals an error by THROWING, and a normally-returned
@@ -216,26 +217,38 @@ nothing, say exactly what you tried to break and how.`;
216
217
  * not a second build (on staging 2026-10-28, advanced-tier verifiers ran 34
217
218
  * minutes after an 11-minute build). */
218
219
  export const VERIFICATION_WALL_TIMEOUT_MS = 15 * 60_000;
219
- /** The diamond's reduce layer: refute-first review of completed work on the
220
- * peak tier (judgment is the whole job, so the premium is worth paying; the
221
- * server exempts this caller from its advanced ceiling for subagents). One by
222
- * default, on a fifteen-minute clock. It has no edit/write tools; bash is
223
- * included for `git diff`-style inspection and is restricted to read-only USE
224
- * by the persona, the same prompt-level stance as /go's reviewer — not a
225
- * technical guarantee. */
226
- const VERIFICATION_AGENT = {
227
- name: "verification",
228
- description: "Adversarial verification of completed work: tries to refute a change, " +
229
- "plan, or claim and reports concrete failure scenarios. Does not edit " +
230
- "files. Send one; send two or three with different lenses (correctness, " +
231
- "edge cases, codebase fit) only for a risky change such as a migration, " +
232
- "auth or money, and treat agreement as confirmation.",
233
- model: "peak",
234
- tools: ["read", "grep", "find", "ls", "bash", "ask_yagni"],
235
- body: VERIFICATION_BODY,
236
- source: "builtin",
237
- wallTimeoutMs: VERIFICATION_WALL_TIMEOUT_MS,
238
- };
220
+ /**
221
+ * The tier the built-in verification agent asks for: `peak` in a session-lane
222
+ * turn (`pipeline/sessionLane.ts`), the one place the proxy serves a verifier
223
+ * above its advanced ceiling for subagents, and `advanced` everywhere else (the
224
+ * CLI, desktop, the attempt lane's stage graph), so a request never asks for a
225
+ * tier it would not be served. A repo's own `verification` agent keeps the tier
226
+ * it declares.
227
+ */
228
+ export function verificationTier(env = process.env) {
229
+ return isSessionLaneTurn(env) ? "peak" : "advanced";
230
+ }
231
+ /** The diamond's reduce layer: refute-first review of completed work, on
232
+ * {@link verificationTier} (judgment is the whole job, so the premium is worth
233
+ * paying where the lane serves it). One by default, on a fifteen-minute clock.
234
+ * It has no edit/write tools; bash is included for `git diff`-style inspection
235
+ * and is restricted to read-only USE by the persona, the same prompt-level
236
+ * stance as /go's reviewer, not a technical guarantee. */
237
+ function verificationAgent(env) {
238
+ return {
239
+ name: "verification",
240
+ description: "Adversarial verification of completed work: tries to refute a change, " +
241
+ "plan, or claim and reports concrete failure scenarios. Does not edit " +
242
+ "files. Send one; send two or three with different lenses (correctness, " +
243
+ "edge cases, codebase fit) only for a risky change such as a migration, " +
244
+ "auth or money, and treat agreement as confirmation.",
245
+ model: verificationTier(env),
246
+ tools: ["read", "grep", "find", "ls", "bash", "ask_yagni"],
247
+ body: VERIFICATION_BODY,
248
+ source: "builtin",
249
+ wallTimeoutMs: VERIFICATION_WALL_TIMEOUT_MS,
250
+ };
251
+ }
239
252
  // Concrete tiers a subagent can actually run on. `balanced` is deliberately NOT
240
253
  // a member here even though it is a member of `ModelTier`: a subagent needs
241
254
  // ONE model for its whole run, and balanced is a session-level routing policy,
@@ -331,9 +344,10 @@ function loadAgentsFromDir(dir, source) {
331
344
  */
332
345
  export function discoverSubagents(deps) {
333
346
  const home = deps.homeDir ?? homedir();
347
+ const env = deps.env ?? process.env;
334
348
  const layers = [
335
- [GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT, VERIFICATION_AGENT],
336
- ...pluginAgentDirs(deps.env ?? process.env).map((dir) => loadAgentsFromDir(dir, "plugin")),
349
+ [GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT, verificationAgent(env)],
350
+ ...pluginAgentDirs(env).map((dir) => loadAgentsFromDir(dir, "plugin")),
337
351
  loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
338
352
  loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
339
353
  loadAgentsFromDir(join(deps.cwd, ".claude", "agents"), "project-claude"),
@@ -353,9 +367,7 @@ const SOURCE_LABEL = {
353
367
  "project-claude": ".claude/agents",
354
368
  };
355
369
  export function formatAgentList(agents) {
356
- return agents
357
- .map((a) => `${a.name} (${SOURCE_LABEL[a.source]}): ${a.description}`)
358
- .join("\n");
370
+ return agents.map((a) => `${a.name} (${SOURCE_LABEL[a.source]}): ${a.description}`).join("\n");
359
371
  }
360
372
  /**
361
373
  * A subagent invocation is a synthetic pipeline stage: the task rides in as
@@ -450,7 +462,9 @@ export function buildSubagentStage(def, task, opts = {}) {
450
462
  id: "implement",
451
463
  agent: def.name,
452
464
  model: def.model,
453
- tools: grounded ? def.tools ?? DEFAULT_SUBAGENT_TOOLS : stripGroundingTool(def.tools ?? DEFAULT_SUBAGENT_TOOLS),
465
+ tools: grounded
466
+ ? (def.tools ?? DEFAULT_SUBAGENT_TOOLS)
467
+ : stripGroundingTool(def.tools ?? DEFAULT_SUBAGENT_TOOLS),
454
468
  taskTemplate: "{ticket}",
455
469
  },
456
470
  ctx: { ticket: task, grounded },
@@ -463,7 +477,9 @@ const parameters = Type.Object({
463
477
  agent: Type.Optional(Type.String({ description: "Agent name (see /agents). Defaults to general." })),
464
478
  tasks: Type.Optional(Type.Array(Type.Object({
465
479
  task: Type.String(),
466
- agent: Type.Optional(Type.String({ description: "Agent name; defaults to the top-level `agent`, then general." })),
480
+ agent: Type.Optional(Type.String({
481
+ description: "Agent name; defaults to the top-level `agent`, then general.",
482
+ })),
467
483
  }), {
468
484
  description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}; ${MAX_PARALLEL_SUBAGENTS_ULTRA} in ultra mode). Use INSTEAD of task. Entries without their own agent run as the top-level agent.`,
469
485
  })),
@@ -504,7 +520,11 @@ export function subagentRunPolicy(wallTimeoutMs = SUBAGENT_WALL_TIMEOUT_MS) {
504
520
  export function subagentTelemetry(env) {
505
521
  return (rec) => logEvent({
506
522
  source: "subagent",
507
- level: rec.outcome === "ok" ? "info" : rec.outcome === "timeout" || rec.outcome === "fatal" ? "error" : "warn",
523
+ level: rec.outcome === "ok"
524
+ ? "info"
525
+ : rec.outcome === "timeout" || rec.outcome === "fatal"
526
+ ? "error"
527
+ : "warn",
508
528
  event: `attempt_${rec.outcome}`,
509
529
  sessionId: env.YAGNI_SESSION_ID,
510
530
  fields: {
@@ -563,7 +583,7 @@ export function makeSubagentTool(deps = {}) {
563
583
  // Fails soft: discovery problems never block the tool.
564
584
  let agentListBlock = "";
565
585
  try {
566
- const discovered = discover({ cwd: process.cwd(), homeDir: deps.homeDir });
586
+ const discovered = discover({ cwd: process.cwd(), homeDir: deps.homeDir, env: deps.env });
567
587
  if (discovered.length > 0) {
568
588
  agentListBlock =
569
589
  " Available agents (first sentence only; /agents lists them in full):\n" +
@@ -589,7 +609,8 @@ export function makeSubagentTool(deps = {}) {
589
609
  "spell out the task completely. Agents come from this repo's .claude/agents and .pi/agents " +
590
610
  "(list them with /agents); omit `agent` for the general-purpose one. The list below was " +
591
611
  "captured at session launch — if the session's cwd differs from the launch cwd it may be " +
592
- "stale; an unknown name is a hard error, so trust /agents over this list when they differ." + agentListBlock,
612
+ "stale; an unknown name is a hard error, so trust /agents over this list when they differ." +
613
+ agentListBlock,
593
614
  promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
594
615
  parameters,
595
616
  // Self-framed: the condensed transcript look has no tinted tool boxes.
@@ -619,7 +640,7 @@ export function makeSubagentTool(deps = {}) {
619
640
  return fail(`Error: at most ${maxParallel} parallel tasks per call.`);
620
641
  }
621
642
  const cwd = ctx?.cwd ?? process.cwd();
622
- const agents = discover({ cwd, homeDir: deps.homeDir });
643
+ const agents = discover({ cwd, homeDir: deps.homeDir, env: deps.env });
623
644
  const resolved = [];
624
645
  for (const req of requested) {
625
646
  // A tasks[] entry names its own agent first; absent that, it inherits
@@ -647,7 +668,11 @@ export function makeSubagentTool(deps = {}) {
647
668
  onUpdate?.({
648
669
  content: [{ type: "text", text: progressSummaryText(progresses, now) }],
649
670
  details: {
650
- tasks: progresses.map((p) => ({ ...p, actions: [...p.actions], usage: { ...p.usage } })),
671
+ tasks: progresses.map((p) => ({
672
+ ...p,
673
+ actions: [...p.actions],
674
+ usage: { ...p.usage },
675
+ })),
651
676
  },
652
677
  });
653
678
  const working = formatWorkingMessage(progresses, now);
@@ -678,7 +703,7 @@ export function makeSubagentTool(deps = {}) {
678
703
  // Computed from the tools the runner will actually pass the
679
704
  // child (`--tools`), so a custom definition cannot opt out by
680
705
  // name.
681
- stageWriteCapable: (stage.tools).some((t) => t === "edit" || t === "write"),
706
+ stageWriteCapable: stage.tools.some((t) => t === "edit" || t === "write"),
682
707
  // Content-free task discriminator: which entry of THIS call's
683
708
  // task list the attempt belongs to (parallel same-agent
684
709
  // failures stay attributable without leaking task text).
@@ -727,7 +752,10 @@ export function makeSubagentTool(deps = {}) {
727
752
  // The verification agent's contract is a `## Verdict` header; an
728
753
  // output without it is not a verdict, and the driver must not treat
729
754
  // a truncated probe log as "HOLDS".
730
- const missingVerdict = o.def.source === "builtin" && o.agent === "verification" && o.result.exitCode === 0 && !output.includes("## Verdict")
755
+ const missingVerdict = o.def.source === "builtin" &&
756
+ o.agent === "verification" &&
757
+ o.result.exitCode === 0 &&
758
+ !output.includes("## Verdict")
731
759
  ? "\n\n(no verdict — the verification run did not produce its verdict section)"
732
760
  : "";
733
761
  return outcomes.length === 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.13-staging.2171.1",
3
+ "version": "1.2.13-staging.2176.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": "4c225e34b3364d25830d9d4d88f071ffedf799f3"
61
+ "yagniSourceSha": "aae5dcff2b6e88c794f1213893df2c24ec7be19e"
62
62
  }