@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1002.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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client-side decisions surface (spec §4, W4): the `/decide` and
|
|
3
|
+
* `/decisions` commands plus the shared write helper the bless-capture flow
|
|
4
|
+
* reuses.
|
|
5
|
+
*
|
|
6
|
+
* - `bankDecision` POSTs a `source: 'cli_correction'` decision with a generated
|
|
7
|
+
* idempotencyKey through the durable write-spool (part A), so a transport
|
|
8
|
+
* failure or 5xx spools it for replay instead of losing it, and a replay can
|
|
9
|
+
* never double-bank (server dedups on the key).
|
|
10
|
+
* - `fetchDecisions` / `supersedeDecision` reuse the shared authed-fetch seam,
|
|
11
|
+
* so a GET/PATCH inherits the one 401-refresh-then-retry-once behaviour.
|
|
12
|
+
* - `/decide <question> => <decision>` records ONE decision directly (the user
|
|
13
|
+
* typed it — explicit intent, no extra confirm or debounce).
|
|
14
|
+
* - `/decisions` lists recent decisions (id-prefixed, compact); `/decisions
|
|
15
|
+
* supersede <id>` flips one to superseded.
|
|
16
|
+
*
|
|
17
|
+
* Formatters and the arg parser are pure; the network calls are injectable via
|
|
18
|
+
* the shared `SpoolClientOpts` so tests never touch disk or the network.
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
22
|
+
import { sendOrSpool } from "./spool.js";
|
|
23
|
+
/**
|
|
24
|
+
* Bank a `cli_correction` decision durably. Every logical write carries a
|
|
25
|
+
* generated idempotencyKey; the backend dedups on it, so a spool replay or a
|
|
26
|
+
* 401 retry can never bank the same decision twice. Transport failures and 5xx
|
|
27
|
+
* are spooled for replay instead of lost (R4 write half). Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export function bankDecision(opts, params, signal) {
|
|
30
|
+
const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
|
|
31
|
+
return sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
|
|
32
|
+
question: params.question,
|
|
33
|
+
decision: params.decision,
|
|
34
|
+
rationale: params.rationale,
|
|
35
|
+
repo: params.repo,
|
|
36
|
+
source: "cli_correction",
|
|
37
|
+
}, idempotencyKey, signal);
|
|
38
|
+
}
|
|
39
|
+
export const DEFAULT_LIST_LIMIT = 20;
|
|
40
|
+
/** GET recent decisions (any read scope). Throws a friendly error on non-2xx. */
|
|
41
|
+
export async function fetchDecisions(opts, list = {}, signal) {
|
|
42
|
+
const limit = list.limit ?? DEFAULT_LIST_LIMIT;
|
|
43
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
44
|
+
if (list.repo)
|
|
45
|
+
query.set("repo", list.repo);
|
|
46
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/decisions?${query.toString()}`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl, signal });
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error(await friendlyFetchError("list decisions", res));
|
|
49
|
+
const data = (await res.json());
|
|
50
|
+
return Array.isArray(data.decisions) ? data.decisions : [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* PATCH one decision to `superseded` (scope yagni_code:write). Idempotent flip,
|
|
54
|
+
* so a single attempt is enough — no client retry (invariant §0.8). Throws a
|
|
55
|
+
* friendly error on non-2xx.
|
|
56
|
+
*/
|
|
57
|
+
export async function supersedeDecision(opts, id, supersededBy, signal) {
|
|
58
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/decisions/${encodeURIComponent(id)}`, {
|
|
59
|
+
method: "PATCH",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ status: "superseded", ...(supersededBy ? { supersededBy } : {}) }),
|
|
65
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
66
|
+
if (!res.ok)
|
|
67
|
+
throw new Error(await friendlyFetchError("supersede decision", res));
|
|
68
|
+
}
|
|
69
|
+
/** Pure: parse `<question> => <decision>`; null when either half is missing. */
|
|
70
|
+
export function parseDecideArgs(raw) {
|
|
71
|
+
const idx = raw.indexOf("=>");
|
|
72
|
+
if (idx < 0)
|
|
73
|
+
return null;
|
|
74
|
+
const question = raw.slice(0, idx).trim();
|
|
75
|
+
const decision = raw.slice(idx + 2).trim();
|
|
76
|
+
if (!question || !decision)
|
|
77
|
+
return null;
|
|
78
|
+
return { question, decision };
|
|
79
|
+
}
|
|
80
|
+
/** Pure: a short id prefix for compact display. */
|
|
81
|
+
export function shortId(id) {
|
|
82
|
+
return id.slice(0, 8);
|
|
83
|
+
}
|
|
84
|
+
/** Pure: render the compact, id-prefixed decisions list (no em-dashes). */
|
|
85
|
+
export function formatDecisionsList(items) {
|
|
86
|
+
if (items.length === 0) {
|
|
87
|
+
return "No decisions recorded yet. Use /decide <question> => <decision> to bank one.";
|
|
88
|
+
}
|
|
89
|
+
const header = `Recorded decisions (${items.length}):`;
|
|
90
|
+
const lines = items.map((d) => {
|
|
91
|
+
const flag = d.status === "active" ? "" : ` [${d.status}]`;
|
|
92
|
+
const cited = d.citedCount > 0 ? ` (cited ${d.citedCount})` : "";
|
|
93
|
+
return `${shortId(d.id)}${flag} ${d.question} => ${d.decision}${cited}`;
|
|
94
|
+
});
|
|
95
|
+
return [header, ...lines].join("\n");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a displayed short-id prefix (or a full id) to the full decision id by
|
|
99
|
+
* matching against the current list. Returns the full id, the raw arg when no
|
|
100
|
+
* match is found (let the backend answer an honest 404), or "ambiguous" when
|
|
101
|
+
* more than one decision shares the prefix. Fail-soft: a list error falls back
|
|
102
|
+
* to the raw arg.
|
|
103
|
+
*/
|
|
104
|
+
async function resolveDecisionId(opts, arg, signal) {
|
|
105
|
+
let items = [];
|
|
106
|
+
try {
|
|
107
|
+
items = await fetchDecisions(opts, {}, signal);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return arg; // cannot resolve → let the PATCH itself surface the outcome
|
|
111
|
+
}
|
|
112
|
+
const matches = items.filter((d) => d.id === arg || d.id.startsWith(arg));
|
|
113
|
+
if (matches.length === 1)
|
|
114
|
+
return matches[0].id;
|
|
115
|
+
if (matches.length > 1)
|
|
116
|
+
return "ambiguous";
|
|
117
|
+
return arg;
|
|
118
|
+
}
|
|
119
|
+
/** A friendly one-liner for a bank outcome, or null when nothing to say. */
|
|
120
|
+
function bankOutcomeNotice(outcome) {
|
|
121
|
+
if (outcome.kind === "ok")
|
|
122
|
+
return { message: "Recorded the decision in YAGNI.", type: "info" };
|
|
123
|
+
if (outcome.kind === "spooled") {
|
|
124
|
+
return {
|
|
125
|
+
message: `Could not reach YAGNI (${outcome.reason}). Saved the decision locally; it will sync automatically.`,
|
|
126
|
+
type: "warning",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { message: outcome.message, type: "error" };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wire the `/decide` and `/decisions` commands. Both are fail-soft: any
|
|
133
|
+
* transport error surfaces as an honest notice, never a thrown command.
|
|
134
|
+
*/
|
|
135
|
+
export function registerDecisionCommands(pi, opts) {
|
|
136
|
+
pi.registerCommand("decide", {
|
|
137
|
+
description: "Record a product-intent decision directly: /decide <question> => <decision>. YAGNI answers the same question next time.",
|
|
138
|
+
handler: async (args, ctx) => {
|
|
139
|
+
const notify = (m, t) => {
|
|
140
|
+
if (ctx.hasUI)
|
|
141
|
+
ctx.ui.notify(m, t);
|
|
142
|
+
};
|
|
143
|
+
const parsed = parseDecideArgs(args);
|
|
144
|
+
if (!parsed) {
|
|
145
|
+
notify("Usage: /decide <question> => <decision>", "warning");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const outcome = await bankDecision(opts, { question: parsed.question, decision: parsed.decision }, ctx.signal);
|
|
150
|
+
const notice = bankOutcomeNotice(outcome);
|
|
151
|
+
notify(notice.message, notice.type);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
notify(`Could not record the decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
pi.registerCommand("decisions", {
|
|
159
|
+
description: "List recent recorded decisions. /decisions supersede <id> marks one superseded.",
|
|
160
|
+
handler: async (args, ctx) => {
|
|
161
|
+
const notify = (m, t) => {
|
|
162
|
+
if (ctx.hasUI)
|
|
163
|
+
ctx.ui.notify(m, t);
|
|
164
|
+
};
|
|
165
|
+
const trimmed = args.trim();
|
|
166
|
+
if (trimmed.toLowerCase().startsWith("supersede")) {
|
|
167
|
+
const arg = trimmed.slice("supersede".length).trim();
|
|
168
|
+
if (!arg) {
|
|
169
|
+
notify("Usage: /decisions supersede <id>", "warning");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
// The list shows short id prefixes, so resolve the argument against
|
|
174
|
+
// the current list to the full UUID the backend PATCH requires. A
|
|
175
|
+
// failed lookup falls through with the raw arg (the backend then
|
|
176
|
+
// answers an honest 404).
|
|
177
|
+
const id = await resolveDecisionId(opts, arg, ctx.signal);
|
|
178
|
+
if (id === "ambiguous") {
|
|
179
|
+
notify(`Ambiguous decision id "${arg}"; use more characters.`, "warning");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await supersedeDecision(opts, id, undefined, ctx.signal);
|
|
183
|
+
notify(`Superseded decision ${shortId(id)}.`, "info");
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
notify(`Could not supersede decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const items = await fetchDecisions(opts, {}, ctx.signal);
|
|
192
|
+
await pi.sendUserMessage(formatDecisionsList(items));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
notify(`Could not list decisions: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=decisions.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI diagnostics for the image-paste feature — layer C of the
|
|
3
|
+
* observability decision (A=debug flag, B=backend events, C=local log+report).
|
|
4
|
+
*
|
|
5
|
+
* The CLI is a terminal app, so it cannot log to stdout without corrupting the
|
|
6
|
+
* TUI. Instead it appends sanitized, single-line JSON events to a small
|
|
7
|
+
* rotating log under `~/.yagni-code/logs/`. "Sanitized" means event names,
|
|
8
|
+
* counts, mime types, byte sizes, and boolean outcomes — NEVER image bytes,
|
|
9
|
+
* message content, tokens, or file contents. Paths are recorded by basename
|
|
10
|
+
* only (the tmp `clipboard-*.png` name), never the full directory.
|
|
11
|
+
*
|
|
12
|
+
* A support flow can tail this file and POST it with the user's consent;
|
|
13
|
+
* nothing is uploaded automatically.
|
|
14
|
+
*/
|
|
15
|
+
export declare function _setDiagnosticsHomeForTest(dir: string | null): void;
|
|
16
|
+
export declare function diagnosticsLogPath(): string;
|
|
17
|
+
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
18
|
+
export declare function isDebug(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
export interface ImagePasteEvent {
|
|
20
|
+
/** Stable event name: "paste_path" | "chip_drop" | "transform" | "backend_status". */
|
|
21
|
+
event: string;
|
|
22
|
+
/** Outcome: "ok" | "passthrough" | "error" | a status code as string. */
|
|
23
|
+
outcome?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
bytes?: number;
|
|
26
|
+
imageCount?: number;
|
|
27
|
+
/** basename of the tmp image file only (never a full path). */
|
|
28
|
+
file?: string;
|
|
29
|
+
/** Extra debug-only detail; written only when YAGNI_DEBUG is on. */
|
|
30
|
+
detail?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Append one sanitized event. Fail-soft: a logging failure must never break the
|
|
34
|
+
* prompt. `detail` is included only when YAGNI_DEBUG is on.
|
|
35
|
+
*/
|
|
36
|
+
export declare function logImagePaste(ev: ImagePasteEvent): void;
|
|
37
|
+
/** Read the most recent log content (for a user-triggered report). */
|
|
38
|
+
export declare function readRecentDiagnostics(maxBytes?: number): string;
|
|
39
|
+
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
40
|
+
export declare function listDiagnosticFiles(): string[];
|
|
41
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI diagnostics for the image-paste feature — layer C of the
|
|
3
|
+
* observability decision (A=debug flag, B=backend events, C=local log+report).
|
|
4
|
+
*
|
|
5
|
+
* The CLI is a terminal app, so it cannot log to stdout without corrupting the
|
|
6
|
+
* TUI. Instead it appends sanitized, single-line JSON events to a small
|
|
7
|
+
* rotating log under `~/.yagni-code/logs/`. "Sanitized" means event names,
|
|
8
|
+
* counts, mime types, byte sizes, and boolean outcomes — NEVER image bytes,
|
|
9
|
+
* message content, tokens, or file contents. Paths are recorded by basename
|
|
10
|
+
* only (the tmp `clipboard-*.png` name), never the full directory.
|
|
11
|
+
*
|
|
12
|
+
* A support flow can tail this file and POST it with the user's consent;
|
|
13
|
+
* nothing is uploaded automatically.
|
|
14
|
+
*/
|
|
15
|
+
import { appendFileSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, } from "node:fs";
|
|
16
|
+
import { dirname, join, basename } from "node:path";
|
|
17
|
+
import { codeStateHome } from "./stateHome.js";
|
|
18
|
+
/** Test seam (mirrors `_setYagniCodeHomeForTest`): point the log at a tmpdir. */
|
|
19
|
+
let homeOverride = null;
|
|
20
|
+
export function _setDiagnosticsHomeForTest(dir) {
|
|
21
|
+
homeOverride = dir;
|
|
22
|
+
}
|
|
23
|
+
function yagniCodeHome() {
|
|
24
|
+
return codeStateHome(homeOverride);
|
|
25
|
+
}
|
|
26
|
+
export function diagnosticsLogPath() {
|
|
27
|
+
return join(yagniCodeHome(), "logs", "image-paste.log");
|
|
28
|
+
}
|
|
29
|
+
const MAX_LOG_BYTES = 256 * 1024; // rotate the active file past this
|
|
30
|
+
const KEEP_ROTATIONS = 2; // keep image-paste.log.1 and .2 alongside the active file
|
|
31
|
+
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
32
|
+
export function isDebug(env = process.env) {
|
|
33
|
+
const v = env.YAGNI_DEBUG;
|
|
34
|
+
return v === "1" || v === "true";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Append one sanitized event. Fail-soft: a logging failure must never break the
|
|
38
|
+
* prompt. `detail` is included only when YAGNI_DEBUG is on.
|
|
39
|
+
*/
|
|
40
|
+
export function logImagePaste(ev) {
|
|
41
|
+
try {
|
|
42
|
+
// Hermetic under `node --test`: never touch the real home dir from a test
|
|
43
|
+
// unless the test explicitly stubbed it. Writing to the runner's $HOME made
|
|
44
|
+
// the suite's exit depend on the CI filesystem (a read-only or slow $HOME
|
|
45
|
+
// could stall the write), which is the leading suspect for the CI hang.
|
|
46
|
+
if (process.env.NODE_TEST_CONTEXT && homeOverride === null)
|
|
47
|
+
return;
|
|
48
|
+
const line = {
|
|
49
|
+
ts: new Date().toISOString(),
|
|
50
|
+
event: ev.event,
|
|
51
|
+
...(ev.outcome !== undefined ? { outcome: ev.outcome } : {}),
|
|
52
|
+
...(ev.mimeType !== undefined ? { mimeType: ev.mimeType } : {}),
|
|
53
|
+
...(ev.bytes !== undefined ? { bytes: ev.bytes } : {}),
|
|
54
|
+
...(ev.imageCount !== undefined ? { imageCount: ev.imageCount } : {}),
|
|
55
|
+
...(ev.file !== undefined ? { file: basename(ev.file) } : {}),
|
|
56
|
+
...(ev.detail !== undefined && isDebug() ? { detail: ev.detail } : {}),
|
|
57
|
+
};
|
|
58
|
+
const path = diagnosticsLogPath();
|
|
59
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
60
|
+
rotateIfNeeded(path);
|
|
61
|
+
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* logging must never throw into the editor */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Shift image-paste.log -> .1 -> .2 once the active file passes the size cap. */
|
|
68
|
+
function rotateIfNeeded(path) {
|
|
69
|
+
try {
|
|
70
|
+
if (statSync(path).size < MAX_LOG_BYTES)
|
|
71
|
+
return;
|
|
72
|
+
for (let i = KEEP_ROTATIONS; i >= 1; i--) {
|
|
73
|
+
const from = i === 1 ? path : `${path}.${i - 1}`;
|
|
74
|
+
const to = `${path}.${i}`;
|
|
75
|
+
try {
|
|
76
|
+
renameSync(from, to);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* absent source — fine */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* rotation is best-effort */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Read the most recent log content (for a user-triggered report). */
|
|
88
|
+
export function readRecentDiagnostics(maxBytes = 64 * 1024) {
|
|
89
|
+
try {
|
|
90
|
+
const data = readFileSync(diagnosticsLogPath(), "utf8");
|
|
91
|
+
return data.length > maxBytes ? data.slice(data.length - maxBytes) : data;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
98
|
+
export function listDiagnosticFiles() {
|
|
99
|
+
try {
|
|
100
|
+
const dir = dirname(diagnosticsLogPath());
|
|
101
|
+
return readdirSync(dir)
|
|
102
|
+
.filter((f) => f.startsWith("image-paste.log"))
|
|
103
|
+
.sort()
|
|
104
|
+
.map((f) => join(dir, f));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
3
|
+
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
4
|
+
import { type TokenProvider } from "./tokenProvider.js";
|
|
5
|
+
import { type ContextBrief, type ModelEntry } from "./config.js";
|
|
6
|
+
/**
|
|
7
|
+
* YAGNI Code extension entry point.
|
|
8
|
+
*
|
|
9
|
+
* pi awaits async factory functions before startup, so fetching the model
|
|
10
|
+
* catalog here is safe. The factory:
|
|
11
|
+
* 1. registers the `yagni` model provider (catalog fetched from the backend),
|
|
12
|
+
* 2. registers the `ask_yagni` tool,
|
|
13
|
+
* 3. rebrands the system prompt as YAGNI Code on every turn (no "pi" leaks), and
|
|
14
|
+
* 4. sets the terminal title + a footer brand mark.
|
|
15
|
+
*
|
|
16
|
+
* If the catalog fetch fails, the error propagates (fail loud) carrying an
|
|
17
|
+
* actionable `yagni login` message.
|
|
18
|
+
*/
|
|
19
|
+
/** Injectable seams so the factory is unit-testable without pi or a network. */
|
|
20
|
+
export interface RegisterYagniDeps {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
getToken?: () => string | undefined;
|
|
23
|
+
/** The token's ISO expiry, forwarded by the launcher (for the expiry notice). */
|
|
24
|
+
getTokenExpiresAt?: () => string | undefined;
|
|
25
|
+
/** Clock seam for the expiry notice (defaults to Date.now). */
|
|
26
|
+
now?: () => number;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
fetchCatalog?: (opts: {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
getToken: () => string | undefined;
|
|
31
|
+
fetchImpl?: typeof fetch;
|
|
32
|
+
}) => Promise<ModelEntry[]>;
|
|
33
|
+
fetchContextBrief?: (opts: {
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
getToken: () => string | undefined;
|
|
36
|
+
fetchImpl?: typeof fetch;
|
|
37
|
+
}) => Promise<ContextBrief | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
40
|
+
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
|
41
|
+
*/
|
|
42
|
+
runInitPass?: typeof defaultRunInitPass;
|
|
43
|
+
/** The workspace id the token is bound to (keys the one-time init marker). */
|
|
44
|
+
getWorkspaceId?: () => string | undefined;
|
|
45
|
+
/** Has the init pass already run once for this workspace? Injectable (no disk in tests). */
|
|
46
|
+
isInitDone?: (workspaceId: string) => boolean;
|
|
47
|
+
/** Record that the init pass ran once for this workspace. Injectable (no disk in tests). */
|
|
48
|
+
markInitDone?: (workspaceId: string) => void;
|
|
49
|
+
/**
|
|
50
|
+
* The refreshing token provider (spec §4). Injectable so tests control the
|
|
51
|
+
* rotation surface; the default provider seeds from `getToken`/
|
|
52
|
+
* `getTokenExpiresAt` (falling back to the env) and owns the proactive
|
|
53
|
+
* refresh timer + the 401 refresh-and-retry-once seam.
|
|
54
|
+
*/
|
|
55
|
+
tokenProvider?: TokenProvider;
|
|
56
|
+
/** The spool flush (R4 write half), injectable so tests never touch disk. */
|
|
57
|
+
flushSpool?: (opts: SpoolClientOpts) => Promise<FlushOutcome>;
|
|
58
|
+
env?: NodeJS.ProcessEnv;
|
|
59
|
+
}
|
|
60
|
+
export declare function registerYagni(pi: ExtensionAPI, deps?: RegisterYagniDeps): Promise<void>;
|
|
61
|
+
export default function (pi: ExtensionAPI): Promise<void>;
|
|
62
|
+
export { makeAskYagniTool } from "./askYagniTool.js";
|
|
63
|
+
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
64
|
+
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
65
|
+
export type { Citation, MakeAskYagniToolOptions } from "./askYagniTool.js";
|
|
66
|
+
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
67
|
+
export type { MakeReviewToolOptions } from "./reviewTool.js";
|
|
68
|
+
export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
69
|
+
export type { MakeRecordContextToolOptions } from "./recordContextTool.js";
|
|
70
|
+
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
71
|
+
export type { MakeRecordDecisionToolOptions } from "./recordDecisionTool.js";
|
|
72
|
+
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
73
|
+
export type { MakeNextWorkToolOptions, DefaultNextAction, NextActionOption, } from "./nextWorkTool.js";
|
|
74
|
+
export { recordDecision } from "./recordDecisionTool.js";
|
|
75
|
+
export type { RecordDecisionParams } from "./recordDecisionTool.js";
|
|
76
|
+
export { runInitPass, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
77
|
+
export type { RunInitPassDeps, InitPassOutcome, RepoIntake, RepoIntakeFs, RepoAdr, EngineeringDraft, DraftTeam, } from "./initPass.js";
|
|
78
|
+
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
79
|
+
export { brandSystemPrompt, YAGNI_IDENTITY, BRAND_NAME } from "./branding.js";
|
|
80
|
+
export { fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl } from "./config.js";
|
|
81
|
+
export type { FetchCatalogOptions, ModelEntry } from "./config.js";
|
|
82
|
+
export { buildYagniProvider } from "./provider.js";
|
|
83
|
+
export { registerGoCommand } from "./pipeline/goCommand.js";
|
|
84
|
+
export type { RegisterGoDeps } from "./pipeline/goCommand.js";
|
|
85
|
+
export { runPipeline } from "./pipeline/orchestrator.js";
|
|
86
|
+
export type { RunPipelineDeps } from "./pipeline/orchestrator.js";
|
|
87
|
+
export { withResilience, classifyTransient, composeAbortSignal } from "./pipeline/resilience.js";
|
|
88
|
+
export type { ResilienceAttemptRecord, ResilienceSeams, RunStageFn } from "./pipeline/resilience.js";
|
|
89
|
+
export { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
90
|
+
export type { ResiliencePolicy } from "./pipeline/types.js";
|
|
91
|
+
export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures } from "./pipeline/verify.js";
|
|
92
|
+
export type { VerifyCommand, VerifyOutcome } from "./pipeline/verify.js";
|
|
93
|
+
export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
|
|
94
|
+
export type { ComparisonReport, LaneFit, LaneOutcome } from "./pipeline/eval.js";
|
|
95
|
+
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
96
|
+
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
97
|
+
export type { SubagentDef, SubagentSource } from "./subagents.js";
|
|
98
|
+
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
99
|
+
export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
|
|
100
|
+
export { decideGate, registerPermissionGate, filterStalePlanContext, DEFAULT_PERMISSION_POLICY, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
101
|
+
export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission.js";
|
|
102
|
+
export { makeBlessStore, blessPath } from "./bless.js";
|
|
103
|
+
export type { BlessStore, BlessRule } from "./bless.js";
|
|
104
|
+
export { bankDecision, fetchDecisions, supersedeDecision, parseDecideArgs, formatDecisionsList, shortId, registerDecisionCommands, DEFAULT_LIST_LIMIT, } from "./decisions.js";
|
|
105
|
+
export type { DecisionClientOpts, DecisionListItem, BankDecisionParams, DecideArgs } from "./decisions.js";
|
|
106
|
+
export { makeDecisionCapture, CAPTURE_DEBOUNCE_MS } from "./decisionCapture.js";
|
|
107
|
+
export type { DecisionCapture, DecisionCaptureDeps, BlessCaptureInfo, CaptureCtx } from "./decisionCapture.js";
|
|
108
|
+
export { registerAmbientRecall, fetchRecall, formatRecallBlock, toRepoRelativePath, normalizeRecall, RECALL_TIMEOUT_MS, RECALL_MIN_DECISIONS, } from "./recall.js";
|
|
109
|
+
export type { RecallResult, RecallClientOpts, RegisterRecallDeps } from "./recall.js";
|
|
110
|
+
export { makeCostAccumulator, formatCostLine, registerCostCommand } from "./costHud.js";
|
|
111
|
+
export type { CostSnapshot, Headroom } from "./costHud.js";
|
|
112
|
+
export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE_REFRESH_WINDOW_MS, REFRESH_RETRY_MS, } from "./tokenProvider.js";
|
|
113
|
+
export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
|
|
114
|
+
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
115
|
+
export type { SpoolEntry, SpoolClientOpts, FlushOutcome, JudgmentWriteOutcome } from "./spool.js";
|
|
116
|
+
//# sourceMappingURL=index.d.ts.map
|