@yagni-app/code 1.0.0 → 1.0.2
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 +14 -0
- package/dist/extension/askAdvisorTool.js +16 -4
- package/dist/extension/askYagniTool.d.ts +1 -1
- package/dist/extension/askYagniTool.js +21 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/childUsage.d.ts +40 -0
- package/dist/extension/childUsage.js +43 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +97 -0
- package/dist/extension/condensedTools.js +396 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +15 -1
- package/dist/extension/footer.js +35 -15
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +126 -6
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/permission/gate.d.ts +6 -0
- package/dist/extension/permission/gate.js +11 -2
- package/dist/extension/permission/guardian.d.ts +20 -0
- package/dist/extension/permission/guardian.js +16 -1
- package/dist/extension/pipeline/goCommand.d.ts +8 -0
- package/dist/extension/pipeline/goCommand.js +8 -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 +15 -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/slashCommandFilter.d.ts +30 -0
- package/dist/extension/slashCommandFilter.js +89 -0
- package/dist/extension/subagents.d.ts +21 -1
- package/dist/extension/subagents.js +34 -5
- 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/webFetchTool.js +2 -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 +3 -2
package/README.md
CHANGED
|
@@ -171,6 +171,43 @@ Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the acti
|
|
|
171
171
|
environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
|
|
172
172
|
`~/.yagni-code/credentials.json` is migrated automatically on first run.
|
|
173
173
|
|
|
174
|
+
### OTel export (opt-in)
|
|
175
|
+
|
|
176
|
+
Point sessions at **your own** OpenTelemetry collector (Datadog Agent, Grafana
|
|
177
|
+
Alloy, an OTLP-native backend) and every session — including `/go` stage
|
|
178
|
+
children and subagents — emits a per-prompt span tree: interaction → LLM
|
|
179
|
+
request → tool calls, following the OTel GenAI semantic conventions. Nothing is
|
|
180
|
+
exported unless you configure an endpoint.
|
|
181
|
+
|
|
182
|
+
Enable it one of three ways (first match wins):
|
|
183
|
+
|
|
184
|
+
- `OTEL_EXPORTER_OTLP_ENDPOINT=http://<collector>:4317` in the environment
|
|
185
|
+
(a personal override — handy for pointing one session at a scratch
|
|
186
|
+
collector), or
|
|
187
|
+
- **workspace settings** (the zero-setup path): a workspace admin sets the
|
|
188
|
+
endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
|
|
189
|
+
in the web app under Settings → YAGNI Code → Trace export. Every session in
|
|
190
|
+
the workspace picks it up at launch — nothing to install or configure on
|
|
191
|
+
developer machines. Header values are encrypted at rest server-side and the
|
|
192
|
+
launch-time copy is cached at mode `0600`, the same posture as your device
|
|
193
|
+
token. Or,
|
|
194
|
+
- commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` to the repo's
|
|
195
|
+
`.pi/settings.json` so one repo's sessions export without per-machine setup.
|
|
196
|
+
|
|
197
|
+
Standard OTel env vars are honored (`OTEL_EXPORTER_OTLP_PROTOCOL`,
|
|
198
|
+
`OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` — defaults to `yagni-code`),
|
|
199
|
+
and `PI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
|
|
200
|
+
export state.
|
|
201
|
+
|
|
202
|
+
Two things are enforced and not configurable:
|
|
203
|
+
|
|
204
|
+
- **Metadata only.** Token counts, cost, tier, finish reasons, and tool-call
|
|
205
|
+
ids export; prompt and response text never do — a settings file or env var
|
|
206
|
+
asking for content capture is overridden.
|
|
207
|
+
- **Cost is your contracted rate.** `pi.cost.usd` is computed from your
|
|
208
|
+
workspace's tier rate card, and the exported model name is the opaque tier
|
|
209
|
+
id (`advanced`, `peak`, …), so traces never fingerprint the backing model.
|
|
210
|
+
|
|
174
211
|
Device tokens are revocable from both ends: `yagni logout` revokes the current
|
|
175
212
|
one, and a workspace admin can list every connected device and revoke any token
|
|
176
213
|
from the web app (Settings, YAGNI Code, Connected devices). Tokens are stored
|
|
@@ -196,6 +233,11 @@ precisely:
|
|
|
196
233
|
third-party crash service, so we can fix the crash before you have to report
|
|
197
234
|
it. Disable with `YAGNI_DISABLE_CRASH_REPORTS=1`.
|
|
198
235
|
|
|
236
|
+
A third flow exists only when you turn it on: **OTel export** (above) sends
|
|
237
|
+
session *metadata* — never prompt or response text — to a collector **you**
|
|
238
|
+
configure and operate. It is off unless an OTLP endpoint is set, and YAGNI
|
|
239
|
+
never receives these traces.
|
|
240
|
+
|
|
199
241
|
## Troubleshooting
|
|
200
242
|
|
|
201
243
|
- **`Not logged in to environment "<name>" … Run \`yagni login\` first.`** — no
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { spawn } from "node:child_process";
|
|
|
16
16
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { createInterface } from "node:readline/promises";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
20
|
import { PI_CONFIG_NAME } from "./branding.js";
|
|
21
21
|
import { claudeCompatArgs } from "./claudeCompat.js";
|
|
22
22
|
import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
@@ -27,14 +27,18 @@ import { login } from "./login.js";
|
|
|
27
27
|
import { logout } from "./logout.js";
|
|
28
28
|
import { tokenCommand } from "./token.js";
|
|
29
29
|
import { buildLaunch } from "./launch.js";
|
|
30
|
+
import { resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
31
|
+
import { parseOutputFormat, parseJsonEvents, buildResultObject, readGuardianEvents, } from "./outputFormat.js";
|
|
32
|
+
import { feedbackCommand } from "./feedback.js";
|
|
30
33
|
import { runDoctor } from "./doctor.js";
|
|
31
34
|
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
32
35
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
33
36
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
34
37
|
import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
|
|
35
38
|
import { PAD_X } from "./padding.js";
|
|
39
|
+
import { parseWorktreeFlag, validateWorktreeLaunchArgs } from "./worktreeArgs.js";
|
|
36
40
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
37
|
-
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
41
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveSessionWorktreePath } from "./paths.js";
|
|
38
42
|
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
39
43
|
// Present as "yagni" in process listings, not "node".
|
|
40
44
|
process.title = DISTRIBUTION.commandName;
|
|
@@ -159,6 +163,18 @@ export function seedHideThinkingBlock(piAgentDir) {
|
|
|
159
163
|
return seedSetting(piAgentDir, "hideThinkingBlock", true);
|
|
160
164
|
}
|
|
161
165
|
async function runDefault(passthroughArgs) {
|
|
166
|
+
// `-w / --worktree [name]` is its own launch path: create/resume a worktree
|
|
167
|
+
// and enter a session there. Gate strictly on `requested` (not `name`) so a
|
|
168
|
+
// bare `-w` (random slug) is honored too. Returning early keeps the standard
|
|
169
|
+
// path below physically unreachable by any `-w` bug.
|
|
170
|
+
const worktree = parseWorktreeFlag(passthroughArgs);
|
|
171
|
+
if (worktree.requested) {
|
|
172
|
+
return runWorktreeLaunch(worktree.remainingArgs, worktree.name);
|
|
173
|
+
}
|
|
174
|
+
// Parse --output-format out of argv before passing to pi (pi doesn't know
|
|
175
|
+
// about it). The format determines how we handle pi's stdout: text = inherit,
|
|
176
|
+
// stream-json = inherit with --mode json, json = pipe + post-process.
|
|
177
|
+
const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
|
|
162
178
|
// Cache-backed update nudge (never a network wait), then a background cache
|
|
163
179
|
// refresh that completes while the session runs. Both fail soft.
|
|
164
180
|
await maybeNudgeAndRefresh({ current: cliVersion() });
|
|
@@ -230,6 +246,16 @@ async function runDefault(passthroughArgs) {
|
|
|
230
246
|
catch {
|
|
231
247
|
compat = { argv: [], env: {} };
|
|
232
248
|
}
|
|
249
|
+
// OTel export: load pi-otel only when an OTLP endpoint is configured — the
|
|
250
|
+
// user's env, the workspace's admin-set config (fetched fail-soft, cached),
|
|
251
|
+
// or the repo's .pi/settings.json, in that order. undefined keeps the
|
|
252
|
+
// launch untouched. See otel.ts for the policy.
|
|
253
|
+
const otel = await resolveOtelLaunchWithWorkspace({
|
|
254
|
+
env: process.env,
|
|
255
|
+
cwd: process.cwd(),
|
|
256
|
+
creds,
|
|
257
|
+
profileName: profile.name,
|
|
258
|
+
});
|
|
233
259
|
// buildLaunch runs the token-expiry preflight: it throws (with an actionable
|
|
234
260
|
// login prompt) on an already-expired token so we never spawn a session that
|
|
235
261
|
// immediately 401s, and returns non-fatal warnings (e.g. expiry approaching).
|
|
@@ -237,7 +263,7 @@ async function runDefault(passthroughArgs) {
|
|
|
237
263
|
// re-login in another terminal is picked up here with no stale cached token.
|
|
238
264
|
let plan;
|
|
239
265
|
try {
|
|
240
|
-
plan = buildLaunch(creds,
|
|
266
|
+
plan = buildLaunch(creds, remainingArgs, {
|
|
241
267
|
extensionPath: resolveExtensionPath(),
|
|
242
268
|
agentDir: piAgentDir,
|
|
243
269
|
piPackageDir: shadowPiDir,
|
|
@@ -250,6 +276,7 @@ async function runDefault(passthroughArgs) {
|
|
|
250
276
|
cliVersion: cliVersion(),
|
|
251
277
|
baseEnv: process.env,
|
|
252
278
|
...(seededHideThinking ? { hideThinkingSeeded: true } : {}),
|
|
279
|
+
...(otel ? { otel } : {}),
|
|
253
280
|
});
|
|
254
281
|
}
|
|
255
282
|
catch (err) {
|
|
@@ -260,24 +287,210 @@ async function runDefault(passthroughArgs) {
|
|
|
260
287
|
process.stderr.write(`${warning}\n`);
|
|
261
288
|
}
|
|
262
289
|
const { env, argv } = plan;
|
|
290
|
+
return spawnPiAndAwait({
|
|
291
|
+
argv,
|
|
292
|
+
env,
|
|
293
|
+
remainingArgs,
|
|
294
|
+
outputFormat,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Spawn pi and await its exit, mapping to the launcher's exit code. Extracted
|
|
299
|
+
* so `runDefault` (cwd = current) and `runWorktreeLaunch` (cwd = worktree) share
|
|
300
|
+
* the exact same json/stream-json post-processing, signal forwarding, and exit
|
|
301
|
+
* code mapping — the two paths can never drift on the output contract.
|
|
302
|
+
*/
|
|
303
|
+
async function spawnPiAndAwait(opts) {
|
|
304
|
+
const { argv, env, remainingArgs, outputFormat, cwd } = opts;
|
|
305
|
+
// For json/stream-json output, inject --mode json so pi emits NDJSON events.
|
|
306
|
+
const userChoseMode = remainingArgs.some((a) => a === "--mode" || a.startsWith("--mode="));
|
|
307
|
+
const childArgv = outputFormat === "text" || userChoseMode
|
|
308
|
+
? argv
|
|
309
|
+
: [...argv, "--mode", "json"];
|
|
310
|
+
// text + stream-json: inherit stdout (passthrough). json: pipe stdout so we
|
|
311
|
+
// can post-process the NDJSON into a single result object.
|
|
312
|
+
const stdio = outputFormat === "json"
|
|
313
|
+
? ["inherit", "pipe", "inherit"]
|
|
314
|
+
: "inherit";
|
|
263
315
|
const piCli = resolvePiCliPath();
|
|
316
|
+
const startMs = Date.now();
|
|
264
317
|
return await new Promise((resolve) => {
|
|
265
|
-
const child = spawn(process.execPath, [piCli, ...
|
|
266
|
-
stdio:
|
|
318
|
+
const child = spawn(process.execPath, [piCli, ...childArgv], {
|
|
319
|
+
stdio: stdio,
|
|
267
320
|
env,
|
|
321
|
+
...(cwd ? { cwd } : {}),
|
|
268
322
|
});
|
|
323
|
+
// Collect pi's stdout when piping for --output-format json.
|
|
324
|
+
let stdoutChunks = "";
|
|
325
|
+
if (outputFormat === "json" && child.stdout) {
|
|
326
|
+
child.stdout.setEncoding("utf8");
|
|
327
|
+
child.stdout.on("data", (chunk) => {
|
|
328
|
+
stdoutChunks += chunk;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
269
331
|
// Forward termination signals to pi instead of dying around it (see
|
|
270
332
|
// signalForward.ts for the policy: first signal graceful, second tree-kill).
|
|
271
333
|
installSignalForwarding(child);
|
|
272
334
|
// 128+n for a signal death (bash parity), so a cancelled/killed run never
|
|
273
335
|
// reads as success to scripts or CI.
|
|
274
|
-
child.on("exit", (code, signal) =>
|
|
336
|
+
child.on("exit", (code, signal) => {
|
|
337
|
+
const exitCode = exitCodeFor(code, signal);
|
|
338
|
+
if (outputFormat === "json") {
|
|
339
|
+
const durationMs = Date.now() - startMs;
|
|
340
|
+
const events = parseJsonEvents(stdoutChunks);
|
|
341
|
+
const yagniSessionId = env.YAGNI_SESSION_ID ?? "";
|
|
342
|
+
const guardianEvents = readGuardianEvents(yagniSessionId);
|
|
343
|
+
const verbose = remainingArgs.includes("--verbose");
|
|
344
|
+
const result = buildResultObject({
|
|
345
|
+
events,
|
|
346
|
+
guardianEvents,
|
|
347
|
+
durationMs,
|
|
348
|
+
verbose,
|
|
349
|
+
});
|
|
350
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
351
|
+
}
|
|
352
|
+
resolve(exitCode);
|
|
353
|
+
});
|
|
275
354
|
child.on("error", (err) => {
|
|
276
355
|
process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
|
|
277
356
|
resolve(1);
|
|
278
357
|
});
|
|
279
358
|
});
|
|
280
359
|
}
|
|
360
|
+
async function defaultLoadSessionWorktree() {
|
|
361
|
+
const mod = (await import(pathToFileURL(resolveSessionWorktreePath()).href));
|
|
362
|
+
if (typeof mod?.createOrResume !== "function") {
|
|
363
|
+
throw new Error("The bundled extension is missing its session-worktree entry point (is the CLI up to date?).");
|
|
364
|
+
}
|
|
365
|
+
return mod;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* `yagni -w [name]` — create/resume a worktree and enter a session there.
|
|
369
|
+
*
|
|
370
|
+
* Delegates all git behavior to the extension's `sessionWorktree` entry; this
|
|
371
|
+
* launcher path only resolves credentials, builds the plan, and spawns pi with
|
|
372
|
+
* `cwd` = the worktree. The worktree is DURABLE — nothing is ever removed.
|
|
373
|
+
*/
|
|
374
|
+
async function runWorktreeLaunch(passthroughArgs, worktreeName, loadSessionWorktree = defaultLoadSessionWorktree) {
|
|
375
|
+
// Validate the name + argv before any side effect (slug guard + `-c`
|
|
376
|
+
// disallow). The extension re-validates the mapped slug for PR refs.
|
|
377
|
+
const launchError = validateWorktreeLaunchArgs(worktreeName, passthroughArgs);
|
|
378
|
+
if (launchError !== undefined) {
|
|
379
|
+
process.stderr.write(`${launchError}\n`);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
|
|
383
|
+
// Load the session-worktree entry first, so a stale bundled extension fails
|
|
384
|
+
// honestly before we mutate anything.
|
|
385
|
+
let sessionWorktree;
|
|
386
|
+
try {
|
|
387
|
+
sessionWorktree = await loadSessionWorktree();
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
// Create/resume resolves the main repo root + branch + dir, and performs
|
|
394
|
+
// `git worktree add` / resume. Never throws on a normal path; catch-all maps
|
|
395
|
+
// to a clean stderr + exit 1.
|
|
396
|
+
let result;
|
|
397
|
+
try {
|
|
398
|
+
result = await sessionWorktree.createOrResume(worktreeName, {
|
|
399
|
+
repoCwd: process.cwd(),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
// Reuse the standard credential + env + plan flow (same token refresh, shadow
|
|
407
|
+
// package, compat args, preflight). The worktree becomes the project cwd, so
|
|
408
|
+
// compat assets resolve from there.
|
|
409
|
+
await maybeNudgeAndRefresh({ current: cliVersion() });
|
|
410
|
+
const profile = await readActiveProfile();
|
|
411
|
+
let creds = credentialsFromProfile(profile);
|
|
412
|
+
if (!creds?.token) {
|
|
413
|
+
process.stderr.write(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.\n`);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
const refresh = await maybeRefreshAtLaunch(creds, {
|
|
417
|
+
persist: (c) => persistProfileTokenRotation(profile.name, c),
|
|
418
|
+
});
|
|
419
|
+
for (const warning of refresh.warnings) {
|
|
420
|
+
process.stderr.write(`${warning}\n`);
|
|
421
|
+
}
|
|
422
|
+
creds = refresh.creds;
|
|
423
|
+
const piAgentDir = agentDir(profile.name);
|
|
424
|
+
mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
|
|
425
|
+
seedEditorPadding(piAgentDir);
|
|
426
|
+
seedCollapseChangelog(piAgentDir);
|
|
427
|
+
seedHideThinkingBlock(piAgentDir);
|
|
428
|
+
let shadowPiDir;
|
|
429
|
+
try {
|
|
430
|
+
shadowPiDir = ensureShadowPiPackage({
|
|
431
|
+
realPiDir: resolvePiPackageDir(),
|
|
432
|
+
shadowDir: piPackageDir(),
|
|
433
|
+
name: PI_CONFIG_NAME,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
shadowPiDir = undefined;
|
|
438
|
+
}
|
|
439
|
+
let compat = { argv: [], env: {} };
|
|
440
|
+
try {
|
|
441
|
+
compat = await claudeCompatArgs({
|
|
442
|
+
cwd: result.worktreePath,
|
|
443
|
+
agentDir: piAgentDir,
|
|
444
|
+
confirm: confirmOnTty,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
compat = { argv: [], env: {} };
|
|
449
|
+
}
|
|
450
|
+
// OTel gate reads the WORKTREE's .pi/settings.json — that is the session's
|
|
451
|
+
// cwd, so a repo-committed otel config applies to its worktrees too.
|
|
452
|
+
const otel = await resolveOtelLaunchWithWorkspace({
|
|
453
|
+
env: process.env,
|
|
454
|
+
cwd: result.worktreePath,
|
|
455
|
+
creds,
|
|
456
|
+
profileName: profile.name,
|
|
457
|
+
});
|
|
458
|
+
let plan;
|
|
459
|
+
try {
|
|
460
|
+
plan = buildLaunch(creds, remainingArgs, {
|
|
461
|
+
extensionPath: resolveExtensionPath(),
|
|
462
|
+
agentDir: piAgentDir,
|
|
463
|
+
piPackageDir: shadowPiDir,
|
|
464
|
+
extraAgentArgs: compat.argv,
|
|
465
|
+
extraEnv: compat.env,
|
|
466
|
+
profilePath: profilePath(profile.name),
|
|
467
|
+
stateDir: credentialsDir(),
|
|
468
|
+
cliVersion: cliVersion(),
|
|
469
|
+
baseEnv: process.env,
|
|
470
|
+
...(otel ? { otel } : {}),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
for (const warning of plan.warnings) {
|
|
478
|
+
process.stderr.write(`${warning}\n`);
|
|
479
|
+
}
|
|
480
|
+
const exitCode = await spawnPiAndAwait({
|
|
481
|
+
argv: plan.argv,
|
|
482
|
+
env: plan.env,
|
|
483
|
+
remainingArgs,
|
|
484
|
+
outputFormat,
|
|
485
|
+
cwd: result.worktreePath,
|
|
486
|
+
});
|
|
487
|
+
// Durable by default: nothing is removed. Tell the user where their work
|
|
488
|
+
// lives (stderr only — never stdout, to keep json clean).
|
|
489
|
+
process.stderr.write(`[yagni] worktree ${result.existed ? "resumed" : "created"}: ${result.worktreePath}\n` +
|
|
490
|
+
`[yagni] branch: ${result.branch}\n` +
|
|
491
|
+
`[yagni] resume: cd ${result.worktreePath} && yagni\n`);
|
|
492
|
+
return exitCode;
|
|
493
|
+
}
|
|
281
494
|
export const HELP_TEXT = [
|
|
282
495
|
"YAGNI Code — a business-context-grounded terminal coding agent.",
|
|
283
496
|
"",
|
|
@@ -285,10 +498,16 @@ export const HELP_TEXT = [
|
|
|
285
498
|
" yagni [args…] Launch the agent in the current repo.",
|
|
286
499
|
" yagni -c Continue the most recent session.",
|
|
287
500
|
" yagni -r Browse and resume a previous session.",
|
|
501
|
+
" yagni -w [name] Create/resume a worktree and enter a session there.",
|
|
288
502
|
' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
|
|
503
|
+
' yagni -p "prompt" Use --output-format json for a machine-readable',
|
|
504
|
+
' --output-format json result object with tools, cost, guardian reviews.',
|
|
289
505
|
" yagni login Authorize the active environment (device-code flow).",
|
|
290
506
|
" yagni logout Revoke and clear the active environment's token.",
|
|
291
507
|
" yagni doctor Check that everything is ready (green/red checklist).",
|
|
508
|
+
" yagni feedback [sessionId] File a bug report from the shell. Lists recent",
|
|
509
|
+
" sessions to pick, or pass a session ID directly.",
|
|
510
|
+
" Attaches transcript + error trail, then submits.",
|
|
292
511
|
" yagni go --headless Run the /go pipeline without a session, for scripts",
|
|
293
512
|
" and CI: --ticket-file <path> [--plan-file <path>]",
|
|
294
513
|
" [--memo-file <path>] [--run-id <id>] [--json].",
|
|
@@ -307,6 +526,9 @@ export const HELP_TEXT = [
|
|
|
307
526
|
" --model <tier> Model tier (fixed to advanced).",
|
|
308
527
|
" --thinking <level> off | minimal | low | medium | high | xhigh | max",
|
|
309
528
|
" --session <id> Open a specific session; --fork <id> branches one.",
|
|
529
|
+
" --output-format <fmt> Output format: text (default), json, stream-json.",
|
|
530
|
+
" json emits a single result object; stream-json",
|
|
531
|
+
" emits NDJSON events (same as --mode json).",
|
|
310
532
|
" --mode json Emit machine-readable events (for scripts and CI).",
|
|
311
533
|
"",
|
|
312
534
|
"In a session:",
|
|
@@ -437,6 +659,9 @@ export async function main(argv) {
|
|
|
437
659
|
if (command === "go") {
|
|
438
660
|
return goCommand(rest, {}, cliVersion());
|
|
439
661
|
}
|
|
662
|
+
if (command === "feedback") {
|
|
663
|
+
return feedbackCommand(rest, {}, cliVersion());
|
|
664
|
+
}
|
|
440
665
|
if (command === "token") {
|
|
441
666
|
return tokenCommand();
|
|
442
667
|
}
|
package/dist/crashReport.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
|
41
41
|
export declare function runningUnderTest(env?: NodeJS.ProcessEnv): boolean;
|
|
42
42
|
/** Reporting is off when the user disabled it OR this is a test process. */
|
|
43
43
|
export declare function crashReportsSuppressed(env?: NodeJS.ProcessEnv): boolean;
|
|
44
|
+
export declare const SECRET_PATTERNS: Array<[RegExp, string]>;
|
|
44
45
|
export interface SanitizeCrashOptions {
|
|
45
46
|
/** Environment whose values get redacted (defaults to process.env). */
|
|
46
47
|
env?: NodeJS.ProcessEnv;
|
|
@@ -57,6 +58,13 @@ export interface SanitizeCrashOptions {
|
|
|
57
58
|
* `node_modules/` on, so dependency frames stay diagnosable)
|
|
58
59
|
* Over-redacts rather than under-redacts; pure; never throws.
|
|
59
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* Lightweight secret-only scrub — applies SECRET_PATTERNS and nothing else.
|
|
63
|
+
* Matches the extension's `scrubSecrets` exactly (no path collapse, no env
|
|
64
|
+
* redaction). Use this for feedback transcripts where file paths and code
|
|
65
|
+
* context must stay readable; the backend re-normalizes home paths on receipt.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scrubSecrets(text: string): string;
|
|
60
68
|
export declare function sanitizeCrashText(text: string, opts?: SanitizeCrashOptions): string;
|
|
61
69
|
export interface SanitizedCrash {
|
|
62
70
|
errorClass: string;
|
package/dist/crashReport.js
CHANGED
|
@@ -62,7 +62,7 @@ export function crashReportsSuppressed(env = process.env) {
|
|
|
62
62
|
}
|
|
63
63
|
// Mirrors scrubSecrets (backend yagniCode/scrubSecrets.ts and
|
|
64
64
|
// pi-extension-yagni pipeline/scrubSecrets.ts) — keep in sync.
|
|
65
|
-
const SECRET_PATTERNS = [
|
|
65
|
+
export const SECRET_PATTERNS = [
|
|
66
66
|
[/\b([a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+):[^\s:@/]+@/gi, "$1:[REDACTED]@"],
|
|
67
67
|
[/\b(sk-[A-Za-z0-9]{16,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|rk_(?:live|test)_[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
|
|
68
68
|
[/\b([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|token|private[_-]?key|access[_-]?key)[A-Za-z0-9_]*)\b(\s*[:=]\s*)("[^"]+"|'[^']+'|`[^`]+`|[^\s"']+)/gi, "$1$2[REDACTED]"],
|
|
@@ -106,6 +106,18 @@ function collapsePathToken(token) {
|
|
|
106
106
|
* `node_modules/` on, so dependency frames stay diagnosable)
|
|
107
107
|
* Over-redacts rather than under-redacts; pure; never throws.
|
|
108
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* Lightweight secret-only scrub — applies SECRET_PATTERNS and nothing else.
|
|
111
|
+
* Matches the extension's `scrubSecrets` exactly (no path collapse, no env
|
|
112
|
+
* redaction). Use this for feedback transcripts where file paths and code
|
|
113
|
+
* context must stay readable; the backend re-normalizes home paths on receipt.
|
|
114
|
+
*/
|
|
115
|
+
export function scrubSecrets(text) {
|
|
116
|
+
let out = text;
|
|
117
|
+
for (const [re, repl] of SECRET_PATTERNS)
|
|
118
|
+
out = out.replace(re, repl);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
109
121
|
export function sanitizeCrashText(text, opts = {}) {
|
|
110
122
|
let out = text;
|
|
111
123
|
const env = opts.env ?? process.env;
|
package/dist/doctor.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
14
|
*/
|
|
15
15
|
import { type TokenExpiryStatus } from "./launch.js";
|
|
16
|
+
import { type OtelLaunchConfig } from "./otel.js";
|
|
16
17
|
import { type Profile } from "./profiles.js";
|
|
17
18
|
export type CheckStatus = "ok" | "warn" | "fail";
|
|
18
19
|
export interface CheckResult {
|
|
@@ -60,6 +61,12 @@ export declare function checkCliUpdate(probe: {
|
|
|
60
61
|
latest: string | null;
|
|
61
62
|
}): CheckResult;
|
|
62
63
|
export declare function checkGh(onPath: boolean): CheckResult;
|
|
64
|
+
/**
|
|
65
|
+
* Advisory OTel-export line: says whether sessions will stream traces to an
|
|
66
|
+
* OTLP collector, and from which config source. Never flips the exit code —
|
|
67
|
+
* most machines have no collector, and that is the healthy default.
|
|
68
|
+
*/
|
|
69
|
+
export declare function checkOtelExport(config: OtelLaunchConfig | undefined): CheckResult;
|
|
63
70
|
/** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
|
|
64
71
|
export interface BashProbe {
|
|
65
72
|
found: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -17,6 +17,7 @@ import { delimiter, join } from "node:path";
|
|
|
17
17
|
import { credentialsDir } from "./credentials.js";
|
|
18
18
|
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
19
19
|
import { classifyTokenExpiry } from "./launch.js";
|
|
20
|
+
import { resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
20
21
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
21
22
|
import { readActiveProfile } from "./profiles.js";
|
|
22
23
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
@@ -198,6 +199,32 @@ export function checkGh(onPath) {
|
|
|
198
199
|
required: false,
|
|
199
200
|
};
|
|
200
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Advisory OTel-export line: says whether sessions will stream traces to an
|
|
204
|
+
* OTLP collector, and from which config source. Never flips the exit code —
|
|
205
|
+
* most machines have no collector, and that is the healthy default.
|
|
206
|
+
*/
|
|
207
|
+
export function checkOtelExport(config) {
|
|
208
|
+
if (!config) {
|
|
209
|
+
return {
|
|
210
|
+
name: "otel export (optional)",
|
|
211
|
+
status: "ok",
|
|
212
|
+
detail: "off (no OTLP endpoint configured)",
|
|
213
|
+
required: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const source = config.source === "env"
|
|
217
|
+
? "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
218
|
+
: config.source === "workspace"
|
|
219
|
+
? "workspace settings"
|
|
220
|
+
: ".pi/settings.json";
|
|
221
|
+
return {
|
|
222
|
+
name: "otel export (optional)",
|
|
223
|
+
status: "ok",
|
|
224
|
+
detail: `on → ${config.endpoint} (${source}, metadata-only)`,
|
|
225
|
+
required: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
201
228
|
export function checkBash(probe) {
|
|
202
229
|
if (!probe.found) {
|
|
203
230
|
return {
|
|
@@ -379,6 +406,12 @@ export async function gatherChecks(deps = {}) {
|
|
|
379
406
|
checks.push(checkBackend(backend));
|
|
380
407
|
checks.push(checkStateDir(probeStateDir()));
|
|
381
408
|
checks.push(checkGh(ghOnPath()));
|
|
409
|
+
checks.push(checkOtelExport(await resolveOtelLaunchWithWorkspace({
|
|
410
|
+
env: process.env,
|
|
411
|
+
cwd: process.cwd(),
|
|
412
|
+
creds: profile.token ? { baseUrl: profile.baseUrl, token: profile.token } : null,
|
|
413
|
+
profileName: profile.name,
|
|
414
|
+
})));
|
|
382
415
|
return checks;
|
|
383
416
|
}
|
|
384
417
|
/**
|
|
@@ -29,6 +29,8 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
29
29
|
import { type Component } from "@earendil-works/pi-tui";
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
32
|
+
import type { ChildUsageHandle } from "./childUsage.js";
|
|
33
|
+
import type { WorkingLineHandle } from "./workingLine.js";
|
|
32
34
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
33
35
|
import { type PipelineStage } from "./pipeline/types.js";
|
|
34
36
|
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
@@ -49,6 +51,18 @@ export interface MakeAskAdvisorToolOptions {
|
|
|
49
51
|
limits?: AdvisorLimits;
|
|
50
52
|
/** Injectable so tests never spawn a child. */
|
|
51
53
|
runStage?: typeof defaultRunStage;
|
|
54
|
+
/**
|
|
55
|
+
* The session working-line manager (workingLine.ts). When present, live
|
|
56
|
+
* consult progress goes through it (so the elapsed/token suffix survives);
|
|
57
|
+
* absent, the tool falls back to ui.setWorkingMessage directly.
|
|
58
|
+
*/
|
|
59
|
+
workingLine?: WorkingLineHandle;
|
|
60
|
+
/**
|
|
61
|
+
* Session child-usage accumulator (childUsage.ts), so the footer's session
|
|
62
|
+
* totals include advisor consults — the spend the consult-cost line already
|
|
63
|
+
* prints. Absent = not wired (tests/harnesses): no recording.
|
|
64
|
+
*/
|
|
65
|
+
childUsage?: ChildUsageHandle;
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
@@ -125,9 +125,11 @@ export function makeAskAdvisorTool(opts) {
|
|
|
125
125
|
"The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
|
|
126
126
|
],
|
|
127
127
|
parameters,
|
|
128
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
129
|
+
renderShell: "self",
|
|
128
130
|
renderCall: renderAdvisorCall,
|
|
129
131
|
renderResult: renderSubagentResult,
|
|
130
|
-
async execute(
|
|
132
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
131
133
|
// Read the LIVE session model: pi's picker can change it after this tool
|
|
132
134
|
// was registered, in both directions.
|
|
133
135
|
const decision = decideConsult({
|
|
@@ -159,9 +161,12 @@ export function makeAskAdvisorTool(opts) {
|
|
|
159
161
|
},
|
|
160
162
|
});
|
|
161
163
|
const working = formatWorkingMessage([progress], now);
|
|
162
|
-
if (
|
|
164
|
+
if (working !== lastWorking) {
|
|
163
165
|
lastWorking = working;
|
|
164
|
-
|
|
166
|
+
if (opts.workingLine)
|
|
167
|
+
opts.workingLine.setActivity(working);
|
|
168
|
+
else
|
|
169
|
+
ui?.setWorkingMessage?.(working);
|
|
165
170
|
}
|
|
166
171
|
};
|
|
167
172
|
emit();
|
|
@@ -181,11 +186,18 @@ export function makeAskAdvisorTool(opts) {
|
|
|
181
186
|
}
|
|
182
187
|
finally {
|
|
183
188
|
// Restore the default "Working…" text whether we resolved or threw.
|
|
184
|
-
|
|
189
|
+
if (opts.workingLine)
|
|
190
|
+
opts.workingLine.setActivity(undefined);
|
|
191
|
+
else
|
|
192
|
+
ui?.setWorkingMessage?.();
|
|
185
193
|
}
|
|
186
194
|
const cost = result.usage?.cost ?? 0;
|
|
187
195
|
const state = opts.state.record(cost);
|
|
188
196
|
finalizeTask(progress, result, Date.now());
|
|
197
|
+
// Footer session totals include the consult. Keyed by toolCallId — one
|
|
198
|
+
// consult per call, unique even for parallel consults minted in the same
|
|
199
|
+
// millisecond — so a re-fired finalize for the same call is a no-op.
|
|
200
|
+
opts.childUsage?.record("advisor", toolCallId, result.usage);
|
|
189
201
|
if (result.exitCode !== 0 && !result.finalOutput.trim()) {
|
|
190
202
|
// Fail honestly rather than returning an empty recommendation. The
|
|
191
203
|
// consult still counts: it spawned, and it may well have spent.
|
|
@@ -12,7 +12,7 @@ export interface Citation {
|
|
|
12
12
|
* AskStanding — mirrored locally by this extension's no-workspace-imports
|
|
13
13
|
* convention, see costHud.ts).
|
|
14
14
|
*/
|
|
15
|
-
export type AskStanding = "confirmed" | "asserted" | "inferred" | "no_position";
|
|
15
|
+
export type AskStanding = "confirmed" | "asserted" | "inferred" | "no_position" | "contested";
|
|
16
16
|
/** One line the TUI shows above the answer, per standing. */
|
|
17
17
|
export declare const STANDING_LINES: Record<AskStanding, string>;
|
|
18
18
|
/** Options for {@link makeAskYagniTool}. */
|
|
@@ -10,6 +10,7 @@ export const STANDING_LINES = {
|
|
|
10
10
|
asserted: "Grounded in a recorded assumption, not yet verified",
|
|
11
11
|
inferred: "Inferred from workspace context, not a recorded decision",
|
|
12
12
|
no_position: "No recorded position in this workspace",
|
|
13
|
+
contested: "Two recorded decisions conflict — escalate to a human",
|
|
13
14
|
};
|
|
14
15
|
function standingLine(value) {
|
|
15
16
|
return typeof value === "string" && value in STANDING_LINES
|
|
@@ -56,6 +57,8 @@ export function makeAskYagniTool(opts) {
|
|
|
56
57
|
"Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
|
|
57
58
|
],
|
|
58
59
|
parameters,
|
|
60
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
61
|
+
renderShell: "self",
|
|
59
62
|
renderCall(args, theme) {
|
|
60
63
|
const t = theme;
|
|
61
64
|
let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
|
|
@@ -139,11 +142,29 @@ export function makeAskYagniTool(opts) {
|
|
|
139
142
|
text = `${text}\n\n${suggestion}`;
|
|
140
143
|
}
|
|
141
144
|
}
|
|
145
|
+
// P0 (Run 8 review): a contested answer must reach the MODEL, not just
|
|
146
|
+
// the TUI. The backend composes an ordinary answer and only overrides
|
|
147
|
+
// standing client-side, so without this the coding agent can act on one
|
|
148
|
+
// conflicting side without ever seeing the do-not-proceed instruction.
|
|
149
|
+
// The refusal + both positions are appended VERBATIM to tool content.
|
|
150
|
+
if (data.standing === "contested") {
|
|
151
|
+
const lines = [];
|
|
152
|
+
if (data.contestedInstruction)
|
|
153
|
+
lines.push(data.contestedInstruction);
|
|
154
|
+
for (const p of data.contestedPositions ?? []) {
|
|
155
|
+
lines.push(`- (${p.id}) ${p.question}: ${p.decision}`);
|
|
156
|
+
}
|
|
157
|
+
if (lines.length > 0) {
|
|
158
|
+
text = `${data.answer}\n\n${lines.join("\n")}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
142
161
|
return {
|
|
143
162
|
content: [{ type: "text", text }],
|
|
144
163
|
details: {
|
|
145
164
|
citations: data.citations ?? [],
|
|
146
165
|
...(data.standing ? { standing: data.standing } : {}),
|
|
166
|
+
...(data.contestedInstruction ? { contestedInstruction: data.contestedInstruction } : {}),
|
|
167
|
+
...(data.contestedPositions ? { contestedPositions: data.contestedPositions } : {}),
|
|
147
168
|
},
|
|
148
169
|
};
|
|
149
170
|
},
|