@yagni-app/code 0.3.5 → 1.0.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.
- package/dist/extension/askAdvisorTool.js +2 -0
- package/dist/extension/askUserQuestionTool.d.ts +54 -0
- package/dist/extension/askUserQuestionTool.js +621 -0
- package/dist/extension/cmux/state.js +9 -16
- package/dist/extension/crashReport.js +12 -0
- package/dist/extension/decisionCapture.js +3 -0
- package/dist/extension/decisions.js +4 -0
- package/dist/extension/diagnostics.d.ts +31 -0
- package/dist/extension/diagnostics.js +53 -55
- package/dist/extension/errorSink.d.ts +64 -0
- package/dist/extension/errorSink.js +180 -0
- package/dist/extension/feedbackCommand.d.ts +38 -0
- package/dist/extension/feedbackCommand.js +151 -0
- package/dist/extension/hooks.js +12 -12
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +91 -40
- package/dist/extension/mineBeat.js +13 -0
- package/dist/extension/pipeline/goCommand.js +2 -0
- package/dist/extension/pipeline/runner.js +9 -0
- package/dist/extension/silentTurnReminder.js +18 -14
- package/dist/extension/turnLog.js +17 -46
- package/dist/extension/webFetch.d.ts +85 -0
- package/dist/extension/webFetch.js +192 -0
- package/dist/extension/webFetchTool.d.ts +34 -0
- package/dist/extension/webFetchTool.js +104 -0
- package/package.json +3 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/feedback` (alias `/bug`) command + the `/diagnostics` companion
|
|
3
|
+
* (YAG-580).
|
|
4
|
+
*
|
|
5
|
+
* `/feedback` captures the session transcript (pi's own append-only JSONL, read
|
|
6
|
+
* via `ctx.sessionManager` — never reconstructed from YAGNI_SESSION_ID, which is
|
|
7
|
+
* the proxy-attribution id), the session-scoped error trail (from the unified
|
|
8
|
+
* sink), any child `/go` run transcripts, a sanitized `yagni doctor` report, and
|
|
9
|
+
* git metadata — sanitizes the whole bundle with the shared scrub contract, and
|
|
10
|
+
* POSTs it to the YAGNI backend (opt-in, gated, named-human).
|
|
11
|
+
*
|
|
12
|
+
* `/diagnostics` is the read-only companion: it prints the last N sink lines for
|
|
13
|
+
* THIS session so the user can see what failed before deciding to attach it.
|
|
14
|
+
*
|
|
15
|
+
* Both treat the transcript as best-effort enrichment: pi's flush-to-file timing
|
|
16
|
+
* at command-invoke is not guaranteed, so a report never claims the reporting
|
|
17
|
+
* turn is captured unless it verifiably is.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
21
|
+
import { readSessionTrail } from "./errorSink.js";
|
|
22
|
+
const MAX_DESCRIPTION = 512;
|
|
23
|
+
const MAX_TRANSCRIPT_READ_BYTES = 512 * 1024;
|
|
24
|
+
const notify = (ctx, message, type) => {
|
|
25
|
+
if (ctx.hasUI)
|
|
26
|
+
ctx.ui.notify(message, type);
|
|
27
|
+
};
|
|
28
|
+
function redact(text) {
|
|
29
|
+
return scrubSecrets(text);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the durable transcript, clamped by byte size. Returns empty on any
|
|
33
|
+
* failure or when too large (mirrors Claude's MAX_TRANSCRIPT_READ_BYTES guard).
|
|
34
|
+
*/
|
|
35
|
+
function readTranscript(sessionFile) {
|
|
36
|
+
if (!sessionFile)
|
|
37
|
+
return "";
|
|
38
|
+
try {
|
|
39
|
+
const data = readFileSync(sessionFile, "utf8");
|
|
40
|
+
if (Buffer.byteLength(data, "utf8") > MAX_TRANSCRIPT_READ_BYTES)
|
|
41
|
+
return "";
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function handleFeedback(args, ctx, deps) {
|
|
49
|
+
const sessionId = ctx.sessionManager.getSessionId?.() ?? deps.env?.YAGNI_SESSION_ID ?? "";
|
|
50
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
51
|
+
const cwd = ctx.cwd;
|
|
52
|
+
const description = args.trim()
|
|
53
|
+
? args.trim()
|
|
54
|
+
: await ctx.ui.input("Describe the issue", "What went wrong, in one or two lines?");
|
|
55
|
+
if (!description || description.trim().length === 0) {
|
|
56
|
+
notify(ctx, "Feedback cancelled.", "info");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!ctx.isIdle()) {
|
|
60
|
+
notify(ctx, "YAGNI Code is busy; wait for the current turn to finish before /feedback.", "warning");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const transcript = readTranscript(sessionFile);
|
|
64
|
+
const trail = readSessionTrail(sessionId);
|
|
65
|
+
const doctorReport = deps.getDoctorReport
|
|
66
|
+
? await deps.getDoctorReport().catch(() => undefined)
|
|
67
|
+
: undefined;
|
|
68
|
+
const git = deps.getGitState
|
|
69
|
+
? await deps.getGitState(cwd).catch(() => ({}))
|
|
70
|
+
: {};
|
|
71
|
+
const childTranscripts = deps.getChildTranscripts
|
|
72
|
+
? await deps.getChildTranscripts(cwd, sessionFile).catch(() => ({}))
|
|
73
|
+
: {};
|
|
74
|
+
// Consent: enumerate exactly what is about to leave the machine.
|
|
75
|
+
const lines = [
|
|
76
|
+
"Your feedback description",
|
|
77
|
+
`This session's transcript${transcript ? "" : " (could not be read — possibly one turn stale)"}`,
|
|
78
|
+
`${Object.keys(childTranscripts).length} child run transcript(s)`,
|
|
79
|
+
"Recent error trail for this session",
|
|
80
|
+
...(doctorReport ? ["Sanitized yagni doctor report"] : []),
|
|
81
|
+
...(git.branch ? ["Git metadata (branch/commit/remote/dirty)"] : []),
|
|
82
|
+
];
|
|
83
|
+
const ok = await ctx.ui.confirm("Submit feedback?", lines.join("\n - ") + "\n\nSend this report?");
|
|
84
|
+
if (!ok) {
|
|
85
|
+
notify(ctx, "Feedback cancelled.", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const payload = {
|
|
89
|
+
client: "cli",
|
|
90
|
+
clientVersion: deps.env?.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
91
|
+
platform: `${process.platform} ${process.arch}`,
|
|
92
|
+
description: redact(description).slice(0, MAX_DESCRIPTION),
|
|
93
|
+
sessionId,
|
|
94
|
+
...(transcript ? { transcriptJsonl: redact(transcript) } : {}),
|
|
95
|
+
...(trail ? { errorTrailJsonl: redact(trail) } : {}),
|
|
96
|
+
...(Object.keys(childTranscripts).length > 0
|
|
97
|
+
? { childTranscripts: Object.fromEntries(Object.entries(childTranscripts).map(([k, v]) => [k, redact(v)])) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(doctorReport ? { doctorReport: redact(doctorReport) } : {}),
|
|
100
|
+
...(git.branch ? { gitBranch: git.branch } : {}),
|
|
101
|
+
...(git.commit ? { gitCommit: git.commit } : {}),
|
|
102
|
+
...(git.remote ? { gitRemote: git.remote } : {}),
|
|
103
|
+
...(git.dirty !== undefined ? { gitDirty: git.dirty } : {}),
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
107
|
+
const res = await fetchImpl(`${deps.baseUrl.replace(/\/$/, "")}/api/yagni-code/feedback`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
authorization: `Bearer ${deps.getToken() ?? ""}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(payload),
|
|
114
|
+
signal: AbortSignal.timeout(30_000),
|
|
115
|
+
});
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
notify(ctx, "Feedback submitted. Thank you!", "info");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
notify(ctx, "Could not submit feedback. Please try again.", "error");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
notify(ctx, "Could not submit feedback (network error). Please try again.", "error");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function registerFeedbackCommands(pi, deps) {
|
|
128
|
+
pi.registerCommand("feedback", {
|
|
129
|
+
description: "File a bug report with your session transcript + error trail attached.",
|
|
130
|
+
handler: (args, ctx) => handleFeedback(args, ctx, deps),
|
|
131
|
+
});
|
|
132
|
+
pi.registerCommand("bug", {
|
|
133
|
+
description: "Alias for /feedback.",
|
|
134
|
+
handler: (args, ctx) => handleFeedback(args, ctx, deps),
|
|
135
|
+
});
|
|
136
|
+
pi.registerCommand("diagnostics", {
|
|
137
|
+
description: "Show recent error-trail lines for this session.",
|
|
138
|
+
handler: async (_args, ctx) => {
|
|
139
|
+
const sessionId = ctx.sessionManager.getSessionId?.() ?? deps.env?.YAGNI_SESSION_ID ?? "";
|
|
140
|
+
const trail = readSessionTrail(sessionId, 16 * 1024);
|
|
141
|
+
if (!trail) {
|
|
142
|
+
notify(ctx, "No recent diagnostics for this session.", "info");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const lines = trail.split("\n").filter(Boolean);
|
|
146
|
+
const tail = lines.slice(-20);
|
|
147
|
+
notify(ctx, `Recent diagnostics (${lines.length} events):\n${tail.join("\n")}`, "info");
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=feedbackCommand.js.map
|
package/dist/extension/hooks.js
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* Fail-soft: a broken hook degrades to "no hook," never to "broken session."
|
|
18
18
|
*/
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { codeStateHome } from "./stateHome.js";
|
|
24
24
|
import { isDebug } from "./diagnostics.js";
|
|
25
|
+
import { logEvent } from "./errorSink.js";
|
|
25
26
|
const SUPPORTED_EVENTS = [
|
|
26
27
|
"SessionStart",
|
|
27
28
|
"UserPromptSubmit",
|
|
@@ -286,16 +287,15 @@ export function parseCompactCancel(stdout) {
|
|
|
286
287
|
function logHookEvent(env, payload) {
|
|
287
288
|
if (!isDebug(env))
|
|
288
289
|
return;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
290
|
+
const event = typeof payload.event === "string" ? payload.event : "hook_event";
|
|
291
|
+
const { event: _ignored, ...fields } = payload;
|
|
292
|
+
logEvent({
|
|
293
|
+
source: "hooks",
|
|
294
|
+
level: "debug",
|
|
295
|
+
event,
|
|
296
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
297
|
+
fields,
|
|
298
|
+
});
|
|
299
299
|
}
|
|
300
300
|
/** Filter hook groups by workspace trust: project-level groups are skipped when untrusted. */
|
|
301
301
|
function filterByTrust(groups, isTrusted) {
|
|
@@ -120,6 +120,7 @@ export default function (pi: ExtensionAPI): Promise<void>;
|
|
|
120
120
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
121
121
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
122
122
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
123
|
+
export { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
123
124
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
124
125
|
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
|
|
125
126
|
export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./permission/guardian.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
3
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
4
|
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
|
|
7
5
|
import { redactCommand } from "./redact.js";
|
|
8
6
|
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs } from "./permission/guardian.js";
|
|
9
7
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
8
|
+
import { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
10
9
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
10
|
+
import { makeWebFetchTool } from "./webFetchTool.js";
|
|
11
11
|
import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
12
12
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
13
13
|
import { registerCmuxBridge } from "./cmux/index.js";
|
|
@@ -18,6 +18,8 @@ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, Y
|
|
|
18
18
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
19
19
|
import { registerCostCommand } from "./costHud.js";
|
|
20
20
|
import { isDebug } from "./diagnostics.js";
|
|
21
|
+
import { logEvent } from "./errorSink.js";
|
|
22
|
+
import { registerFeedbackCommands } from "./feedbackCommand.js";
|
|
21
23
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
22
24
|
import { codeStateHome } from "./stateHome.js";
|
|
23
25
|
import { logTurnLifecycle } from "./turnLog.js";
|
|
@@ -159,6 +161,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
159
161
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
160
162
|
const flywheelState = makeFlywheelState();
|
|
161
163
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
164
|
+
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
165
|
+
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
166
|
+
pi.registerTool(makeWebFetchTool(toolOpts));
|
|
162
167
|
// Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
|
|
163
168
|
// workspace tracker, attributed to the developer via per-user credentials.
|
|
164
169
|
if (!evalMode) {
|
|
@@ -177,6 +182,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
177
182
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
178
183
|
// draws on the same cap rather than opening a side channel around it.
|
|
179
184
|
registerAdviseCommand(pi, askAdvisorTool);
|
|
185
|
+
// Structured human questions: the model poses a closed 2-4-option question
|
|
186
|
+
// and gets a clean machine-readable answer via ctx.ui.custom.
|
|
187
|
+
pi.registerTool(makeAskUserQuestionTool());
|
|
180
188
|
// The differentiated business-grounded tools (loop bricks): review a change
|
|
181
189
|
// for business fit, rank the next work by business priority, and record the
|
|
182
190
|
// engineering rationale back onto the work-item.
|
|
@@ -244,6 +252,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
244
252
|
// M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
|
|
245
253
|
// the business-fit delta. Never wired to routing.
|
|
246
254
|
registerGoCompareCommand(pi);
|
|
255
|
+
// YAG-580: /feedback (alias /bug) + /diagnostics. The capture/upload is the
|
|
256
|
+
// whole point, so it is gated to non-eval mode like every external side
|
|
257
|
+
// effect; the command's deps (doctor report, git state, child transcripts)
|
|
258
|
+
// are injected so the handler stays unit-testable without spawning git or
|
|
259
|
+
// reading the real session dir.
|
|
260
|
+
if (!evalMode) {
|
|
261
|
+
registerFeedbackCommands(pi, {
|
|
262
|
+
baseUrl,
|
|
263
|
+
getToken: getTokenFn,
|
|
264
|
+
fetchImpl: deps.fetchImpl,
|
|
265
|
+
env: deps.env,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
247
268
|
// W4 judgment loop: the decisions surface (/decide + /decisions) and the
|
|
248
269
|
// bless-with-remember capture are the same product-intent write as the record
|
|
249
270
|
// tools, so both are gated together (skipped in eval mode).
|
|
@@ -288,18 +309,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
288
309
|
...(guardianMaxAttemptsAdvertised !== undefined ? { maxAttempts: guardianMaxAttemptsAdvertised } : {}),
|
|
289
310
|
});
|
|
290
311
|
guardianLimits.timeoutMs = guardianTimeoutMs;
|
|
291
|
-
// YAG-510:
|
|
292
|
-
// never the command). The remote guardian-events stream below is
|
|
293
|
-
// separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
312
|
+
// YAG-510: Guardian events go to the unified sink under source:"guardian"
|
|
313
|
+
// (hash-only, never the command). The remote guardian-events stream below is
|
|
314
|
+
// the separate, opt-in, per-workspace analytics sink; the two are independent.
|
|
294
315
|
const guardianLogSink = (payload) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
const event = typeof payload.event === "string" ? payload.event : "guardian_event";
|
|
317
|
+
const { event: _ignored, ...fields } = payload;
|
|
318
|
+
logEvent({
|
|
319
|
+
source: "guardian",
|
|
320
|
+
level: event === "guardian_event_post_failed" ? "error" : "info",
|
|
321
|
+
event,
|
|
322
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
323
|
+
fields,
|
|
324
|
+
});
|
|
303
325
|
};
|
|
304
326
|
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
305
327
|
// at startup (grants added by other concurrent sessions appear next launch —
|
|
@@ -331,7 +353,8 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
331
353
|
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
332
354
|
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
333
355
|
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
334
|
-
// fail-soft to guardian
|
|
356
|
+
// fail-soft to the unified sink (source:"guardian") — a storage outage
|
|
357
|
+
// never touches the session.
|
|
335
358
|
onGuardianEvent: guardianStorageTier === "off" || evalMode
|
|
336
359
|
? undefined
|
|
337
360
|
: (ev) => {
|
|
@@ -369,10 +392,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
369
392
|
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
370
393
|
}
|
|
371
394
|
}
|
|
372
|
-
catch
|
|
395
|
+
catch {
|
|
373
396
|
guardianLogSink({
|
|
374
397
|
event: "guardian_event_post_failed",
|
|
375
|
-
|
|
398
|
+
kind: "network",
|
|
376
399
|
});
|
|
377
400
|
}
|
|
378
401
|
})();
|
|
@@ -454,15 +477,13 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
454
477
|
onDivergence: (driverServerUsd, localUsd) => {
|
|
455
478
|
if (!isDebug(env))
|
|
456
479
|
return;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
/* a diagnostic must never break /cost */
|
|
465
|
-
}
|
|
480
|
+
logEvent({
|
|
481
|
+
source: "cost",
|
|
482
|
+
level: "debug",
|
|
483
|
+
event: "cost_divergence",
|
|
484
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
485
|
+
fields: { driverServerUsd, localUsd },
|
|
486
|
+
});
|
|
466
487
|
},
|
|
467
488
|
// Carry-over (/cost re-review): surfaces sessionRuns.ts's dropped-run-id
|
|
468
489
|
// count as costHud's "Excludes N earlier /go runs." note.
|
|
@@ -691,21 +712,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
691
712
|
// session token is expired and refresh failed — the most critical
|
|
692
713
|
// failure signal is no longer silently dropped.
|
|
693
714
|
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
694
|
-
// YAG-500 Fix F:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
catch {
|
|
706
|
-
// A diagnostic must never break the session.
|
|
707
|
-
}
|
|
708
|
-
}
|
|
715
|
+
// YAG-500 Fix F: auth-401 signal is content-free (status + refresh
|
|
716
|
+
// boolean), so it is always-on and upload-safe by the default-on
|
|
717
|
+
// invariant. No message content, no tokens, no headers.
|
|
718
|
+
logEvent({
|
|
719
|
+
source: "auth",
|
|
720
|
+
level: "info",
|
|
721
|
+
event: "auth_401",
|
|
722
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
723
|
+
fields: { status: 401, refresh: rotated ? "succeeded" : "failed" },
|
|
724
|
+
});
|
|
709
725
|
return { message: { ...msg, errorMessage: explanation } };
|
|
710
726
|
}
|
|
711
727
|
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
@@ -759,6 +775,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
759
775
|
// and a "Pi can explain its own features…" line) with a YAGNI Code masthead,
|
|
760
776
|
// set the terminal title, and add a footer brand mark. TUI only.
|
|
761
777
|
pi.on("session_start", async (event, ctx) => {
|
|
778
|
+
// Surface Pi's native find/grep/ls on the driver. These are Pi's
|
|
779
|
+
// equivalents of Claude Code's Glob/Grep/LS; the /go stages and subagents
|
|
780
|
+
// already pass them explicitly, but the driver defaults to read/bash/edit/write
|
|
781
|
+
// and otherwise reaches for `bash` find/grep/ls.
|
|
782
|
+
try {
|
|
783
|
+
pi.setActiveTools([...pi.getActiveTools(), "grep", "find", "ls"]);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Tool enrichment must never break session start.
|
|
787
|
+
}
|
|
762
788
|
ctx.ui?.setTitle(BRAND_NAME);
|
|
763
789
|
if (ctx.mode === "tui") {
|
|
764
790
|
ctx.ui?.setStatus?.("brand", BRAND_NAME);
|
|
@@ -889,6 +915,30 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
889
915
|
footerInvalidateHandle.invalidateGit();
|
|
890
916
|
}
|
|
891
917
|
});
|
|
918
|
+
// Seed the unified error trail from tool-exec failures. A tool's SUCCESS
|
|
919
|
+
// is content (it lives in the transcript); its FAILURE is an error and belongs
|
|
920
|
+
// in the sink. We log only the tool name + error class — never args, partial
|
|
921
|
+
// results, or result bodies (those are content and stay out of the upload-safe
|
|
922
|
+
// tier). This is the tool-failure half of the error trail the ticket asks for.
|
|
923
|
+
pi.on("tool_execution_end", (event) => {
|
|
924
|
+
if (!event.isError)
|
|
925
|
+
return;
|
|
926
|
+
// `event.result` is any; its `.error`/message can echo a path or secret
|
|
927
|
+
// (a tool's own failure text). Keep the always-on trail content-free: use
|
|
928
|
+
// ONLY the Error subclass name, never the message or a String() of the
|
|
929
|
+
// result's error payload.
|
|
930
|
+
const errorClass = event.result instanceof Error
|
|
931
|
+
? event.result.name || "Error"
|
|
932
|
+
: "tool_error";
|
|
933
|
+
logEvent({
|
|
934
|
+
source: "tool",
|
|
935
|
+
level: "error",
|
|
936
|
+
event: "tool_failed",
|
|
937
|
+
sessionId: sessionIdForLog(),
|
|
938
|
+
flush: "sync",
|
|
939
|
+
fields: { toolName: event.toolName, errorClass },
|
|
940
|
+
});
|
|
941
|
+
});
|
|
892
942
|
}
|
|
893
943
|
export default async function (pi) {
|
|
894
944
|
await registerYagni(pi);
|
|
@@ -902,6 +952,7 @@ export default async function (pi) {
|
|
|
902
952
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
903
953
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
904
954
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
955
|
+
export { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
905
956
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
906
957
|
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
|
|
907
958
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
21
21
|
import * as fs from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
+
import { logEvent } from "./errorSink.js";
|
|
23
24
|
import { METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
24
25
|
/**
|
|
25
26
|
* Client-side bounds on the mining corpus — mirror of the backend's
|
|
@@ -173,6 +174,12 @@ export async function maybeOfferMiningBeat(ctx, opts) {
|
|
|
173
174
|
}, { fetchImpl: opts.fetchImpl, policy: METERED_POST_FETCH_POLICY });
|
|
174
175
|
if (!res.ok) {
|
|
175
176
|
// No marker: the offer stays available next session.
|
|
177
|
+
logEvent({
|
|
178
|
+
source: "mine-beat",
|
|
179
|
+
level: "error",
|
|
180
|
+
event: "mine_failed",
|
|
181
|
+
fields: { status: res.status },
|
|
182
|
+
});
|
|
176
183
|
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
177
184
|
return { offered: true, accepted: true };
|
|
178
185
|
}
|
|
@@ -186,6 +193,12 @@ export async function maybeOfferMiningBeat(ctx, opts) {
|
|
|
186
193
|
return { offered: true, accepted: true, banked };
|
|
187
194
|
}
|
|
188
195
|
catch {
|
|
196
|
+
logEvent({
|
|
197
|
+
source: "mine-beat",
|
|
198
|
+
level: "error",
|
|
199
|
+
event: "mine_failed",
|
|
200
|
+
fields: { kind: "network" },
|
|
201
|
+
});
|
|
189
202
|
ctx.ui.notify("Seeding the decision ledger failed; YAGNI Code will offer again next session.", "error");
|
|
190
203
|
return { offered: true, accepted: true };
|
|
191
204
|
}
|
|
@@ -71,6 +71,7 @@ import { formatRunCostTable } from "./runCostTable.js";
|
|
|
71
71
|
import { makeCombinedCheckpointStore, makeFileCheckpointStore, makePiJournalCheckpointStore, } from "./checkpoint.js";
|
|
72
72
|
import { getToken as defaultGetToken, resolveBaseUrl } from "../config.js";
|
|
73
73
|
import { makeCrashReporter } from "../crashReport.js";
|
|
74
|
+
import { logEvent } from "../errorSink.js";
|
|
74
75
|
import { scrubSecrets } from "./scrubSecrets.js";
|
|
75
76
|
import { isDesktopSurface } from "../surface.js";
|
|
76
77
|
import { runFinish as defaultRunFinish, verifyTrailerValue, } from "./finish.js";
|
|
@@ -1104,6 +1105,7 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
1104
1105
|
// return results instead) — report it, fire-and-forget. The default
|
|
1105
1106
|
// reporter never rejects; the catch guards an injected one.
|
|
1106
1107
|
void reportCrash(err, "go", runCwd).catch(() => { });
|
|
1108
|
+
logEvent({ source: "go", level: "error", event: "go_failed", fields: { runId } });
|
|
1107
1109
|
// The stopReason travels to the backend run record; scrub it like
|
|
1108
1110
|
// every other captured text (a raw error can echo a connection
|
|
1109
1111
|
// string or key).
|
|
@@ -25,6 +25,7 @@ import * as path from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { trackChild } from "./childRegistry.js";
|
|
27
27
|
import { finalOutputFrom, foldEvent, newEventAccumulator } from "./events.js";
|
|
28
|
+
import { logEvent } from "../errorSink.js";
|
|
28
29
|
import { buildStageInvocation, groundedChildArgv } from "./invocation.js";
|
|
29
30
|
import { personaBody } from "./personas.js";
|
|
30
31
|
import { clampTier, resolveTierCap } from "./tierCap.js";
|
|
@@ -311,6 +312,14 @@ export async function runStage(stage, ctx, deps) {
|
|
|
311
312
|
final_output: finalOut,
|
|
312
313
|
}, null, 2));
|
|
313
314
|
dbgLog("stage_end");
|
|
315
|
+
if (overlongLinesDropped > 0) {
|
|
316
|
+
logEvent({
|
|
317
|
+
source: "pipeline",
|
|
318
|
+
level: "debug",
|
|
319
|
+
event: "overlong_lines_dropped",
|
|
320
|
+
fields: { stage: stage.id, dropped: overlongLinesDropped },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
314
323
|
}
|
|
315
324
|
catch { /* ignore */ }
|
|
316
325
|
}
|
|
@@ -177,23 +177,23 @@ export function registerSilentTurnReminder(pi, deps = {}) {
|
|
|
177
177
|
// Never break input handling.
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
|
-
// The nudge
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
180
|
+
// The nudge rides the `context` event, which pi fires before each provider
|
|
181
|
+
// request (transformContext) on a throwaway copy of the message array. That
|
|
182
|
+
// makes it reach the model as the most recent message — and, unlike the old
|
|
183
|
+
// `tool_result` seam, it is never rendered to the user and never persisted
|
|
184
|
+
// to the session file. Appending (not replacing) preserves prior `context`
|
|
185
|
+
// handlers (e.g. gate.ts's mode-context filter), and fail-soft means a
|
|
186
|
+
// reminder must never break a turn.
|
|
187
|
+
pi.on("context", async (event) => {
|
|
184
188
|
try {
|
|
185
|
-
|
|
186
|
-
return;
|
|
187
|
-
if (event.toolName === "todo_write" || event.toolName === "file_ticket" || event.toolName === "update_ticket_status") {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
189
|
+
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
190
190
|
if (!shouldRemind({
|
|
191
191
|
msSinceSpoke: now() - lastSpokeAt,
|
|
192
192
|
turnsSinceSpoke,
|
|
193
193
|
msSinceLastNudge: lastNudgeAt === null ? null : now() - lastNudgeAt,
|
|
194
194
|
intervalMs,
|
|
195
195
|
})) {
|
|
196
|
-
return;
|
|
196
|
+
return undefined;
|
|
197
197
|
}
|
|
198
198
|
nudgeCount += 1;
|
|
199
199
|
lastNudgeAt = now();
|
|
@@ -203,14 +203,18 @@ export function registerSilentTurnReminder(pi, deps = {}) {
|
|
|
203
203
|
secondsSinceSpoke: Math.round((now() - lastSpokeAt) / 1000),
|
|
204
204
|
});
|
|
205
205
|
return {
|
|
206
|
-
|
|
207
|
-
...
|
|
208
|
-
{
|
|
206
|
+
messages: [
|
|
207
|
+
...messages,
|
|
208
|
+
{
|
|
209
|
+
role: "user",
|
|
210
|
+
content: [{ type: "text", text: formatSilentReminder() }],
|
|
211
|
+
timestamp: now(),
|
|
212
|
+
},
|
|
209
213
|
],
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
216
|
catch {
|
|
213
|
-
return;
|
|
217
|
+
return undefined;
|
|
214
218
|
}
|
|
215
219
|
});
|
|
216
220
|
}
|