@yagni-app/code 0.3.4 → 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/branding.d.ts +39 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/cmux/index.d.ts +17 -1
- package/dist/extension/cmux/index.js +47 -8
- package/dist/extension/cmux/state.d.ts +5 -1
- package/dist/extension/cmux/state.js +15 -8
- 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 +97 -40
- package/dist/extension/mineBeat.js +13 -0
- package/dist/extension/pipeline/goCommand.js +2 -0
- package/dist/extension/pipeline/personas.js +9 -0
- package/dist/extension/pipeline/runner.js +9 -0
- package/dist/extension/sessionTitle/summarize.d.ts +40 -0
- package/dist/extension/sessionTitle/summarize.js +63 -0
- package/dist/extension/sessionTitle/title.d.ts +27 -0
- package/dist/extension/sessionTitle/title.js +57 -0
- package/dist/extension/silentTurnReminder.d.ts +109 -0
- package/dist/extension/silentTurnReminder.js +221 -0
- package/dist/extension/turnLog.d.ts +14 -0
- package/dist/extension/turnLog.js +22 -47
- 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 +4 -3
- package/dist/extension/cmux/naming.d.ts +0 -5
- package/dist/extension/cmux/naming.js +0 -23
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
import { codeStateHome } from "./stateHome.js";
|
|
36
|
+
import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
37
|
+
const MAX_LOG_BYTES = 256 * 1024;
|
|
38
|
+
const KEEP_ROTATIONS = 2;
|
|
39
|
+
const RING_MAX_BYTES = 256 * 1024;
|
|
40
|
+
/** Test seam: point the log at a tmpdir (mirrors _setDiagnosticsHomeForTest). */
|
|
41
|
+
let homeOverride = null;
|
|
42
|
+
export function _setErrorSinkHomeForTest(dir) {
|
|
43
|
+
homeOverride = dir;
|
|
44
|
+
}
|
|
45
|
+
function logDir() {
|
|
46
|
+
return join(codeStateHome(homeOverride), "logs");
|
|
47
|
+
}
|
|
48
|
+
function dayStamp(now = new Date()) {
|
|
49
|
+
return now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
50
|
+
}
|
|
51
|
+
export function errorSinkPath(now = new Date()) {
|
|
52
|
+
return join(logDir(), `errors-${dayStamp(now)}.jsonl`);
|
|
53
|
+
}
|
|
54
|
+
/** In-memory ring buffer, byte-budgeted (trailing bytes kept when over cap). */
|
|
55
|
+
class RingBuffer {
|
|
56
|
+
maxBytes;
|
|
57
|
+
chunks = [];
|
|
58
|
+
bytes = 0;
|
|
59
|
+
constructor(maxBytes) {
|
|
60
|
+
this.maxBytes = maxBytes;
|
|
61
|
+
}
|
|
62
|
+
push(line) {
|
|
63
|
+
const b = Buffer.byteLength(line, "utf8");
|
|
64
|
+
if (b >= this.maxBytes) {
|
|
65
|
+
this.chunks = [line];
|
|
66
|
+
this.bytes = b;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
while (this.bytes + b > this.maxBytes && this.chunks.length > 0) {
|
|
70
|
+
const dropped = this.chunks.shift();
|
|
71
|
+
this.bytes -= Buffer.byteLength(dropped, "utf8");
|
|
72
|
+
}
|
|
73
|
+
this.chunks.push(line);
|
|
74
|
+
this.bytes += b;
|
|
75
|
+
}
|
|
76
|
+
snapshot() {
|
|
77
|
+
return this.chunks.join("");
|
|
78
|
+
}
|
|
79
|
+
clear() {
|
|
80
|
+
this.chunks = [];
|
|
81
|
+
this.bytes = 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const ring = new RingBuffer(RING_MAX_BYTES);
|
|
85
|
+
export function _clearErrorSinkRingForTest() {
|
|
86
|
+
ring.clear();
|
|
87
|
+
}
|
|
88
|
+
export function errorSinkInMemory() {
|
|
89
|
+
return ring.snapshot();
|
|
90
|
+
}
|
|
91
|
+
function rotateIfNeeded(path) {
|
|
92
|
+
try {
|
|
93
|
+
if (!statSync(path).isFile() || statSync(path).size < MAX_LOG_BYTES)
|
|
94
|
+
return;
|
|
95
|
+
for (let i = KEEP_ROTATIONS; i >= 1; i--) {
|
|
96
|
+
const from = i === 1 ? path : `${path}.${i - 1}`;
|
|
97
|
+
const to = `${path}.${i}`;
|
|
98
|
+
try {
|
|
99
|
+
renameSync(from, to);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* absent source — fine */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* rotation is best-effort */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isDebug(env = process.env) {
|
|
111
|
+
const v = env.YAGNI_DEBUG;
|
|
112
|
+
return v === "1" || v === "true";
|
|
113
|
+
}
|
|
114
|
+
function sessionIdFor(ev) {
|
|
115
|
+
return ev.sessionId ?? process.env.YAGNI_SESSION_ID ?? "";
|
|
116
|
+
}
|
|
117
|
+
function serialize(ev) {
|
|
118
|
+
const line = {
|
|
119
|
+
ts: new Date().toISOString(),
|
|
120
|
+
source: ev.source,
|
|
121
|
+
level: ev.level,
|
|
122
|
+
event: ev.event,
|
|
123
|
+
sessionId: sessionIdFor(ev),
|
|
124
|
+
...(ev.fields ?? {}),
|
|
125
|
+
};
|
|
126
|
+
return JSON.stringify(line) + "\n";
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Append one event to both the ring and the durable trail. Fail-soft: a logging
|
|
130
|
+
* failure must never break the session. `flush: "sync"` (default for
|
|
131
|
+
* error-level events and lifecycle turns) bypasses any future buffering so a
|
|
132
|
+
* turn that starts but never ends still leaves a durable `turn_start`.
|
|
133
|
+
*/
|
|
134
|
+
export function logEvent(ev) {
|
|
135
|
+
try {
|
|
136
|
+
if (process.env.NODE_TEST_CONTEXT && homeOverride === null)
|
|
137
|
+
return;
|
|
138
|
+
const line = serialize(ev);
|
|
139
|
+
ring.push(line);
|
|
140
|
+
const path = errorSinkPath();
|
|
141
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
142
|
+
rotateIfNeeded(path);
|
|
143
|
+
appendFileSync(path, line, "utf8");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* logging must never throw into the editor */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read recent trail lines for ONE session, filtered by `sessionId`, up to
|
|
151
|
+
* `maxBytes`. Never returns `level: debug` lines — the default-on tier is the
|
|
152
|
+
* upload-safe tier, and DEBUG may contain content that must not leave the machine.
|
|
153
|
+
*/
|
|
154
|
+
export function readSessionTrail(sessionId, maxBytes = 64 * 1024) {
|
|
155
|
+
try {
|
|
156
|
+
const data = readFileSync(errorSinkPath(), "utf8");
|
|
157
|
+
// Defense-in-depth: scrub each kept line so /diagnostics and /feedback
|
|
158
|
+
// never surface a secret or local path, even if a future caller slipped a
|
|
159
|
+
// content-bearing value onto an always-on line.
|
|
160
|
+
const lines = data
|
|
161
|
+
.split("\n")
|
|
162
|
+
.filter((l) => l.length > 0)
|
|
163
|
+
.filter((l) => {
|
|
164
|
+
try {
|
|
165
|
+
const obj = JSON.parse(l);
|
|
166
|
+
return obj.sessionId === sessionId && obj.level !== "debug";
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.map((l) => scrubSecrets(l))
|
|
173
|
+
.join("\n");
|
|
174
|
+
return lines.length > maxBytes ? lines.slice(lines.length - maxBytes) : lines;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=errorSink.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
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 type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
export interface FeedbackDeps {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
getToken: () => string | undefined;
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
/** Sanitized `yagni doctor` output (string), or undefined to omit. */
|
|
26
|
+
getDoctorReport?: () => Promise<string | undefined>;
|
|
27
|
+
/** Git facts for the report; undefined fields are omitted. */
|
|
28
|
+
getGitState?: (cwd: string) => Promise<{
|
|
29
|
+
branch?: string;
|
|
30
|
+
commit?: string;
|
|
31
|
+
remote?: string;
|
|
32
|
+
dirty?: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
/** Child `/go` run transcripts keyed by run id, for the current run tree. */
|
|
35
|
+
getChildTranscripts?: (cwd: string, sessionFile: string | undefined) => Promise<Record<string, string>>;
|
|
36
|
+
}
|
|
37
|
+
export declare function registerFeedbackCommands(pi: ExtensionAPI, deps: FeedbackDeps): void;
|
|
38
|
+
//# sourceMappingURL=feedbackCommand.d.ts.map
|
|
@@ -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";
|
|
@@ -45,6 +47,7 @@ import { buildYagniProvider } from "./provider.js";
|
|
|
45
47
|
import { registerChipEditor } from "./chipEditor.js";
|
|
46
48
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
47
49
|
import { makeFlywheelState } from "./flywheel.js";
|
|
50
|
+
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
48
51
|
function isEvalMode(env = process.env) {
|
|
49
52
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
50
53
|
}
|
|
@@ -158,6 +161,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
158
161
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
159
162
|
const flywheelState = makeFlywheelState();
|
|
160
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));
|
|
161
167
|
// Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
|
|
162
168
|
// workspace tracker, attributed to the developer via per-user credentials.
|
|
163
169
|
if (!evalMode) {
|
|
@@ -176,6 +182,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
176
182
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
177
183
|
// draws on the same cap rather than opening a side channel around it.
|
|
178
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());
|
|
179
188
|
// The differentiated business-grounded tools (loop bricks): review a change
|
|
180
189
|
// for business fit, rank the next work by business priority, and record the
|
|
181
190
|
// engineering rationale back onto the work-item.
|
|
@@ -191,6 +200,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
191
200
|
// above-editor widget, and /todos. Branch-replayed, so forks and resumes
|
|
192
201
|
// show the list as it stood at that point.
|
|
193
202
|
registerTodos(pi);
|
|
203
|
+
// YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
|
|
204
|
+
// process has no direct user to answer, so it is never nudged (same gating
|
|
205
|
+
// as the delegation identity in branding.ts); eval mode is untouched so its
|
|
206
|
+
// measured behavior is not perturbed.
|
|
207
|
+
registerSilentTurnReminder(pi, { isDriver: isDriverCaller(env), evalMode, env: deps.env });
|
|
194
208
|
// Image paste with [Image #N] chips: replaces the editor on
|
|
195
209
|
// session_start (TUI mode) so pasting a screenshot drops a chip instead of a
|
|
196
210
|
// temp path, and registers the input transform that turns chips into image
|
|
@@ -238,6 +252,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
238
252
|
// M6 eval (report-only): /go-compare runs a ticket grounded vs blind and reports
|
|
239
253
|
// the business-fit delta. Never wired to routing.
|
|
240
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
|
+
}
|
|
241
268
|
// W4 judgment loop: the decisions surface (/decide + /decisions) and the
|
|
242
269
|
// bless-with-remember capture are the same product-intent write as the record
|
|
243
270
|
// tools, so both are gated together (skipped in eval mode).
|
|
@@ -282,18 +309,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
282
309
|
...(guardianMaxAttemptsAdvertised !== undefined ? { maxAttempts: guardianMaxAttemptsAdvertised } : {}),
|
|
283
310
|
});
|
|
284
311
|
guardianLimits.timeoutMs = guardianTimeoutMs;
|
|
285
|
-
// YAG-510:
|
|
286
|
-
// never the command). The remote guardian-events stream below is
|
|
287
|
-
// 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.
|
|
288
315
|
const guardianLogSink = (payload) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
});
|
|
297
325
|
};
|
|
298
326
|
// YAG-510: persisted "don't ask again" grants, per-repo keyed. Loaded once
|
|
299
327
|
// at startup (grants added by other concurrent sessions appear next launch —
|
|
@@ -325,7 +353,8 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
325
353
|
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
326
354
|
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
327
355
|
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
328
|
-
// fail-soft to guardian
|
|
356
|
+
// fail-soft to the unified sink (source:"guardian") — a storage outage
|
|
357
|
+
// never touches the session.
|
|
329
358
|
onGuardianEvent: guardianStorageTier === "off" || evalMode
|
|
330
359
|
? undefined
|
|
331
360
|
: (ev) => {
|
|
@@ -363,10 +392,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
363
392
|
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
364
393
|
}
|
|
365
394
|
}
|
|
366
|
-
catch
|
|
395
|
+
catch {
|
|
367
396
|
guardianLogSink({
|
|
368
397
|
event: "guardian_event_post_failed",
|
|
369
|
-
|
|
398
|
+
kind: "network",
|
|
370
399
|
});
|
|
371
400
|
}
|
|
372
401
|
})();
|
|
@@ -448,15 +477,13 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
448
477
|
onDivergence: (driverServerUsd, localUsd) => {
|
|
449
478
|
if (!isDebug(env))
|
|
450
479
|
return;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
/* a diagnostic must never break /cost */
|
|
459
|
-
}
|
|
480
|
+
logEvent({
|
|
481
|
+
source: "cost",
|
|
482
|
+
level: "debug",
|
|
483
|
+
event: "cost_divergence",
|
|
484
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
485
|
+
fields: { driverServerUsd, localUsd },
|
|
486
|
+
});
|
|
460
487
|
},
|
|
461
488
|
// Carry-over (/cost re-review): surfaces sessionRuns.ts's dropped-run-id
|
|
462
489
|
// count as costHud's "Excludes N earlier /go runs." note.
|
|
@@ -685,21 +712,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
685
712
|
// session token is expired and refresh failed — the most critical
|
|
686
713
|
// failure signal is no longer silently dropped.
|
|
687
714
|
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
688
|
-
// YAG-500 Fix F:
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
catch {
|
|
700
|
-
// A diagnostic must never break the session.
|
|
701
|
-
}
|
|
702
|
-
}
|
|
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
|
+
});
|
|
703
725
|
return { message: { ...msg, errorMessage: explanation } };
|
|
704
726
|
}
|
|
705
727
|
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
@@ -753,6 +775,16 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
753
775
|
// and a "Pi can explain its own features…" line) with a YAGNI Code masthead,
|
|
754
776
|
// set the terminal title, and add a footer brand mark. TUI only.
|
|
755
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
|
+
}
|
|
756
788
|
ctx.ui?.setTitle(BRAND_NAME);
|
|
757
789
|
if (ctx.mode === "tui") {
|
|
758
790
|
ctx.ui?.setStatus?.("brand", BRAND_NAME);
|
|
@@ -883,6 +915,30 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
883
915
|
footerInvalidateHandle.invalidateGit();
|
|
884
916
|
}
|
|
885
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
|
+
});
|
|
886
942
|
}
|
|
887
943
|
export default async function (pi) {
|
|
888
944
|
await registerYagni(pi);
|
|
@@ -896,6 +952,7 @@ export default async function (pi) {
|
|
|
896
952
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
897
953
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
898
954
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
955
|
+
export { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
|
|
899
956
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
900
957
|
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
|
|
901
958
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|