@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
@@ -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 { isFreshWorkspace, runInitPass as defaultRunInitPass } from "./initPass.js";
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 evalMode = isEvalMode(deps.env ?? process.env);
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
- pi.registerProvider("yagni", buildYagniProvider(catalog, baseUrl));
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 reports session usage (accumulated off turn_end) + credit headroom.
115
- // The headroom fetch is fail-soft: until the backend /credits endpoint exists it
116
- // resolves null and /cost still shows session usage.
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 (YAG-383 owns the
120
- // real fix, which also covers /go's still-invisible stage spend).
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: 10_000, jitterRatio: 0 } });
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
- // Onramp Door B (spec §5B/§7): on a FRESH workspace (empty/thin grounding
177
- // corpus), the first run reads the repo, drafts the engineering-half brief + a
178
- // proposed Engineering Team, and seeds decisions — instead of a bare prompt. We
179
- // reuse the brief we just fetched (no double round-trip). Skipped in eval mode.
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 init pass writes only to
186
- // `yagni_code_decisions`, which the context endpoint never reads back (it reads
187
- // Vision + Goals), so a Vision/Goals-less workspace looks "fresh" forever and
188
- // would re-seed duplicate decisions on every launch. A per-workspace marker
189
- // short-circuits the pass after its first real run — regardless of brief
190
- // emptiness. (Degrades to freshness-only when the workspace id is unknown.)
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
- // Own the identity + inject live company context on every turn.
199
- pi.on("before_agent_start", (event) => ({
200
- systemPrompt: brandSystemPrompt(event.systemPrompt, { contextBrief }),
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: the CLI init pass (fresh-workspace detection + repo intake +
347
- // engineering-half drafting + decision seeding + one default next action).
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 init pass (Onramp Door B, spec §5B/§7).
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, the first `yagni` run in a repo should not drop the
5
- * user into a bare prompt. Instead it runs a one-time init pass:
6
- * 1. detect a fresh workspace (an empty/thin grounding corpus, via the existing
7
- * `GET /api/yagni-code/context` brief),
8
- * 2. read the repo (README, AGENTS.md/CLAUDE.md, package.json scripts, ADRs),
9
- * 3. draft the ENGINEERING HALF of the company brief + a proposed Engineering
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
- * Honesty rails (non-negotiable, spec §9): when a repo has no README/AGENTS/ADRs
17
- * the engineering half stays THIN and SAYS SO — nothing is fabricated, and no
18
- * decision is seeded from thin air. The proposed Team is only ever a DRAFT; it is
19
- * never created/committed automatically.
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 the whole pass is unit-testable without a network or a filesystem.
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
- /** A single architecture-decision record found in the repo. */
43
- export interface RepoAdr {
44
- /** Repo-relative path, e.g. `docs/adr/0001-use-fly.md`. */
45
- path: string;
46
- /** The record's title (its first heading, or the filename when headingless). */
47
- title: string;
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/ADRs — the brief says so, nothing is fabricated. */
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/ADRs the brief says so and NO decision is seeded.
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 an init-pass attempt. */
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 init pass. Guards on fresh-workspace detection (defensive — the caller
145
- * also guards), reads the repo, drafts the engineering half, presents it for
146
- * approve/edit, and ONLY THEN seeds the corpus (approval-gated: an approve banks
147
- * the drafted decisions, an edit banks the correction, a decline writes nothing),
148
- * before offering the one default next action. Fully fail-soft: a UI or network
149
- * hiccup never throws (it must never break session start).
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