@yagni-app/code-staging 0.1.0-staging.997.1 → 0.2.0-staging.1025.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +58 -9
  2. package/dist/claudeCompat.d.ts +36 -5
  3. package/dist/claudeCompat.js +85 -23
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.js +14 -4
  7. package/dist/crashReport.d.ts +135 -0
  8. package/dist/crashReport.js +291 -0
  9. package/dist/doctor.d.ts +21 -0
  10. package/dist/doctor.js +52 -0
  11. package/dist/extension/askAdvisorTool.js +7 -1
  12. package/dist/extension/bless.js +16 -3
  13. package/dist/extension/boostCommand.d.ts +144 -0
  14. package/dist/extension/boostCommand.js +263 -0
  15. package/dist/extension/branding.d.ts +31 -0
  16. package/dist/extension/branding.js +37 -0
  17. package/dist/extension/chipEditor.js +7 -3
  18. package/dist/extension/claudeRules.d.ts +54 -0
  19. package/dist/extension/claudeRules.js +180 -0
  20. package/dist/extension/config.d.ts +61 -0
  21. package/dist/extension/config.js +86 -0
  22. package/dist/extension/costHud.d.ts +128 -15
  23. package/dist/extension/costHud.js +189 -19
  24. package/dist/extension/crashReport.d.ts +89 -0
  25. package/dist/extension/crashReport.js +241 -0
  26. package/dist/extension/index.d.ts +43 -4
  27. package/dist/extension/index.js +241 -32
  28. package/dist/extension/initPass.d.ts +65 -47
  29. package/dist/extension/initPass.js +145 -145
  30. package/dist/extension/mcpTools.d.ts +57 -0
  31. package/dist/extension/mcpTools.js +132 -0
  32. package/dist/extension/pipeline/eval.d.ts +42 -5
  33. package/dist/extension/pipeline/eval.js +44 -0
  34. package/dist/extension/pipeline/goCommand.d.ts +18 -0
  35. package/dist/extension/pipeline/goCommand.js +139 -26
  36. package/dist/extension/pipeline/goCompareCommand.d.ts +18 -8
  37. package/dist/extension/pipeline/goCompareCommand.js +42 -23
  38. package/dist/extension/pipeline/orchestrator.js +9 -0
  39. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  40. package/dist/extension/pipeline/runCostTable.js +165 -0
  41. package/dist/extension/pipeline/runState.d.ts +19 -0
  42. package/dist/extension/pipeline/runState.js +11 -0
  43. package/dist/extension/pipeline/runner.d.ts +19 -0
  44. package/dist/extension/pipeline/runner.js +13 -1
  45. package/dist/extension/pipeline/scrubSecrets.js +2 -2
  46. package/dist/extension/pipeline/stages.d.ts +3 -1
  47. package/dist/extension/pipeline/stages.js +3 -1
  48. package/dist/extension/pipeline/types.d.ts +7 -4
  49. package/dist/extension/pipeline/verify.js +6 -1
  50. package/dist/extension/pipeline/worktree.js +3 -1
  51. package/dist/extension/provider.d.ts +7 -1
  52. package/dist/extension/provider.js +8 -1
  53. package/dist/extension/recall.js +5 -2
  54. package/dist/extension/rerouteNotice.d.ts +42 -0
  55. package/dist/extension/rerouteNotice.js +67 -0
  56. package/dist/extension/sessionRuns.d.ts +45 -0
  57. package/dist/extension/sessionRuns.js +77 -0
  58. package/dist/extension/subagents.d.ts +17 -7
  59. package/dist/extension/subagents.js +52 -7
  60. package/dist/launch.d.ts +17 -3
  61. package/dist/launch.js +22 -9
  62. package/dist/login.d.ts +7 -0
  63. package/dist/login.js +3 -1
  64. package/package.json +2 -2
