@yagni-app/code 0.3.5 → 1.0.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/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +13 -3
- package/dist/extension/askUserQuestionTool.d.ts +54 -0
- package/dist/extension/askUserQuestionTool.js +621 -0
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/cmux/state.js +9 -16
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- 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/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- 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/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/hooks.js +12 -12
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +161 -42
- package/dist/extension/mineBeat.js +13 -0
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/pipeline/goCommand.js +2 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +24 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/silentTurnReminder.js +18 -14
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- 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 +106 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +4 -2
|
@@ -74,7 +74,7 @@ You are grounded in how THIS company works: call ask_yagni before inferring a co
|
|
|
74
74
|
|
|
75
75
|
Never fabricate file paths, contents, or findings. If you cannot find something, say so.
|
|
76
76
|
|
|
77
|
-
Your final message is your report back to the driving agent, which has NOT seen what you read or did
|
|
77
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. Your final message is your report back to the driving agent, which has NOT seen what you read or did: make it a concise report of what was done and the key findings, since the caller relays it to the user and it only needs the essentials. Cover what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
|
|
78
78
|
const GENERAL_AGENT = {
|
|
79
79
|
name: GENERAL_AGENT_NAME,
|
|
80
80
|
description: "General-purpose agent for research, multi-file changes, and self-contained tasks.",
|
|
@@ -94,6 +94,8 @@ one you actually read with a tool. If you cannot find something, say "not
|
|
|
94
94
|
found" — a plausible-sounding invention is worse than no answer because the
|
|
95
95
|
driving agent trusts your report.
|
|
96
96
|
|
|
97
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
98
|
+
|
|
97
99
|
Your final message is your report back to the driving agent, which has NOT
|
|
98
100
|
seen what you read. Make it compressed and complete: exact file paths, the
|
|
99
101
|
key excerpts, and a one-paragraph map of how the pieces relate. Say what you
|
|
@@ -122,6 +124,8 @@ convention, an ownership rule, or anything organization-specific.
|
|
|
122
124
|
Never fabricate file paths or results. Report what you actually did and what
|
|
123
125
|
you actually found.
|
|
124
126
|
|
|
127
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
128
|
+
|
|
125
129
|
Your final message is your report back to the driving agent, which has NOT
|
|
126
130
|
seen what you did. List every file you touched, what changed in each, the
|
|
127
131
|
commands you ran with their outcomes, and anything you deliberately left
|
|
@@ -154,6 +158,8 @@ a convention, an ownership rule, or anything organization-specific.
|
|
|
154
158
|
Never fabricate file paths or findings. If you could not verify something,
|
|
155
159
|
say exactly what you tried and why you could not.
|
|
156
160
|
|
|
161
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
162
|
+
|
|
157
163
|
Your final message is your verdict back to the driving agent, which has NOT
|
|
158
164
|
seen what you read. Format:
|
|
159
165
|
## Verdict
|
|
@@ -344,6 +350,8 @@ export function makeSubagentTool(deps = {}) {
|
|
|
344
350
|
"(list them with /agents); omit `agent` for the general-purpose one.",
|
|
345
351
|
promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
|
|
346
352
|
parameters,
|
|
353
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
354
|
+
renderShell: "self",
|
|
347
355
|
renderCall: renderSubagentCall,
|
|
348
356
|
renderResult: renderSubagentResult,
|
|
349
357
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -391,9 +399,12 @@ export function makeSubagentTool(deps = {}) {
|
|
|
391
399
|
},
|
|
392
400
|
});
|
|
393
401
|
const working = formatWorkingMessage(progresses, now);
|
|
394
|
-
if (
|
|
402
|
+
if (working !== lastWorking) {
|
|
395
403
|
lastWorking = working;
|
|
396
|
-
|
|
404
|
+
if (deps.workingLine)
|
|
405
|
+
deps.workingLine.setActivity(working);
|
|
406
|
+
else
|
|
407
|
+
ui?.setWorkingMessage?.(working);
|
|
397
408
|
}
|
|
398
409
|
};
|
|
399
410
|
emit();
|
|
@@ -423,7 +434,10 @@ export function makeSubagentTool(deps = {}) {
|
|
|
423
434
|
}
|
|
424
435
|
finally {
|
|
425
436
|
// Restore the default "Working…" text whether we resolved or threw.
|
|
426
|
-
|
|
437
|
+
if (deps.workingLine)
|
|
438
|
+
deps.workingLine.setActivity(undefined);
|
|
439
|
+
else
|
|
440
|
+
ui?.setWorkingMessage?.();
|
|
427
441
|
}
|
|
428
442
|
const allFailed = outcomes.every((o) => o.result.exitCode !== 0);
|
|
429
443
|
const sections = outcomes.map((o) => {
|
|
@@ -105,6 +105,7 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
|
|
|
105
105
|
label: string;
|
|
106
106
|
description: string;
|
|
107
107
|
promptSnippet: string;
|
|
108
|
+
promptGuidelines: string[];
|
|
108
109
|
parameters: Type.TObject<{
|
|
109
110
|
todos: Type.TArray<Type.TObject<{
|
|
110
111
|
text: Type.TString;
|
package/dist/extension/todos.js
CHANGED
|
@@ -204,6 +204,21 @@ export function makeTodoTool(get, set) {
|
|
|
204
204
|
"in_progress at a time, mark items completed the moment they are done, and add newly " +
|
|
205
205
|
"discovered steps as pending. Use it for any task with three or more steps, updating as you go.",
|
|
206
206
|
promptSnippet: "todo_write: keep a user-visible checklist for multi-step work (full-list replacement).",
|
|
207
|
+
promptGuidelines: [
|
|
208
|
+
"Use todo_write proactively when a task needs 3 or more distinct steps, requires careful " +
|
|
209
|
+
"planning, or the user gives you a list of things (numbered or comma-separated).",
|
|
210
|
+
"Capture new instructions as todos the moment you receive them, and mark a step in_progress " +
|
|
211
|
+
"BEFORE you start working on it.",
|
|
212
|
+
"When in doubt, use it — a visible checklist answers \"is it stuck?\" without the user having " +
|
|
213
|
+
"to interrupt.",
|
|
214
|
+
"Skip it when there is only one straightforward task, the work is trivial, or the request is " +
|
|
215
|
+
"purely conversational or informational — in those cases just do the task directly.",
|
|
216
|
+
"Pass the FULL list every call; it replaces the previous one. Keep exactly ONE item in_progress " +
|
|
217
|
+
"at a time.",
|
|
218
|
+
"Mark a step completed the moment it is done (do not batch completions), and add newly " +
|
|
219
|
+
"discovered steps as pending. Only mark a step completed when it is fully done — if tests " +
|
|
220
|
+
"fail or work is partial, leave it in_progress and add a new step for the blocker.",
|
|
221
|
+
],
|
|
207
222
|
parameters,
|
|
208
223
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
209
224
|
const normalized = normalizeTodos(params.todos);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run model for the condensed tool transcript (Claude Code-style).
|
|
3
|
+
*
|
|
4
|
+
* A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
|
|
5
|
+
* listings, successful shell commands, scratchpad edits) uninterrupted by an
|
|
6
|
+
* assistant/user message or a visible row (errors, project writes/edits, image
|
|
7
|
+
* reads). Collapsed, every row in a completed run renders zero lines except the
|
|
8
|
+
* run's tail, which paints one summary line: "Read 2 files, ran 2 shell
|
|
9
|
+
* commands". Expanded (ctrl+o) bypasses this model entirely.
|
|
10
|
+
*
|
|
11
|
+
* The tracker is built purely from render calls (idempotent upserts keyed by
|
|
12
|
+
* toolCallId, in first-render order, which matches display order both live and
|
|
13
|
+
* on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
|
|
14
|
+
* from live `message_start` events only — after a resume, runs that were
|
|
15
|
+
* separated by prose may merge into one summary. That is a deliberate trade:
|
|
16
|
+
* render-derived state is the only state that survives replay.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is PURE (no pi imports) so tests run against plain objects;
|
|
19
|
+
* the pi wiring lives in condensedTools.ts.
|
|
20
|
+
*/
|
|
21
|
+
/** Aggregation category of one tool row. */
|
|
22
|
+
export type RowKind = "read" | "shell" | "search" | "list" | "write" | "edit";
|
|
23
|
+
/** Facts about one tool row; `quiet` is derived, never stored. */
|
|
24
|
+
export interface ToolRow {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: RowKind;
|
|
27
|
+
/** The row's target path lives under the session scratchpad dir. */
|
|
28
|
+
scratchpad: boolean;
|
|
29
|
+
/** The tool call errored (visible regardless of kind). */
|
|
30
|
+
error: boolean;
|
|
31
|
+
/** The result carries inline images (a read screenshot must stay visible). */
|
|
32
|
+
images: boolean;
|
|
33
|
+
/** The result is final (not partial/streaming). */
|
|
34
|
+
final: boolean;
|
|
35
|
+
/** Added lines (scratchpad edits surface as "+N" in the summary). */
|
|
36
|
+
added: number;
|
|
37
|
+
/** An assistant/user message landed between the previous row and this one. */
|
|
38
|
+
breakBefore: boolean;
|
|
39
|
+
/** Repaint hook for this row's component (captured from the render context). */
|
|
40
|
+
invalidate?: () => void;
|
|
41
|
+
/** A deferred repaint is already queued for this row. */
|
|
42
|
+
invalidatePending?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export type ToolRowPatch = Partial<Pick<ToolRow, "kind" | "scratchpad" | "error" | "images" | "final" | "added" | "invalidate">>;
|
|
45
|
+
/** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
|
|
46
|
+
export declare function kindForTool(toolName: string): RowKind | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a row folds into a run summary. Reads, searches, listings, and
|
|
49
|
+
* successful shell commands always do; writes/edits only when they target the
|
|
50
|
+
* scratchpad (project mutations must stay visible). Errors and image-bearing
|
|
51
|
+
* results are always visible.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isQuiet(row: Pick<ToolRow, "kind" | "scratchpad" | "error" | "images">): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* One summary line for a completed run, phrases in first-occurrence order:
|
|
56
|
+
* "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
|
|
57
|
+
*/
|
|
58
|
+
export declare function summarizeRun(rows: readonly Pick<ToolRow, "kind" | "scratchpad" | "added">[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Ordered row registry. Upserts are idempotent and diff-aware: only a change
|
|
61
|
+
* invalidates the affected run's tail (the one component whose output depends
|
|
62
|
+
* on neighbors), so repaints converge instead of looping.
|
|
63
|
+
*/
|
|
64
|
+
export declare class ToolRunTracker {
|
|
65
|
+
private rows;
|
|
66
|
+
private indexById;
|
|
67
|
+
private breakPending;
|
|
68
|
+
/** Record an assistant/user message boundary; the next new row starts a fresh run. */
|
|
69
|
+
markBreak(): void;
|
|
70
|
+
get(id: string): ToolRow | undefined;
|
|
71
|
+
upsert(id: string, patch: ToolRowPatch): ToolRow;
|
|
72
|
+
/**
|
|
73
|
+
* The one summary line for `id`, present only when `id` is the tail of a
|
|
74
|
+
* fully-final quiet run. Every other member of the run gets undefined.
|
|
75
|
+
*/
|
|
76
|
+
summaryFor(id: string): string | undefined;
|
|
77
|
+
/** The contiguous quiet run containing `row` (just `[row]` when visible). */
|
|
78
|
+
private runOf;
|
|
79
|
+
private invalidateTailOf;
|
|
80
|
+
/**
|
|
81
|
+
* Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
|
|
82
|
+
* pi's synchronous `updateDisplay` pass (renderers call them), and the
|
|
83
|
+
* component's `invalidate()` re-enters `updateDisplay` immediately — a
|
|
84
|
+
* synchronous call from a renderer would rebuild the container while the
|
|
85
|
+
* outer frame is still appending to it, stacking duplicate children.
|
|
86
|
+
* Deferring means every repaint runs as its own clean top-level pass; it
|
|
87
|
+
* still cannot loop, because the diff-aware upsert only schedules on an
|
|
88
|
+
* actual change.
|
|
89
|
+
*/
|
|
90
|
+
private scheduleInvalidate;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=toolRuns.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run model for the condensed tool transcript (Claude Code-style).
|
|
3
|
+
*
|
|
4
|
+
* A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
|
|
5
|
+
* listings, successful shell commands, scratchpad edits) uninterrupted by an
|
|
6
|
+
* assistant/user message or a visible row (errors, project writes/edits, image
|
|
7
|
+
* reads). Collapsed, every row in a completed run renders zero lines except the
|
|
8
|
+
* run's tail, which paints one summary line: "Read 2 files, ran 2 shell
|
|
9
|
+
* commands". Expanded (ctrl+o) bypasses this model entirely.
|
|
10
|
+
*
|
|
11
|
+
* The tracker is built purely from render calls (idempotent upserts keyed by
|
|
12
|
+
* toolCallId, in first-render order, which matches display order both live and
|
|
13
|
+
* on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
|
|
14
|
+
* from live `message_start` events only — after a resume, runs that were
|
|
15
|
+
* separated by prose may merge into one summary. That is a deliberate trade:
|
|
16
|
+
* render-derived state is the only state that survives replay.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is PURE (no pi imports) so tests run against plain objects;
|
|
19
|
+
* the pi wiring lives in condensedTools.ts.
|
|
20
|
+
*/
|
|
21
|
+
const KIND_BY_TOOL = {
|
|
22
|
+
read: "read",
|
|
23
|
+
bash: "shell",
|
|
24
|
+
grep: "search",
|
|
25
|
+
find: "list",
|
|
26
|
+
ls: "list",
|
|
27
|
+
write: "write",
|
|
28
|
+
edit: "edit",
|
|
29
|
+
};
|
|
30
|
+
/** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
|
|
31
|
+
export function kindForTool(toolName) {
|
|
32
|
+
return KIND_BY_TOOL[toolName];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a row folds into a run summary. Reads, searches, listings, and
|
|
36
|
+
* successful shell commands always do; writes/edits only when they target the
|
|
37
|
+
* scratchpad (project mutations must stay visible). Errors and image-bearing
|
|
38
|
+
* results are always visible.
|
|
39
|
+
*/
|
|
40
|
+
export function isQuiet(row) {
|
|
41
|
+
if (row.error || row.images)
|
|
42
|
+
return false;
|
|
43
|
+
if (row.kind === "write" || row.kind === "edit")
|
|
44
|
+
return row.scratchpad;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function plural(n, singular, pluralForm = `${singular}s`) {
|
|
48
|
+
return n === 1 ? singular : pluralForm;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One summary line for a completed run, phrases in first-occurrence order:
|
|
52
|
+
* "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
|
|
53
|
+
*/
|
|
54
|
+
export function summarizeRun(rows) {
|
|
55
|
+
const order = [];
|
|
56
|
+
const counts = new Map();
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const category = row.kind === "write" || row.kind === "edit" ? "scratch" : row.kind;
|
|
59
|
+
let entry = counts.get(category);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
entry = { n: 0, added: 0 };
|
|
62
|
+
counts.set(category, entry);
|
|
63
|
+
order.push(category);
|
|
64
|
+
}
|
|
65
|
+
entry.n += 1;
|
|
66
|
+
entry.added += row.added;
|
|
67
|
+
}
|
|
68
|
+
const phrases = order.map((category) => {
|
|
69
|
+
const { n, added } = counts.get(category);
|
|
70
|
+
switch (category) {
|
|
71
|
+
case "read":
|
|
72
|
+
return `read ${n} ${plural(n, "file")}`;
|
|
73
|
+
case "shell":
|
|
74
|
+
return `ran ${n} shell ${plural(n, "command")}`;
|
|
75
|
+
case "search":
|
|
76
|
+
return `searched for ${n} ${plural(n, "pattern")}`;
|
|
77
|
+
case "list":
|
|
78
|
+
return `listed ${n} ${plural(n, "path")}`;
|
|
79
|
+
default:
|
|
80
|
+
return `made ${n} scratchpad ${plural(n, "edit")}${added > 0 ? ` +${added}` : ""}`;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const line = phrases.join(", ");
|
|
84
|
+
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Ordered row registry. Upserts are idempotent and diff-aware: only a change
|
|
88
|
+
* invalidates the affected run's tail (the one component whose output depends
|
|
89
|
+
* on neighbors), so repaints converge instead of looping.
|
|
90
|
+
*/
|
|
91
|
+
export class ToolRunTracker {
|
|
92
|
+
rows = [];
|
|
93
|
+
indexById = new Map();
|
|
94
|
+
breakPending = true;
|
|
95
|
+
/** Record an assistant/user message boundary; the next new row starts a fresh run. */
|
|
96
|
+
markBreak() {
|
|
97
|
+
this.breakPending = true;
|
|
98
|
+
}
|
|
99
|
+
get(id) {
|
|
100
|
+
const i = this.indexById.get(id);
|
|
101
|
+
return i === undefined ? undefined : this.rows[i];
|
|
102
|
+
}
|
|
103
|
+
upsert(id, patch) {
|
|
104
|
+
const existingIndex = this.indexById.get(id);
|
|
105
|
+
if (existingIndex === undefined) {
|
|
106
|
+
const row = {
|
|
107
|
+
id,
|
|
108
|
+
kind: patch.kind ?? "shell",
|
|
109
|
+
scratchpad: patch.scratchpad ?? false,
|
|
110
|
+
error: patch.error ?? false,
|
|
111
|
+
images: patch.images ?? false,
|
|
112
|
+
final: patch.final ?? false,
|
|
113
|
+
added: patch.added ?? 0,
|
|
114
|
+
breakBefore: this.breakPending,
|
|
115
|
+
...(patch.invalidate ? { invalidate: patch.invalidate } : {}),
|
|
116
|
+
};
|
|
117
|
+
this.breakPending = false;
|
|
118
|
+
this.indexById.set(id, this.rows.length);
|
|
119
|
+
this.rows.push(row);
|
|
120
|
+
// The previous row may have been its run's tail (painting a summary);
|
|
121
|
+
// now that the run extends past it, repaint it as a hidden member.
|
|
122
|
+
// (invalidateTailOf(prev) would be wrong here: prev's run now includes
|
|
123
|
+
// this new row, so its tail is the row currently painting, not prev.)
|
|
124
|
+
const prev = this.rows[this.rows.length - 2];
|
|
125
|
+
if (prev && isQuiet(prev) && !row.breakBefore)
|
|
126
|
+
this.scheduleInvalidate(prev);
|
|
127
|
+
return row;
|
|
128
|
+
}
|
|
129
|
+
const row = this.rows[existingIndex];
|
|
130
|
+
if (patch.invalidate)
|
|
131
|
+
row.invalidate = patch.invalidate;
|
|
132
|
+
let changed = false;
|
|
133
|
+
for (const key of ["kind", "scratchpad", "error", "images", "final", "added"]) {
|
|
134
|
+
const next = patch[key];
|
|
135
|
+
if (next !== undefined && row[key] !== next) {
|
|
136
|
+
row[key] = next;
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (changed)
|
|
141
|
+
this.invalidateTailOf(row);
|
|
142
|
+
return row;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The one summary line for `id`, present only when `id` is the tail of a
|
|
146
|
+
* fully-final quiet run. Every other member of the run gets undefined.
|
|
147
|
+
*/
|
|
148
|
+
summaryFor(id) {
|
|
149
|
+
const row = this.get(id);
|
|
150
|
+
if (!row || !isQuiet(row) || !row.final)
|
|
151
|
+
return undefined;
|
|
152
|
+
const run = this.runOf(row);
|
|
153
|
+
if (run[run.length - 1] !== row)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (run.some((r) => !r.final))
|
|
156
|
+
return undefined;
|
|
157
|
+
return summarizeRun(run);
|
|
158
|
+
}
|
|
159
|
+
/** The contiguous quiet run containing `row` (just `[row]` when visible). */
|
|
160
|
+
runOf(row) {
|
|
161
|
+
if (!isQuiet(row))
|
|
162
|
+
return [row];
|
|
163
|
+
const i = this.indexById.get(row.id);
|
|
164
|
+
let start = i;
|
|
165
|
+
while (start > 0 && !this.rows[start].breakBefore && isQuiet(this.rows[start - 1]))
|
|
166
|
+
start--;
|
|
167
|
+
let end = i;
|
|
168
|
+
while (end < this.rows.length - 1 &&
|
|
169
|
+
!this.rows[end + 1].breakBefore &&
|
|
170
|
+
isQuiet(this.rows[end + 1])) {
|
|
171
|
+
end++;
|
|
172
|
+
}
|
|
173
|
+
return this.rows.slice(start, end + 1);
|
|
174
|
+
}
|
|
175
|
+
invalidateTailOf(row) {
|
|
176
|
+
const run = this.runOf(row);
|
|
177
|
+
const tail = run[run.length - 1];
|
|
178
|
+
if (tail)
|
|
179
|
+
this.scheduleInvalidate(tail);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
|
|
183
|
+
* pi's synchronous `updateDisplay` pass (renderers call them), and the
|
|
184
|
+
* component's `invalidate()` re-enters `updateDisplay` immediately — a
|
|
185
|
+
* synchronous call from a renderer would rebuild the container while the
|
|
186
|
+
* outer frame is still appending to it, stacking duplicate children.
|
|
187
|
+
* Deferring means every repaint runs as its own clean top-level pass; it
|
|
188
|
+
* still cannot loop, because the diff-aware upsert only schedules on an
|
|
189
|
+
* actual change.
|
|
190
|
+
*/
|
|
191
|
+
scheduleInvalidate(row) {
|
|
192
|
+
if (row.invalidatePending)
|
|
193
|
+
return;
|
|
194
|
+
row.invalidatePending = true;
|
|
195
|
+
queueMicrotask(() => {
|
|
196
|
+
row.invalidatePending = false;
|
|
197
|
+
row.invalidate?.();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=toolRuns.js.map
|
|
@@ -12,41 +12,20 @@
|
|
|
12
12
|
* rotating file under `~/.yagni-code/logs/`, fail-soft, hermetic under
|
|
13
13
|
* `node --test`.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { readdirSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { _setErrorSinkHomeForTest, logEvent, errorSinkPath, } from "./errorSink.js";
|
|
18
18
|
/** Test seam: point the log at a tmpdir (mirrors _setDiagnosticsHomeForTest). */
|
|
19
19
|
let homeOverride = null;
|
|
20
20
|
export function _setTurnLogHomeForTest(dir) {
|
|
21
21
|
homeOverride = dir;
|
|
22
|
+
_setErrorSinkHomeForTest(dir);
|
|
22
23
|
}
|
|
23
24
|
function turnLogDir() {
|
|
24
|
-
return
|
|
25
|
+
return dirname(errorSinkPath());
|
|
25
26
|
}
|
|
26
27
|
export function turnLogPath() {
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
const MAX_LOG_BYTES = 256 * 1024;
|
|
30
|
-
const KEEP_ROTATIONS = 2;
|
|
31
|
-
/** Shift turn-lifecycle.log -> .1 -> .2 once the active file passes the cap. */
|
|
32
|
-
function rotateIfNeeded(path) {
|
|
33
|
-
try {
|
|
34
|
-
if (!statSync(path).isFile() || statSync(path).size < MAX_LOG_BYTES)
|
|
35
|
-
return;
|
|
36
|
-
for (let i = KEEP_ROTATIONS; i >= 1; i--) {
|
|
37
|
-
const from = i === 1 ? path : `${path}.${i - 1}`;
|
|
38
|
-
const to = `${path}.${i}`;
|
|
39
|
-
try {
|
|
40
|
-
renameSync(from, to);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
/* absent source — fine */
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
/* rotation is best-effort */
|
|
49
|
-
}
|
|
28
|
+
return errorSinkPath();
|
|
50
29
|
}
|
|
51
30
|
/**
|
|
52
31
|
* Append one sanitized lifecycle record. Fail-soft — a logging failure must
|
|
@@ -54,16 +33,15 @@ function rotateIfNeeded(path) {
|
|
|
54
33
|
* raw error message.
|
|
55
34
|
*/
|
|
56
35
|
export function logTurnLifecycle(ev) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...(ev.sessionId ? { sessionId: ev.sessionId } : {}),
|
|
36
|
+
// Turn lifecycle is the crash-survivable WAL: it must flush SYNCHRONOUSLY so
|
|
37
|
+
// a turn that starts but never ends still leaves a durable turn_start.
|
|
38
|
+
logEvent({
|
|
39
|
+
source: "turn",
|
|
40
|
+
level: "info",
|
|
41
|
+
event: ev.kind,
|
|
42
|
+
sessionId: ev.sessionId,
|
|
43
|
+
flush: "sync",
|
|
44
|
+
fields: {
|
|
67
45
|
...((ev.kind === "turn_start" || ev.kind === "turn_end") && ev.turnIndex !== undefined ? { turnIndex: ev.turnIndex } : {}),
|
|
68
46
|
...(ev.kind === "turn_end" && ev.stopReason !== undefined ? { stopReason: ev.stopReason } : {}),
|
|
69
47
|
...(ev.kind === "turn_end" && ev.elapsedMs !== undefined ? { elapsedMs: ev.elapsedMs } : {}),
|
|
@@ -71,22 +49,15 @@ export function logTurnLifecycle(ev) {
|
|
|
71
49
|
...(ev.kind === "silent_turn" && ev.turnsSinceSpoke !== undefined ? { turnsSinceSpoke: ev.turnsSinceSpoke } : {}),
|
|
72
50
|
...(ev.kind === "silent_turn_nudge" && ev.nudgesInStretch !== undefined ? { nudgesInStretch: ev.nudgesInStretch } : {}),
|
|
73
51
|
...(ev.kind === "silent_turn_nudge" && ev.secondsSinceSpoke !== undefined ? { secondsSinceSpoke: ev.secondsSinceSpoke } : {}),
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
77
|
-
rotateIfNeeded(path);
|
|
78
|
-
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
/* logging must never throw into the editor */
|
|
82
|
-
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
83
54
|
}
|
|
84
55
|
/** List existing lifecycle log files (active + rotations), for a report. */
|
|
85
56
|
export function listTurnLogFiles() {
|
|
86
57
|
try {
|
|
87
58
|
const dir = turnLogDir();
|
|
88
59
|
return readdirSync(dir)
|
|
89
|
-
.filter((f) => f.startsWith("
|
|
60
|
+
.filter((f) => f.startsWith("errors-"))
|
|
90
61
|
.sort()
|
|
91
62
|
.map((f) => join(dir, f));
|
|
92
63
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebFetch core: fetch + convert + extract, adapted from Claude Code's
|
|
3
|
+
* WebFetchTool (src/tools/WebFetchTool/utils.ts). Pure and pi-free so it is
|
|
4
|
+
* fully unit-testable against fixtures, mirroring resilientFetch.ts.
|
|
5
|
+
*
|
|
6
|
+
* Three stages:
|
|
7
|
+
* 1. FETCH an arbitrary URL with a PLAIN fetch impl (deliberately NOT the
|
|
8
|
+
* authed fetch — see the token-leak note below).
|
|
9
|
+
* 2. CONVERT HTML → markdown via turndown; non-HTML passes through raw.
|
|
10
|
+
* 3. EXTRACT a `standard`-tier completion against the user's prompt.
|
|
11
|
+
*
|
|
12
|
+
* SECURITY (token leak): the extension's `makeAuthedFetch` transparently
|
|
13
|
+
* re-sends a second request WITH a fresh `Bearer $YAGNI_TOKEN` whenever the
|
|
14
|
+
* first returns 401 (tokenProvider.ts). Routing that wrapper at an arbitrary
|
|
15
|
+
* host would leak the token to any site that answers 401. The URL fetch must
|
|
16
|
+
* therefore use a plain global fetch, never opts.fetchImpl.
|
|
17
|
+
*/
|
|
18
|
+
/** Upper bound on the markdown handed to the extraction model (Claude's cap). */
|
|
19
|
+
export declare const MAX_MARKDOWN_LENGTH = 100000;
|
|
20
|
+
/** Upper bound on the raw HTTP body we will accept (Claude's PSR cap). */
|
|
21
|
+
export declare const MAX_HTTP_CONTENT_LENGTH: number;
|
|
22
|
+
/** Per-request wall clock for the URL fetch (Claude's 60s). */
|
|
23
|
+
export declare const FETCH_TIMEOUT_MS = 60000;
|
|
24
|
+
/** Same-host redirect hop cap (Claude's 10, matching common client defaults). */
|
|
25
|
+
export declare const MAX_REDIRECTS = 10;
|
|
26
|
+
/** The marker the tool returns when a redirect jumps host. */
|
|
27
|
+
export interface RedirectInfo {
|
|
28
|
+
type: "redirect";
|
|
29
|
+
originalUrl: string;
|
|
30
|
+
redirectUrl: string;
|
|
31
|
+
statusCode: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FetchedContent {
|
|
34
|
+
content: string;
|
|
35
|
+
bytes: number;
|
|
36
|
+
code: number;
|
|
37
|
+
codeText: string;
|
|
38
|
+
contentType: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reject URLs that could exfiltrate credentials or reach the user's own
|
|
42
|
+
* machine/network. Ported from Claude's validateURL: >2k chars, embedded
|
|
43
|
+
* userinfo, and single-label (non-public) hostnames are all refused.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateUrl(url: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a redirect may be followed: same protocol/port, no userinfo, and the
|
|
48
|
+
* host differs only by an optional leading `www.`. Ported from Claude.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isPermittedRedirect(originalUrl: string, redirectUrl: string): boolean;
|
|
51
|
+
export interface FetchMarkdownOptions {
|
|
52
|
+
/** Plain global fetch for the arbitrary URL (never an authed wrapper). */
|
|
53
|
+
fetchImpl?: typeof fetch;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fetch `url` (following only same-host redirects) and convert the body to
|
|
58
|
+
* markdown. Returns the converted content, or a {@link RedirectInfo} when the
|
|
59
|
+
* response routes to a different host (the caller re-invokes with the new URL).
|
|
60
|
+
*/
|
|
61
|
+
export declare function fetchMarkdown(url: string, opts?: FetchMarkdownOptions): Promise<FetchedContent | RedirectInfo>;
|
|
62
|
+
export interface ExtractionOptions {
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
getToken: () => string | undefined;
|
|
65
|
+
/** The authed fetch (makeAuthedFetch) for the extraction call. */
|
|
66
|
+
fetchImpl?: typeof fetch;
|
|
67
|
+
attribution: () => Record<string, string>;
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the extraction prompt: the fetched content plus the user's prompt plus
|
|
72
|
+
* the "answer only from the content" guardrails, mirroring Claude's
|
|
73
|
+
* makeSecondaryModelPrompt.
|
|
74
|
+
*/
|
|
75
|
+
export declare function makeExtractionPrompt(markdownContent: string, prompt: string): string;
|
|
76
|
+
export interface ExtractResult {
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run the extraction: one `standard`-tier completion against the metric proxy
|
|
81
|
+
* (the same /v1/chat/completions surface pi's `yagni` provider drives). The
|
|
82
|
+
* content was already fetched locally; only this summarization rides the proxy.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extract(markdownContent: string, prompt: string, opts: ExtractionOptions): Promise<ExtractResult>;
|
|
85
|
+
//# sourceMappingURL=webFetch.d.ts.map
|