@yagni-app/code 0.1.0 → 0.2.0

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 (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +194 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +42 -0
  126. package/dist/extension/rerouteNotice.js +67 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. package/package.json +11 -15
@@ -0,0 +1,597 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
5
+ import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
6
+ import { registerBoostCommand } from "./boostCommand.js";
7
+ import { makeAskYagniTool } from "./askYagniTool.js";
8
+ import { makeReviewBusinessMatchTool } from "./reviewTool.js";
9
+ import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
10
+ import { makeRecordDecisionTool } from "./recordDecisionTool.js";
11
+ import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
12
+ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER } from "./branding.js";
13
+ import { appendClaudeRules, claudeRulesSection } from "./claudeRules.js";
14
+ import { registerCostCommand } from "./costHud.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";
20
+ import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
21
+ import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
22
+ import { registerGoCommand } from "./pipeline/goCommand.js";
23
+ import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
24
+ import { DEFAULT_PERMISSION_POLICY, registerPermissionGate } from "./permission.js";
25
+ import { registerSubagents } from "./subagents.js";
26
+ import { registerTodos } from "./todos.js";
27
+ import { registerDecisionCommands } from "./decisions.js";
28
+ import { makeDecisionCapture } from "./decisionCapture.js";
29
+ import { registerAmbientRecall } from "./recall.js";
30
+ import { resilientFetch } from "./resilientFetch.js";
31
+ import { installUncaughtExceptionMonitor } from "./crashReport.js";
32
+ import { flushSpool as defaultFlushSpool } from "./spool.js";
33
+ import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
34
+ import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
35
+ import { buildYagniProvider } from "./provider.js";
36
+ import { registerChipEditor } from "./chipEditor.js";
37
+ function isEvalMode(env = process.env) {
38
+ return env.YAGNI_CODE_EVAL_MODE === "1";
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
+ }
81
+ export async function registerYagni(pi, deps = {}) {
82
+ const baseUrl = deps.baseUrl ?? resolveBaseUrl();
83
+ const now = deps.now ?? (() => Date.now());
84
+ const fetchCatalog = deps.fetchCatalog ?? defaultFetchCatalog;
85
+ const env = deps.env ?? process.env;
86
+ const evalMode = isEvalMode(env);
87
+ // The refreshing TokenProvider replaces the old boot-time token snapshot:
88
+ // tools read the CURRENT token per call, a proactive unref'd timer rotates
89
+ // it near expiry, and `authedFetch` gives every tool's 401 path a single
90
+ // serialized refresh + one retry at THIS one seam (no per-tool copies).
91
+ const tokenProvider = deps.tokenProvider ??
92
+ makeTokenProvider({
93
+ baseUrl,
94
+ env: deps.env,
95
+ initialToken: deps.getToken ? deps.getToken() : getToken(deps.env),
96
+ initialExpiresAt: deps.getTokenExpiresAt
97
+ ? deps.getTokenExpiresAt()
98
+ : defaultGetTokenExpiresAt(deps.env),
99
+ fetchImpl: deps.fetchImpl,
100
+ now: deps.now,
101
+ });
102
+ const getTokenFn = () => tokenProvider.getToken();
103
+ const authedFetch = makeAuthedFetch(tokenProvider, deps.fetchImpl);
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
+ }
114
+ const catalog = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
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)));
120
+ const toolOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
121
+ pi.registerTool(makeAskYagniTool(toolOpts));
122
+ // The peak-tier escalation for Balanced sessions (YAG-380). Registered
123
+ // UNCONDITIONALLY and gated at execute time on the live session model: pi's
124
+ // picker can switch the model after activation, so a registration-time tier
125
+ // check would both leave the tool live after switching away from Balanced and
126
+ // hide it forever from a session that switched TO Balanced. One state handle
127
+ // per session, shared with /advise so they draw on the same cap.
128
+ const advisorState = makeAdvisorState();
129
+ const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
130
+ pi.registerTool(askAdvisorTool);
131
+ // /advise runs the SAME tool, sharing the state handle, so a manual consult
132
+ // draws on the same cap rather than opening a side channel around it.
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 });
140
+ // The differentiated business-grounded tools (loop bricks): review a change
141
+ // for business fit, rank the next work by business priority, and record the
142
+ // engineering rationale back onto the work-item.
143
+ pi.registerTool(makeReviewBusinessMatchTool(toolOpts));
144
+ pi.registerTool(makeSuggestNextWorkTool(toolOpts));
145
+ if (!evalMode) {
146
+ pi.registerTool(makeRecordEngineeringContextTool(toolOpts));
147
+ // The capture half of the judgment loop: bank a product-intent decision so
148
+ // ask_yagni answers the same question next time instead of interrupting a human.
149
+ pi.registerTool(makeRecordDecisionTool(toolOpts));
150
+ }
151
+ // The visible checklist for multi-step work: the todo_write tool, its
152
+ // above-editor widget, and /todos. Branch-replayed, so forks and resumes
153
+ // show the list as it stood at that point.
154
+ registerTodos(pi);
155
+ // Image paste with [Image #N] chips: replaces the editor on
156
+ // session_start (TUI mode) so pasting a screenshot drops a chip instead of a
157
+ // temp path, and registers the input transform that turns chips into image
158
+ // attachments for the model. No-op outside TUI mode.
159
+ registerChipEditor(pi);
160
+ // The general subagent tool: delegate self-contained tasks (optionally in
161
+ // parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
162
+ // riding the /go pipeline's child runner. /agents lists what's available.
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;
169
+ // The grounded multi-agent pipeline entry point: /go <ticket> runs
170
+ // map → plan → implement → review → fix, each child grounded by inheritance.
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
+ });
190
+ // M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
191
+ // the business-fit delta. Never wired to routing.
192
+ registerGoCompareCommand(pi);
193
+ // W4 judgment loop: the decisions surface (/decide + /decisions) and the
194
+ // bless-with-remember capture are the same product-intent write as the record
195
+ // tools, so both are gated together (skipped in eval mode).
196
+ const decisionClientOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
197
+ const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
198
+ if (!evalMode)
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 });
210
+ // P3 + W4: interactive permission tiers + plan mode via /mode, now with a
211
+ // session bless store (three-way review-mode select) and a capture hook that
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.
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
+ : {}),
230
+ onBlessRemember: decisionCapture
231
+ ? (ctx, info) => decisionCapture.captureFromBless(ctx, info)
232
+ : undefined,
233
+ });
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.
245
+ registerCostCommand(pi, {
246
+ // Advisor consults run in a child process, so they never reach /cost's
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.
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(),
256
+ fetchHeadroom: async (signal) => {
257
+ try {
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 } });
259
+ if (!res.ok)
260
+ return null;
261
+ const data = (await res.json());
262
+ if (typeof data.remaining !== "number")
263
+ return null;
264
+ return { remaining: data.remaining, unit: typeof data.unit === "string" ? data.unit : "credits" };
265
+ }
266
+ catch {
267
+ return null;
268
+ }
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
+ }
322
+ });
323
+ // /exit — alias for pi's built-in /quit. Delegates to ctx.shutdown() which
324
+ // performs graceful TUI teardown (restores terminal state, emits
325
+ // session_shutdown for other extensions, disposes the runtime host, prints
326
+ // the resume hint) before calling process.exit(0). When idle (!isStreaming),
327
+ // pi's shutdownHandler calls this.shutdown() immediately; when streaming, it
328
+ // defers to agent_end — both correct.
329
+ pi.registerCommand("exit", {
330
+ description: "Exit YAGNI Code (alias for /quit).",
331
+ handler: async (_args, ctx) => {
332
+ ctx.shutdown();
333
+ },
334
+ });
335
+ // Boot grounded: fetch the company brief once (best-effort — a failure here
336
+ // must never block the agent), then inject it into the system prompt so the
337
+ // very first turn already knows how this company and codebase work.
338
+ const fetchBrief = deps.fetchContextBrief ?? defaultFetchContextBrief;
339
+ let contextBrief;
340
+ let briefResult = null;
341
+ if (!evalMode) {
342
+ try {
343
+ briefResult = await fetchBrief({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
344
+ contextBrief = briefResult?.brief?.trim() ? briefResult.brief : undefined;
345
+ }
346
+ catch {
347
+ briefResult = null;
348
+ contextBrief = undefined;
349
+ }
350
+ }
351
+ // M1 ambient judgment recall: after a `read`, append the recorded judgment for
352
+ // that path to the tool result (result modification is supported on pi's
353
+ // `tool_result` event — verified against 0.80.2). No-op when the corpus is thin
354
+ // (<3 active decisions from /context.counts) or in eval mode.
355
+ registerAmbientRecall(pi, {
356
+ baseUrl,
357
+ getToken: getTokenFn,
358
+ fetchImpl: authedFetch,
359
+ decisionsCount: briefResult?.counts?.decisions ?? 0,
360
+ evalMode,
361
+ });
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.
371
+ const runInitPassFn = deps.runInitPass ?? defaultRunInitPass;
372
+ const getWorkspaceIdFn = deps.getWorkspaceId ?? (() => defaultGetWorkspaceId(deps.env));
373
+ const isInitDoneFn = deps.isInitDone ?? defaultIsInitDone;
374
+ const markInitDoneFn = deps.markInitDone ?? defaultMarkInitDone;
375
+ const workspaceId = getWorkspaceIdFn();
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.)
382
+ const alreadyInit = !evalMode && !!workspaceId && isInitDoneFn(workspaceId);
383
+ // F2b — fail CLOSED: a FAILED context fetch (network / non-2xx / throw) surfaces
384
+ // as a `null` brief here (fetchContextBrief and the catch above both map failure
385
+ // to null; a successful-but-empty response is a non-null object). A failed fetch
386
+ // must NOT be treated as fresh, or a transient outage would spuriously re-seed.
387
+ // Only a genuinely-empty SUCCESSFUL response (non-null, thin) counts as fresh.
388
+ const freshWorkspace = !evalMode && !alreadyInit && briefResult !== null && isFreshWorkspace(briefResult);
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
+ });
410
+ // Two finalized-message guards share this handler (their conditions are
411
+ // mutually exclusive: YAG-460 takes error-stopped messages, YAG-466 takes
412
+ // stop/length ones without an errorMessage).
413
+ //
414
+ // YAG-460: a context-too-large rejection from the backend proxy gets a
415
+ // readable message in place of raw JSON (see the branch below for why it
416
+ // must keep the `request_too_large` marker and must not compact).
417
+ //
418
+ // YAG-466 belt-and-braces: a turn that ends "successfully" with zero content
419
+ // renders as literal silence in the TUI — the Working indicator vanishes and
420
+ // nothing appears, which reads as a frozen session. The backend now surfaces
421
+ // its own stalls as errors, so any empty completion that still gets here is
422
+ // unexplained; rewrite it into a visible error instead of letting it vanish.
423
+ // Only `stop`/`length` qualify: `aborted` is the user's own Esc (often with an
424
+ // empty message and no errorMessage — not a failure), and `error` already
425
+ // renders. The message text deliberately matches neither pi's overflow nor
426
+ // retryable-error patterns: a deterministic empty response should not burn
427
+ // auto-retries or trigger compaction — the user decides what to do next.
428
+ pi.on("message_end", (event, ctx) => {
429
+ const msg = event.message;
430
+ if (msg.role !== "assistant")
431
+ return;
432
+ // YAG-460: the backend proxy answers an oversized conversation with an
433
+ // OpenAI-format 413 whose type is `request_too_large`. pi's own overflow
434
+ // detection matches that marker and runs full recovery — compact, then
435
+ // auto-retry the failed turn — so this branch must NOT call ctx.compact()
436
+ // (it would race the built-in recovery and lose the retry). Its only job
437
+ // is UX: replace the raw `413: {"error":{...}}` JSON with a readable
438
+ // message and tell the user what is happening. The rewritten text KEEPS
439
+ // the `request_too_large` marker verbatim: pi's _checkCompaction reads the
440
+ // post-replacement message, and dropping the marker would defeat the very
441
+ // recovery this error exists to trigger.
442
+ if (msg.stopReason === "error" && msg.errorMessage) {
443
+ const isContextTooLarge = /request_too_large|context_too_large/i.test(msg.errorMessage);
444
+ if (!isContextTooLarge)
445
+ return;
446
+ const explanation = "The conversation context is too large for the model (request_too_large). "
447
+ + "YAGNI Code is compacting the session and retrying automatically — "
448
+ + "if it does not recover, run /compact or start a new session.";
449
+ if (ctx.hasUI) {
450
+ try {
451
+ ctx.ui.notify(explanation, "warning");
452
+ }
453
+ catch {
454
+ // Surfacing the problem must never break the session itself.
455
+ }
456
+ }
457
+ return { message: { ...msg, errorMessage: explanation } };
458
+ }
459
+ if (msg.stopReason !== "stop" && msg.stopReason !== "length")
460
+ return;
461
+ if (msg.errorMessage)
462
+ return;
463
+ const hasVisibleContent = msg.content.some((c) => (c.type === "text" && c.text.trim().length > 0) ||
464
+ (c.type === "thinking" && c.thinking.trim().length > 0) ||
465
+ c.type === "toolCall");
466
+ if (hasVisibleContent)
467
+ return;
468
+ const explanation = `The model returned an empty response (stop reason: ${msg.stopReason}). ` +
469
+ "This usually means the provider stalled — please try again.";
470
+ if (ctx.hasUI) {
471
+ try {
472
+ ctx.ui.notify(explanation, "warning");
473
+ }
474
+ catch {
475
+ // Surfacing the problem must never break the session itself.
476
+ }
477
+ }
478
+ return { message: { ...msg, stopReason: "error", errorMessage: explanation } };
479
+ });
480
+ // Chrome: replace pi's built-in startup header (which shows "pi v<version>"
481
+ // and a "Pi can explain its own features…" line) with a YAGNI Code masthead,
482
+ // set the terminal title, and add a footer brand mark. TUI only.
483
+ pi.on("session_start", async (event, ctx) => {
484
+ ctx.ui?.setTitle(BRAND_NAME);
485
+ if (ctx.mode === "tui") {
486
+ ctx.ui?.setStatus?.("brand", BRAND_NAME);
487
+ // setHeader replaces the built-in header in place (verified against pi
488
+ // 0.80.2 setExtensionHeader); the factory returns a simple Text component.
489
+ ctx.ui?.setHeader?.((_tui, theme) => new Text(buildMastheadString(theme)));
490
+ }
491
+ // Best-effort, once at session start: if the token is at or near expiry, say
492
+ // so via a single notice so a long session does not silently start 401-ing
493
+ // mid-flight (both completions and grounding tools). The provider's expiry
494
+ // is preferred over the env snapshot because a proactive/401 refresh may
495
+ // already have rotated the token by the time the session starts.
496
+ if (ctx.hasUI) {
497
+ try {
498
+ const notice = tokenExpiryNotice(tokenProvider.getExpiresAt(), now());
499
+ if (notice)
500
+ ctx.ui.notify(notice.message, notice.type);
501
+ }
502
+ catch {
503
+ // A notice must never disrupt session start.
504
+ }
505
+ }
506
+ // R4 write half: drain any judgment writes spooled by a previous session
507
+ // (transport/5xx failures of record_decision / record_engineering_context).
508
+ // Fire-and-forget and fail-soft — a flush must never delay or break session
509
+ // start; honest notices (replays, expiry drops) surface when it finishes.
510
+ if (!evalMode) {
511
+ void flushSpoolFn({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch })
512
+ .then((outcome) => {
513
+ if (!ctx.hasUI)
514
+ return;
515
+ for (const noticeText of outcome.notices)
516
+ ctx.ui.notify(noticeText, "info");
517
+ })
518
+ .catch(() => { });
519
+ }
520
+ // Run the init pass only on a genuine first-run (a fresh startup, not a
521
+ // reload/resume/fork) of a fresh workspace. Fully fail-soft: it must never
522
+ // break session start. The pass itself also re-guards on freshness.
523
+ if (event.reason === "startup" && freshWorkspace) {
524
+ try {
525
+ const outcome = await runInitPassFn(pi, ctx, {
526
+ baseUrl,
527
+ getToken: getTokenFn,
528
+ fetchImpl: authedFetch,
529
+ brief: briefResult,
530
+ });
531
+ // F2a: mark the workspace init-done on any real run (ran === true — thin
532
+ // or not), so it never re-seeds. A skip (not_fresh / non_interactive) is
533
+ // ran:false and does NOT mark, leaving the seed for a later real run.
534
+ if (outcome.ran && workspaceId)
535
+ markInitDoneFn(workspaceId);
536
+ }
537
+ catch {
538
+ // The init pass is best-effort; a failure never blocks the session.
539
+ }
540
+ }
541
+ });
542
+ }
543
+ export default async function (pi) {
544
+ await registerYagni(pi);
545
+ }
546
+ export { makeAskYagniTool } from "./askYagniTool.js";
547
+ export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
548
+ export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
549
+ export { makeReviewBusinessMatchTool } from "./reviewTool.js";
550
+ export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
551
+ export { makeRecordDecisionTool } from "./recordDecisionTool.js";
552
+ export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
553
+ export { recordDecision } from "./recordDecisionTool.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";
557
+ // Onramp Door B (F2a): the one-time init-pass idempotency marker.
558
+ export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.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";
561
+ export { buildYagniProvider } from "./provider.js";
562
+ export { registerGoCommand } from "./pipeline/goCommand.js";
563
+ export { runPipeline } from "./pipeline/orchestrator.js";
564
+ // R1: the in-loop resilience HOF a dev can compose with (or replace at) the
565
+ // `runStage` seam, plus its policy type and the default policy.
566
+ export { withResilience, classifyTransient, composeAbortSignal } from "./pipeline/resilience.js";
567
+ export { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
568
+ // P4: the deterministic verify gate seam (override the command via .yagni-code/verify).
569
+ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures } from "./pipeline/verify.js";
570
+ // M6: the report-only grounded-vs-blind eval engine.
571
+ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
572
+ export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
573
+ // The general subagent tool: Claude Code-format agent discovery + fan-out.
574
+ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
575
+ // The session todo checklist: todo_write tool, widget renderer, /todos.
576
+ export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
577
+ // P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
578
+ // the session bless-with-remember capture hook.
579
+ export { decideGate, registerPermissionGate, filterStalePlanContext, DEFAULT_PERMISSION_POLICY, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
580
+ // W4 judgment loop: the session bless store (tool + path-prefix, session-only).
581
+ export { makeBlessStore, blessPath } from "./bless.js";
582
+ // W4 judgment loop: the decisions surface (/decide + /decisions) + shared bank.
583
+ export { bankDecision, fetchDecisions, supersedeDecision, parseDecideArgs, formatDecisionsList, shortId, registerDecisionCommands, DEFAULT_LIST_LIMIT, } from "./decisions.js";
584
+ // W4 judgment loop: the debounced, TUI-confirmed decision capture at the gate.
585
+ export { makeDecisionCapture, CAPTURE_DEBOUNCE_MS } from "./decisionCapture.js";
586
+ // M1: ambient judgment recall appended to `read` results (pure cores + fetch).
587
+ export { registerAmbientRecall, fetchRecall, formatRecallBlock, toRepoRelativePath, normalizeRecall, RECALL_TIMEOUT_MS, RECALL_MIN_DECISIONS, } from "./recall.js";
588
+ // P2: the session cost accumulator + /cost command.
589
+ export { makeCostAccumulator, formatCostLine, registerCostCommand } from "./costHud.js";
590
+ // W4 trust plumbing: the refreshing TokenProvider (kills the boot snapshot) and
591
+ // the ONE 401 refresh-then-retry-once fetch seam every tool shares.
592
+ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE_REFRESH_WINDOW_MS, REFRESH_RETRY_MS, } from "./tokenProvider.js";
593
+ // W4 trust plumbing: the durable write-spool for the judgment-capture tools
594
+ // (idempotencyKey-replayed; server-side dedup makes flush safe).
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";
597
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The CLI init-pass idempotency marker (Onramp Door B, spec §5B/§7).
3
+ *
4
+ * The init pass writes only to `yagni_code_decisions`, but freshness is computed
5
+ * from `GET /api/yagni-code/context` — which reads only Vision + active Goals.
6
+ * A workspace with neither (the common case for a fresh CLI-only user) therefore
7
+ * looks "fresh" on EVERY launch, so without a marker the init pass would re-run
8
+ * and accumulate duplicate decisions each time.
9
+ *
10
+ * The fix is a one-time, per-workspace marker file, dropped after the first real
11
+ * run. It lives in the same `~/.yagni-code` home as the run journal
12
+ * ({@link checkpoint.ts}), is keyed on a truncated SHA-256 of the workspace id so
13
+ * the filename is path-safe and reveals no identity, and holds only a timestamp
14
+ * (never a token or any PII). Every operation is fail-soft: a missing marker
15
+ * simply means "not yet run", and a write failure never breaks session start.
16
+ */
17
+ export declare function _setInitDoneHomeForTest(dir: string | null): void;
18
+ /**
19
+ * The marker path for a workspace. The id is hashed (SHA-256, first 16 hex chars)
20
+ * so an arbitrary/opaque workspace id is path-safe and the raw id never lands in
21
+ * a filename.
22
+ */
23
+ export declare function initDoneMarkerFile(workspaceId: string): string;
24
+ /** Has the init pass already run once for this workspace? Fail-soft → false. */
25
+ export declare function isInitDone(workspaceId: string): boolean;
26
+ /** Record that the init pass has run once for this workspace. Fail-soft (never throws). */
27
+ export declare function markInitDone(workspaceId: string): void;
28
+ //# sourceMappingURL=initDone.d.ts.map