@yagni-app/code 0.2.1 → 0.3.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/dist/cli.d.ts +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +44 -1
- package/dist/extension/config.js +37 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
package/dist/extension/index.js
CHANGED
|
@@ -1,39 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
5
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
|
+
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./approvedPrefixes.js";
|
|
7
|
+
import { redactCommand } from "./redact.js";
|
|
8
|
+
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./guardian.js";
|
|
5
9
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
6
|
-
import { registerBoostCommand } from "./boostCommand.js";
|
|
7
10
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
11
|
+
import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
8
12
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
13
|
+
import { registerCmuxBridge } from "./cmux/index.js";
|
|
9
14
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
10
15
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
11
16
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
12
|
-
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER } from "./branding.js";
|
|
13
|
-
import {
|
|
17
|
+
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
|
|
18
|
+
import { claudeRulesSection } from "./claudeRules.js";
|
|
14
19
|
import { registerCostCommand } from "./costHud.js";
|
|
15
20
|
import { isDebug } from "./diagnostics.js";
|
|
16
21
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
17
22
|
import { codeStateHome } from "./stateHome.js";
|
|
23
|
+
import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
|
|
18
24
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
19
25
|
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
20
26
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
21
27
|
import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
|
|
22
28
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
23
29
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
24
|
-
import { DEFAULT_PERMISSION_POLICY, registerPermissionGate } from "./permission.js";
|
|
30
|
+
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
|
|
25
31
|
import { registerSubagents } from "./subagents.js";
|
|
32
|
+
import { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
26
33
|
import { registerTodos } from "./todos.js";
|
|
27
34
|
import { registerDecisionCommands } from "./decisions.js";
|
|
28
35
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
29
36
|
import { registerAmbientRecall } from "./recall.js";
|
|
30
37
|
import { resilientFetch } from "./resilientFetch.js";
|
|
31
|
-
import { installUncaughtExceptionMonitor } from "./crashReport.js";
|
|
38
|
+
import { installUncaughtExceptionMonitor, makeCrashReporter } from "./crashReport.js";
|
|
32
39
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
33
40
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
34
41
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
35
42
|
import { buildYagniProvider } from "./provider.js";
|
|
36
43
|
import { registerChipEditor } from "./chipEditor.js";
|
|
44
|
+
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
45
|
+
import { makeFlywheelState } from "./flywheel.js";
|
|
37
46
|
function isEvalMode(env = process.env) {
|
|
38
47
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
39
48
|
}
|
|
@@ -111,19 +120,48 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
111
120
|
if (!evalMode) {
|
|
112
121
|
installUncaughtExceptionMonitor({ baseUrl, getToken: getTokenFn, env: deps.env });
|
|
113
122
|
}
|
|
114
|
-
|
|
123
|
+
// YAG-500 Fix E: non-fatal auth-event reporter for 401s on the model path.
|
|
124
|
+
// Reuses the crash endpoint (/api/yagni-code/crash) with a distinct context
|
|
125
|
+
// so auth failures are visible in Sentry/backend logs. Gated on !evalMode
|
|
126
|
+
// like every other external side effect; injectable for tests.
|
|
127
|
+
const authReporter = deps.authReporter ??
|
|
128
|
+
(!evalMode
|
|
129
|
+
? makeCrashReporter({ baseUrl, getToken: getTokenFn, fetchImpl: deps.fetchImpl, env: deps.env })
|
|
130
|
+
: async () => { });
|
|
131
|
+
// YAG-500 Fix A+C: the model-path 401 recovery outcome, set by the
|
|
132
|
+
// message_end handler so it can produce the right user-facing message. The
|
|
133
|
+
// after_provider_response event does NOT fire on a 401 (the OpenAI SDK throws
|
|
134
|
+
// before onResponse is reached), so message_end is the only seam.
|
|
135
|
+
let lastAuthRecovery = null;
|
|
136
|
+
const { models: fullCatalog, guardianEnabled: workspaceGuardianEnabled, guardianStorage: guardianStorageTier, } = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
137
|
+
// Lock the interactive session to the `advanced` tier only. The backend
|
|
138
|
+
// catalog returns all tiers, but only `advanced` is registered with the
|
|
139
|
+
// `yagni` provider, so /model and Ctrl+P show a single entry. Child
|
|
140
|
+
// processes (/go, subagents, advisor) fetch their own catalog and register
|
|
141
|
+
// their own provider, so they are unaffected by this filter.
|
|
142
|
+
const catalog = fullCatalog.filter((m) => m.id === "advanced");
|
|
115
143
|
// YAG-471: the driver's own completions carry attribution headers read from
|
|
116
144
|
// this process's env (YAGNI_SESSION_ID minted by the launcher; YAGNI_CALLER
|
|
117
145
|
// defaults to "driver" when unset, i.e. every session that is not a /go
|
|
118
146
|
// child, a subagent, or an advisor consult).
|
|
119
147
|
pi.registerProvider("yagni", buildYagniProvider(catalog, baseUrl, attributionHeaders(deps.env)));
|
|
120
148
|
const toolOpts = { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch };
|
|
121
|
-
|
|
122
|
-
//
|
|
149
|
+
// One flywheel state per session, shared by ask_yagni (caps how many
|
|
150
|
+
// no-position record suggestions reach the model) and record_decision
|
|
151
|
+
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
152
|
+
const flywheelState = makeFlywheelState();
|
|
153
|
+
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
154
|
+
// Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
|
|
155
|
+
// workspace tracker, attributed to the developer via per-user credentials.
|
|
156
|
+
if (!evalMode) {
|
|
157
|
+
pi.registerTool(makeFileTicketTool(toolOpts));
|
|
158
|
+
pi.registerTool(makeUpdateTicketStatusTool(toolOpts));
|
|
159
|
+
}
|
|
160
|
+
// The peak-tier escalation for Advanced sessions (YAG-380). Registered
|
|
123
161
|
// UNCONDITIONALLY and gated at execute time on the live session model: pi's
|
|
124
162
|
// picker can switch the model after activation, so a registration-time tier
|
|
125
|
-
// check would both leave the tool live after switching away from
|
|
126
|
-
// hide it forever from a session that switched TO
|
|
163
|
+
// check would both leave the tool live after switching away from Advanced and
|
|
164
|
+
// hide it forever from a session that switched TO Advanced. One state handle
|
|
127
165
|
// per session, shared with /advise so they draw on the same cap.
|
|
128
166
|
const advisorState = makeAdvisorState();
|
|
129
167
|
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
|
|
@@ -131,12 +169,6 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
131
169
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
132
170
|
// draws on the same cap rather than opening a side channel around it.
|
|
133
171
|
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
172
|
// The differentiated business-grounded tools (loop bricks): review a change
|
|
141
173
|
// for business fit, rank the next work by business priority, and record the
|
|
142
174
|
// engineering rationale back onto the work-item.
|
|
@@ -146,7 +178,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
146
178
|
pi.registerTool(makeRecordEngineeringContextTool(toolOpts));
|
|
147
179
|
// The capture half of the judgment loop: bank a product-intent decision so
|
|
148
180
|
// ask_yagni answers the same question next time instead of interrupting a human.
|
|
149
|
-
pi.registerTool(makeRecordDecisionTool(toolOpts));
|
|
181
|
+
pi.registerTool(makeRecordDecisionTool({ ...toolOpts, flywheel: flywheelState }));
|
|
150
182
|
}
|
|
151
183
|
// The visible checklist for multi-step work: the todo_write tool, its
|
|
152
184
|
// above-editor widget, and /todos. Branch-replayed, so forks and resumes
|
|
@@ -160,7 +192,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
160
192
|
// The general subagent tool: delegate self-contained tasks (optionally in
|
|
161
193
|
// parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
|
|
162
194
|
// riding the /go pipeline's child runner. /agents lists what's available.
|
|
163
|
-
|
|
195
|
+
// Ultra mode (/ultra) is an explicit orchestration dial: it swaps the
|
|
196
|
+
// driver's delegation identity for the diamond directive (see the
|
|
197
|
+
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
198
|
+
const ultraHolder = createUltraHolder();
|
|
199
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
200
|
+
registerUltraCommand(pi, ultraHolder);
|
|
164
201
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
165
202
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
166
203
|
// 5s, not the 10s other boot/grounding fetches use, because the user is
|
|
@@ -212,7 +249,101 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
212
249
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
213
250
|
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
214
251
|
// holds them, review mode confirms them.
|
|
252
|
+
const modeHolder = createModeHolder();
|
|
253
|
+
const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
|
|
254
|
+
const guardianState = makeGuardianState();
|
|
255
|
+
// Disabled by the local env override OR the workspace kill switch
|
|
256
|
+
// (yagni_code.guardian, read from the catalog response at launch). The env
|
|
257
|
+
// var wins for a single developer's debugging; the flag turns it off for
|
|
258
|
+
// every session in the workspace.
|
|
259
|
+
const guardianDisabled = env.YAGNI_DISABLE_GUARDIAN === "1" ||
|
|
260
|
+
env.YAGNI_DISABLE_GUARDIAN === "true" ||
|
|
261
|
+
!workspaceGuardianEnabled;
|
|
262
|
+
const guardianTier = env.YAGNI_GUARDIAN_TIER ?? GUARDIAN_MODEL_TIER;
|
|
263
|
+
const guardianLimits = resolveGuardianLimits(env);
|
|
264
|
+
// YAG-510: guardian.log stays the sanitized local debug sink (hash-only,
|
|
265
|
+
// never the command). The remote guardian-events stream below is the
|
|
266
|
+
// separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
267
|
+
const guardianLogSink = (payload) => {
|
|
268
|
+
try {
|
|
269
|
+
if (process.env.NODE_TEST_CONTEXT)
|
|
270
|
+
return;
|
|
271
|
+
const logPath = join(codeStateHome(null), "logs", "guardian.log");
|
|
272
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
273
|
+
appendFileSync(logPath, JSON.stringify({ ts: new Date().toISOString(), ...payload }) + "\n", "utf8");
|
|
274
|
+
}
|
|
275
|
+
catch { /* logging must never break the session */ }
|
|
276
|
+
};
|
|
277
|
+
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
278
|
+
// at startup (grants added by other concurrent sessions appear next launch —
|
|
279
|
+
// the startup load is the trust boundary; live reload was reviewed and
|
|
280
|
+
// rejected as a same-session self-authorization path, PR #1698).
|
|
281
|
+
const sessionGrants = evalMode ? [] : loadGrants();
|
|
282
|
+
const GUARDIAN_EVENT_TIMEOUT_MS = 5_000;
|
|
215
283
|
registerPermissionGate(pi, {
|
|
284
|
+
modeHolder,
|
|
285
|
+
guardianState,
|
|
286
|
+
guardianLimits,
|
|
287
|
+
guardianTier,
|
|
288
|
+
guardianDisabled,
|
|
289
|
+
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
290
|
+
onGuardianReview: (ev) => guardianLogSink(ev),
|
|
291
|
+
grants: sessionGrants,
|
|
292
|
+
resolveRepoKey,
|
|
293
|
+
persistGrant: (grant) => {
|
|
294
|
+
if (!evalMode)
|
|
295
|
+
appendGrant(grant);
|
|
296
|
+
},
|
|
297
|
+
// Opt-in storage stream (YAG-510). Tier decides what leaves the machine:
|
|
298
|
+
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
299
|
+
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
300
|
+
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
301
|
+
// fail-soft to guardian.log — a storage outage never touches the session.
|
|
302
|
+
onGuardianEvent: guardianStorageTier === "off" || evalMode
|
|
303
|
+
? undefined
|
|
304
|
+
: (ev) => {
|
|
305
|
+
void (async () => {
|
|
306
|
+
try {
|
|
307
|
+
const body = {
|
|
308
|
+
sessionId: env.YAGNI_SESSION_ID ?? null,
|
|
309
|
+
commandHash: createHash("sha256").update(ev.command).digest("hex"),
|
|
310
|
+
commandPrefix: storagePrefix(ev.command),
|
|
311
|
+
outcome: ev.outcome,
|
|
312
|
+
mode: ev.mode,
|
|
313
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
314
|
+
...(ev.riskLevel ? { riskLevel: ev.riskLevel } : {}),
|
|
315
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
316
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
317
|
+
};
|
|
318
|
+
if (guardianStorageTier === "raw") {
|
|
319
|
+
body.command = redactCommand(ev.command);
|
|
320
|
+
if (ev.rationale)
|
|
321
|
+
body.rationale = redactCommand(ev.rationale);
|
|
322
|
+
}
|
|
323
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-events`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: {
|
|
326
|
+
"content-type": "application/json",
|
|
327
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
328
|
+
...attributionHeaders(deps.env),
|
|
329
|
+
},
|
|
330
|
+
body: JSON.stringify(body),
|
|
331
|
+
}, {
|
|
332
|
+
fetchImpl: authedFetch,
|
|
333
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
334
|
+
});
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
guardianLogSink({
|
|
341
|
+
event: "guardian_event_post_failed",
|
|
342
|
+
error: err instanceof Error ? err.message : "unknown",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
})();
|
|
346
|
+
},
|
|
216
347
|
...(mcpMutatingTools.length > 0
|
|
217
348
|
? {
|
|
218
349
|
policy: {
|
|
@@ -224,6 +355,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
224
355
|
...DEFAULT_PERMISSION_POLICY.reviewConfirmTools,
|
|
225
356
|
...mcpMutatingTools,
|
|
226
357
|
],
|
|
358
|
+
alwaysConfirmTools: DEFAULT_PERMISSION_POLICY.alwaysConfirmTools,
|
|
227
359
|
},
|
|
228
360
|
}
|
|
229
361
|
: {}),
|
|
@@ -247,12 +379,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
247
379
|
// turn_end accumulator. Thread the subtotal in explicitly for the local
|
|
248
380
|
// fallback line; the server-authoritative line already counts advisor
|
|
249
381
|
// spend as an ordinary caller row.
|
|
250
|
-
advisorSubtotal: () =>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
isBoosted: () => boostCommand.isBoosted(),
|
|
382
|
+
advisorSubtotal: () => {
|
|
383
|
+
const advisor = formatAdvisorSubtotal(advisorState.read(), DEFAULT_ADVISOR_LIMITS);
|
|
384
|
+
const guardian = formatGuardianSubtotal(guardianState.read(), guardianLimits);
|
|
385
|
+
return [advisor, guardian].filter(Boolean).join(" ");
|
|
386
|
+
},
|
|
256
387
|
fetchHeadroom: async (signal) => {
|
|
257
388
|
try {
|
|
258
389
|
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 } });
|
|
@@ -336,11 +467,27 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
336
467
|
// must never block the agent), then inject it into the system prompt so the
|
|
337
468
|
// very first turn already knows how this company and codebase work.
|
|
338
469
|
const fetchBrief = deps.fetchContextBrief ?? defaultFetchContextBrief;
|
|
470
|
+
// Resolved BEFORE the brief fetch so /context can answer repoDecisionCount
|
|
471
|
+
// in the same round-trip (the mining beat's signal). Best-effort: undefined
|
|
472
|
+
// outside a git repo or without an origin remote.
|
|
473
|
+
const resolveRepoFn = deps.resolveRepoFullName ?? ((cwd) => defaultMineBeatGit.repoFullName(cwd));
|
|
474
|
+
let sessionRepo;
|
|
475
|
+
try {
|
|
476
|
+
sessionRepo = evalMode ? undefined : resolveRepoFn(process.cwd());
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
sessionRepo = undefined;
|
|
480
|
+
}
|
|
339
481
|
let contextBrief;
|
|
340
482
|
let briefResult = null;
|
|
341
483
|
if (!evalMode) {
|
|
342
484
|
try {
|
|
343
|
-
briefResult = await fetchBrief({
|
|
485
|
+
briefResult = await fetchBrief({
|
|
486
|
+
baseUrl,
|
|
487
|
+
getToken: getTokenFn,
|
|
488
|
+
fetchImpl: authedFetch,
|
|
489
|
+
repo: sessionRepo,
|
|
490
|
+
});
|
|
344
491
|
contextBrief = briefResult?.brief?.trim() ? briefResult.brief : undefined;
|
|
345
492
|
}
|
|
346
493
|
catch {
|
|
@@ -401,11 +548,21 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
401
548
|
// assembled prompt passes through byte-exact (no rewrite, no brief
|
|
402
549
|
// injection, no rules section, no delegation identity).
|
|
403
550
|
const noBranding = brandingDisabled(env);
|
|
404
|
-
|
|
551
|
+
// Resolved per turn (not once at activation): /ultra can flip the driver
|
|
552
|
+
// between the delegation and diamond identities mid-session.
|
|
553
|
+
const isDriver = isDriverCaller(env);
|
|
405
554
|
pi.on("before_agent_start", (event) => noBranding
|
|
406
555
|
? undefined
|
|
407
556
|
: {
|
|
408
|
-
systemPrompt:
|
|
557
|
+
systemPrompt: brandSystemPrompt(event.systemPrompt, {
|
|
558
|
+
contextBrief,
|
|
559
|
+
identity: isDriver
|
|
560
|
+
? ultraHolder.get()
|
|
561
|
+
? YAGNI_IDENTITY_ULTRA
|
|
562
|
+
: YAGNI_IDENTITY_DRIVER
|
|
563
|
+
: undefined,
|
|
564
|
+
rulesSection,
|
|
565
|
+
}),
|
|
409
566
|
});
|
|
410
567
|
// Two finalized-message guards share this handler (their conditions are
|
|
411
568
|
// mutually exclusive: YAG-460 takes error-stopped messages, YAG-466 takes
|
|
@@ -425,7 +582,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
425
582
|
// renders. The message text deliberately matches neither pi's overflow nor
|
|
426
583
|
// retryable-error patterns: a deterministic empty response should not burn
|
|
427
584
|
// auto-retries or trigger compaction — the user decides what to do next.
|
|
428
|
-
pi.on("message_end", (event, ctx) => {
|
|
585
|
+
pi.on("message_end", async (event, ctx) => {
|
|
429
586
|
const msg = event.message;
|
|
430
587
|
if (msg.role !== "assistant")
|
|
431
588
|
return;
|
|
@@ -440,6 +597,71 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
440
597
|
// post-replacement message, and dropping the marker would defeat the very
|
|
441
598
|
// recovery this error exists to trigger.
|
|
442
599
|
if (msg.stopReason === "error" && msg.errorMessage) {
|
|
600
|
+
// YAG-500: a 401 from the model proxy means the session token expired
|
|
601
|
+
// (or was revoked). Unlike tool 401s (handled by makeAuthedFetch), the
|
|
602
|
+
// model completion path has no 401-retry seam — pi's retryProviderRequest
|
|
603
|
+
// treats 401 as non-retryable, and after_provider_response never fires
|
|
604
|
+
// (the SDK throws before onResponse is reached). So message_end is the
|
|
605
|
+
// only place to detect it and trigger recovery. The regex matches
|
|
606
|
+
// "yagni login" (the backend's auth-error message) but NOT
|
|
607
|
+
// "request_too_large" (YAG-460's overflow marker).
|
|
608
|
+
const isAuthError = /yagni login/i.test(msg.errorMessage)
|
|
609
|
+
&& !/request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
610
|
+
if (isAuthError) {
|
|
611
|
+
lastAuthRecovery = null;
|
|
612
|
+
let rotated = false;
|
|
613
|
+
try {
|
|
614
|
+
rotated = await tokenProvider.refresh();
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
rotated = false;
|
|
618
|
+
}
|
|
619
|
+
lastAuthRecovery = rotated ? "refreshed" : "failed";
|
|
620
|
+
const explanation = rotated
|
|
621
|
+
? "Your session token expired but was refreshed automatically. Re-send your prompt to continue."
|
|
622
|
+
: "Your session token expired and could not be refreshed. Run `yagni login`, then re-send your prompt. If the issue persists, restart YAGNI Code.";
|
|
623
|
+
if (ctx.hasUI) {
|
|
624
|
+
try {
|
|
625
|
+
ctx.ui.notify(explanation, rotated ? "info" : "error");
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// Surfacing the problem must never break the session itself.
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// YAG-500 Fix E: fire a non-fatal crash report so auth failures are
|
|
632
|
+
// visible in Sentry/backend logs. Reuses the crash endpoint with a
|
|
633
|
+
// distinct context. Fire-and-forget, fail-soft. The crash endpoint is
|
|
634
|
+
// now public (no token required), so the report lands even when the
|
|
635
|
+
// session token is expired and refresh failed — the most critical
|
|
636
|
+
// failure signal is no longer silently dropped.
|
|
637
|
+
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
638
|
+
// YAG-500 Fix F: local diagnostics log under YAGNI_DEBUG.
|
|
639
|
+
if (isDebug(env)) {
|
|
640
|
+
try {
|
|
641
|
+
const logPath = join(codeStateHome(null, env), "logs", "auth-events.log");
|
|
642
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
643
|
+
appendFileSync(logPath, JSON.stringify({
|
|
644
|
+
ts: new Date().toISOString(),
|
|
645
|
+
status: 401,
|
|
646
|
+
refresh: rotated ? "succeeded" : "failed",
|
|
647
|
+
}) + "\n", "utf8");
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// A diagnostic must never break the session.
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { message: { ...msg, errorMessage: explanation } };
|
|
654
|
+
}
|
|
655
|
+
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
656
|
+
// OpenAI-format 413 whose type is `request_too_large`. pi's own overflow
|
|
657
|
+
// detection matches that marker and runs full recovery — compact, then
|
|
658
|
+
// auto-retry the failed turn — so this branch must NOT call ctx.compact()
|
|
659
|
+
// (it would race the built-in recovery and lose the retry). Its only job
|
|
660
|
+
// is UX: replace the raw `413: {"error":{...}}` JSON with a readable
|
|
661
|
+
// message and tell the user what is happening. The rewritten text KEEPS
|
|
662
|
+
// the `request_too_large` marker verbatim: pi's _checkCompaction reads the
|
|
663
|
+
// post-replacement message, and dropping the marker would defeat the very
|
|
664
|
+
// recovery this error exists to trigger.
|
|
443
665
|
const isContextTooLarge = /request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
444
666
|
if (!isContextTooLarge)
|
|
445
667
|
return;
|
|
@@ -486,7 +708,47 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
486
708
|
ctx.ui?.setStatus?.("brand", BRAND_NAME);
|
|
487
709
|
// setHeader replaces the built-in header in place (verified against pi
|
|
488
710
|
// 0.80.2 setExtensionHeader); the factory returns a simple Text component.
|
|
489
|
-
|
|
711
|
+
// Pass the launcher-forwarded CLI version (YAGNI_CODE_VERSION) and the
|
|
712
|
+
// home-collapsed cwd so the masthead shows them like the reference.
|
|
713
|
+
const mastheadVersion = process.env.YAGNI_CODE_VERSION?.trim() || undefined;
|
|
714
|
+
const mastheadCwd = formatCwd(process.cwd(), process.env.HOME);
|
|
715
|
+
ctx.ui?.setHeader?.((_tui, theme) => new Text(buildMastheadString(theme, { version: mastheadVersion, cwd: mastheadCwd })));
|
|
716
|
+
// Replace the built-in footer with the YAGNI status bar: folder +
|
|
717
|
+
// [worktree] + branch on line 1, model + token/cost stats + integer
|
|
718
|
+
// context % on line 2, and extension statuses (brand, todos, mode) on
|
|
719
|
+
// line 3. The factory captures ctx so the footer can read session data
|
|
720
|
+
// (token stats, context usage) that isn't on the footerData provider.
|
|
721
|
+
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle)(tui, theme, footerData));
|
|
722
|
+
ctx.ui?.onTerminalInput?.((data) => {
|
|
723
|
+
if (isShiftTab(data)) {
|
|
724
|
+
modeHolder.set(cyclePermissionMode(modeHolder.get()));
|
|
725
|
+
footerInvalidateHandle.requestRender();
|
|
726
|
+
return { consume: true };
|
|
727
|
+
}
|
|
728
|
+
return undefined;
|
|
729
|
+
});
|
|
730
|
+
// Label the collapsed chain-of-thought line so users know it is reasoning
|
|
731
|
+
// and how to reveal the full trace. Harmless when reasoning is expanded
|
|
732
|
+
// (the label only shows on hidden thinking blocks). Fails closed to pi's
|
|
733
|
+
// default "Thinking..." label.
|
|
734
|
+
try {
|
|
735
|
+
ctx.ui?.setHiddenThinkingLabel?.("Reasoning — Ctrl+T to expand");
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// A label must never disrupt session start.
|
|
739
|
+
}
|
|
740
|
+
// One-time hint naming what changed now that reasoning is collapsed by
|
|
741
|
+
// default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
|
|
742
|
+
// marker so it fires only on the launch that actually seeded the key — it
|
|
743
|
+
// never re-nags on subsequent runs, even though the label above persists.
|
|
744
|
+
if (env.YAGNI_HIDE_THINKING_SEEDED === "1") {
|
|
745
|
+
try {
|
|
746
|
+
ctx.ui?.notify("Reasoning is collapsed by default — Ctrl+T shows the full trace at any time.", "info");
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// A hint must never disrupt session start.
|
|
750
|
+
}
|
|
751
|
+
}
|
|
490
752
|
}
|
|
491
753
|
// Best-effort, once at session start: if the token is at or near expiry, say
|
|
492
754
|
// so via a single notice so a long session does not silently start 401-ing
|
|
@@ -538,14 +800,54 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
538
800
|
// The init pass is best-effort; a failure never blocks the session.
|
|
539
801
|
}
|
|
540
802
|
}
|
|
803
|
+
// Run 7: the opt-in repo seeding beat. Fires only on a genuine startup
|
|
804
|
+
// when /context reported an explicitly-empty repo-scoped ledger, once per
|
|
805
|
+
// repo (marker store). Fully fail-soft — a beat failure never blocks the
|
|
806
|
+
// session.
|
|
807
|
+
if (event.reason === "startup" && !evalMode) {
|
|
808
|
+
try {
|
|
809
|
+
const offerBeat = deps.offerMiningBeat ?? defaultMaybeOfferMiningBeat;
|
|
810
|
+
const markers = deps.mineBeatMarkers ?? fileMineBeatMarkers(codeStateHome(null, env));
|
|
811
|
+
await offerBeat(ctx, {
|
|
812
|
+
repoFullName: sessionRepo,
|
|
813
|
+
repoDecisionCount: briefResult?.repoDecisionCount,
|
|
814
|
+
cwd: process.cwd(),
|
|
815
|
+
baseUrl,
|
|
816
|
+
getToken: getTokenFn,
|
|
817
|
+
fetchImpl: authedFetch,
|
|
818
|
+
markers,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
// Seeding is best-effort; a failure never blocks the session.
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
pi.on("tool_result", (event) => {
|
|
827
|
+
if (event.toolName !== "bash")
|
|
828
|
+
return;
|
|
829
|
+
const command = typeof event.input?.command === "string"
|
|
830
|
+
? event.input.command
|
|
831
|
+
: "";
|
|
832
|
+
if (GIT_MUTATING_PATTERN.test(command)) {
|
|
833
|
+
footerInvalidateHandle.invalidateGit();
|
|
834
|
+
}
|
|
541
835
|
});
|
|
542
836
|
}
|
|
543
837
|
export default async function (pi) {
|
|
544
838
|
await registerYagni(pi);
|
|
839
|
+
// cmux session bridge: report lifecycle state (running/idle), workspace
|
|
840
|
+
// naming, and resume bindings to cmux when running inside it. Complete
|
|
841
|
+
// no-op outside cmux — guarded by CMUX_SURFACE_ID. Registered in the
|
|
842
|
+
// default export (not registerYagni) so tests that call registerYagni
|
|
843
|
+
// directly don't get the bridge's handlers.
|
|
844
|
+
registerCmuxBridge(pi);
|
|
545
845
|
}
|
|
546
846
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
847
|
+
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
547
848
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
548
849
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
850
|
+
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
|
|
549
851
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
550
852
|
export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
551
853
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
@@ -556,7 +858,9 @@ export { recordDecision } from "./recordDecisionTool.js";
|
|
|
556
858
|
export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
557
859
|
// Onramp Door B (F2a): the one-time init-pass idempotency marker.
|
|
558
860
|
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
559
|
-
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
|
|
861
|
+
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
|
|
862
|
+
// Ultra mode (/ultra): the aggressive fan-out/verify/synthesize dial.
|
|
863
|
+
export { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
560
864
|
export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
|
|
561
865
|
export { buildYagniProvider } from "./provider.js";
|
|
562
866
|
export { registerGoCommand } from "./pipeline/goCommand.js";
|
|
@@ -571,12 +875,13 @@ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures
|
|
|
571
875
|
export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
|
|
572
876
|
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
573
877
|
// 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";
|
|
878
|
+
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
575
879
|
// 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";
|
|
880
|
+
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
577
881
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
578
882
|
// the session bless-with-remember capture hook.
|
|
579
|
-
export { decideGate, registerPermissionGate, filterStalePlanContext, DEFAULT_PERMISSION_POLICY, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
883
|
+
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
884
|
+
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
|
|
580
885
|
// W4 judgment loop: the session bless store (tool + path-prefix, session-only).
|
|
581
886
|
export { makeBlessStore, blessPath } from "./bless.js";
|
|
582
887
|
// W4 judgment loop: the decisions surface (/decide + /decisions) + shared bank.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-in repo seeding beat (org plane, Run 7).
|
|
3
|
+
*
|
|
4
|
+
* On the first interactive session in a repo whose repo-scoped decision
|
|
5
|
+
* ledger is empty (`/context?repo=` reports `repoDecisionCount: 0`), YAGNI
|
|
6
|
+
* Code offers — once — to read the repo's own decision record and seed the
|
|
7
|
+
* ledger: "Read this repo's ADRs and docs and seed the decision ledger?".
|
|
8
|
+
* One keystroke, then the backend extracts, dedupes, and banks at most 12
|
|
9
|
+
* `repo_mined` asserted rows, and the beat prints the banked count plus the
|
|
10
|
+
* Library review deep link.
|
|
11
|
+
*
|
|
12
|
+
* Never automatic, never a nag: a decline writes a per-repo marker so the
|
|
13
|
+
* beat does not recur; an accepted pass marks the repo only after the server
|
|
14
|
+
* confirms (a transport failure leaves the offer available next session).
|
|
15
|
+
* The corpus is collected at git HEAD (same echo-chamber guard as
|
|
16
|
+
* repoDocs.ts) — ADR directories, agent-instruction root docs, top-level
|
|
17
|
+
* docs/*.md, and recent merge subjects — bounded client-side to the same
|
|
18
|
+
* limits the backend enforces.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Client-side bounds on the mining corpus — mirror of the backend's
|
|
22
|
+
* MINE_INPUT_LIMITS so nothing is silently truncated server-side.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MINE_BEAT_LIMITS: {
|
|
25
|
+
readonly maxDocs: 40;
|
|
26
|
+
readonly maxPathChars: 300;
|
|
27
|
+
readonly maxExcerptChars: 8000;
|
|
28
|
+
readonly maxCommitSubjects: 200;
|
|
29
|
+
readonly maxSubjectChars: 200;
|
|
30
|
+
};
|
|
31
|
+
/** One document offered to the mining pass. */
|
|
32
|
+
export interface MineBeatDoc {
|
|
33
|
+
path: string;
|
|
34
|
+
excerpt: string;
|
|
35
|
+
}
|
|
36
|
+
/** Injectable git seam (default: execFileSync; every call fail-soft). */
|
|
37
|
+
export interface MineBeatGit {
|
|
38
|
+
/** `owner/name` from the origin remote, or undefined outside a repo. */
|
|
39
|
+
repoFullName: (cwd: string) => string | undefined;
|
|
40
|
+
/** Tracked paths at HEAD, or null when cwd is not a usable git tree. */
|
|
41
|
+
listAtHead: (cwd: string) => string[] | null;
|
|
42
|
+
/** One file's HEAD content, or null when unreadable. */
|
|
43
|
+
readAtHead: (cwd: string, rel: string) => string | null;
|
|
44
|
+
/** Recent merge-commit subjects (fallback: plain subjects on squash repos). */
|
|
45
|
+
mergeSubjects: (cwd: string, max: number) => string[];
|
|
46
|
+
}
|
|
47
|
+
/** Parse `owner/name` from an origin remote URL (ssh or https). */
|
|
48
|
+
export declare function parseRepoFullName(remote: string | null | undefined): string | undefined;
|
|
49
|
+
export declare const defaultMineBeatGit: MineBeatGit;
|
|
50
|
+
/** Is this HEAD-tracked path part of the repo's decision record? */
|
|
51
|
+
export declare function isMineCandidate(rel: string): boolean;
|
|
52
|
+
/** Collect the HEAD-era mining corpus, bounded to MINE_BEAT_LIMITS. */
|
|
53
|
+
export declare function collectMineCorpus(cwd: string, gitImpl?: MineBeatGit): {
|
|
54
|
+
docs: MineBeatDoc[];
|
|
55
|
+
commitSubjects: string[];
|
|
56
|
+
};
|
|
57
|
+
/** Injectable marker-store seam (default: a JSON file per repo on disk). */
|
|
58
|
+
export interface MineBeatMarkers {
|
|
59
|
+
has: (repoFullName: string) => boolean;
|
|
60
|
+
write: (repoFullName: string, status: "declined" | "seeded") => void;
|
|
61
|
+
}
|
|
62
|
+
/** Filesystem path of one repo's beat marker under the state home. */
|
|
63
|
+
export declare function mineMarkerPath(stateHome: string, repoFullName: string): string;
|
|
64
|
+
export declare function fileMineBeatMarkers(stateHome: string): MineBeatMarkers;
|
|
65
|
+
/** The subset of the session-start ctx the beat needs. */
|
|
66
|
+
export interface MineBeatUi {
|
|
67
|
+
hasUI: boolean;
|
|
68
|
+
ui: {
|
|
69
|
+
confirm?: (title: string, message: string) => Promise<boolean>;
|
|
70
|
+
notify: (message: string, level: "info" | "error") => void;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface OfferMiningBeatOptions {
|
|
74
|
+
repoFullName: string | undefined;
|
|
75
|
+
/** From `/context?repo=`; the beat fires ONLY on an explicit 0. */
|
|
76
|
+
repoDecisionCount: number | undefined;
|
|
77
|
+
cwd: string;
|
|
78
|
+
baseUrl: string;
|
|
79
|
+
getToken: () => string | undefined;
|
|
80
|
+
fetchImpl?: typeof fetch;
|
|
81
|
+
markers: MineBeatMarkers;
|
|
82
|
+
gitImpl?: MineBeatGit;
|
|
83
|
+
collect?: typeof collectMineCorpus;
|
|
84
|
+
}
|
|
85
|
+
export interface MiningBeatOutcome {
|
|
86
|
+
offered: boolean;
|
|
87
|
+
accepted?: boolean;
|
|
88
|
+
banked?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Offer (once per repo) and, on yes, run the seeding pass. Fail-soft
|
|
92
|
+
* throughout: any error surfaces as a notify at most, never a broken session.
|
|
93
|
+
*/
|
|
94
|
+
export declare function maybeOfferMiningBeat(ctx: MineBeatUi, opts: OfferMiningBeatOptions): Promise<MiningBeatOutcome>;
|
|
95
|
+
//# sourceMappingURL=mineBeat.d.ts.map
|