@@ -177,12 +177,21 @@ export async function runPipeline(ticket, deps) {
177
177
  // stay attributed to review/<lens>. The tap is ALWAYS installed (the
178
178
  // decisionsRecorded count must not depend on a caller wiring onEvent);
179
179
  // forwarding to the caller's onEvent stays optional.
180
+ // YAG-471 attribution: the tracked run's server-assigned id (a UUID, set once
181
+ // `runSession.start` returns) rides every child spawn from here on, so every
182
+ // stage's completions attribute to this run in `llm_usage` — the stage loop,
183
+ // the review lens fan-out, AND the format-recovery reask (it goes through
184
+ // this same seam). Absent when the run isn't tracked (flag off, unauthorized,
185
+ // backend unreachable) — attribution then degrades to the caller label alone,
186
+ // never blocking the pipeline.
187
+ const runId = deps.checkpointMeta?.runId;
180
188
  const stageDepsFor = (tag) => {
181
189
  const onEvent = deps.onEvent;
182
190
  return {
183
191
  cwd: deps.cwd,
184
192
  signal: deps.signal,
185
193
  ...(deps.childEnv ? { env: deps.childEnv } : {}),
194
+ ...(runId ? { attribution: { runId } } : {}),
186
195
  onEvent: (ev) => {
187
196
  if (ev.type === "tool_execution_end" && ev.toolName === "record_decision" && !ev.isError) {
188
197
  decisionsRecorded += 1;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Task 8 (/go summary prints a per-stage, server-priced cost table): a pure
3
+ * formatter over `GET /api/yagni-code/spend?runId=`'s response (see
4
+ * costHud.ts's SpendResponse/SpendRow mirror for the shape this reads).
5
+ *
6
+ * Kept out of costHud.ts deliberately: that module's `formatServerCostLines`
7
+ * renders a SESSION's spend grouped by rate tier (for /cost); this renders ONE
8
+ * RUN's spend grouped by PIPELINE STAGE (for /go's end-of-run summary), and the
9
+ * grouping/merge rules below (fold the three review lenses into one line,
10
+ * mixed-tier tie-break) are specific enough to this call site that sharing a
11
+ * module would just make costHud.ts harder to read for its own surface.
12
+ *
13
+ * Caller labels are minted by runner.ts as `go:<stage.id>` (or
14
+ * `go:<stage.id>:<lens>` for the fanned-out review stage) — see
15
+ * `callerLabel` in runner.ts. This module's `stageKeyForCaller` is the inverse
16
+ * mapping back to a display row.
17
+ */
18
+ import type { SpendResponse } from "../costHud.js";
19
+ /**
20
+ * Render /go's end-of-run, server-priced cost table. Pure, no em-dashes,
21
+ * multi-line (joined with "\n"), leading space on the header/total lines so it
22
+ * reads as a continuation of the preceding recap sentence (matches
23
+ * runCostNote's own leading-space convention in goCommand.ts).
24
+ *
25
+ * Example:
26
+ * ```
27
+ * Run cost (server):
28
+ * map standard $0.05
29
+ * plan peak $0.31
30
+ * implement standard $0.12
31
+ * review peak $0.34
32
+ * fix standard $0.04
33
+ * Run: $0.84 billed. $2.71 at frontier list. 69% saved.
34
+ * ```
35
+ */
36
+ export declare function formatRunCostTable(spend: SpendResponse): string;
37
+ //# sourceMappingURL=runCostTable.d.ts.map
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Task 8 (/go summary prints a per-stage, server-priced cost table): a pure
3
+ * formatter over `GET /api/yagni-code/spend?runId=`'s response (see
4
+ * costHud.ts's SpendResponse/SpendRow mirror for the shape this reads).
5
+ *
6
+ * Kept out of costHud.ts deliberately: that module's `formatServerCostLines`
7
+ * renders a SESSION's spend grouped by rate tier (for /cost); this renders ONE
8
+ * RUN's spend grouped by PIPELINE STAGE (for /go's end-of-run summary), and the
9
+ * grouping/merge rules below (fold the three review lenses into one line,
10
+ * mixed-tier tie-break) are specific enough to this call site that sharing a
11
+ * module would just make costHud.ts harder to read for its own surface.
12
+ *
13
+ * Caller labels are minted by runner.ts as `go:<stage.id>` (or
14
+ * `go:<stage.id>:<lens>` for the fanned-out review stage) — see
15
+ * `callerLabel` in runner.ts. This module's `stageKeyForCaller` is the inverse
16
+ * mapping back to a display row.
17
+ */
18
+ import { usd } from "../costHud.js";
19
+ import { PIPELINE_V1 } from "./stages.js";
20
+ /**
21
+ * Pipeline stages in the order /go actually runs them, DERIVED from
22
+ * PIPELINE_V1 (stages.ts's single source of truth) rather than a hand-copied
23
+ * list — a future stage added there self-registers here in order, with
24
+ * nothing in this file to update or drift out of sync.
25
+ */
26
+ const STAGE_ORDER = PIPELINE_V1.map((s) => s.id);
27
+ /**
28
+ * Map a `GET /spend` caller label onto its display stage, or undefined for a
29
+ * caller this table does not recognize (surfaced instead as its own row,
30
+ * verbatim, after the known stages — see {@link buildRows}). `go:review` and
31
+ * every `go:review:<lens>` caller fold onto the single "review" row: the three
32
+ * adversarial lenses are one conceptual review pass, and a per-lens
33
+ * breakdown would just be noise in an end-of-run summary.
34
+ *
35
+ * Derives the mapping from STAGE_ORDER (advisor is the one non-pipeline
36
+ * exception, matched exactly) rather than hand-listing each `go:<id>`
37
+ * caller, so a stage added to PIPELINE_V1 needs no update here either: strip
38
+ * the `go:` prefix, take the first `:`-delimited segment (folds `go:review`
39
+ * and every `go:review:<lens>` onto the same "review" segment), and check it
40
+ * against the known stage ids.
41
+ */
42
+ function stageKeyForCaller(caller) {
43
+ if (caller === "advisor")
44
+ return "advisor";
45
+ if (!caller.startsWith("go:"))
46
+ return undefined;
47
+ const segment = caller.slice(3).split(":")[0];
48
+ return STAGE_ORDER.includes(segment) ? segment : undefined;
49
+ }
50
+ const sumSell = (rows) => rows.reduce((acc, r) => acc + r.sellMillicents, 0);
51
+ /**
52
+ * The tier shown for a (possibly multi-row) line: the rows' COMMON tier when
53
+ * they all billed under the same one, else the tier of whichever contributing
54
+ * row spent the most (a re-ask retry on `efficient`, or a server-side reroute,
55
+ * can leave one caller split across two tiers — the biggest-spend tier is the
56
+ * more honest single label than an arbitrary first-seen one).
57
+ */
58
+ function bucketTier(rows) {
59
+ const first = rows[0];
60
+ if (!first)
61
+ return "";
62
+ if (rows.every((r) => r.rateTier === first.rateTier))
63
+ return first.rateTier;
64
+ let biggest = first;
65
+ for (const r of rows) {
66
+ if (r.sellMillicents > biggest.sellMillicents)
67
+ biggest = r;
68
+ }
69
+ return biggest.rateTier;
70
+ }
71
+ /**
72
+ * Group spend rows into display rows: known pipeline stages (in pipeline
73
+ * order, review merged), then advisor when present, then every unrecognized
74
+ * caller (e.g. `subagent:foo`, `unattributed`, a bare `driver`) as its own
75
+ * row, sorted by spend descending (ties broken alphabetically, so output is
76
+ * deterministic). A stage with zero recorded dispatches is omitted rather
77
+ * than shown at $0.00: /go always runs map/plan/implement, but review/fix are
78
+ * absent on a run that stopped before reaching them (e.g. `no_changes`).
79
+ */
80
+ function buildRows(spendRows) {
81
+ const known = new Map();
82
+ const unknown = new Map();
83
+ for (const row of spendRows) {
84
+ const key = stageKeyForCaller(row.caller);
85
+ const bucket = key ?? row.caller;
86
+ const target = key ? known : unknown;
87
+ const list = target.get(bucket) ?? [];
88
+ list.push(row);
89
+ target.set(bucket, list);
90
+ }
91
+ const rows = [];
92
+ for (const stage of STAGE_ORDER) {
93
+ const stageRows = known.get(stage);
94
+ if (!stageRows || stageRows.length === 0)
95
+ continue;
96
+ rows.push({ label: stage, tier: bucketTier(stageRows), sellMillicents: sumSell(stageRows) });
97
+ }
98
+ const advisorRows = known.get("advisor");
99
+ if (advisorRows && advisorRows.length > 0) {
100
+ rows.push({ label: "advisor", tier: bucketTier(advisorRows), sellMillicents: sumSell(advisorRows) });
101
+ }
102
+ const unknownRows = [...unknown.entries()]
103
+ .map(([caller, r]) => ({ label: caller, tier: bucketTier(r), sellMillicents: sumSell(r) }))
104
+ .sort((a, b) => b.sellMillicents - a.sellMillicents || a.label.localeCompare(b.label));
105
+ rows.push(...unknownRows);
106
+ return rows;
107
+ }
108
+ /**
109
+ * `Run: $A billed. $B at frontier list. NN% saved.` — the "at frontier
110
+ * list" + savings clause only renders when `savingsPercent` resolved (typeof
111
+ * guard, matching formatServerCostLines's own malformed-response caution: a
112
+ * network response is untyped at runtime).
113
+ *
114
+ * `counterfactualIncomplete` and a numeric `savingsPercent` are MUTUALLY
115
+ * EXCLUSIVE by construction on the server (yagniCodeSpend.ts sets
116
+ * `savingsPercent` null whenever `counterfactualIncomplete` is true — there is
117
+ * no honest percentage to report when some billed spend fell on a tier with no
118
+ * Anthropic list-price counterfactual). So `counterfactualIncomplete` is
119
+ * checked on the non-number branch, not as a caveat appended to the number
120
+ * branch (that would be unreachable dead code, and would silently drop the
121
+ * whole savings clause on a partial counterfactual with no explanation).
122
+ */
123
+ function totalLine(spend) {
124
+ const billed = `$${usd(spend.totalSellMillicents)} billed.`;
125
+ if (typeof spend.savingsPercent !== "number") {
126
+ return spend.counterfactualIncomplete
127
+ ? ` Run: ${billed} Savings not shown: some spend billed on an unpriced tier.`
128
+ : ` Run: ${billed}`;
129
+ }
130
+ return ` Run: ${billed} $${usd(spend.totalCounterfactualMillicents)} at frontier list. ${spend.savingsPercent}% saved.`;
131
+ }
132
+ /**
133
+ * Render /go's end-of-run, server-priced cost table. Pure, no em-dashes,
134
+ * multi-line (joined with "\n"), leading space on the header/total lines so it
135
+ * reads as a continuation of the preceding recap sentence (matches
136
+ * runCostNote's own leading-space convention in goCommand.ts).
137
+ *
138
+ * Example:
139
+ * ```
140
+ * Run cost (server):
141
+ * map standard $0.05
142
+ * plan peak $0.31
143
+ * implement standard $0.12
144
+ * review peak $0.34
145
+ * fix standard $0.04
146
+ * Run: $0.84 billed. $2.71 at frontier list. 69% saved.
147
+ * ```
148
+ */
149
+ export function formatRunCostTable(spend) {
150
+ const rows = buildRows(spend.rows);
151
+ const labelWidth = rows.reduce((m, r) => Math.max(m, r.label.length), 0);
152
+ const tierWidth = rows.reduce((m, r) => Math.max(m, r.tier.length), 0);
153
+ const lines = [" Run cost (server):"];
154
+ for (const r of rows) {
155
+ lines.push(` ${r.label.padEnd(labelWidth + 2)}${r.tier.padEnd(tierWidth + 2)}$${usd(r.sellMillicents)}`);
156
+ }
157
+ lines.push(totalLine(spend));
158
+ if (spend.unbilledSellMillicents > 0) {
159
+ // Matches formatServerCostLines' + YagniCodeUsageView.tsx's wording for
160
+ // the same figure.
161
+ lines.push(` Includes $${usd(spend.unbilledSellMillicents)} not yet billed (insufficient credits or a billing error).`);
162
+ }
163
+ return lines.join("\n");
164
+ }
165
+ //# sourceMappingURL=runCostTable.js.map
@@ -92,6 +92,17 @@ export interface GoRunState {
92
92
  endedAt?: number;
93
93
  stopReason?: StopReason;
94
94
  agents: AgentNode[];
95
+ /**
96
+ * The backend's run UUID (`handle.runId` from `POST /runs`, goCommand.ts's
97
+ * `openFreshRun`) — NOT `runId` above, which is the local 8-char short id
98
+ * (`runId.slice(0, 8)`) used for widget/status keys. `/api/yagni-code/spend`
99
+ * joins on the server UUID, so the desktop needs this to ever widen its
100
+ * spend query with a /go run's own dispatches. Absent until `openFreshRun`
101
+ * resolves (a beat after the run starts), and absent for good on an
102
+ * untracked run (flag off, unauthorized, or unreachable — see
103
+ * `UNTRACKED_NOTICE` in goCommand.ts): there is no server id to give it.
104
+ */
105
+ serverRunId?: string;
95
106
  }
96
107
  /**
97
108
  * Stable agent id: a lens child is keyed by its lens AND round, and fix — the
@@ -113,6 +124,7 @@ export declare class RunState {
113
124
  private readonly startedAt;
114
125
  private endedAt;
115
126
  private stopReason;
127
+ private serverRunId;
116
128
  private round;
117
129
  private findingsTotal;
118
130
  private findingsBlocking;
@@ -139,6 +151,13 @@ export declare class RunState {
139
151
  applyUsage(ev: JsonEvent, tag: StageTag, at?: number): void;
140
152
  /** Stamp the run's end. Separate from `done` because FINISH runs after the pipeline returns. */
141
153
  markEnded(at: number): void;
154
+ /**
155
+ * Record the backend's run UUID once `openFreshRun` resolves (goCommand.ts,
156
+ * a beat after the run starts — never known at construction). The caller is
157
+ * responsible for repainting immediately after this so the desktop learns
158
+ * it as soon as possible rather than on the next debounced paint.
159
+ */
160
+ setServerRunId(id: string): void;
142
161
  /**
143
162
  * Stage rows for the panel: one row per stage, in order. Lens children are the
144
163
  * desktop's business alone, and a looped stage that re-executes per round
@@ -54,6 +54,7 @@ export class RunState {
54
54
  startedAt;
55
55
  endedAt;
56
56
  stopReason;
57
+ serverRunId;
57
58
  round = 0;
58
59
  findingsTotal = 0;
59
60
  findingsBlocking = 0;
@@ -221,6 +222,15 @@ export class RunState {
221
222
  markEnded(at) {
222
223
  this.endedAt = at;
223
224
  }
225
+ /**
226
+ * Record the backend's run UUID once `openFreshRun` resolves (goCommand.ts,
227
+ * a beat after the run starts — never known at construction). The caller is
228
+ * responsible for repainting immediately after this so the desktop learns
229
+ * it as soon as possible rather than on the next debounced paint.
230
+ */
231
+ setServerRunId(id) {
232
+ this.serverRunId = id;
233
+ }
224
234
  // ---- Terminal renderer accessors (never serialized) ----------------------
225
235
  /**
226
236
  * Stage rows for the panel: one row per stage, in order. Lens children are the
@@ -258,6 +268,7 @@ export class RunState {
258
268
  ...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
259
269
  ...(this.stopReason ? { stopReason: this.stopReason } : {}),
260
270
  agents: this.agents,
271
+ ...(this.serverRunId !== undefined ? { serverRunId: this.serverRunId } : {}),
261
272
  };
262
273
  }
263
274
  }
@@ -35,6 +35,25 @@ export interface RunStageDeps {
35
35
  };
36
36
  /** Optional environment for spawned child stages. Defaults to Node's inherited env. */
37
37
  env?: NodeJS.ProcessEnv;
38
+ /**
39
+ * YAG-471 attribution: the tracked run's server-assigned id (a UUID), sent to
40
+ * the child as `YAGNI_RUN_ID` so the model proxy's `x-yagni-run-id` header
41
+ * wins over the session id for `llm_usage.session_id`. Absent when the run is
42
+ * untracked (recording disabled/failed) — the child still gets a caller
43
+ * label, just not a run id; attribution degrades gracefully rather than
44
+ * blocking anything.
45
+ */
46
+ attribution?: {
47
+ runId?: string;
48
+ };
49
+ /**
50
+ * YAG-471 attribution: the child's caller label, sent as `YAGNI_CALLER` (the
51
+ * model proxy's `x-yagni-caller` header). Defaults to `go:<stage.id>` (or
52
+ * `go:<stage.id>:<lens>` for a fanned-out review lens) when absent, so the
53
+ * /go pipeline's own stage loop never has to set this explicitly. The
54
+ * subagent tool and the advisor pass their own label through this seam.
55
+ */
56
+ callerLabel?: string;
38
57
  /**
39
58
  * Additive live tap: invoked once per parsed NDJSON event, in stream order,
40
59
  * right after it is buffered for the reducers. PURE side-channel for the
@@ -132,10 +132,22 @@ export async function runStage(stage, ctx, deps) {
132
132
  }
133
133
  catch { /* ignore */ }
134
134
  };
135
+ // YAG-471 attribution: every child builds its OWN provider config at init
136
+ // (reading its own env), so a per-child env var becomes a per-child header.
137
+ // The caller label defaults from the stage/lens being run; a run id is
138
+ // included only when the caller (the orchestrator, once its run is tracked)
139
+ // supplied one — its absence just means this child's completions attribute
140
+ // to the session id alone.
141
+ const callerLabel = deps.callerLabel ?? (ctx.lens ? `go:${stage.id}:${ctx.lens}` : `go:${stage.id}`);
142
+ const childEnv = {
143
+ ...(deps.env ?? process.env),
144
+ YAGNI_CALLER: callerLabel,
145
+ ...(deps.attribution?.runId ? { YAGNI_RUN_ID: deps.attribution.runId } : {}),
146
+ };
135
147
  const exitCode = await new Promise((resolve) => {
136
148
  const proc = spawnFn(process.execPath, argv, {
137
149
  cwd: deps.cwd,
138
- ...(deps.env ? { env: deps.env } : {}),
150
+ env: childEnv,
139
151
  shell: false,
140
152
  stdio: ["ignore", "pipe", "pipe"],
141
153
  });
@@ -16,8 +16,8 @@ const PATTERNS = [
16
16
  // (the password segment has no secret-named key and is usually too short for the
17
17
  // base64 rule, so it would otherwise survive — common in config/env diffs).
18
18
  [/\b([a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+):[^\s:@/]+@/gi, "$1:[REDACTED]@"],
19
- // Provider key prefixes (OpenAI, GitHub, Slack, Stripe, AWS access key id, etc.)
20
- [/\b(sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
19
+ // Provider key prefixes (OpenAI, Stripe secret/restricted, GitHub, Slack, AWS access key id, etc.)
20
+ [/\b(sk-[A-Za-z0-9]{16,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|rk_(?:live|test)_[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
21
21
  // key = "value" / key: value for secret-named keys
22
22
  [/\b([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|token|private[_-]?key|access[_-]?key)[A-Za-z0-9_]*)\b(\s*[:=]\s*)("[^"]+"|'[^']+'|`[^`]+`|[^\s"']+)/gi, "$1$2[REDACTED]"],
23
23
  // Long base64-ish blobs (likely keys/JWTs)
@@ -6,7 +6,9 @@
6
6
  * Routing follows the judgment, not the typing: the strongest `peak` tier on the
7
7
  * steps that decide-what / catch-wrong (Plan, Review); the `standard` tier on the
8
8
  * execution steps (Map, Implement, Fix). The cheaper `efficient` tier stays a
9
- * selectable catalog tier but is intentionally not wired into a stage here.
9
+ * candidate for Map (its job is compression, not judgment) — the Task 13
10
+ * `/go-compare --map-tier` eval lane exists to settle that; map moves down-tier
11
+ * only once a live eval run clears it.
10
12
  *
11
13
  * Peak on judgment is affordable precisely BECAUSE the judgment stages are short
12
14
  * (YAG-380): plan plus the three review lenses is roughly 21c per round on
@@ -6,7 +6,9 @@
6
6
  * Routing follows the judgment, not the typing: the strongest `peak` tier on the
7
7
  * steps that decide-what / catch-wrong (Plan, Review); the `standard` tier on the
8
8
  * execution steps (Map, Implement, Fix). The cheaper `efficient` tier stays a
9
- * selectable catalog tier but is intentionally not wired into a stage here.
9
+ * candidate for Map (its job is compression, not judgment) — the Task 13
10
+ * `/go-compare --map-tier` eval lane exists to settle that; map moves down-tier
11
+ * only once a live eval run clears it.
10
12
  *
11
13
  * Peak on judgment is affordable precisely BECAUSE the judgment stages are short
12
14
  * (YAG-380): plan plus the three review lenses is roughly 21c per round on
@@ -14,11 +14,14 @@ import type { RunUsage } from "./budget.js";
14
14
  * `efficient` for the format-recovery re-ask; `advanced` is not wired into a
15
15
  * stage but is the Balanced session's driver tier, so it stays valid here.
16
16
  *
17
- * `balanced` is deliberately NOT a member: it is a session strategy the picker
18
- * offers, not something a pipeline stage can request. A stage names the tier it
19
- * actually runs on.
17
+ * `balanced` IS a member of this union — but only as a "router-decide" landing
18
+ * spot for agent frontmatter that names the session policy rather than a
19
+ * concrete tier: `subagents.ts`'s `mapModelTier` maps a `balanced` frontmatter
20
+ * value to the concrete tier `"standard"` (see its comment), so a stage or
21
+ * subagent never actually RUNS "balanced" as a resolved tier. A pipeline stage
22
+ * otherwise names the concrete tier it runs on.
20
23
  */
21
- export type ModelTier = "peak" | "advanced" | "standard" | "efficient";
24
+ export type ModelTier = "peak" | "advanced" | "standard" | "efficient" | "balanced";
22
25
  /** The fixed v1 pipeline stages, in order. */
23
26
  export type StageId = "map" | "plan" | "implement" | "review" | "fix";
24
27
  /**
@@ -46,7 +46,12 @@
46
46
  */
47
47
  import { execFile } from "node:child_process";
48
48
  import { readFileSync } from "node:fs";
49
- import { basename, dirname, join } from "node:path";
49
+ // posix path math on purpose, on every platform: the rel paths here come from
50
+ // git porcelain output, which is forward-slash even on Windows, and Windows fs
51
+ // APIs accept forward-slash paths — while win32 join/dirname would emit
52
+ // backslashed strings that no longer compare equal to the git-derived repoRoot
53
+ // and would break the walk-up termination check.
54
+ import { basename, dirname, join } from "node:path/posix";
50
55
  import { composeAbortSignal } from "./resilience.js";
51
56
  import { scrubSecrets } from "./scrubSecrets.js";
52
57
  import { snapshotWorkspace } from "./workspace.js";
@@ -25,7 +25,9 @@
25
25
  */
26
26
  import { execFile } from "node:child_process";
27
27
  import { mkdirSync, readFileSync } from "node:fs";
28
- import { dirname, join } from "node:path";
28
+ // posix join on purpose (see verify.ts): worktree paths are composed from
29
+ // git output and compared as strings; Windows fs APIs accept forward slashes.
30
+ import { dirname, join } from "node:path/posix";
29
31
  import { composeAbortSignal } from "./resilience.js";
30
32
  import { buildVerifyEnv, detectPackageManager } from "./verify.js";
31
33
  /** Wall-clock cap on the best-effort worktree bootstrap install (spec §3b). */
@@ -6,6 +6,12 @@ import type { ModelEntry } from "./config.js";
6
6
  * The provider speaks the `openai-completions` API against `${baseUrl}/v1` and
7
7
  * authenticates with the `$YAGNI_TOKEN` env var (resolved by pi at request time).
8
8
  * pi — not this extension — performs the actual model calls.
9
+ *
10
+ * `headers` (optional) carries the YAG-471 attribution labels — session id, run
11
+ * id, caller, boost — as plain header values; pi forwards them verbatim on
12
+ * every completion request this provider makes. They are labels only: the
13
+ * model proxy uses them to attribute `llm_usage` rows and never rejects a
14
+ * request over a missing or malformed one.
9
15
  */
10
- export declare function buildYagniProvider(catalog: ModelEntry[], baseUrl: string): ProviderConfig;
16
+ export declare function buildYagniProvider(catalog: ModelEntry[], baseUrl: string, headers?: Record<string, string>): ProviderConfig;
11
17
  //# sourceMappingURL=provider.d.ts.map
@@ -4,8 +4,14 @@
4
4
  * The provider speaks the `openai-completions` API against `${baseUrl}/v1` and
5
5
  * authenticates with the `$YAGNI_TOKEN` env var (resolved by pi at request time).
6
6
  * pi — not this extension — performs the actual model calls.
7
+ *
8
+ * `headers` (optional) carries the YAG-471 attribution labels — session id, run
9
+ * id, caller, boost — as plain header values; pi forwards them verbatim on
10
+ * every completion request this provider makes. They are labels only: the
11
+ * model proxy uses them to attribute `llm_usage` rows and never rejects a
12
+ * request over a missing or malformed one.
7
13
  */
8
- export function buildYagniProvider(catalog, baseUrl) {
14
+ export function buildYagniProvider(catalog, baseUrl, headers) {
9
15
  const models = catalog.map((m) => ({
10
16
  id: m.id,
11
17
  name: m.name,
@@ -21,6 +27,7 @@ export function buildYagniProvider(catalog, baseUrl) {
21
27
  apiKey: "$YAGNI_TOKEN",
22
28
  api: "openai-completions",
23
29
  models,
30
+ ...(headers ? { headers } : {}),
24
31
  };
25
32
  }
26
33
  //# sourceMappingURL=provider.js.map
@@ -22,7 +22,7 @@
22
22
  * - skipped entirely when the corpus is thin (<3 active decisions) or eval mode.
23
23
  * - any failure degrades to no injection.
24
24
  */
25
- import { isAbsolute, relative } from "node:path";
25
+ import { isAbsolute, relative, sep } from "node:path";
26
26
  import { composeAbortSignal } from "./pipeline/resilience.js";
27
27
  /** Recall must never block a read beyond this (spec §4). */
28
28
  export const RECALL_TIMEOUT_MS = 1500;
@@ -60,7 +60,10 @@ export function normalizeRecall(data) {
60
60
  export function toRepoRelativePath(readPath, cwd) {
61
61
  let p = readPath.trim();
62
62
  if (isAbsolute(p)) {
63
- const rel = relative(cwd, p);
63
+ // The backend keys judgments by forward-slash repo paths, so the rebased
64
+ // form must not carry win32 separators (also keeps the per-path session
65
+ // cache keyed identically across platforms).
66
+ const rel = relative(cwd, p).split(sep).join("/");
64
67
  if (rel && !rel.startsWith("..") && !isAbsolute(rel))
65
68
  p = rel;
66
69
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * YAG-471 - client-side acknowledgment of a vision-triggered model reroute.
3
+ *
4
+ * The backend sets `x-yagni-model-reroute: <requested>-><effective>:vision` on
5
+ * every response it silently upgrades for image content (yagniCodeV1.ts). pi's
6
+ * `after_provider_response` event is the only client-side seam that sees raw
7
+ * response headers (verified against pi 0.83.0's AfterProviderResponseEvent:
8
+ * `{ type: "after_provider_response", status: number, headers: Record<string,
9
+ * string> }` in dist/core/extensions/types.d.ts). Its docs (extensions.md,
10
+ * "after_provider_response") describe `event.headers` as "normalized response
11
+ * headers" and the shipped example reads `event.headers["retry-after"]` in
12
+ * lowercase, so header names always arrive lowercase-normalized; this module
13
+ * matches the header name as given rather than re-normalizing case.
14
+ */
15
+ /** A parsed reroute: the tier the caller requested vs the tier that actually served it. */
16
+ export interface Reroute {
17
+ from: string;
18
+ to: string;
19
+ reason: string;
20
+ }
21
+ /** pi's AfterProviderResponseEvent types headers as Record<string, string>. */
22
+ export type HeaderMap = Record<string, string | undefined>;
23
+ /**
24
+ * Parse the `x-yagni-model-reroute` header. Returns null when the header is
25
+ * absent, does not match `<from>-><to>:<reason>`, or the reason is not
26
+ * "vision" (the notice copy says "Your image", which only a vision reroute
27
+ * can honestly claim).
28
+ */
29
+ export declare function parseReroute(headers: HeaderMap): Reroute | null;
30
+ /**
31
+ * Dedupe wrapper around {@link parseReroute}: `observe` returns the notice
32
+ * message at most once per distinct from->to pair for the life of the
33
+ * instance (i.e. per session), and null otherwise (absent, malformed, or
34
+ * already-seen). Returning the message rather than taking a notify callback
35
+ * keeps the caller in charge of the ctx it has on hand at call time, instead
36
+ * of this class holding a stale reference across calls.
37
+ */
38
+ export declare class RerouteNotifier {
39
+ private readonly seen;
40
+ observe(headers: HeaderMap): string | null;
41
+ }
42
+ //# sourceMappingURL=rerouteNotice.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * YAG-471 - client-side acknowledgment of a vision-triggered model reroute.
3
+ *
4
+ * The backend sets `x-yagni-model-reroute: <requested>-><effective>:vision` on
5
+ * every response it silently upgrades for image content (yagniCodeV1.ts). pi's
6
+ * `after_provider_response` event is the only client-side seam that sees raw
7
+ * response headers (verified against pi 0.83.0's AfterProviderResponseEvent:
8
+ * `{ type: "after_provider_response", status: number, headers: Record<string,
9
+ * string> }` in dist/core/extensions/types.d.ts). Its docs (extensions.md,
10
+ * "after_provider_response") describe `event.headers` as "normalized response
11
+ * headers" and the shipped example reads `event.headers["retry-after"]` in
12
+ * lowercase, so header names always arrive lowercase-normalized; this module
13
+ * matches the header name as given rather than re-normalizing case.
14
+ */
15
+ const REROUTE_HEADER = "x-yagni-model-reroute";
16
+ const REROUTE_PATTERN = /^(.+?)->(.+?):(\w+)$/;
17
+ /**
18
+ * Parse the `x-yagni-model-reroute` header. Returns null when the header is
19
+ * absent, does not match `<from>-><to>:<reason>`, or the reason is not
20
+ * "vision" (the notice copy says "Your image", which only a vision reroute
21
+ * can honestly claim).
22
+ */
23
+ export function parseReroute(headers) {
24
+ const value = headers[REROUTE_HEADER];
25
+ if (!value)
26
+ return null;
27
+ const match = REROUTE_PATTERN.exec(value);
28
+ if (!match)
29
+ return null;
30
+ const [, from, to, reason] = match;
31
+ if (reason !== "vision")
32
+ return null;
33
+ return { from, to, reason };
34
+ }
35
+ /**
36
+ * Dedupe wrapper around {@link parseReroute}: `observe` returns the notice
37
+ * message at most once per distinct from->to pair for the life of the
38
+ * instance (i.e. per session), and null otherwise (absent, malformed, or
39
+ * already-seen). Returning the message rather than taking a notify callback
40
+ * keeps the caller in charge of the ctx it has on hand at call time, instead
41
+ * of this class holding a stale reference across calls.
42
+ */
43
+ export class RerouteNotifier {
44
+ seen = new Set();
45
+ observe(headers) {
46
+ const reroute = parseReroute(headers);
47
+ if (!reroute)
48
+ return null;
49
+ const key = `${reroute.from}->${reroute.to}`;
50
+ if (this.seen.has(key))
51
+ return null;
52
+ this.seen.add(key);
53
+ // balanced is a ROUTING POLICY, never a price (rateCard.ts's
54
+ // assertBillableTier throws if it ever reaches pricing) — for a balanced
55
+ // reroute the bill IS the resolved tier (`reroute.to`; yagniCodeV1.ts's
56
+ // `billedTier` takes its `effectiveModel` arm here), so a "Billed as
57
+ // balanced" clause would name a non-price. Only a CONCRETE from-tier
58
+ // still bills at its own pre-reroute rate (Task 3's invariant: a re-route
59
+ // never moves anyone's price), so only then is a distinct "Billed as X"
60
+ // line honest.
61
+ if (reroute.from === "balanced") {
62
+ return `Your image was handled on the ${reroute.to} tier.`;
63
+ }
64
+ return `Your image was handled on the ${reroute.to} tier. Billed as ${reroute.from}.`;
65
+ }
66
+ }
67
+ //# sourceMappingURL=rerouteNotice.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * YAG-383: tracks the `/go` run ids a driver session has launched, so /cost's
3
+ * server-authoritative spend fetch can widen its query to include them.
4
+ *
5
+ * The backend writes a run-attributed dispatch's `llm_usage.session_id` as
6
+ * the RUN id, not the driver session id (see attributionHeaders in
7
+ * config.ts: a `/go` child's completions carry `x-yagni-run-id`, and the
8
+ * proxy stores THAT in `session_id` — the run id wins, by design, so a
9
+ * per-run query works on its own). A plain `GET /api/yagni-code/spend?
10
+ * sessionId=<driver>` therefore never sees a single `/go` run's spend: it is
11
+ * real spend, billed under this session's work, but invisible to that query.
12
+ * `/cost` (index.ts's `fetchSpend`) closes that gap by also asking for every
13
+ * run id this process has launched, via `runIds=`.
14
+ *
15
+ * Module-level state is the right shape here: one CLI process IS one driver
16
+ * session, so "run ids this process has launched" and "run ids this session
17
+ * has launched" are the same list. Capped at the most recent
18
+ * `MAX_TRACKED_RUN_IDS` so an extremely long-lived session's /cost call never
19
+ * grows an unbounded query string (the backend independently caps `runIds` at
20
+ * the same number — see MAX_SPEND_RUN_IDS in routes/yagniCode.ts — capping
21
+ * symmetrically here keeps a stable, useful most-recent set instead of
22
+ * trailing off into a 400 from the server).
23
+ */
24
+ /**
25
+ * Record a `/go` run id once it is known (the goCommand.ts seam right after
26
+ * `runSession.start()` resolves). Ignores anything that is not UUID-shaped
27
+ * and de-dupes a repeat call for the same run (a resumed/re-adopted run can
28
+ * report the same id twice) — both defensive, since a malformed value here
29
+ * must never corrupt /cost's query string.
30
+ */
31
+ export declare function recordSessionRun(runId: string): void;
32
+ /** The run ids recorded so far this process, oldest first, capped. */
33
+ export declare function sessionRunIds(): string[];
34
+ /**
35
+ * How many run ids this session has ever dropped off the front of the
36
+ * `MAX_TRACKED_RUN_IDS` cap. Zero for the overwhelming majority of sessions
37
+ * (20 `/go` runs in one process is a lot); /cost's server rendering
38
+ * (costHud.ts's `formatServerCostLines`) surfaces this as an "Excludes N
39
+ * earlier /go runs." note when it is nonzero, so a long session's spend total
40
+ * is never silently missing runs with no explanation.
41
+ */
42
+ export declare function droppedSessionRuns(): number;
43
+ /** Test-only reset: module-level state otherwise leaks across test files/cases. */
44
+ export declare function _resetSessionRunsForTest(): void;
45
+ //# sourceMappingURL=sessionRuns.d.ts.map