@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.
- package/README.md +58 -9
- package/dist/claudeCompat.d.ts +36 -5
- package/dist/claudeCompat.js +85 -23
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.js +14 -4
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +52 -0
- package/dist/extension/askAdvisorTool.js +7 -1
- package/dist/extension/bless.js +16 -3
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +31 -0
- package/dist/extension/branding.js +37 -0
- package/dist/extension/chipEditor.js +7 -3
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +61 -0
- package/dist/extension/config.js +86 -0
- package/dist/extension/costHud.d.ts +128 -15
- package/dist/extension/costHud.js +189 -19
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/index.d.ts +43 -4
- package/dist/extension/index.js +241 -32
- package/dist/extension/initPass.d.ts +65 -47
- package/dist/extension/initPass.js +145 -145
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/pipeline/eval.d.ts +42 -5
- package/dist/extension/pipeline/eval.js +44 -0
- package/dist/extension/pipeline/goCommand.d.ts +18 -0
- package/dist/extension/pipeline/goCommand.js +139 -26
- package/dist/extension/pipeline/goCompareCommand.d.ts +18 -8
- package/dist/extension/pipeline/goCompareCommand.js +42 -23
- package/dist/extension/pipeline/orchestrator.js +9 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runState.d.ts +19 -0
- package/dist/extension/pipeline/runState.js +11 -0
- package/dist/extension/pipeline/runner.d.ts +19 -0
- package/dist/extension/pipeline/runner.js +13 -1
- package/dist/extension/pipeline/scrubSecrets.js +2 -2
- package/dist/extension/pipeline/stages.d.ts +3 -1
- package/dist/extension/pipeline/stages.js +3 -1
- package/dist/extension/pipeline/types.d.ts +7 -4
- package/dist/extension/pipeline/verify.js +6 -1
- package/dist/extension/pipeline/worktree.js +3 -1
- package/dist/extension/provider.d.ts +7 -1
- package/dist/extension/provider.js +8 -1
- package/dist/extension/recall.js +5 -2
- package/dist/extension/rerouteNotice.d.ts +42 -0
- package/dist/extension/rerouteNotice.js +67 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/subagents.d.ts +17 -7
- package/dist/extension/subagents.js +52 -7
- package/dist/launch.d.ts +17 -3
- package/dist/launch.js +22 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- 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
|
-
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/extension/recall.js
CHANGED
|
@@ -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
|
-
|
|
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
|