@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni doctor` — a green/red readiness checklist.
|
|
3
|
+
*
|
|
4
|
+
* Diagnoses whether a `yagni` launch will actually work: the pi engine is built,
|
|
5
|
+
* the extension is built, the active profile has a live (unexpired) token, the
|
|
6
|
+
* backend is reachable with YAGNI Code enabled, the state dir is locked down, and
|
|
7
|
+
* `gh` is available for `/go --pr`. Every failing/soft check carries a one-line
|
|
8
|
+
* actionable fix.
|
|
9
|
+
*
|
|
10
|
+
* The check LOGIC is pure (given probe inputs) so it is fully unit-testable; the
|
|
11
|
+
* impure probes (fs, fetch, profile read) are injectable seams. `runDoctor`
|
|
12
|
+
* returns the process exit code: 0 when all REQUIRED checks pass, 1 otherwise.
|
|
13
|
+
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { delimiter, join } from "node:path";
|
|
17
|
+
import { credentialsDir } from "./credentials.js";
|
|
18
|
+
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
19
|
+
import { classifyTokenExpiry } from "./launch.js";
|
|
20
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
21
|
+
import { readActiveProfile } from "./profiles.js";
|
|
22
|
+
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
23
|
+
export function checkPiEngine(probe) {
|
|
24
|
+
if (!probe.binPath || !probe.binExists) {
|
|
25
|
+
return {
|
|
26
|
+
name: "pi engine",
|
|
27
|
+
status: "fail",
|
|
28
|
+
detail: "not found or not built",
|
|
29
|
+
hint: "reinstall it: npm i -g @yagni-app/code (monorepo: npm i -g ./packages/yagni-code-cli)",
|
|
30
|
+
required: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: "pi engine",
|
|
35
|
+
status: "ok",
|
|
36
|
+
detail: probe.version ? `installed and built (v${probe.version})` : "installed and built",
|
|
37
|
+
required: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function checkExtension(probe) {
|
|
41
|
+
if (!probe.path || !probe.exists) {
|
|
42
|
+
return {
|
|
43
|
+
name: "extension",
|
|
44
|
+
status: "fail",
|
|
45
|
+
detail: "YAGNI extension not found (dist/extension/index.js missing)",
|
|
46
|
+
hint: "reinstall it: npm i -g @yagni-app/code (monorepo: pnpm --filter @yagni-app/code build)",
|
|
47
|
+
required: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { name: "extension", status: "ok", detail: "YAGNI extension loaded", required: true };
|
|
51
|
+
}
|
|
52
|
+
export function checkProfileToken(profile) {
|
|
53
|
+
if (!profile.token) {
|
|
54
|
+
return {
|
|
55
|
+
name: "profile",
|
|
56
|
+
status: "fail",
|
|
57
|
+
detail: `not logged in to "${profile.name}"`,
|
|
58
|
+
hint: "run `yagni login`",
|
|
59
|
+
required: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
name: "profile",
|
|
64
|
+
status: "ok",
|
|
65
|
+
detail: `logged in to "${profile.name}"`,
|
|
66
|
+
required: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function checkTokenExpiry(status) {
|
|
70
|
+
switch (status.kind) {
|
|
71
|
+
case "expired":
|
|
72
|
+
return {
|
|
73
|
+
name: "token",
|
|
74
|
+
status: "fail",
|
|
75
|
+
detail: "expired",
|
|
76
|
+
hint: "run `yagni login` to re-authenticate",
|
|
77
|
+
required: true,
|
|
78
|
+
};
|
|
79
|
+
case "approaching":
|
|
80
|
+
return {
|
|
81
|
+
name: "token",
|
|
82
|
+
status: "warn",
|
|
83
|
+
detail: `expires in about ${status.minutesRemaining} minute${status.minutesRemaining === 1 ? "" : "s"}`,
|
|
84
|
+
hint: "run `yagni login` (launch also auto-refreshes within 7 days of expiry)",
|
|
85
|
+
required: false,
|
|
86
|
+
};
|
|
87
|
+
case "valid":
|
|
88
|
+
return { name: "token", status: "ok", detail: "valid", required: false };
|
|
89
|
+
case "unknown":
|
|
90
|
+
default:
|
|
91
|
+
return { name: "token", status: "ok", detail: "no expiry recorded (older login)", required: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function checkBackend(probe) {
|
|
95
|
+
if (probe.kind === "skipped") {
|
|
96
|
+
return {
|
|
97
|
+
name: "backend",
|
|
98
|
+
status: "warn",
|
|
99
|
+
detail: "skipped (not logged in)",
|
|
100
|
+
hint: "run `yagni login`",
|
|
101
|
+
required: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (probe.kind === "network") {
|
|
105
|
+
return {
|
|
106
|
+
name: "backend",
|
|
107
|
+
status: "warn",
|
|
108
|
+
detail: "could not reach the backend (network)",
|
|
109
|
+
hint: "check your connection, or `yagni use <env>` to point at the right host",
|
|
110
|
+
required: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const { status } = probe;
|
|
114
|
+
if (status === 200) {
|
|
115
|
+
return { name: "backend", status: "ok", detail: "reachable, YAGNI Code enabled", required: true };
|
|
116
|
+
}
|
|
117
|
+
if (status === 401 || status === 403) {
|
|
118
|
+
return {
|
|
119
|
+
name: "backend",
|
|
120
|
+
status: "fail",
|
|
121
|
+
detail: `token rejected (HTTP ${status})`,
|
|
122
|
+
hint: "run `yagni login` to re-authenticate",
|
|
123
|
+
required: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (status === 404) {
|
|
127
|
+
return {
|
|
128
|
+
name: "backend",
|
|
129
|
+
status: "fail",
|
|
130
|
+
detail: "YAGNI Code is not enabled for this workspace (HTTP 404)",
|
|
131
|
+
hint: "ask an admin to enable the yagni_code.enabled feature flag",
|
|
132
|
+
required: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
name: "backend",
|
|
137
|
+
status: "warn",
|
|
138
|
+
detail: `unexpected response (HTTP ${status})`,
|
|
139
|
+
hint: "retry in a moment; if it persists, contact support",
|
|
140
|
+
required: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function checkStateDir(probe) {
|
|
144
|
+
if (!probe.exists || probe.mode === null) {
|
|
145
|
+
return {
|
|
146
|
+
name: "state dir",
|
|
147
|
+
status: "ok",
|
|
148
|
+
detail: `${probe.path} not created yet`,
|
|
149
|
+
required: false,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Any group/other bit set on a dir that holds live tokens is a leak.
|
|
153
|
+
if ((probe.mode & 0o077) !== 0) {
|
|
154
|
+
return {
|
|
155
|
+
name: "state dir",
|
|
156
|
+
status: "warn",
|
|
157
|
+
detail: `${probe.path} is ${toOctal(probe.mode)} (should be 0700)`,
|
|
158
|
+
hint: `run: chmod 700 ${probe.path}`,
|
|
159
|
+
required: false,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { name: "state dir", status: "ok", detail: `${probe.path} is 0700`, required: false };
|
|
163
|
+
}
|
|
164
|
+
export function checkCliUpdate(probe) {
|
|
165
|
+
if (probe.latest === null) {
|
|
166
|
+
return {
|
|
167
|
+
name: "cli version",
|
|
168
|
+
status: "ok",
|
|
169
|
+
detail: `v${probe.current} (update check unavailable)`,
|
|
170
|
+
required: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (isNewerVersion(probe.current, probe.latest)) {
|
|
174
|
+
return {
|
|
175
|
+
name: "cli version",
|
|
176
|
+
status: "warn",
|
|
177
|
+
detail: `v${probe.current} — v${probe.latest} is available`,
|
|
178
|
+
hint: "run `yagni upgrade`",
|
|
179
|
+
required: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { name: "cli version", status: "ok", detail: `v${probe.current} (latest)`, required: false };
|
|
183
|
+
}
|
|
184
|
+
export function checkGh(onPath) {
|
|
185
|
+
if (onPath) {
|
|
186
|
+
return {
|
|
187
|
+
name: "gh (optional)",
|
|
188
|
+
status: "ok",
|
|
189
|
+
detail: "on PATH (for `yagni /go --pr`)",
|
|
190
|
+
required: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
name: "gh (optional)",
|
|
195
|
+
status: "warn",
|
|
196
|
+
detail: "not found (only needed for `yagni /go --pr`)",
|
|
197
|
+
hint: "install the GitHub CLI: https://cli.github.com",
|
|
198
|
+
required: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function toOctal(mode) {
|
|
202
|
+
return `0${(mode & 0o777).toString(8).padStart(3, "0")}`;
|
|
203
|
+
}
|
|
204
|
+
/** Exit 1 iff any REQUIRED check failed; advisory warns never flip it. */
|
|
205
|
+
export function buildDoctorReport(checks) {
|
|
206
|
+
const failedRequired = checks.some((c) => c.required && c.status === "fail");
|
|
207
|
+
return { checks, exitCode: failedRequired ? 1 : 0 };
|
|
208
|
+
}
|
|
209
|
+
const SYMBOL = { ok: "✓", warn: "!", fail: "✗" };
|
|
210
|
+
export function formatDoctorReport(report) {
|
|
211
|
+
const lines = ["YAGNI Code doctor", ""];
|
|
212
|
+
const width = Math.max(...report.checks.map((c) => c.name.length), 0);
|
|
213
|
+
for (const c of report.checks) {
|
|
214
|
+
lines.push(` ${SYMBOL[c.status]} ${c.name.padEnd(width)} ${c.detail}`);
|
|
215
|
+
if (c.status !== "ok" && c.hint) {
|
|
216
|
+
lines.push(` ${" ".repeat(width + 3)}→ ${c.hint}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
lines.push("");
|
|
220
|
+
const problems = report.checks.filter((c) => c.required && c.status === "fail").length;
|
|
221
|
+
const advisories = report.checks.filter((c) => c.status === "warn").length;
|
|
222
|
+
if (problems > 0) {
|
|
223
|
+
lines.push(`${problems} problem${problems === 1 ? "" : "s"} found — see the hints above.`);
|
|
224
|
+
}
|
|
225
|
+
else if (advisories > 0) {
|
|
226
|
+
lines.push(`All required checks passed (${advisories} advisory note${advisories === 1 ? "" : "s"}).`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
lines.push("All checks passed. You're ready to run `yagni`.");
|
|
230
|
+
}
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
function defaultProbePiEngine() {
|
|
234
|
+
try {
|
|
235
|
+
const binPath = resolvePiCliPath();
|
|
236
|
+
const binExists = existsSync(binPath);
|
|
237
|
+
let version;
|
|
238
|
+
try {
|
|
239
|
+
const pkg = JSON.parse(readFileSync(join(resolvePiPackageDir(), "package.json"), "utf8"));
|
|
240
|
+
version = typeof pkg.version === "string" ? pkg.version : undefined;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* version is nice-to-have */
|
|
244
|
+
}
|
|
245
|
+
return { binPath, binExists, version };
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return { binExists: false };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function defaultProbeExtension() {
|
|
252
|
+
try {
|
|
253
|
+
const path = resolveExtensionPath();
|
|
254
|
+
return { path, exists: existsSync(path) };
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return { exists: false };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function defaultProbeStateDir() {
|
|
261
|
+
const path = credentialsDir();
|
|
262
|
+
try {
|
|
263
|
+
const st = statSync(path);
|
|
264
|
+
return { path, exists: true, mode: st.mode & 0o777 };
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return { path, exists: false, mode: null };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function defaultProbeBackend(baseUrl, token) {
|
|
271
|
+
if (!token)
|
|
272
|
+
return { kind: "skipped" };
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch(`${baseUrl}/api/yagni-code/models`, {
|
|
275
|
+
method: "GET",
|
|
276
|
+
headers: { authorization: `Bearer ${token}` },
|
|
277
|
+
signal: AbortSignal.timeout(10_000),
|
|
278
|
+
});
|
|
279
|
+
return { kind: "status", status: res.status };
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return { kind: "network" };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
|
286
|
+
export function ghOnPathDefault(env = process.env) {
|
|
287
|
+
const raw = env.PATH ?? "";
|
|
288
|
+
const names = process.platform === "win32" ? ["gh.exe", "gh.cmd", "gh"] : ["gh"];
|
|
289
|
+
for (const dir of raw.split(delimiter)) {
|
|
290
|
+
if (!dir)
|
|
291
|
+
continue;
|
|
292
|
+
for (const name of names) {
|
|
293
|
+
if (existsSync(join(dir, name)))
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Gather every check result against the (injectable) probes. Pure ordering; each
|
|
301
|
+
* individual check is a pure function of its probe.
|
|
302
|
+
*/
|
|
303
|
+
export async function gatherChecks(deps = {}) {
|
|
304
|
+
const now = deps.now ?? Date.now;
|
|
305
|
+
const probePiEngine = deps.probePiEngine ?? defaultProbePiEngine;
|
|
306
|
+
const probeExtension = deps.probeExtension ?? defaultProbeExtension;
|
|
307
|
+
const readProfile = deps.readActiveProfile ?? readActiveProfile;
|
|
308
|
+
const probeBackend = deps.probeBackend ?? defaultProbeBackend;
|
|
309
|
+
const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
|
|
310
|
+
const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
|
|
311
|
+
const probeLatestVersion = deps.probeLatestVersion ?? (() => fetchLatestVersion());
|
|
312
|
+
const checks = [];
|
|
313
|
+
checks.push(checkPiEngine(probePiEngine()));
|
|
314
|
+
checks.push(checkExtension(probeExtension()));
|
|
315
|
+
checks.push(checkCliUpdate({
|
|
316
|
+
current: deps.currentVersion ?? currentCliVersion(),
|
|
317
|
+
latest: await probeLatestVersion(),
|
|
318
|
+
}));
|
|
319
|
+
const profile = await readProfile();
|
|
320
|
+
checks.push(checkProfileToken(profile));
|
|
321
|
+
if (profile.token) {
|
|
322
|
+
checks.push(checkTokenExpiry(classifyTokenExpiry(profile.expiresAt, now())));
|
|
323
|
+
}
|
|
324
|
+
const backend = profile.token
|
|
325
|
+
? await probeBackend(profile.baseUrl, profile.token)
|
|
326
|
+
: { kind: "skipped" };
|
|
327
|
+
checks.push(checkBackend(backend));
|
|
328
|
+
checks.push(checkStateDir(probeStateDir()));
|
|
329
|
+
checks.push(checkGh(ghOnPath()));
|
|
330
|
+
return checks;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Run the full doctor flow: gather → assemble → print → return the exit code.
|
|
334
|
+
* Never throws (a probe failure degrades to a failing/soft check, not a crash).
|
|
335
|
+
*/
|
|
336
|
+
export async function runDoctor(deps = {}) {
|
|
337
|
+
const log = deps.log ?? ((m) => process.stdout.write(`${m}\n`));
|
|
338
|
+
const checks = await gatherChecks(deps);
|
|
339
|
+
const report = buildDoctorReport(checks);
|
|
340
|
+
log(formatDoctorReport(report));
|
|
341
|
+
return report.exitCode;
|
|
342
|
+
}
|
|
343
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE half of the ask_advisor escalation (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Balanced sessions drive on `advanced` and escalate hard judgment calls to
|
|
5
|
+
* `peak`. This module owns every decision about whether a consult may happen and
|
|
6
|
+
* what it cost; `askAdvisorTool.ts` owns the spawn. Same split as `permission.ts`
|
|
7
|
+
* (`decideGate` pure, wiring separate), for the same reason: the rules are what
|
|
8
|
+
* need exhaustive tests, and they must not require a child process to exercise.
|
|
9
|
+
*
|
|
10
|
+
* Two bounds, both necessarily POST-HOC. pi exposes no pre-emptive turn or token
|
|
11
|
+
* ceiling (see `pipeline/budget.ts`), so a consult's cost is only knowable once
|
|
12
|
+
* the child has exited:
|
|
13
|
+
*
|
|
14
|
+
* - `maxConsults` is the real product bound. It makes escalation predictable
|
|
15
|
+
* and priceable, which is the point of the cap.
|
|
16
|
+
* - `maxSpend` is a RUNAWAY CATCH, not a budget — deliberately generous, in
|
|
17
|
+
* the same spirit as `DEFAULT_RUN_BUDGET`. A thorough consult on a large
|
|
18
|
+
* repo runs $0.40-$1.00, so a full 5-consult session lands near $2-$5 and
|
|
19
|
+
* never approaches this. It exists only so an advisor stuck in a loop has
|
|
20
|
+
* something to stop it.
|
|
21
|
+
*
|
|
22
|
+
* State is held in a closure via {@link makeAdvisorState}; no module-level
|
|
23
|
+
* mutable state, so two sessions in one process cannot bleed into each other.
|
|
24
|
+
*/
|
|
25
|
+
/** The session tier that may escalate. Balanced only, by design. */
|
|
26
|
+
export declare const ADVISOR_TIER = "balanced";
|
|
27
|
+
/** The tier a consult itself runs on. */
|
|
28
|
+
export declare const ADVISOR_MODEL_TIER = "peak";
|
|
29
|
+
/** Bounds on escalation within a single session. */
|
|
30
|
+
export interface AdvisorLimits {
|
|
31
|
+
/** Hard cap on consults per session. */
|
|
32
|
+
maxConsults: number;
|
|
33
|
+
/** Runaway-catch ceiling on aggregate advisor spend, in the same unit as StageUsage.cost. */
|
|
34
|
+
maxSpend: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_ADVISOR_LIMITS: AdvisorLimits;
|
|
37
|
+
/** Mutable per-session advisor accounting. */
|
|
38
|
+
export interface AdvisorState {
|
|
39
|
+
consults: number;
|
|
40
|
+
spend: number;
|
|
41
|
+
}
|
|
42
|
+
/** A live session's advisor accounting, held in a closure. */
|
|
43
|
+
export interface AdvisorStateHandle {
|
|
44
|
+
read(): AdvisorState;
|
|
45
|
+
/** Record a completed consult. Returns the state after recording. */
|
|
46
|
+
record(cost: number): AdvisorState;
|
|
47
|
+
}
|
|
48
|
+
export declare function makeAdvisorState(): AdvisorStateHandle;
|
|
49
|
+
/** The outcome of the pre-consult gate. */
|
|
50
|
+
export interface ConsultDecision {
|
|
51
|
+
allow: boolean;
|
|
52
|
+
/** Plain-text explanation when disallowed, surfaced to the model verbatim. */
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ConsultGateInput {
|
|
56
|
+
/** The LIVE session model id (pi's picker can change this mid-session). */
|
|
57
|
+
model: string | undefined;
|
|
58
|
+
state: AdvisorState;
|
|
59
|
+
limits: AdvisorLimits;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether one consult may proceed. PURE.
|
|
63
|
+
*
|
|
64
|
+
* The tier check reads the model at CALL time rather than at registration time:
|
|
65
|
+
* tools register once at activation but pi's picker can switch the session model
|
|
66
|
+
* afterwards, so a registration-time check would both leave the tool live after
|
|
67
|
+
* switching away from Balanced and hide it forever from a session that switched
|
|
68
|
+
* to Balanced.
|
|
69
|
+
*/
|
|
70
|
+
export declare function decideConsult(input: ConsultGateInput): ConsultDecision;
|
|
71
|
+
/**
|
|
72
|
+
* The line appended to a consult's advice so spend is visible at the moment it
|
|
73
|
+
* happens. `/cost` cannot see child processes at all (it accumulates off the
|
|
74
|
+
* parent's `turn_end`), so without this an advisor consult is invisible — see
|
|
75
|
+
* YAG-383 for the server-authoritative fix.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatConsultCost(input: {
|
|
78
|
+
cost: number;
|
|
79
|
+
consults: number;
|
|
80
|
+
limits: AdvisorLimits;
|
|
81
|
+
}): string;
|
|
82
|
+
/** The advisor subtotal line `/cost` appends when a session has escalated. */
|
|
83
|
+
export declare function formatAdvisorSubtotal(state: AdvisorState, limits: AdvisorLimits): string;
|
|
84
|
+
//# sourceMappingURL=advisor.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE half of the ask_advisor escalation (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Balanced sessions drive on `advanced` and escalate hard judgment calls to
|
|
5
|
+
* `peak`. This module owns every decision about whether a consult may happen and
|
|
6
|
+
* what it cost; `askAdvisorTool.ts` owns the spawn. Same split as `permission.ts`
|
|
7
|
+
* (`decideGate` pure, wiring separate), for the same reason: the rules are what
|
|
8
|
+
* need exhaustive tests, and they must not require a child process to exercise.
|
|
9
|
+
*
|
|
10
|
+
* Two bounds, both necessarily POST-HOC. pi exposes no pre-emptive turn or token
|
|
11
|
+
* ceiling (see `pipeline/budget.ts`), so a consult's cost is only knowable once
|
|
12
|
+
* the child has exited:
|
|
13
|
+
*
|
|
14
|
+
* - `maxConsults` is the real product bound. It makes escalation predictable
|
|
15
|
+
* and priceable, which is the point of the cap.
|
|
16
|
+
* - `maxSpend` is a RUNAWAY CATCH, not a budget — deliberately generous, in
|
|
17
|
+
* the same spirit as `DEFAULT_RUN_BUDGET`. A thorough consult on a large
|
|
18
|
+
* repo runs $0.40-$1.00, so a full 5-consult session lands near $2-$5 and
|
|
19
|
+
* never approaches this. It exists only so an advisor stuck in a loop has
|
|
20
|
+
* something to stop it.
|
|
21
|
+
*
|
|
22
|
+
* State is held in a closure via {@link makeAdvisorState}; no module-level
|
|
23
|
+
* mutable state, so two sessions in one process cannot bleed into each other.
|
|
24
|
+
*/
|
|
25
|
+
/** The session tier that may escalate. Balanced only, by design. */
|
|
26
|
+
export const ADVISOR_TIER = "balanced";
|
|
27
|
+
/** The tier a consult itself runs on. */
|
|
28
|
+
export const ADVISOR_MODEL_TIER = "peak";
|
|
29
|
+
export const DEFAULT_ADVISOR_LIMITS = {
|
|
30
|
+
maxConsults: 5,
|
|
31
|
+
maxSpend: 10,
|
|
32
|
+
};
|
|
33
|
+
export function makeAdvisorState() {
|
|
34
|
+
const state = { consults: 0, spend: 0 };
|
|
35
|
+
return {
|
|
36
|
+
read: () => ({ ...state }),
|
|
37
|
+
record(cost) {
|
|
38
|
+
state.consults += 1;
|
|
39
|
+
// A child that reports no usage still counts as a consult — the cap is
|
|
40
|
+
// about escalation attempts, not only about money.
|
|
41
|
+
state.spend += Number.isFinite(cost) && cost > 0 ? cost : 0;
|
|
42
|
+
return { ...state };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether one consult may proceed. PURE.
|
|
48
|
+
*
|
|
49
|
+
* The tier check reads the model at CALL time rather than at registration time:
|
|
50
|
+
* tools register once at activation but pi's picker can switch the session model
|
|
51
|
+
* afterwards, so a registration-time check would both leave the tool live after
|
|
52
|
+
* switching away from Balanced and hide it forever from a session that switched
|
|
53
|
+
* to Balanced.
|
|
54
|
+
*/
|
|
55
|
+
export function decideConsult(input) {
|
|
56
|
+
const { model, state, limits } = input;
|
|
57
|
+
if (model !== ADVISOR_TIER) {
|
|
58
|
+
return {
|
|
59
|
+
allow: false,
|
|
60
|
+
reason: `ask_advisor is only available on the Balanced tier (this session is on ` +
|
|
61
|
+
`"${model ?? "unknown"}"). Switch the model to Balanced to consult the ` +
|
|
62
|
+
`peak-tier advisor, or reason it through on the current tier.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (state.consults >= limits.maxConsults) {
|
|
66
|
+
return {
|
|
67
|
+
allow: false,
|
|
68
|
+
reason: `Advisor consult cap reached (${state.consults} of ${limits.maxConsults} ` +
|
|
69
|
+
`used this session). Escalation is capped so a session's cost stays ` +
|
|
70
|
+
`predictable. Decide this one yourself, or start a fresh session.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (state.spend >= limits.maxSpend) {
|
|
74
|
+
return {
|
|
75
|
+
allow: false,
|
|
76
|
+
reason: `Advisor spend ceiling reached ($${state.spend.toFixed(2)} this session). ` +
|
|
77
|
+
`This is a runaway guard rather than a budget, so hitting it usually ` +
|
|
78
|
+
`means a consult looped. Decide this one yourself, or start a fresh session.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { allow: true };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The line appended to a consult's advice so spend is visible at the moment it
|
|
85
|
+
* happens. `/cost` cannot see child processes at all (it accumulates off the
|
|
86
|
+
* parent's `turn_end`), so without this an advisor consult is invisible — see
|
|
87
|
+
* YAG-383 for the server-authoritative fix.
|
|
88
|
+
*/
|
|
89
|
+
export function formatConsultCost(input) {
|
|
90
|
+
const { cost, consults, limits } = input;
|
|
91
|
+
return `Advisor consult: $${cost.toFixed(2)} (${consults} of ${limits.maxConsults} used this session).`;
|
|
92
|
+
}
|
|
93
|
+
/** The advisor subtotal line `/cost` appends when a session has escalated. */
|
|
94
|
+
export function formatAdvisorSubtotal(state, limits) {
|
|
95
|
+
if (state.consults === 0)
|
|
96
|
+
return "";
|
|
97
|
+
const plural = state.consults === 1 ? "consult" : "consults";
|
|
98
|
+
return (`Advisor: ${state.consults} ${plural} of ${limits.maxConsults}, ` +
|
|
99
|
+
`$${state.spend.toFixed(2)} on peak.`);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=advisor.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ask_advisor` — the peak-tier escalation available to Balanced sessions (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Cheap driver, expensive consultant: a Balanced session drives on `advanced` and
|
|
5
|
+
* calls this when it hits a judgment call worth the strongest model. The consult
|
|
6
|
+
* spawns a child pi at `peak` through the same `runStage` seam `/go` uses, and
|
|
7
|
+
* returns the advice as plain TEXT the driver acts on — text we own, so
|
|
8
|
+
* `record_decision` can bank it into the corpus afterwards.
|
|
9
|
+
*
|
|
10
|
+
* Two deliberate design calls, both of which cost money and buy quality:
|
|
11
|
+
*
|
|
12
|
+
* 1. The advisor gets REAL read-only recon (read/grep/find/ls), the same set
|
|
13
|
+
* the plan stage uses. An advisor limited to what the driver pre-digested
|
|
14
|
+
* rubber-stamps the driver's framing; half the value of escalation is
|
|
15
|
+
* looking where the driver did not think to look. This makes a consult an
|
|
16
|
+
* agentic loop, so a thorough one on a large repo runs $0.40-$1.00 rather
|
|
17
|
+
* than the ~4c a single-shot completion would cost. That is the price of
|
|
18
|
+
* the advisor being useful. It gets NO bash: a consultant reasons and
|
|
19
|
+
* reads, it must not build, run, or write.
|
|
20
|
+
* 2. The input schema is NARROW on purpose — question, what was tried,
|
|
21
|
+
* relevant excerpts. There is deliberately no field a conversation
|
|
22
|
+
* transcript fits into, because passing the transcript is how a consult
|
|
23
|
+
* silently becomes more expensive than just running peak.
|
|
24
|
+
*
|
|
25
|
+
* The gate, the cap and the spend ceiling live in `advisor.ts` and are pure;
|
|
26
|
+
* this module is the I/O.
|
|
27
|
+
*/
|
|
28
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { Type } from "typebox";
|
|
30
|
+
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
31
|
+
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
32
|
+
import type { PipelineStage } from "./pipeline/types.js";
|
|
33
|
+
/**
|
|
34
|
+
* Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
|
|
35
|
+
* reason documented there: enough tools to navigate instead of flailing on a
|
|
36
|
+
* single `read`. No `bash`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ADVISOR_TOOLS: string[];
|
|
39
|
+
declare const parameters: Type.TObject<{
|
|
40
|
+
question: Type.TString;
|
|
41
|
+
tried: Type.TOptional<Type.TString>;
|
|
42
|
+
excerpts: Type.TOptional<Type.TString>;
|
|
43
|
+
}>;
|
|
44
|
+
export interface MakeAskAdvisorToolOptions {
|
|
45
|
+
/** Per-session consult accounting. Created once per session in index.ts. */
|
|
46
|
+
state: AdvisorStateHandle;
|
|
47
|
+
limits?: AdvisorLimits;
|
|
48
|
+
/** Injectable so tests never spawn a child. */
|
|
49
|
+
runStage?: typeof defaultRunStage;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
53
|
+
* this framing on trust, so the brief orients rather than constrains.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildConsultBrief(params: {
|
|
56
|
+
question: string;
|
|
57
|
+
tried?: string;
|
|
58
|
+
excerpts?: string;
|
|
59
|
+
}): string;
|
|
60
|
+
/**
|
|
61
|
+
* The synthetic stage a consult runs as.
|
|
62
|
+
*
|
|
63
|
+
* `id` reuses the existing `plan` StageId rather than adding an advisor member:
|
|
64
|
+
* the advisor is NOT a pipeline stage, and a new StageId would ripple into the
|
|
65
|
+
* feed/activity reducers that switch over it. `agent` is what selects the
|
|
66
|
+
* persona, and that is the advisor's own.
|
|
67
|
+
*/
|
|
68
|
+
export declare function advisorStage(): PipelineStage;
|
|
69
|
+
export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, {
|
|
70
|
+
consults: number;
|
|
71
|
+
cost: number;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* `/advise <question>` — the manual escalation lever.
|
|
75
|
+
*
|
|
76
|
+
* Runs the SAME tool the model calls, sharing one state handle, so a manual
|
|
77
|
+
* consult draws on the same cap rather than opening a side channel around it.
|
|
78
|
+
* The advice is sent into the conversation (like /go-compare's report) so the
|
|
79
|
+
* driver sees it in context and can act on it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function registerAdviseCommand(pi: ExtensionAPI, tool: ReturnType<typeof makeAskAdvisorTool>): void;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=askAdvisorTool.d.ts.map
|