@yagni-app/code-staging 1.0.0-staging.1174.1 → 1.0.0-staging.1177.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.js +53 -4
- package/dist/extension/askAdvisorTool.js +2 -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 +6 -3
- package/dist/extension/diagnostics.js +47 -85
- 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.js +72 -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/turnLog.js +17 -46
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { login } from "./login.js";
|
|
|
27
27
|
import { logout } from "./logout.js";
|
|
28
28
|
import { tokenCommand } from "./token.js";
|
|
29
29
|
import { buildLaunch } from "./launch.js";
|
|
30
|
+
import { parseOutputFormat, parseJsonEvents, buildResultObject, readGuardianEvents, } from "./outputFormat.js";
|
|
30
31
|
import { runDoctor } from "./doctor.js";
|
|
31
32
|
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
32
33
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
@@ -159,6 +160,10 @@ export function seedHideThinkingBlock(piAgentDir) {
|
|
|
159
160
|
return seedSetting(piAgentDir, "hideThinkingBlock", true);
|
|
160
161
|
}
|
|
161
162
|
async function runDefault(passthroughArgs) {
|
|
163
|
+
// Parse --output-format out of argv before passing to pi (pi doesn't know
|
|
164
|
+
// about it). The format determines how we handle pi's stdout: text = inherit,
|
|
165
|
+
// stream-json = inherit with --mode json, json = pipe + post-process.
|
|
166
|
+
const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
|
|
162
167
|
// Cache-backed update nudge (never a network wait), then a background cache
|
|
163
168
|
// refresh that completes while the session runs. Both fail soft.
|
|
164
169
|
await maybeNudgeAndRefresh({ current: cliVersion() });
|
|
@@ -237,7 +242,7 @@ async function runDefault(passthroughArgs) {
|
|
|
237
242
|
// re-login in another terminal is picked up here with no stale cached token.
|
|
238
243
|
let plan;
|
|
239
244
|
try {
|
|
240
|
-
plan = buildLaunch(creds,
|
|
245
|
+
plan = buildLaunch(creds, remainingArgs, {
|
|
241
246
|
extensionPath: resolveExtensionPath(),
|
|
242
247
|
agentDir: piAgentDir,
|
|
243
248
|
piPackageDir: shadowPiDir,
|
|
@@ -260,18 +265,57 @@ async function runDefault(passthroughArgs) {
|
|
|
260
265
|
process.stderr.write(`${warning}\n`);
|
|
261
266
|
}
|
|
262
267
|
const { env, argv } = plan;
|
|
268
|
+
// For json/stream-json output, inject --mode json so pi emits NDJSON events.
|
|
269
|
+
// Don't override if the user already chose --mode (mirrors userChoseProvider/
|
|
270
|
+
// userChoseModel in buildLaunch). Appended at the end — pi's flag parser
|
|
271
|
+
// handles --mode anywhere in argv.
|
|
272
|
+
const userChoseMode = remainingArgs.some((a) => a === "--mode" || a.startsWith("--mode="));
|
|
273
|
+
const childArgv = outputFormat === "text" || userChoseMode
|
|
274
|
+
? argv
|
|
275
|
+
: [...argv, "--mode", "json"];
|
|
276
|
+
// text + stream-json: inherit stdout (passthrough). json: pipe stdout so we
|
|
277
|
+
// can post-process the NDJSON into a single result object.
|
|
278
|
+
const stdio = outputFormat === "json"
|
|
279
|
+
? ["inherit", "pipe", "inherit"]
|
|
280
|
+
: "inherit";
|
|
263
281
|
const piCli = resolvePiCliPath();
|
|
282
|
+
const startMs = Date.now();
|
|
264
283
|
return await new Promise((resolve) => {
|
|
265
|
-
const child = spawn(process.execPath, [piCli, ...
|
|
266
|
-
stdio:
|
|
284
|
+
const child = spawn(process.execPath, [piCli, ...childArgv], {
|
|
285
|
+
stdio: stdio,
|
|
267
286
|
env,
|
|
268
287
|
});
|
|
288
|
+
// Collect pi's stdout when piping for --output-format json.
|
|
289
|
+
let stdoutChunks = "";
|
|
290
|
+
if (outputFormat === "json" && child.stdout) {
|
|
291
|
+
child.stdout.setEncoding("utf8");
|
|
292
|
+
child.stdout.on("data", (chunk) => {
|
|
293
|
+
stdoutChunks += chunk;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
269
296
|
// Forward termination signals to pi instead of dying around it (see
|
|
270
297
|
// signalForward.ts for the policy: first signal graceful, second tree-kill).
|
|
271
298
|
installSignalForwarding(child);
|
|
272
299
|
// 128+n for a signal death (bash parity), so a cancelled/killed run never
|
|
273
300
|
// reads as success to scripts or CI.
|
|
274
|
-
child.on("exit", (code, signal) =>
|
|
301
|
+
child.on("exit", (code, signal) => {
|
|
302
|
+
const exitCode = exitCodeFor(code, signal);
|
|
303
|
+
if (outputFormat === "json") {
|
|
304
|
+
const durationMs = Date.now() - startMs;
|
|
305
|
+
const events = parseJsonEvents(stdoutChunks);
|
|
306
|
+
const yagniSessionId = env.YAGNI_SESSION_ID ?? "";
|
|
307
|
+
const guardianEvents = readGuardianEvents(yagniSessionId);
|
|
308
|
+
const verbose = remainingArgs.includes("--verbose");
|
|
309
|
+
const result = buildResultObject({
|
|
310
|
+
events,
|
|
311
|
+
guardianEvents,
|
|
312
|
+
durationMs,
|
|
313
|
+
verbose,
|
|
314
|
+
});
|
|
315
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
316
|
+
}
|
|
317
|
+
resolve(exitCode);
|
|
318
|
+
});
|
|
275
319
|
child.on("error", (err) => {
|
|
276
320
|
process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
|
|
277
321
|
resolve(1);
|
|
@@ -286,6 +330,8 @@ export const HELP_TEXT = [
|
|
|
286
330
|
" yagni -c Continue the most recent session.",
|
|
287
331
|
" yagni -r Browse and resume a previous session.",
|
|
288
332
|
' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
|
|
333
|
+
' yagni -p "prompt" Use --output-format json for a machine-readable',
|
|
334
|
+
' --output-format json result object with tools, cost, guardian reviews.',
|
|
289
335
|
" yagni login Authorize the active environment (device-code flow).",
|
|
290
336
|
" yagni logout Revoke and clear the active environment's token.",
|
|
291
337
|
" yagni doctor Check that everything is ready (green/red checklist).",
|
|
@@ -307,6 +353,9 @@ export const HELP_TEXT = [
|
|
|
307
353
|
" --model <tier> Model tier (fixed to advanced).",
|
|
308
354
|
" --thinking <level> off | minimal | low | medium | high | xhigh | max",
|
|
309
355
|
" --session <id> Open a specific session; --fork <id> branches one.",
|
|
356
|
+
" --output-format <fmt> Output format: text (default), json, stream-json.",
|
|
357
|
+
" json emits a single result object; stream-json",
|
|
358
|
+
" emits NDJSON events (same as --mode json).",
|
|
310
359
|
" --mode json Emit machine-readable events (for scripts and CI).",
|
|
311
360
|
"",
|
|
312
361
|
"In a session:",
|
|
@@ -32,6 +32,7 @@ import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
|
|
|
32
32
|
import { withResilience } from "./pipeline/resilience.js";
|
|
33
33
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
34
34
|
import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
35
|
+
import { logEvent } from "./errorSink.js";
|
|
35
36
|
import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, receiptLine, renderSubagentResult, runningLines, } from "./subagentRender.js";
|
|
36
37
|
/**
|
|
37
38
|
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
@@ -288,6 +289,7 @@ export function registerAdviseCommand(pi, tool) {
|
|
|
288
289
|
}
|
|
289
290
|
catch (err) {
|
|
290
291
|
const message = err instanceof Error ? err.message : String(err);
|
|
292
|
+
logEvent({ source: "advisor", level: "error", event: "advise_failed", fields: { kind: "consult" } });
|
|
291
293
|
notify(`/advise failed: ${message}`, "error");
|
|
292
294
|
await pi.sendUserMessage(`/advise failed: ${message}`);
|
|
293
295
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { codeStateHome } from "../stateHome.js";
|
|
1
|
+
import { logEvent } from "../errorSink.js";
|
|
4
2
|
export function firstString(...values) {
|
|
5
3
|
for (const value of values) {
|
|
6
4
|
if (typeof value === "string" && value.trim().length > 0)
|
|
@@ -132,20 +130,15 @@ export function settleTurn(sessionStates, sessionId) {
|
|
|
132
130
|
return completion;
|
|
133
131
|
}
|
|
134
132
|
export function warn(ctx, message, details = {}, notifyUser = false) {
|
|
135
|
-
const payload = { source: "yagni-cmux-bridge", level: "warning", message, ...details };
|
|
136
133
|
// The TUI is in raw mode: writing to stdout/stderr corrupts the terminal (the
|
|
137
|
-
// JSON was landing at the prompt cursor). Route warnings/errors to
|
|
138
|
-
// local
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
/* logging must never throw into the editor */
|
|
148
|
-
}
|
|
134
|
+
// JSON was landing at the prompt cursor). Route warnings/errors to the unified
|
|
135
|
+
// local sink instead — never to the terminal.
|
|
136
|
+
logEvent({
|
|
137
|
+
source: "cmux",
|
|
138
|
+
level: "warn",
|
|
139
|
+
event: "cmux_bridge",
|
|
140
|
+
fields: { message, ...details },
|
|
141
|
+
});
|
|
149
142
|
if (notifyUser) {
|
|
150
143
|
try {
|
|
151
144
|
ctx?.notifyWarning?.();
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { spawn } from "node:child_process";
|
|
28
28
|
import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
29
29
|
import { isDesktopSurface } from "./surface.js";
|
|
30
|
+
import { logEvent } from "./errorSink.js";
|
|
30
31
|
const defaultSpawn = (command, args, options) => spawn(command, args, options);
|
|
31
32
|
export const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
32
33
|
export const CRASH_REPORT_TIMEOUT_MS = 1_500;
|
|
@@ -267,6 +268,17 @@ export function reportFatalCrash(error, opts, context) {
|
|
|
267
268
|
export function installUncaughtExceptionMonitor(opts, proc = process) {
|
|
268
269
|
proc.on("uncaughtExceptionMonitor", (err) => {
|
|
269
270
|
reportFatalCrash(err, opts, "uncaught-exception");
|
|
271
|
+
// Also seed the local error trail (best-effort): the crash report is a
|
|
272
|
+
// sanitized POST, but the ON-DISK trail is what /feedback binds for a
|
|
273
|
+
// report someone files next session.
|
|
274
|
+
logEvent({
|
|
275
|
+
source: "tool",
|
|
276
|
+
level: "error",
|
|
277
|
+
event: "uncaught_exception",
|
|
278
|
+
sessionId: process.env.YAGNI_SESSION_ID ?? undefined,
|
|
279
|
+
flush: "sync",
|
|
280
|
+
fields: { errorClass: err instanceof Error ? err.name || "Error" : "Error" },
|
|
281
|
+
});
|
|
270
282
|
});
|
|
271
283
|
}
|
|
272
284
|
//# sourceMappingURL=crashReport.js.map
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* never affects the tool call that triggered it.
|
|
18
18
|
*/
|
|
19
19
|
import { bankDecision } from "./decisions.js";
|
|
20
|
+
import { logEvent } from "./errorSink.js";
|
|
20
21
|
/** At most one capture prompt per this window (spec: 10 minutes). */
|
|
21
22
|
export const CAPTURE_DEBOUNCE_MS = 10 * 60 * 1000;
|
|
22
23
|
/** Build a session-scoped decision capture (holds the debounce timestamp). */
|
|
@@ -54,10 +55,12 @@ export function makeDecisionCapture(deps) {
|
|
|
54
55
|
ctx.ui.notify("Saved the decision locally; it will sync automatically.", "info");
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
58
|
+
logEvent({ source: "decision-capture", level: "error", event: "capture_failed", fields: { kind: outcome.kind } });
|
|
57
59
|
ctx.ui.notify(outcome.message, "error");
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
catch {
|
|
63
|
+
logEvent({ source: "decision-capture", level: "error", event: "capture_failed", fields: { kind: "threw" } });
|
|
61
64
|
/* fail-soft: a capture failure never affects the tool call */
|
|
62
65
|
}
|
|
63
66
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
21
|
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
22
22
|
import { sendOrSpool } from "./spool.js";
|
|
23
|
+
import { logEvent } from "./errorSink.js";
|
|
23
24
|
/**
|
|
24
25
|
* Bank a `cli_correction` decision durably. Every logical write carries a
|
|
25
26
|
* generated idempotencyKey; the backend dedups on it, so a spool replay or a
|
|
@@ -151,6 +152,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
151
152
|
notify(notice.message, notice.type);
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
155
|
+
logEvent({ source: "decisions", level: "error", event: "decide_failed", fields: { op: "decide" } });
|
|
154
156
|
notify(`Could not record the decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
155
157
|
}
|
|
156
158
|
},
|
|
@@ -183,6 +185,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
183
185
|
notify(`Superseded decision ${shortId(id)}.`, "info");
|
|
184
186
|
}
|
|
185
187
|
catch (err) {
|
|
188
|
+
logEvent({ source: "decisions", level: "error", event: "supersede_failed", fields: { op: "supersede" } });
|
|
186
189
|
notify(`Could not supersede decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
187
190
|
}
|
|
188
191
|
return;
|
|
@@ -192,6 +195,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
192
195
|
await pi.sendUserMessage(formatDecisionsList(items));
|
|
193
196
|
}
|
|
194
197
|
catch (err) {
|
|
198
|
+
logEvent({ source: "decisions", level: "error", event: "list_failed", fields: { op: "list" } });
|
|
195
199
|
notify(`Could not list decisions: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
196
200
|
}
|
|
197
201
|
},
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* A support flow can tail this file and POST it with the user's consent;
|
|
13
13
|
* nothing is uploaded automatically.
|
|
14
14
|
*/
|
|
15
|
+
import { readSessionTrail } from "./errorSink.js";
|
|
15
16
|
export declare function _setDiagnosticsHomeForTest(dir: string | null): void;
|
|
17
|
+
/** Active unified-sink path (one rotating per-day JSONL for all sources). */
|
|
16
18
|
export declare function diagnosticsLogPath(): string;
|
|
17
|
-
/** Dedicated log for the ask_user_question interactive tool's state machine. */
|
|
18
|
-
export declare function askQuestionLogPath(): string;
|
|
19
19
|
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
20
20
|
export declare function isDebug(env?: NodeJS.ProcessEnv): boolean;
|
|
21
21
|
export interface ImagePasteEvent {
|
|
@@ -56,7 +56,8 @@ export interface AskQuestionEvent {
|
|
|
56
56
|
checked?: boolean;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Append one sanitized ask-user-question event to
|
|
59
|
+
* Append one sanitized ask-user-question event to the unified sink
|
|
60
|
+
* (source:"ask-question").
|
|
60
61
|
* Fail-soft and hermetically gated under `node --test` exactly like
|
|
61
62
|
* `logImagePaste`. Never logs user-typed text, question text, or option labels
|
|
62
63
|
* — only indices, lengths, key bytes (escaped), and resolution status.
|
|
@@ -66,4 +67,6 @@ export declare function logAskQuestion(ev: AskQuestionEvent): void;
|
|
|
66
67
|
export declare function readRecentDiagnostics(maxBytes?: number): string;
|
|
67
68
|
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
68
69
|
export declare function listDiagnosticFiles(): string[];
|
|
70
|
+
/** Re-export the session-scoped trail reader for the /feedback flow. */
|
|
71
|
+
export { readSessionTrail };
|
|
69
72
|
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -12,26 +12,19 @@
|
|
|
12
12
|
* A support flow can tail this file and POST it with the user's consent;
|
|
13
13
|
* nothing is uploaded automatically.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
16
16
|
import { dirname, join, basename } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { _setErrorSinkHomeForTest, logEvent, errorSinkPath, readSessionTrail, } from "./errorSink.js";
|
|
18
18
|
/** Test seam (mirrors `_setYagniCodeHomeForTest`): point the log at a tmpdir. */
|
|
19
19
|
let homeOverride = null;
|
|
20
20
|
export function _setDiagnosticsHomeForTest(dir) {
|
|
21
21
|
homeOverride = dir;
|
|
22
|
+
_setErrorSinkHomeForTest(dir);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
return codeStateHome(homeOverride);
|
|
25
|
-
}
|
|
24
|
+
/** Active unified-sink path (one rotating per-day JSONL for all sources). */
|
|
26
25
|
export function diagnosticsLogPath() {
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
/** Dedicated log for the ask_user_question interactive tool's state machine. */
|
|
30
|
-
export function askQuestionLogPath() {
|
|
31
|
-
return join(yagniCodeHome(), "logs", "ask-question.log");
|
|
26
|
+
return errorSinkPath();
|
|
32
27
|
}
|
|
33
|
-
const MAX_LOG_BYTES = 256 * 1024; // rotate the active file past this
|
|
34
|
-
const KEEP_ROTATIONS = 2; // keep image-paste.log.1 and .2 alongside the active file
|
|
35
28
|
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
36
29
|
export function isDebug(env = process.env) {
|
|
37
30
|
const v = env.YAGNI_DEBUG;
|
|
@@ -42,88 +35,55 @@ export function isDebug(env = process.env) {
|
|
|
42
35
|
* prompt. `detail` is included only when YAGNI_DEBUG is on.
|
|
43
36
|
*/
|
|
44
37
|
export function logImagePaste(ev) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
const path = diagnosticsLogPath();
|
|
63
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
64
|
-
rotateIfNeeded(path);
|
|
65
|
-
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
/* logging must never throw into the editor */
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/** Shift image-paste.log -> .1 -> .2 once the active file passes the size cap. */
|
|
72
|
-
function rotateIfNeeded(path) {
|
|
73
|
-
try {
|
|
74
|
-
if (statSync(path).size < MAX_LOG_BYTES)
|
|
75
|
-
return;
|
|
76
|
-
for (let i = KEEP_ROTATIONS; i >= 1; i--) {
|
|
77
|
-
const from = i === 1 ? path : `${path}.${i - 1}`;
|
|
78
|
-
const to = `${path}.${i}`;
|
|
79
|
-
try {
|
|
80
|
-
renameSync(from, to);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
/* absent source — fine */
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
/* rotation is best-effort */
|
|
89
|
-
}
|
|
38
|
+
// The unified sink: content-free fields are always-on, the debug-only
|
|
39
|
+
// `detail` rides level:debug (never bound by /feedback).
|
|
40
|
+
const fields = {
|
|
41
|
+
...(ev.outcome !== undefined ? { outcome: ev.outcome } : {}),
|
|
42
|
+
...(ev.mimeType !== undefined ? { mimeType: ev.mimeType } : {}),
|
|
43
|
+
...(ev.bytes !== undefined ? { bytes: ev.bytes } : {}),
|
|
44
|
+
...(ev.imageCount !== undefined ? { imageCount: ev.imageCount } : {}),
|
|
45
|
+
...(ev.file !== undefined ? { file: basename(ev.file) } : {}),
|
|
46
|
+
...(ev.detail !== undefined && isDebug() ? { detail: ev.detail } : {}),
|
|
47
|
+
};
|
|
48
|
+
logEvent({
|
|
49
|
+
source: "image-paste",
|
|
50
|
+
level: ev.detail !== undefined && isDebug() ? "debug" : "info",
|
|
51
|
+
event: ev.event,
|
|
52
|
+
fields,
|
|
53
|
+
});
|
|
90
54
|
}
|
|
91
55
|
/**
|
|
92
|
-
* Append one sanitized ask-user-question event to
|
|
56
|
+
* Append one sanitized ask-user-question event to the unified sink
|
|
57
|
+
* (source:"ask-question").
|
|
93
58
|
* Fail-soft and hermetically gated under `node --test` exactly like
|
|
94
59
|
* `logImagePaste`. Never logs user-typed text, question text, or option labels
|
|
95
60
|
* — only indices, lengths, key bytes (escaped), and resolution status.
|
|
96
61
|
*/
|
|
97
62
|
export function logAskQuestion(ev) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
/* logging must never throw into the tool */
|
|
121
|
-
}
|
|
63
|
+
// Raw key bytes (`key`) and `detail` are content-ish: gate them behind DEBUG.
|
|
64
|
+
const debug = isDebug();
|
|
65
|
+
const fields = {
|
|
66
|
+
...(ev.selectedIndex !== undefined ? { selectedIndex: ev.selectedIndex } : {}),
|
|
67
|
+
...(ev.otherIndex !== undefined ? { otherIndex: ev.otherIndex } : {}),
|
|
68
|
+
...(ev.otherLen !== undefined ? { otherLen: ev.otherLen } : {}),
|
|
69
|
+
...(ev.status !== undefined ? { status: ev.status } : {}),
|
|
70
|
+
...(ev.multi !== undefined ? { multi: ev.multi } : {}),
|
|
71
|
+
...(ev.qIndex !== undefined ? { qIndex: ev.qIndex } : {}),
|
|
72
|
+
...(ev.checked !== undefined ? { checked: ev.checked } : {}),
|
|
73
|
+
...(ev.key !== undefined && debug ? { key: ev.key } : {}),
|
|
74
|
+
...(ev.detail !== undefined && debug ? { detail: ev.detail } : {}),
|
|
75
|
+
};
|
|
76
|
+
logEvent({
|
|
77
|
+
source: "ask-question",
|
|
78
|
+
level: debug ? "debug" : "info",
|
|
79
|
+
event: ev.event,
|
|
80
|
+
fields,
|
|
81
|
+
});
|
|
122
82
|
}
|
|
123
83
|
/** Read the most recent log content (for a user-triggered report). */
|
|
124
84
|
export function readRecentDiagnostics(maxBytes = 64 * 1024) {
|
|
125
85
|
try {
|
|
126
|
-
const data = readFileSync(
|
|
86
|
+
const data = readFileSync(errorSinkPath(), "utf8");
|
|
127
87
|
return data.length > maxBytes ? data.slice(data.length - maxBytes) : data;
|
|
128
88
|
}
|
|
129
89
|
catch {
|
|
@@ -133,9 +93,9 @@ export function readRecentDiagnostics(maxBytes = 64 * 1024) {
|
|
|
133
93
|
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
134
94
|
export function listDiagnosticFiles() {
|
|
135
95
|
try {
|
|
136
|
-
const dir = dirname(
|
|
96
|
+
const dir = dirname(errorSinkPath());
|
|
137
97
|
return readdirSync(dir)
|
|
138
|
-
.filter((f) => f.startsWith("
|
|
98
|
+
.filter((f) => f.startsWith("errors-"))
|
|
139
99
|
.sort()
|
|
140
100
|
.map((f) => join(dir, f));
|
|
141
101
|
}
|
|
@@ -143,4 +103,6 @@ export function listDiagnosticFiles() {
|
|
|
143
103
|
return [];
|
|
144
104
|
}
|
|
145
105
|
}
|
|
106
|
+
/** Re-export the session-scoped trail reader for the /feedback flow. */
|
|
107
|
+
export { readSessionTrail };
|
|
146
108
|
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified local error/log sink for YAGNI Code (YAG-580).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the previous ~9 hand-rolled JSONL writers (image-paste.log,
|
|
5
|
+
* ask-question.log, turn-lifecycle.log, guardian.log, cost-divergence.log,
|
|
6
|
+
* auth-events.log, cmux-bridge.log, hooks.log, *.stream.log) with ONE sink.
|
|
7
|
+
*
|
|
8
|
+
* Two storage layers, purpose-named so their roles stay clear:
|
|
9
|
+
*
|
|
10
|
+
* 1. The DURABLE TRAIL — one rotating per-day JSONL under
|
|
11
|
+
* `~/.yagni-code/logs/errors-<date>.jsonl` (size-capped + 2 rotations).
|
|
12
|
+
* This is the crash-survivable WAL: `turn_start` without a matching
|
|
13
|
+
* `turn_end` still leaves a record even if the process is killed. Critical
|
|
14
|
+
* events append SYNCHRONOUSLY for exactly that reason.
|
|
15
|
+
*
|
|
16
|
+
* 2. The IN-MEMORY CAPTURE — a byte-budgeted ring buffer (not line-counted).
|
|
17
|
+
* This is the `/feedback` binding convenience, NOT durability (an in-memory
|
|
18
|
+
* ring does not survive a crash). Mirrors Codex's CodexFeedback ring and
|
|
19
|
+
* Claude's inMemoryErrorLog.
|
|
20
|
+
*
|
|
21
|
+
* Every line carries a REQUIRED `sessionId` and a `source`/`level`/`event`
|
|
22
|
+
* triple so a shared file stays filterable: `jq 'select(.source=="guardian")'`
|
|
23
|
+
* reproduces today's per-file tail exactly, and /feedback reads the trail
|
|
24
|
+
* filtered by sessionId (never the raw file) so one session's report never
|
|
25
|
+
* leaks another session's errors.
|
|
26
|
+
*
|
|
27
|
+
* Default-on vs DEBUG invariant (the thing that makes "log everything by
|
|
28
|
+
* default" safe): default-on == scrub-safe == upload-safe. Any field carrying
|
|
29
|
+
* raw content (tool arguments, partial/result bodies, provider payloads, raw
|
|
30
|
+
* key bytes) must be gated behind YAGNI_DEBUG, and the /feedback reader refuses
|
|
31
|
+
* to bind any `level: debug` line. DEBUG == may-contain-content == never-uploads.
|
|
32
|
+
*/
|
|
33
|
+
export type SinkLevel = "error" | "warn" | "info" | "debug";
|
|
34
|
+
export interface SinkEvent {
|
|
35
|
+
/** Former filename / subsystem: tool, turn, guardian, image-paste, ask-question, cost, auth, cmux, hooks. */
|
|
36
|
+
source: string;
|
|
37
|
+
level: SinkLevel;
|
|
38
|
+
/** Stable machine name (e.g. "turn_start", "bash.exit_1", "denied"). */
|
|
39
|
+
event: string;
|
|
40
|
+
/** Additional structured fields. NEVER raw content on a non-debug line. */
|
|
41
|
+
fields?: Record<string, unknown>;
|
|
42
|
+
/** Session id so the trail is filterable and scoped per feedback. Defaults to YAGNI_SESSION_ID. */
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
/** "sync" flushes immediately (critical events); "buffered" is fine for high-volume debug. */
|
|
45
|
+
flush?: "sync" | "buffered";
|
|
46
|
+
}
|
|
47
|
+
export declare function _setErrorSinkHomeForTest(dir: string | null): void;
|
|
48
|
+
export declare function errorSinkPath(now?: Date): string;
|
|
49
|
+
export declare function _clearErrorSinkRingForTest(): void;
|
|
50
|
+
export declare function errorSinkInMemory(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Append one event to both the ring and the durable trail. Fail-soft: a logging
|
|
53
|
+
* failure must never break the session. `flush: "sync"` (default for
|
|
54
|
+
* error-level events and lifecycle turns) bypasses any future buffering so a
|
|
55
|
+
* turn that starts but never ends still leaves a durable `turn_start`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function logEvent(ev: SinkEvent): void;
|
|
58
|
+
/**
|
|
59
|
+
* Read recent trail lines for ONE session, filtered by `sessionId`, up to
|
|
60
|
+
* `maxBytes`. Never returns `level: debug` lines — the default-on tier is the
|
|
61
|
+
* upload-safe tier, and DEBUG may contain content that must not leave the machine.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readSessionTrail(sessionId: string, maxBytes?: number): string;
|
|
64
|
+
//# sourceMappingURL=errorSink.d.ts.map
|