@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
package/dist/extension/index.js
CHANGED
|
@@ -1,37 +1,89 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
1
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
4
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
3
5
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
6
|
+
import { registerBoostCommand } from "./boostCommand.js";
|
|
4
7
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
5
8
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
6
9
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
7
10
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
8
11
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
9
|
-
import { BRAND_NAME, brandSystemPrompt, buildMastheadString } from "./branding.js";
|
|
12
|
+
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER } from "./branding.js";
|
|
13
|
+
import { appendClaudeRules, claudeRulesSection } from "./claudeRules.js";
|
|
10
14
|
import { registerCostCommand } from "./costHud.js";
|
|
11
|
-
import {
|
|
15
|
+
import { isDebug } from "./diagnostics.js";
|
|
16
|
+
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
17
|
+
import { codeStateHome } from "./stateHome.js";
|
|
18
|
+
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
19
|
+
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
12
20
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
21
|
+
import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
|
|
13
22
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
14
23
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
15
|
-
import { registerPermissionGate } from "./permission.js";
|
|
24
|
+
import { DEFAULT_PERMISSION_POLICY, registerPermissionGate } from "./permission.js";
|
|
16
25
|
import { registerSubagents } from "./subagents.js";
|
|
17
26
|
import { registerTodos } from "./todos.js";
|
|
18
27
|
import { registerDecisionCommands } from "./decisions.js";
|
|
19
28
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
20
29
|
import { registerAmbientRecall } from "./recall.js";
|
|
21
30
|
import { resilientFetch } from "./resilientFetch.js";
|
|
31
|
+
import { installUncaughtExceptionMonitor } from "./crashReport.js";
|
|
22
32
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
23
33
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
24
|
-
import { fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
34
|
+
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
25
35
|
import { buildYagniProvider } from "./provider.js";
|
|
26
36
|
import { registerChipEditor } from "./chipEditor.js";
|
|
27
37
|
function isEvalMode(env = process.env) {
|
|
28
38
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
29
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Build GET /api/yagni-code/spend's URL. Pure (no fetch), exported for tests.
|
|
42
|
+
*
|
|
43
|
+
* YAG-383: a `/go` run's dispatches bill under its OWN run id
|
|
44
|
+
* (`llm_usage.session_id` holds the run id, not the driver session id — see
|
|
45
|
+
* attributionHeaders in config.ts), so `sessionId=` alone misses every run
|
|
46
|
+
* this session launched. `runIds` (comma-separated) widens the same query to
|
|
47
|
+
* include them; empty is omitted entirely rather than sent as `runIds=`.
|
|
48
|
+
*/
|
|
49
|
+
export function buildSpendUrl(baseUrl, sessionId, runIds) {
|
|
50
|
+
const url = `${baseUrl}/api/yagni-code/spend?sessionId=${encodeURIComponent(sessionId)}`;
|
|
51
|
+
return runIds.length > 0 ? `${url}&runIds=${runIds.join(",")}` : url;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build GET /api/yagni-code/spend's URL for ONE `/go` run's own spend
|
|
55
|
+
* (`?runId=`, the sibling of {@link buildSpendUrl}'s session-wide
|
|
56
|
+
* `?sessionId=&runIds=`). Pure (no fetch), exported for tests. The backend
|
|
57
|
+
* route treats `runId` and `sessionId` as aliases into the same lookup (see
|
|
58
|
+
* routes/yagniCode.ts's `/spend` handler), so this is just the more honest
|
|
59
|
+
* query param name for "this one run" rather than reusing `buildSpendUrl`
|
|
60
|
+
* with the run id awkwardly standing in for a session id.
|
|
61
|
+
*/
|
|
62
|
+
export function buildRunSpendUrl(baseUrl, runId) {
|
|
63
|
+
return `${baseUrl}/api/yagni-code/spend?runId=${encodeURIComponent(runId)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate + narrow a `GET /api/yagni-code/spend` response body before
|
|
67
|
+
* trusting it as a {@link SpendResponse}. A network response is untyped at
|
|
68
|
+
* runtime, so this is the ONE shared guard both the session-wide `/cost`
|
|
69
|
+
* fetch and the per-run `/go` fetch rely on (drift-risk fix: they used to
|
|
70
|
+
* each carry their own copy of this same check). Pure, exported for tests.
|
|
71
|
+
* Returns null on anything that doesn't look like the real shape.
|
|
72
|
+
*/
|
|
73
|
+
export function parseSpendResponse(data) {
|
|
74
|
+
if (!data || typeof data !== "object")
|
|
75
|
+
return null;
|
|
76
|
+
const d = data;
|
|
77
|
+
if (typeof d.totalSellMillicents !== "number" || !Array.isArray(d.rows))
|
|
78
|
+
return null;
|
|
79
|
+
return d;
|
|
80
|
+
}
|
|
30
81
|
export async function registerYagni(pi, deps = {}) {
|
|
31
82
|
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
32
83
|
const now = deps.now ?? (() => Date.now());
|
|
33
84
|
const fetchCatalog = deps.fetchCatalog ?? defaultFetchCatalog;
|
|
34
|
-
const
|
|
85
|
+
const env = deps.env ?? process.env;
|
|
86
|
+
const evalMode = isEvalMode(env);
|
|
35
87
|
// The refreshing TokenProvider replaces the old boot-time token snapshot:
|
|
36
88
|
// tools read the CURRENT token per call, a proactive unref'd timer rotates
|
|
37
89
|
// it near expiry, and `authedFetch` gives every tool's 401 path a single
|
|
@@ -50,8 +102,21 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
50
102
|
const getTokenFn = () => tokenProvider.getToken();
|
|
51
103
|
const authedFetch = makeAuthedFetch(tokenProvider, deps.fetchImpl);
|
|
52
104
|
const flushSpoolFn = deps.flushSpool ?? defaultFlushSpool;
|
|
105
|
+
// Crash reporting (docs/superpowers/specs/2026-08-08-crash-reporting-design.md):
|
|
106
|
+
// observe a fatal crash in pi's process via the behavior-neutral
|
|
107
|
+
// uncaughtExceptionMonitor hook and deliver through a detached one-shot
|
|
108
|
+
// sender (a dying process cannot complete its own fetch). Sanitized, fail-
|
|
109
|
+
// soft, opt-out via YAGNI_DISABLE_CRASH_REPORTS=1; off in eval mode like
|
|
110
|
+
// every other external side effect.
|
|
111
|
+
if (!evalMode) {
|
|
112
|
+
installUncaughtExceptionMonitor({ baseUrl, getToken: getTokenFn, env: deps.env });
|
|
113
|
+
}
|
|
53
114
|
const catalog = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
54
|
-
|
|
115
|
+
// YAG-471: the driver's own completions carry attribution headers read from
|
|
116
|
+
// this process's env (YAGNI_SESSION_ID minted by the launcher; YAGNI_CALLER
|
|
117
|
+
// defaults to "driver" when unset, i.e. every session that is not a /go
|
|
118
|
+
// child, a subagent, or an advisor consult).
|
|
119
|
+
pi.registerProvider("yagni", buildYagniProvider(catalog, baseUrl, attributionHeaders(deps.env)));
|
|
55
120
|
const toolOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
|
|
56
121
|
pi.registerTool(makeAskYagniTool(toolOpts));
|
|
57
122
|
// The peak-tier escalation for Balanced sessions (YAG-380). Registered
|
|
@@ -66,6 +131,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
66
131
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
67
132
|
// draws on the same cap rather than opening a side channel around it.
|
|
68
133
|
registerAdviseCommand(pi, askAdvisorTool);
|
|
134
|
+
// /boost — the sanctioned session-scoped escalation to Peak (spec §7,
|
|
135
|
+
// YAG-380 follow-on). Unlike /advise's per-call consult, this flips the
|
|
136
|
+
// DRIVER's own live model to peak until /boost off or the session ends.
|
|
137
|
+
// The returned handle threads isBoosted() into /cost below, the same way
|
|
138
|
+
// advisorState threads into advisorSubtotal.
|
|
139
|
+
const boostCommand = registerBoostCommand(pi, { env });
|
|
69
140
|
// The differentiated business-grounded tools (loop bricks): review a change
|
|
70
141
|
// for business fit, rank the next work by business priority, and record the
|
|
71
142
|
// engineering rationale back onto the work-item.
|
|
@@ -90,9 +161,32 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
90
161
|
// parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
|
|
91
162
|
// riding the /go pipeline's child runner. /agents lists what's available.
|
|
92
163
|
registerSubagents(pi);
|
|
164
|
+
// Shared fetch timeout for the small, interactive display-path reads below
|
|
165
|
+
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
166
|
+
// 5s, not the 10s other boot/grounding fetches use, because the user is
|
|
167
|
+
// waiting on a prompt or a run's completion, not backgrounded work.
|
|
168
|
+
const COST_FETCH_TIMEOUT_MS = 5_000;
|
|
93
169
|
// The grounded multi-agent pipeline entry point: /go <ticket> runs
|
|
94
170
|
// map → plan → implement → review → fix, each child grounded by inheritance.
|
|
95
|
-
registerGoCommand(pi
|
|
171
|
+
registerGoCommand(pi, {
|
|
172
|
+
// Task 8: /go's end-of-run summary prefers the server-priced per-stage
|
|
173
|
+
// breakdown for the ONE run that just finished (`?runId=`, aliasing the
|
|
174
|
+
// spend endpoint's `sessionId` param — see the backend route), over
|
|
175
|
+
// /cost's session-wide `?sessionId=&runIds=`. Fail-soft like every other
|
|
176
|
+
// fetch here: goCommand.ts's `resolveCostText` falls back to the local
|
|
177
|
+
// client estimate on a throw, a non-2xx, or a malformed body.
|
|
178
|
+
fetchRunSpend: async (runId, signal) => {
|
|
179
|
+
try {
|
|
180
|
+
const res = await resilientFetch(buildRunSpendUrl(baseUrl, runId), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
|
|
181
|
+
if (!res.ok)
|
|
182
|
+
return null;
|
|
183
|
+
return parseSpendResponse(await res.json());
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
96
190
|
// M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
|
|
97
191
|
// the business-fit delta. Never wired to routing.
|
|
98
192
|
registerGoCompareCommand(pi);
|
|
@@ -103,25 +197,65 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
103
197
|
const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
|
|
104
198
|
if (!evalMode)
|
|
105
199
|
registerDecisionCommands(pi, decisionClientOpts);
|
|
200
|
+
// Workspace MCP servers (YAG-446): register one tool per enabled server
|
|
201
|
+
// tool, executing through the backend proxy, plus the /mcp listing
|
|
202
|
+
// command. Fail-soft fetch (older backend / un-rescoped token → no MCP
|
|
203
|
+
// tools); skipped in eval mode like the other external-side-effect tools.
|
|
204
|
+
const fetchMcp = deps.fetchMcpServers ?? defaultFetchMcpServers;
|
|
205
|
+
const mcpServers = evalMode
|
|
206
|
+
? []
|
|
207
|
+
: await fetchMcp({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
208
|
+
const { mutatingToolNames: mcpMutatingTools } = registerMcpTools(pi, mcpServers, { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
209
|
+
registerMcpCommand(pi, mcpServers, { baseUrl });
|
|
106
210
|
// P3 + W4: interactive permission tiers + plan mode via /mode, now with a
|
|
107
211
|
// session bless store (three-way review-mode select) and a capture hook that
|
|
108
212
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
213
|
+
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
214
|
+
// holds them, review mode confirms them.
|
|
109
215
|
registerPermissionGate(pi, {
|
|
216
|
+
...(mcpMutatingTools.length > 0
|
|
217
|
+
? {
|
|
218
|
+
policy: {
|
|
219
|
+
planBlockTools: [
|
|
220
|
+
...DEFAULT_PERMISSION_POLICY.planBlockTools,
|
|
221
|
+
...mcpMutatingTools,
|
|
222
|
+
],
|
|
223
|
+
reviewConfirmTools: [
|
|
224
|
+
...DEFAULT_PERMISSION_POLICY.reviewConfirmTools,
|
|
225
|
+
...mcpMutatingTools,
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: {}),
|
|
110
230
|
onBlessRemember: decisionCapture
|
|
111
231
|
? (ctx, info) => decisionCapture.captureFromBless(ctx, info)
|
|
112
232
|
: undefined,
|
|
113
233
|
});
|
|
114
|
-
// P2: /cost
|
|
115
|
-
//
|
|
116
|
-
//
|
|
234
|
+
// P2/YAG-383: /cost prefers the server-authoritative session spend (covers
|
|
235
|
+
// subagents and advisor consults directly, since they bill under this same
|
|
236
|
+
// session id — PLUS every /go run this session launched, widened in via
|
|
237
|
+
// runIds because a run's dispatches bill under its OWN run id instead; see
|
|
238
|
+
// sessionRuns.ts and buildSpendUrl below), plus credit headroom. Both
|
|
239
|
+
// fetches are fail-soft: a down backend still leaves /cost showing the
|
|
240
|
+
// local, driver-only accumulator.
|
|
241
|
+
const yagniSessionId = env.YAGNI_SESSION_ID;
|
|
242
|
+
// COST_FETCH_TIMEOUT_MS (declared above, shared with /go's fetchRunSpend):
|
|
243
|
+
// the two fetches below now run concurrently (see costHud.ts's Promise.all)
|
|
244
|
+
// rather than serially, so it bounds worst-case /cost silence to 5s, not 10-20s.
|
|
117
245
|
registerCostCommand(pi, {
|
|
118
246
|
// Advisor consults run in a child process, so they never reach /cost's
|
|
119
|
-
// turn_end accumulator. Thread the subtotal in explicitly
|
|
120
|
-
//
|
|
247
|
+
// turn_end accumulator. Thread the subtotal in explicitly for the local
|
|
248
|
+
// fallback line; the server-authoritative line already counts advisor
|
|
249
|
+
// spend as an ordinary caller row.
|
|
121
250
|
advisorSubtotal: () => formatAdvisorSubtotal(advisorState.read(), DEFAULT_ADVISOR_LIMITS),
|
|
251
|
+
// YAG-380 follow-on: a boosted session that only chats never produces a
|
|
252
|
+
// server-side boosted row (see boostCommand.ts's KNOWN-asymmetry
|
|
253
|
+
// docblock), so /cost needs the live client-side toggle on top of
|
|
254
|
+
// whatever the server rows show.
|
|
255
|
+
isBoosted: () => boostCommand.isBoosted(),
|
|
122
256
|
fetchHeadroom: async (signal) => {
|
|
123
257
|
try {
|
|
124
|
-
const res = await resilientFetch(`${baseUrl}/api/yagni-code/credits`, { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs:
|
|
258
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/credits`, { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
|
|
125
259
|
if (!res.ok)
|
|
126
260
|
return null;
|
|
127
261
|
const data = (await res.json());
|
|
@@ -133,6 +267,58 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
133
267
|
return null;
|
|
134
268
|
}
|
|
135
269
|
},
|
|
270
|
+
// Skip the fetch entirely when there is no session id to ask about (e.g.
|
|
271
|
+
// a harness that never set YAGNI_SESSION_ID) rather than issuing a request
|
|
272
|
+
// guaranteed to 400.
|
|
273
|
+
fetchSpend: yagniSessionId
|
|
274
|
+
? async (signal) => {
|
|
275
|
+
try {
|
|
276
|
+
const res = await resilientFetch(buildSpendUrl(baseUrl, yagniSessionId, sessionRunIds()), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
|
|
277
|
+
if (!res.ok)
|
|
278
|
+
return null;
|
|
279
|
+
return parseSpendResponse(await res.json());
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
: undefined,
|
|
286
|
+
// Quiet debug signal only (never user-facing): a session-scoped log under
|
|
287
|
+
// YAGNI_DEBUG, mirroring diagnostics.ts's layer-A verbose mode. No-op when
|
|
288
|
+
// YAGNI_DEBUG is unset, so a healthy session never touches disk for this.
|
|
289
|
+
onDivergence: (driverServerUsd, localUsd) => {
|
|
290
|
+
if (!isDebug(env))
|
|
291
|
+
return;
|
|
292
|
+
try {
|
|
293
|
+
const path = join(codeStateHome(null, env), "logs", "cost-divergence.log");
|
|
294
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
295
|
+
const line = { ts: new Date().toISOString(), driverServerUsd, localUsd };
|
|
296
|
+
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* a diagnostic must never break /cost */
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
// Carry-over (/cost re-review): surfaces sessionRuns.ts's dropped-run-id
|
|
303
|
+
// count as costHud's "Excludes N earlier /go runs." note.
|
|
304
|
+
droppedSessionRuns,
|
|
305
|
+
});
|
|
306
|
+
// YAG-471: surface the backend's vision reroute header as a quiet notice
|
|
307
|
+
// (see rerouteNotice.ts for the event/header contract). One notifier per
|
|
308
|
+
// session dedupes on the from->to pair so a long session does not repeat
|
|
309
|
+
// itself on every image turn.
|
|
310
|
+
const rerouteNotifier = new RerouteNotifier();
|
|
311
|
+
pi.on("after_provider_response", (event, ctx) => {
|
|
312
|
+
try {
|
|
313
|
+
if (!ctx.hasUI)
|
|
314
|
+
return;
|
|
315
|
+
const message = rerouteNotifier.observe(event.headers);
|
|
316
|
+
if (message)
|
|
317
|
+
ctx.ui.notify(message, "info");
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// A notice must never break a turn.
|
|
321
|
+
}
|
|
136
322
|
});
|
|
137
323
|
// /exit — alias for pi's built-in /quit. Delegates to ctx.shutdown() which
|
|
138
324
|
// performs graceful TUI teardown (restores terminal state, emits
|
|
@@ -173,21 +359,26 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
173
359
|
decisionsCount: briefResult?.counts?.decisions ?? 0,
|
|
174
360
|
evalMode,
|
|
175
361
|
});
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
362
|
+
// ADR-0033: the Team-drafting flow is an explicit command, never a first-run
|
|
363
|
+
// ceremony. /setup-team drafts the Engineering Team + engineering brief from
|
|
364
|
+
// the repo and banks the brief as one decision on approval.
|
|
365
|
+
registerTeamSetupCommand(pi, { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
366
|
+
// Onramp Door B (spec §5B/§7, as amended by ADR-0033): on a FRESH workspace
|
|
367
|
+
// (empty/thin grounding corpus), the first run shows a short welcome + one
|
|
368
|
+
// next-work offer, with free text first-class — instead of a bare prompt. It
|
|
369
|
+
// records nothing; Team drafting lives behind /setup-team. We reuse the brief
|
|
370
|
+
// we just fetched (no double round-trip). Skipped in eval mode.
|
|
180
371
|
const runInitPassFn = deps.runInitPass ?? defaultRunInitPass;
|
|
181
372
|
const getWorkspaceIdFn = deps.getWorkspaceId ?? (() => defaultGetWorkspaceId(deps.env));
|
|
182
373
|
const isInitDoneFn = deps.isInitDone ?? defaultIsInitDone;
|
|
183
374
|
const markInitDoneFn = deps.markInitDone ?? defaultMarkInitDone;
|
|
184
375
|
const workspaceId = getWorkspaceIdFn();
|
|
185
|
-
// F2a — one-time marker (idempotency): the
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
376
|
+
// F2a — one-time marker (idempotency): the context endpoint never reflects
|
|
377
|
+
// the welcome (it reads Vision + Goals), so a Vision/Goals-less workspace
|
|
378
|
+
// looks "fresh" forever and would re-welcome on every launch. A per-workspace
|
|
379
|
+
// marker short-circuits the pass after its first real run — regardless of
|
|
380
|
+
// brief emptiness. (Degrades to freshness-only when the workspace id is
|
|
381
|
+
// unknown.)
|
|
191
382
|
const alreadyInit = !evalMode && !!workspaceId && isInitDoneFn(workspaceId);
|
|
192
383
|
// F2b — fail CLOSED: a FAILED context fetch (network / non-2xx / throw) surfaces
|
|
193
384
|
// as a `null` brief here (fetchContextBrief and the catch above both map failure
|
|
@@ -195,10 +386,27 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
195
386
|
// must NOT be treated as fresh, or a transient outage would spuriously re-seed.
|
|
196
387
|
// Only a genuinely-empty SUCCESSFUL response (non-null, thin) counts as fresh.
|
|
197
388
|
const freshWorkspace = !evalMode && !alreadyInit && briefResult !== null && isFreshWorkspace(briefResult);
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
389
|
+
// Claude Code `.claude/rules` compat: the launcher passes the trusted rules
|
|
390
|
+
// dirs via env; unscoped rules inline, path-scoped rules become read-first
|
|
391
|
+
// pointers. Computed once per activation (rules are launch-time state, like
|
|
392
|
+
// pi's own skill discovery); fail-soft to null.
|
|
393
|
+
const rulesSection = claudeRulesSection(deps.env ?? process.env);
|
|
394
|
+
// Own the identity + inject live company context (and repo rules) on every
|
|
395
|
+
// turn. The extension loads identically in every pi process this app spawns —
|
|
396
|
+
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
397
|
+
// consult — so the delegation-first paragraph (which assumes a `subagent`
|
|
398
|
+
// tool that only the driver has registered) is gated to the driver's own
|
|
399
|
+
// identity variant via isDriverCaller's env.YAGNI_CALLER check (config.ts).
|
|
400
|
+
// Under YAGNI_DISABLE_BRANDING the handler returns nothing, so pi's
|
|
401
|
+
// assembled prompt passes through byte-exact (no rewrite, no brief
|
|
402
|
+
// injection, no rules section, no delegation identity).
|
|
403
|
+
const noBranding = brandingDisabled(env);
|
|
404
|
+
const identity = isDriverCaller(env) ? YAGNI_IDENTITY_DRIVER : undefined;
|
|
405
|
+
pi.on("before_agent_start", (event) => noBranding
|
|
406
|
+
? undefined
|
|
407
|
+
: {
|
|
408
|
+
systemPrompt: appendClaudeRules(brandSystemPrompt(event.systemPrompt, { contextBrief, identity }), rulesSection),
|
|
409
|
+
});
|
|
202
410
|
// Two finalized-message guards share this handler (their conditions are
|
|
203
411
|
// mutually exclusive: YAG-460 takes error-stopped messages, YAG-466 takes
|
|
204
412
|
// stop/length ones without an errorMessage).
|
|
@@ -343,13 +551,13 @@ export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
|
343
551
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
344
552
|
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
345
553
|
export { recordDecision } from "./recordDecisionTool.js";
|
|
346
|
-
// Onramp Door B
|
|
347
|
-
//
|
|
348
|
-
export { runInitPass, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
554
|
+
// Onramp Door B (as amended by ADR-0033): the first-run welcome (write-free,
|
|
555
|
+
// free text first-class) + the explicit /setup-team drafting flow.
|
|
556
|
+
export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
349
557
|
// Onramp Door B (F2a): the one-time init-pass idempotency marker.
|
|
350
558
|
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
351
|
-
export { brandSystemPrompt, YAGNI_IDENTITY, BRAND_NAME } from "./branding.js";
|
|
352
|
-
export { fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl } from "./config.js";
|
|
559
|
+
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
|
|
560
|
+
export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
|
|
353
561
|
export { buildYagniProvider } from "./provider.js";
|
|
354
562
|
export { registerGoCommand } from "./pipeline/goCommand.js";
|
|
355
563
|
export { runPipeline } from "./pipeline/orchestrator.js";
|
|
@@ -385,4 +593,5 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
385
593
|
// W4 trust plumbing: the durable write-spool for the judgment-capture tools
|
|
386
594
|
// (idempotencyKey-replayed; server-side dedup makes flush safe).
|
|
387
595
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
596
|
+
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
388
597
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The CLI
|
|
2
|
+
* The CLI first-run experience (Onramp Door B, spec §5B/§7, as amended by
|
|
3
|
+
* ADR-0033: first run is a welcome, not a Team ceremony).
|
|
3
4
|
*
|
|
4
|
-
* On a FRESH workspace
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Team, and record the salient ADRs/conventions as decisions so the corpus
|
|
11
|
-
* is non-empty for the very first `/go`,
|
|
12
|
-
* 4. present the draft for approve/edit in-terminal (mirrored to the app via the
|
|
13
|
-
* recorded decisions), NEVER auto-committing the Team, and
|
|
14
|
-
* 5. offer the ONE default next action (`suggest_next_work`) with escape hatches.
|
|
5
|
+
* On a FRESH workspace (an empty/thin grounding corpus, via the existing
|
|
6
|
+
* `GET /api/yagni-code/context` brief), the first interactive `yagni` run shows
|
|
7
|
+
* a short WELCOME: how to work with YAGNI in one notice, then one offer (ask
|
|
8
|
+
* @yagni what to work on next) with free text as a first-class choice.
|
|
9
|
+
* Escape/cancel land in an empty editor ready to type into, never a forced
|
|
10
|
+
* prompt. The welcome records NOTHING and reads nothing from the repo.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* The Team-drafting flow (repo intake -> engineering-half draft -> approve/edit
|
|
13
|
+
* -> bank the brief as ONE decision) lives behind the explicit `/setup-team`
|
|
14
|
+
* command ({@link registerTeamSetupCommand}) so onboarding can invoke it
|
|
15
|
+
* deliberately. Honesty rails are unchanged (spec §9, F2/F13): approval-gated
|
|
16
|
+
* writes, honest-when-thin (nothing fabricated), the Team is only ever a DRAFT
|
|
17
|
+
* and never created automatically, and the repo intake reads only universal
|
|
18
|
+
* signals (README, AGENTS.md/CLAUDE.md, package.json scripts) so it behaves
|
|
19
|
+
* the same on any customer repo.
|
|
20
20
|
*
|
|
21
21
|
* No new backend transport: the only writes are through the existing token-scoped
|
|
22
22
|
* `record_decision` endpoint (reused via {@link recordDecision}). Every seam is
|
|
23
|
-
* injectable so
|
|
23
|
+
* injectable so both flows are unit-testable without a network or a filesystem.
|
|
24
24
|
*/
|
|
25
25
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { fetchContextBrief as defaultFetchContextBrief, type ContextBrief } from "./config.js";
|
|
@@ -39,16 +39,12 @@ export declare const FRESH_BRIEF_MIN_CHARS = 40;
|
|
|
39
39
|
* populated and the init pass must be skipped.
|
|
40
40
|
*/
|
|
41
41
|
export declare function isFreshWorkspace(brief: ContextBrief | null | undefined): boolean;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/** A short first-paragraph summary, when present. */
|
|
49
|
-
summary?: string;
|
|
50
|
-
}
|
|
51
|
-
/** What the repo intake could find. All fields are best-effort / optional. */
|
|
42
|
+
/**
|
|
43
|
+
* What the repo intake could find. All fields are best-effort / optional, and
|
|
44
|
+
* every signal is a UNIVERSAL repo convention (README, agents file, package
|
|
45
|
+
* scripts) — never a house-specific layout like docs/adr, so the intake
|
|
46
|
+
* behaves the same on any customer repo.
|
|
47
|
+
*/
|
|
52
48
|
export interface RepoIntake {
|
|
53
49
|
/** README body (capped), when present. */
|
|
54
50
|
readme?: string;
|
|
@@ -56,8 +52,6 @@ export interface RepoIntake {
|
|
|
56
52
|
agents?: string;
|
|
57
53
|
/** package.json `scripts` map (empty when absent/unparseable). */
|
|
58
54
|
packageScripts: Record<string, string>;
|
|
59
|
-
/** Architecture decision records found under docs/adr (etc.). */
|
|
60
|
-
adrs: RepoAdr[];
|
|
61
55
|
/** The literal test command (package.json `scripts.test`), when present. */
|
|
62
56
|
testCommand?: string;
|
|
63
57
|
/** The literal build command (package.json `scripts.build`), when present. */
|
|
@@ -66,7 +60,6 @@ export interface RepoIntake {
|
|
|
66
60
|
/** Injectable filesystem seam so repo intake is unit-testable without disk. */
|
|
67
61
|
export interface RepoIntakeFs {
|
|
68
62
|
readFile(p: string): Promise<string>;
|
|
69
|
-
readdir(p: string): Promise<string[]>;
|
|
70
63
|
}
|
|
71
64
|
/**
|
|
72
65
|
* Read the repo at `cwd` for the signals that seed the engineering half of the
|
|
@@ -81,13 +74,12 @@ export interface DraftTeam {
|
|
|
81
74
|
playbookRules: string[];
|
|
82
75
|
testCommand?: string;
|
|
83
76
|
buildCommand?: string;
|
|
84
|
-
riskAreas: string[];
|
|
85
77
|
}
|
|
86
78
|
/** The drafted engineering half: the brief, seeded decisions, and a Team draft. */
|
|
87
79
|
export interface EngineeringDraft {
|
|
88
80
|
/** The engineering-half brief text (honest-when-thin). */
|
|
89
81
|
brief: string;
|
|
90
|
-
/** True when the repo had no README/AGENTS
|
|
82
|
+
/** True when the repo had no README/AGENTS — the brief says so, nothing is fabricated. */
|
|
91
83
|
thin: boolean;
|
|
92
84
|
/** Decisions to seed into the corpus. EMPTY when thin (no fabrication). */
|
|
93
85
|
decisions: RecordDecisionParams[];
|
|
@@ -96,10 +88,10 @@ export interface EngineeringDraft {
|
|
|
96
88
|
}
|
|
97
89
|
/**
|
|
98
90
|
* Draft the engineering half of the brief from repo intake. PURE. Honest-when-thin:
|
|
99
|
-
* with no README/AGENTS
|
|
91
|
+
* with no README/AGENTS the brief says so and NO decision is seeded.
|
|
100
92
|
*/
|
|
101
93
|
export declare function draftEngineering(intake: RepoIntake): EngineeringDraft;
|
|
102
|
-
/** Injectable dependencies for {@link runInitPass}. */
|
|
94
|
+
/** Injectable dependencies for {@link runInitPass} (the first-run welcome). */
|
|
103
95
|
export interface RunInitPassDeps {
|
|
104
96
|
baseUrl: string;
|
|
105
97
|
getToken: () => string | undefined;
|
|
@@ -111,23 +103,34 @@ export interface RunInitPassDeps {
|
|
|
111
103
|
*/
|
|
112
104
|
brief?: ContextBrief | null;
|
|
113
105
|
fetchContextBrief?: typeof defaultFetchContextBrief;
|
|
114
|
-
readRepoIntake?: (cwd: string) => Promise<RepoIntake>;
|
|
115
|
-
recordDecision?: typeof defaultRecordDecision;
|
|
116
106
|
}
|
|
117
|
-
/** The outcome of
|
|
107
|
+
/** The outcome of a first-run welcome attempt. The welcome never writes. */
|
|
118
108
|
export type InitPassOutcome = {
|
|
119
109
|
ran: false;
|
|
120
110
|
reason: "not_fresh" | "non_interactive";
|
|
111
|
+
} | {
|
|
112
|
+
ran: true;
|
|
113
|
+
/** What the user picked; `free_text` covers the typing option AND escape/cancel. */
|
|
114
|
+
chosenAction: NextActionOption["id"] | "free_text";
|
|
115
|
+
};
|
|
116
|
+
/** Injectable dependencies for {@link runTeamSetup} (the `/setup-team` flow). */
|
|
117
|
+
export interface RunTeamSetupDeps {
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
getToken: () => string | undefined;
|
|
120
|
+
fetchImpl?: typeof fetch;
|
|
121
|
+
readRepoIntake?: (cwd: string) => Promise<RepoIntake>;
|
|
122
|
+
recordDecision?: typeof defaultRecordDecision;
|
|
123
|
+
}
|
|
124
|
+
/** The outcome of a `/setup-team` run. */
|
|
125
|
+
export type TeamSetupOutcome = {
|
|
126
|
+
ran: false;
|
|
127
|
+
reason: "non_interactive";
|
|
121
128
|
} | {
|
|
122
129
|
ran: true;
|
|
123
130
|
thin: boolean;
|
|
124
131
|
decisionsRecorded: number;
|
|
125
|
-
/** The Team was drafted for the user. */
|
|
126
|
-
teamDrafted: true;
|
|
127
132
|
/** The Team is NEVER created/committed automatically. Always false. */
|
|
128
133
|
teamCommitted: false;
|
|
129
|
-
/** Which next action the user picked (undefined in headless mode). */
|
|
130
|
-
chosenAction?: NextActionOption["id"];
|
|
131
134
|
};
|
|
132
135
|
/**
|
|
133
136
|
* Compact one-screen PREVIEW of the draft for the approve/edit dialog.
|
|
@@ -141,12 +144,27 @@ export type InitPassOutcome = {
|
|
|
141
144
|
*/
|
|
142
145
|
export declare function summarizeDraft(draft: EngineeringDraft): string;
|
|
143
146
|
/**
|
|
144
|
-
* Run the
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
147
|
+
* Run the first-run welcome (ADR-0033). Guards on fresh-workspace detection
|
|
148
|
+
* (defensive — the caller also guards), shows a short how-to-work-with-YAGNI
|
|
149
|
+
* notice, and offers ONE default next action (ask @yagni what to work on next)
|
|
150
|
+
* with free text as a first-class choice. Picking "Just start typing",
|
|
151
|
+
* pressing escape, or cancelling all leave the editor untouched so the user
|
|
152
|
+
* can type whatever they want to start on. Records NOTHING, reads nothing from
|
|
153
|
+
* the repo, and is fully fail-soft (it must never break session start).
|
|
150
154
|
*/
|
|
151
155
|
export declare function runInitPass(pi: ExtensionAPI, ctx: ExtensionContext, deps: RunInitPassDeps): Promise<InitPassOutcome>;
|
|
156
|
+
/**
|
|
157
|
+
* Run the `/setup-team` flow: read the repo's universal signals, draft the
|
|
158
|
+
* engineering half + a proposed Team, present it for approve/edit, and ONLY
|
|
159
|
+
* THEN bank the brief as one decision (approval-gated: an approve banks the
|
|
160
|
+
* draft, an edit banks the correction, a decline writes nothing). Fully
|
|
161
|
+
* fail-soft; the Team itself is never created automatically.
|
|
162
|
+
*/
|
|
163
|
+
export declare function runTeamSetup(pi: ExtensionAPI, ctx: ExtensionContext, deps: RunTeamSetupDeps): Promise<TeamSetupOutcome>;
|
|
164
|
+
/**
|
|
165
|
+
* Register `/setup-team`: the explicit, approval-gated flow that drafts the
|
|
166
|
+
* Engineering Team + engineering brief from the repo. Deliberately NOT part of
|
|
167
|
+
* first-run (ADR-0033) — onboarding invokes it when the workspace is ready.
|
|
168
|
+
*/
|
|
169
|
+
export declare function registerTeamSetupCommand(pi: ExtensionAPI, opts: RunTeamSetupDeps): void;
|
|
152
170
|
//# sourceMappingURL=initPass.d.ts.map
|