@yagni-app/code-staging 1.0.0-staging.1178.1 → 1.0.0-staging.1180.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/dist/cli.js +153 -5
- package/dist/extension/branding.d.ts +2 -0
- package/dist/extension/branding.js +9 -0
- package/dist/extension/index.js +28 -0
- package/dist/extension/pipeline/personas.js +4 -4
- 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/subagents.js +7 -1
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -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 +2 -2
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";
|
|
@@ -35,8 +35,9 @@ import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgra
|
|
|
35
35
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
36
36
|
import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
|
|
37
37
|
import { PAD_X } from "./padding.js";
|
|
38
|
+
import { parseWorktreeFlag, validateWorktreeLaunchArgs } from "./worktreeArgs.js";
|
|
38
39
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
39
|
-
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
40
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveSessionWorktreePath } from "./paths.js";
|
|
40
41
|
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
41
42
|
// Present as "yagni" in process listings, not "node".
|
|
42
43
|
process.title = DISTRIBUTION.commandName;
|
|
@@ -161,6 +162,14 @@ export function seedHideThinkingBlock(piAgentDir) {
|
|
|
161
162
|
return seedSetting(piAgentDir, "hideThinkingBlock", true);
|
|
162
163
|
}
|
|
163
164
|
async function runDefault(passthroughArgs) {
|
|
165
|
+
// `-w / --worktree [name]` is its own launch path: create/resume a worktree
|
|
166
|
+
// and enter a session there. Gate strictly on `requested` (not `name`) so a
|
|
167
|
+
// bare `-w` (random slug) is honored too. Returning early keeps the standard
|
|
168
|
+
// path below physically unreachable by any `-w` bug.
|
|
169
|
+
const worktree = parseWorktreeFlag(passthroughArgs);
|
|
170
|
+
if (worktree.requested) {
|
|
171
|
+
return runWorktreeLaunch(worktree.remainingArgs, worktree.name);
|
|
172
|
+
}
|
|
164
173
|
// Parse --output-format out of argv before passing to pi (pi doesn't know
|
|
165
174
|
// about it). The format determines how we handle pi's stdout: text = inherit,
|
|
166
175
|
// stream-json = inherit with --mode json, json = pipe + post-process.
|
|
@@ -266,10 +275,22 @@ async function runDefault(passthroughArgs) {
|
|
|
266
275
|
process.stderr.write(`${warning}\n`);
|
|
267
276
|
}
|
|
268
277
|
const { env, argv } = plan;
|
|
278
|
+
return spawnPiAndAwait({
|
|
279
|
+
argv,
|
|
280
|
+
env,
|
|
281
|
+
remainingArgs,
|
|
282
|
+
outputFormat,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Spawn pi and await its exit, mapping to the launcher's exit code. Extracted
|
|
287
|
+
* so `runDefault` (cwd = current) and `runWorktreeLaunch` (cwd = worktree) share
|
|
288
|
+
* the exact same json/stream-json post-processing, signal forwarding, and exit
|
|
289
|
+
* code mapping — the two paths can never drift on the output contract.
|
|
290
|
+
*/
|
|
291
|
+
async function spawnPiAndAwait(opts) {
|
|
292
|
+
const { argv, env, remainingArgs, outputFormat, cwd } = opts;
|
|
269
293
|
// For json/stream-json output, inject --mode json so pi emits NDJSON events.
|
|
270
|
-
// Don't override if the user already chose --mode (mirrors userChoseProvider/
|
|
271
|
-
// userChoseModel in buildLaunch). Appended at the end — pi's flag parser
|
|
272
|
-
// handles --mode anywhere in argv.
|
|
273
294
|
const userChoseMode = remainingArgs.some((a) => a === "--mode" || a.startsWith("--mode="));
|
|
274
295
|
const childArgv = outputFormat === "text" || userChoseMode
|
|
275
296
|
? argv
|
|
@@ -285,6 +306,7 @@ async function runDefault(passthroughArgs) {
|
|
|
285
306
|
const child = spawn(process.execPath, [piCli, ...childArgv], {
|
|
286
307
|
stdio: stdio,
|
|
287
308
|
env,
|
|
309
|
+
...(cwd ? { cwd } : {}),
|
|
288
310
|
});
|
|
289
311
|
// Collect pi's stdout when piping for --output-format json.
|
|
290
312
|
let stdoutChunks = "";
|
|
@@ -323,6 +345,131 @@ async function runDefault(passthroughArgs) {
|
|
|
323
345
|
});
|
|
324
346
|
});
|
|
325
347
|
}
|
|
348
|
+
async function defaultLoadSessionWorktree() {
|
|
349
|
+
const mod = (await import(pathToFileURL(resolveSessionWorktreePath()).href));
|
|
350
|
+
if (typeof mod?.createOrResume !== "function") {
|
|
351
|
+
throw new Error("The bundled extension is missing its session-worktree entry point (is the CLI up to date?).");
|
|
352
|
+
}
|
|
353
|
+
return mod;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* `yagni -w [name]` — create/resume a worktree and enter a session there.
|
|
357
|
+
*
|
|
358
|
+
* Delegates all git behavior to the extension's `sessionWorktree` entry; this
|
|
359
|
+
* launcher path only resolves credentials, builds the plan, and spawns pi with
|
|
360
|
+
* `cwd` = the worktree. The worktree is DURABLE — nothing is ever removed.
|
|
361
|
+
*/
|
|
362
|
+
async function runWorktreeLaunch(passthroughArgs, worktreeName, loadSessionWorktree = defaultLoadSessionWorktree) {
|
|
363
|
+
// Validate the name + argv before any side effect (slug guard + `-c`
|
|
364
|
+
// disallow). The extension re-validates the mapped slug for PR refs.
|
|
365
|
+
const launchError = validateWorktreeLaunchArgs(worktreeName, passthroughArgs);
|
|
366
|
+
if (launchError !== undefined) {
|
|
367
|
+
process.stderr.write(`${launchError}\n`);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
const { format: outputFormat, remainingArgs } = parseOutputFormat(passthroughArgs);
|
|
371
|
+
// Load the session-worktree entry first, so a stale bundled extension fails
|
|
372
|
+
// honestly before we mutate anything.
|
|
373
|
+
let sessionWorktree;
|
|
374
|
+
try {
|
|
375
|
+
sessionWorktree = await loadSessionWorktree();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
// Create/resume resolves the main repo root + branch + dir, and performs
|
|
382
|
+
// `git worktree add` / resume. Never throws on a normal path; catch-all maps
|
|
383
|
+
// to a clean stderr + exit 1.
|
|
384
|
+
let result;
|
|
385
|
+
try {
|
|
386
|
+
result = await sessionWorktree.createOrResume(worktreeName, {
|
|
387
|
+
repoCwd: process.cwd(),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
// Reuse the standard credential + env + plan flow (same token refresh, shadow
|
|
395
|
+
// package, compat args, preflight). The worktree becomes the project cwd, so
|
|
396
|
+
// compat assets resolve from there.
|
|
397
|
+
await maybeNudgeAndRefresh({ current: cliVersion() });
|
|
398
|
+
const profile = await readActiveProfile();
|
|
399
|
+
let creds = credentialsFromProfile(profile);
|
|
400
|
+
if (!creds?.token) {
|
|
401
|
+
process.stderr.write(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.\n`);
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
const refresh = await maybeRefreshAtLaunch(creds, {
|
|
405
|
+
persist: (c) => persistProfileTokenRotation(profile.name, c),
|
|
406
|
+
});
|
|
407
|
+
for (const warning of refresh.warnings) {
|
|
408
|
+
process.stderr.write(`${warning}\n`);
|
|
409
|
+
}
|
|
410
|
+
creds = refresh.creds;
|
|
411
|
+
const piAgentDir = agentDir(profile.name);
|
|
412
|
+
mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
|
|
413
|
+
seedEditorPadding(piAgentDir);
|
|
414
|
+
seedCollapseChangelog(piAgentDir);
|
|
415
|
+
seedHideThinkingBlock(piAgentDir);
|
|
416
|
+
let shadowPiDir;
|
|
417
|
+
try {
|
|
418
|
+
shadowPiDir = ensureShadowPiPackage({
|
|
419
|
+
realPiDir: resolvePiPackageDir(),
|
|
420
|
+
shadowDir: piPackageDir(),
|
|
421
|
+
name: PI_CONFIG_NAME,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
shadowPiDir = undefined;
|
|
426
|
+
}
|
|
427
|
+
let compat = { argv: [], env: {} };
|
|
428
|
+
try {
|
|
429
|
+
compat = await claudeCompatArgs({
|
|
430
|
+
cwd: result.worktreePath,
|
|
431
|
+
agentDir: piAgentDir,
|
|
432
|
+
confirm: confirmOnTty,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
compat = { argv: [], env: {} };
|
|
437
|
+
}
|
|
438
|
+
let plan;
|
|
439
|
+
try {
|
|
440
|
+
plan = buildLaunch(creds, remainingArgs, {
|
|
441
|
+
extensionPath: resolveExtensionPath(),
|
|
442
|
+
agentDir: piAgentDir,
|
|
443
|
+
piPackageDir: shadowPiDir,
|
|
444
|
+
extraAgentArgs: compat.argv,
|
|
445
|
+
extraEnv: compat.env,
|
|
446
|
+
profilePath: profilePath(profile.name),
|
|
447
|
+
stateDir: credentialsDir(),
|
|
448
|
+
cliVersion: cliVersion(),
|
|
449
|
+
baseEnv: process.env,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
for (const warning of plan.warnings) {
|
|
457
|
+
process.stderr.write(`${warning}\n`);
|
|
458
|
+
}
|
|
459
|
+
const exitCode = await spawnPiAndAwait({
|
|
460
|
+
argv: plan.argv,
|
|
461
|
+
env: plan.env,
|
|
462
|
+
remainingArgs,
|
|
463
|
+
outputFormat,
|
|
464
|
+
cwd: result.worktreePath,
|
|
465
|
+
});
|
|
466
|
+
// Durable by default: nothing is removed. Tell the user where their work
|
|
467
|
+
// lives (stderr only — never stdout, to keep json clean).
|
|
468
|
+
process.stderr.write(`[yagni] worktree ${result.existed ? "resumed" : "created"}: ${result.worktreePath}\n` +
|
|
469
|
+
`[yagni] branch: ${result.branch}\n` +
|
|
470
|
+
`[yagni] resume: cd ${result.worktreePath} && yagni\n`);
|
|
471
|
+
return exitCode;
|
|
472
|
+
}
|
|
326
473
|
export const HELP_TEXT = [
|
|
327
474
|
"YAGNI Code — a business-context-grounded terminal coding agent.",
|
|
328
475
|
"",
|
|
@@ -330,6 +477,7 @@ export const HELP_TEXT = [
|
|
|
330
477
|
" yagni [args…] Launch the agent in the current repo.",
|
|
331
478
|
" yagni -c Continue the most recent session.",
|
|
332
479
|
" yagni -r Browse and resume a previous session.",
|
|
480
|
+
" yagni -w [name] Create/resume a worktree and enter a session there.",
|
|
333
481
|
' yagni -p "prompt" Print one response and exit (reads piped stdin too).',
|
|
334
482
|
' yagni -p "prompt" Use --output-format json for a machine-readable',
|
|
335
483
|
' --output-format json result object with tools, cost, guardian reviews.',
|
|
@@ -111,6 +111,8 @@ export interface BrandSystemPromptOptions {
|
|
|
111
111
|
* never brand-rewritten (same exemption as <project_context>).
|
|
112
112
|
*/
|
|
113
113
|
rulesSection?: string | null;
|
|
114
|
+
/** Scratchpad-directory prompt section, supplied when a scratchpad is configured. */
|
|
115
|
+
scratchpadSection?: string;
|
|
114
116
|
}
|
|
115
117
|
/**
|
|
116
118
|
* Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
|
|
@@ -262,6 +262,13 @@ export function brandSystemPrompt(original, opts = {}) {
|
|
|
262
262
|
if (!s.includes(WRITE_FINDINGS_DOWN)) {
|
|
263
263
|
s = `${s}\n\n${WRITE_FINDINGS_DOWN}`;
|
|
264
264
|
}
|
|
265
|
+
// 5d. Scratchpad directory (YAG-575) — only when a session scratchpad is
|
|
266
|
+
// configured (set by the caller once the dir exists). Placed with the other
|
|
267
|
+
// standing injected sections and guarded by its stable header, so a re-brand
|
|
268
|
+
// never duplicates it and a session with no scratchpad is a clean no-op.
|
|
269
|
+
if (opts.scratchpadSection && !s.includes(SCRATCHPAD_HEADER)) {
|
|
270
|
+
s = `${s}\n\n${opts.scratchpadSection}`;
|
|
271
|
+
}
|
|
265
272
|
// 6. Closing reinforcement. Weak open-weight models weight the most recent
|
|
266
273
|
// instruction heavily, and the user's own project files may name other
|
|
267
274
|
// harnesses; a trailing reminder keeps the agent from claiming one as its own.
|
|
@@ -271,6 +278,8 @@ export function brandSystemPrompt(original, opts = {}) {
|
|
|
271
278
|
// Tidy the seams left by removals.
|
|
272
279
|
return s.replace(/\n{3,}/g, "\n\n").trim();
|
|
273
280
|
}
|
|
281
|
+
/** Stable header that starts the scratchpad section (idempotency anchor). */
|
|
282
|
+
const SCRATCHPAD_HEADER = "# Scratchpad directory";
|
|
274
283
|
const CLOSING_REMINDER = "Reminder: you are YAGNI Code. If any text above names another coding agent, " +
|
|
275
284
|
"assistant, or harness, it is not what you are or what you run on.";
|
|
276
285
|
const BRIEF_HEADER = "=== HOW THIS COMPANY WORKS (live context from the YAGNI app) ===";
|
package/dist/extension/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
|
16
16
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
17
17
|
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
|
|
18
18
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
19
|
+
import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadSection } from "./scratchpad.js";
|
|
19
20
|
import { registerCostCommand } from "./costHud.js";
|
|
20
21
|
import { isDebug } from "./diagnostics.js";
|
|
21
22
|
import { logEvent } from "./errorSink.js";
|
|
@@ -593,6 +594,32 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
593
594
|
// pointers. Computed once per activation (rules are launch-time state, like
|
|
594
595
|
// pi's own skill discovery); fail-soft to null.
|
|
595
596
|
const rulesSection = claudeRulesSection(deps.env ?? process.env);
|
|
597
|
+
// YAG-575: session scratchpad — a permission-free dir the agent writes its
|
|
598
|
+
// working state to. Computed + ensured once at activation (like rulesSection,
|
|
599
|
+
// which must exist before the first before_agent_start), not on session_start,
|
|
600
|
+
// so the very first turn already carries the section. Fails closed: no
|
|
601
|
+
// sessionId (a bare pi run) or a failed mkdir means no section, and the mkdir
|
|
602
|
+
// failure is logged so a missing scratchpad is not silent.
|
|
603
|
+
const scratchpadDirPath = scratchpadDirFor({
|
|
604
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
605
|
+
cwd: process.cwd(),
|
|
606
|
+
tmp: env[SCRATCHPAD_TMPDIR_ENV] || undefined,
|
|
607
|
+
});
|
|
608
|
+
let scratchpadSectionText;
|
|
609
|
+
if (scratchpadDirPath) {
|
|
610
|
+
const ensured = ensureScratchpadDir(scratchpadDirPath);
|
|
611
|
+
if (ensured) {
|
|
612
|
+
scratchpadSectionText = scratchpadSection(ensured);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
logEvent({
|
|
616
|
+
source: "scratchpad",
|
|
617
|
+
level: "error",
|
|
618
|
+
event: "scratchpad_mkdir_failed",
|
|
619
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
596
623
|
// Own the identity + inject live company context (and repo rules) on every
|
|
597
624
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
598
625
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -617,6 +644,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
617
644
|
: YAGNI_IDENTITY_DRIVER
|
|
618
645
|
: undefined,
|
|
619
646
|
rulesSection,
|
|
647
|
+
scratchpadSection: scratchpadSectionText,
|
|
620
648
|
}),
|
|
621
649
|
});
|
|
622
650
|
// Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
|
|
@@ -56,7 +56,7 @@ Numbered, small, actionable steps — each names the file/function to touch.
|
|
|
56
56
|
## Risks
|
|
57
57
|
What to watch for, including any decision the worker will be forced to make.
|
|
58
58
|
|
|
59
|
-
Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
|
|
59
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
|
|
60
60
|
|
|
61
61
|
Budget discipline: you have a hard output budget, and a plan that gets cut off mid-thought is worth less than a short plan that ships. Explore only until you can name the files and the steps — do not read broadly for completeness, and do not re-verify what you have already established. Aim for 5-10 short steps; the worker fills small gaps from the ticket. When in doubt, write the plan NOW.
|
|
62
62
|
|
|
@@ -65,7 +65,7 @@ const WORKER_BODY = `You are a worker with full capabilities, operating in an is
|
|
|
65
65
|
|
|
66
66
|
You are grounded. Call ask_yagni before guessing about anything organization- or codebase-specific. Treat a confirmed answer as settled; when an answer is an unverified assumption or an inference and your change leans on it, say so in your Notes so the reviewer knows what to check. Critically: for ANY product-intent call you are forced to make that the plan did not settle — a behavior choice, a tradeoff, an interpretation of intent — call record_decision so the company's decision corpus captures it and the next agent inherits the call instead of re-litigating it. When ask_yagni reports no recorded position, follow its instruction and record the assumption you proceed on.
|
|
67
67
|
|
|
68
|
-
You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code, calling record_decision for any intent you infer. Ending your turn with no write/edit is a failure.
|
|
68
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code, calling record_decision for any intent you infer. Ending your turn with no write/edit is a failure.
|
|
69
69
|
|
|
70
70
|
Output:
|
|
71
71
|
## Completed
|
|
@@ -242,14 +242,14 @@ Numbered, small, actionable steps — each names the file/function to touch.
|
|
|
242
242
|
## Risks
|
|
243
243
|
What to watch for, including any decision the worker will be forced to make.
|
|
244
244
|
|
|
245
|
-
Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
|
|
245
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. Finish the job in ONE turn: do not end your turn on an interstitial like "now let me check X". Your FINAL message MUST be the complete plan in the format above (## Goal / ## Plan / ## Files to modify or create / ## Risks). Keep exploring with your tools until you can write the whole plan, then write it as your last message.
|
|
246
246
|
|
|
247
247
|
Budget discipline: you have a hard output budget, and a plan that gets cut off mid-thought is worth less than a short plan that ships. Explore only until you can name the files and the steps — do not read broadly for completeness, and do not re-verify what you have already established. Aim for 5-10 short steps; the worker fills small gaps from the ticket. When in doubt, write the plan NOW.
|
|
248
248
|
|
|
249
249
|
Keep it concrete; the worker executes it verbatim.`;
|
|
250
250
|
const WORKER_BLIND = `You are a worker with full capabilities, operating in an isolated context to implement a plan. Work autonomously and use the tools as needed.
|
|
251
251
|
|
|
252
|
-
You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code. Ending your turn with no write/edit is a failure.
|
|
252
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. You MUST make the change. If the plan is missing, partial, or appears cut off, do not stop at exploring: implement the ticket directly from the ticket text and the code. Ending your turn with no write/edit is a failure.
|
|
253
253
|
|
|
254
254
|
Output:
|
|
255
255
|
## Completed
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `-w / --worktree` session-worktree plumbing.
|
|
3
|
+
*
|
|
4
|
+
* The launcher (`yagni-code-cli`) reaches this module by file path (the same
|
|
5
|
+
* seam as `headlessGo.ts` → `runHeadlessGo`) so it can create/resume a named
|
|
6
|
+
* YAGNI worktree BEFORE spawning pi into it. The launcher owns the process
|
|
7
|
+
* lifecycle (spawn + `cwd` + exit summary); this module owns the git behavior.
|
|
8
|
+
*
|
|
9
|
+
* Design invariants (see the YAG-594 plan):
|
|
10
|
+
* - **Add-only creation.** Every git op is `worktree add`, `fetch`, `show-ref`,
|
|
11
|
+
* `symbolic-ref`, or `rev-parse`. Nothing deletes, force-resets, or
|
|
12
|
+
* `branch -D`s — the worktree is DURABLE by default and never auto-removed.
|
|
13
|
+
* - **Get-or-resume.** An existing worktree dir is resumed, never recreated.
|
|
14
|
+
* - **Lazy fetch.** Base `origin/<default>` is read from the local ref when
|
|
15
|
+
* present; `git fetch` only runs when that ref is absent, and always with
|
|
16
|
+
* credential prompts disabled.
|
|
17
|
+
* - **Validate before any side effect.** The slug is checked (again, defense
|
|
18
|
+
* in depth against the launcher) before the first git subprocess.
|
|
19
|
+
* - **Canonical root.** `-w` invoked from inside an existing worktree lands in
|
|
20
|
+
* the main repo, never nested.
|
|
21
|
+
*
|
|
22
|
+
* Convention reused from `/wt-new`: branch `agent/<slug>`, dir `.worktrees/<slug>`
|
|
23
|
+
* (both gitignored in-repo). PR refs (`#N`, GitHub PR URLs) map to `pr-<N>` and
|
|
24
|
+
* base on `FETCH_HEAD`.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionWorktreeResult {
|
|
27
|
+
/** Absolute destination (under `<mainRepo>/.worktrees/<slug>`). */
|
|
28
|
+
worktreePath: string;
|
|
29
|
+
/** The `agent/<slug>` branch. */
|
|
30
|
+
branch: string;
|
|
31
|
+
/** True when the worktree already existed (resumed, not created). */
|
|
32
|
+
existed: boolean;
|
|
33
|
+
}
|
|
34
|
+
export type SessionGit = (argv: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<string>;
|
|
35
|
+
export interface CreateOrResumeDeps {
|
|
36
|
+
/** Repo the user ran `yagni -w` from (any path inside it works for git). */
|
|
37
|
+
repoCwd: string;
|
|
38
|
+
/** Injectable git seam (defaults to a real `git` exec). */
|
|
39
|
+
gitImpl?: SessionGit;
|
|
40
|
+
/** Injectable fs seam for existence checks (defaults to node:fs). */
|
|
41
|
+
pathExists?: (p: string) => boolean;
|
|
42
|
+
/** Injectable randomness (defaults to Math.random). */
|
|
43
|
+
random?: () => number;
|
|
44
|
+
}
|
|
45
|
+
/** Turn arbitrary name text into a git-ref-safe slug. Empty input → "worktree". */
|
|
46
|
+
export declare function slugify(name: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Validate a worktree slug before any side effect. Mirrors Claude's guard:
|
|
49
|
+
* length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously.
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateWorktreeSlug(slug: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a PR reference: `#N` or a GitHub-style PR URL. Returns the number or null.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parsePRReference(input: string): number | null;
|
|
56
|
+
/**
|
|
57
|
+
* Create or resume the session worktree for `name`.
|
|
58
|
+
*
|
|
59
|
+
* Throws with a user-surfaced message on any failure; the caller (launcher)
|
|
60
|
+
* catches and prints it to stderr + the diagnostic sink. Never leaves a partial
|
|
61
|
+
* branch/worktree: validation happens first, and `git worktree add` is atomic.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createOrResume(name: string | undefined, deps: CreateOrResumeDeps): Promise<SessionWorktreeResult>;
|
|
64
|
+
//# sourceMappingURL=sessionWorktree.d.ts.map
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `-w / --worktree` session-worktree plumbing.
|
|
3
|
+
*
|
|
4
|
+
* The launcher (`yagni-code-cli`) reaches this module by file path (the same
|
|
5
|
+
* seam as `headlessGo.ts` → `runHeadlessGo`) so it can create/resume a named
|
|
6
|
+
* YAGNI worktree BEFORE spawning pi into it. The launcher owns the process
|
|
7
|
+
* lifecycle (spawn + `cwd` + exit summary); this module owns the git behavior.
|
|
8
|
+
*
|
|
9
|
+
* Design invariants (see the YAG-594 plan):
|
|
10
|
+
* - **Add-only creation.** Every git op is `worktree add`, `fetch`, `show-ref`,
|
|
11
|
+
* `symbolic-ref`, or `rev-parse`. Nothing deletes, force-resets, or
|
|
12
|
+
* `branch -D`s — the worktree is DURABLE by default and never auto-removed.
|
|
13
|
+
* - **Get-or-resume.** An existing worktree dir is resumed, never recreated.
|
|
14
|
+
* - **Lazy fetch.** Base `origin/<default>` is read from the local ref when
|
|
15
|
+
* present; `git fetch` only runs when that ref is absent, and always with
|
|
16
|
+
* credential prompts disabled.
|
|
17
|
+
* - **Validate before any side effect.** The slug is checked (again, defense
|
|
18
|
+
* in depth against the launcher) before the first git subprocess.
|
|
19
|
+
* - **Canonical root.** `-w` invoked from inside an existing worktree lands in
|
|
20
|
+
* the main repo, never nested.
|
|
21
|
+
*
|
|
22
|
+
* Convention reused from `/wt-new`: branch `agent/<slug>`, dir `.worktrees/<slug>`
|
|
23
|
+
* (both gitignored in-repo). PR refs (`#N`, GitHub PR URLs) map to `pr-<N>` and
|
|
24
|
+
* base on `FETCH_HEAD`.
|
|
25
|
+
*/
|
|
26
|
+
import { execFile } from "node:child_process";
|
|
27
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
28
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
29
|
+
import { bootstrapWorktree } from "./worktree.js";
|
|
30
|
+
/** Cap on the slug half (keeps refs & dirs readable). */
|
|
31
|
+
const SLUG_MAX = 40;
|
|
32
|
+
/** Maximum slug characters, mirrored from Claude's guard. */
|
|
33
|
+
const MAX_SLUG_LENGTH = 64;
|
|
34
|
+
/** Allowlist per `/`-separated segment (mirrors Claude's `validateWorktreeSlug`). */
|
|
35
|
+
const VALID_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
36
|
+
/** Env that prevents git/ssh from prompting for credentials (which would hang). */
|
|
37
|
+
const GIT_NO_PROMPT_ENV = {
|
|
38
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
39
|
+
GIT_ASKPASS: "",
|
|
40
|
+
};
|
|
41
|
+
/** Turn arbitrary name text into a git-ref-safe slug. Empty input → "worktree". */
|
|
42
|
+
export function slugify(name) {
|
|
43
|
+
const slug = name
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "")
|
|
47
|
+
.slice(0, SLUG_MAX)
|
|
48
|
+
.replace(/-+$/, "");
|
|
49
|
+
return slug || "worktree";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate a worktree slug before any side effect. Mirrors Claude's guard:
|
|
53
|
+
* length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously.
|
|
54
|
+
*/
|
|
55
|
+
export function validateWorktreeSlug(slug) {
|
|
56
|
+
if (slug.length > MAX_SLUG_LENGTH) {
|
|
57
|
+
throw new Error(`Invalid worktree name: must be ${MAX_SLUG_LENGTH} characters or fewer (got ${slug.length})`);
|
|
58
|
+
}
|
|
59
|
+
for (const segment of slug.split("/")) {
|
|
60
|
+
if (segment === "." || segment === "..") {
|
|
61
|
+
throw new Error(`Invalid worktree name "${slug}": must not contain "." or ".." path segments`);
|
|
62
|
+
}
|
|
63
|
+
if (!VALID_SLUG_SEGMENT.test(segment)) {
|
|
64
|
+
throw new Error(`Invalid worktree name "${slug}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a PR reference: `#N` or a GitHub-style PR URL. Returns the number or null.
|
|
70
|
+
*/
|
|
71
|
+
export function parsePRReference(input) {
|
|
72
|
+
const urlMatch = input.match(/^https?:\/\/[^/]+\/[^/]+\/[^/]+\/pull\/(\d+)\/?(?:[?#].*)?$/i);
|
|
73
|
+
if (urlMatch?.[1])
|
|
74
|
+
return parseInt(urlMatch[1], 10);
|
|
75
|
+
const hashMatch = input.match(/^#(\d+)$/);
|
|
76
|
+
if (hashMatch?.[1])
|
|
77
|
+
return parseInt(hashMatch[1], 10);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function randomSlug(random = Math.random) {
|
|
81
|
+
const adjectives = ["swift", "bright", "calm", "keen", "bold", "quiet", "warm", "true"];
|
|
82
|
+
const nouns = ["fox", "owl", "elm", "oak", "ray", "fern", "pine", "brook"];
|
|
83
|
+
const adj = adjectives[Math.floor(random() * adjectives.length)];
|
|
84
|
+
const noun = nouns[Math.floor(random() * nouns.length)];
|
|
85
|
+
const suffix = Math.floor(random() * 0x10000).toString(36).padStart(4, "0");
|
|
86
|
+
return `${adj}-${noun}-${suffix}`;
|
|
87
|
+
}
|
|
88
|
+
function defaultGit(argv, cwd, env) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
execFile("git", argv, { cwd, env, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
91
|
+
if (err) {
|
|
92
|
+
reject(new Error(stderr.toString().trim() || err.message));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
resolve(stdout.toString().trim());
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** The main repo root, resolved through an existing linked worktree via commondir. */
|
|
100
|
+
async function resolveMainRepo(gitImpl, repoCwd) {
|
|
101
|
+
let topLevel;
|
|
102
|
+
try {
|
|
103
|
+
topLevel = await gitImpl(["rev-parse", "--show-toplevel"], repoCwd);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
throw new Error(`Cannot create a worktree: not inside a git repository. ` +
|
|
107
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
const common = await gitImpl(["rev-parse", "--git-common-dir"], repoCwd);
|
|
110
|
+
const abs = isAbsolute(common) ? common : join(topLevel, common);
|
|
111
|
+
// A linked worktree's commondir points at the shared `.git`; the main repo root
|
|
112
|
+
// is its parent. A main checkout resolves to its own toplevel.
|
|
113
|
+
return basename(abs) === ".git" ? dirname(abs) : topLevel;
|
|
114
|
+
}
|
|
115
|
+
/** Resolve the default branch: origin/HEAD symref, else main, else master. */
|
|
116
|
+
async function resolveDefaultBranch(gitImpl, repoCwd) {
|
|
117
|
+
try {
|
|
118
|
+
const symref = await gitImpl(["symbolic-ref", "refs/remotes/origin/HEAD"], repoCwd);
|
|
119
|
+
const name = symref.replace(/^refs\/remotes\//, "");
|
|
120
|
+
if (name)
|
|
121
|
+
return name;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* no origin/HEAD symref */
|
|
125
|
+
}
|
|
126
|
+
for (const candidate of ["main", "master"]) {
|
|
127
|
+
try {
|
|
128
|
+
await gitImpl(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${candidate}`], repoCwd);
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* keep looking */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return "main";
|
|
136
|
+
}
|
|
137
|
+
/** True when a local branch `refs/heads/<branch>` exists. */
|
|
138
|
+
async function branchExists(gitImpl, repoCwd, branch) {
|
|
139
|
+
try {
|
|
140
|
+
await gitImpl(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repoCwd);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create or resume the session worktree for `name`.
|
|
149
|
+
*
|
|
150
|
+
* Throws with a user-surfaced message on any failure; the caller (launcher)
|
|
151
|
+
* catches and prints it to stderr + the diagnostic sink. Never leaves a partial
|
|
152
|
+
* branch/worktree: validation happens first, and `git worktree add` is atomic.
|
|
153
|
+
*/
|
|
154
|
+
export async function createOrResume(name, deps) {
|
|
155
|
+
const gitImpl = deps.gitImpl ?? defaultGit;
|
|
156
|
+
const pathExists = deps.pathExists ?? ((p) => existsSync(p));
|
|
157
|
+
const random = deps.random ?? Math.random;
|
|
158
|
+
const repoCwd = deps.repoCwd;
|
|
159
|
+
const prNumber = name !== undefined ? parsePRReference(name) : null;
|
|
160
|
+
const slug = prNumber !== null
|
|
161
|
+
? `pr-${prNumber}`
|
|
162
|
+
: slugify(name ?? randomSlug(random));
|
|
163
|
+
validateWorktreeSlug(slug);
|
|
164
|
+
const repoRoot = await resolveMainRepo(gitImpl, repoCwd);
|
|
165
|
+
const branch = `agent/${slug}`;
|
|
166
|
+
const worktreePath = join(repoRoot, ".worktrees", slug);
|
|
167
|
+
// Get-or-resume: an existing dir is resumed, never recreated/fetched/overwritten.
|
|
168
|
+
if (pathExists(worktreePath)) {
|
|
169
|
+
return { worktreePath, branch, existed: true };
|
|
170
|
+
}
|
|
171
|
+
// Collision: the branch already exists locally (dirty leftover from a prior
|
|
172
|
+
// crash) — refuse rather than force-reset.
|
|
173
|
+
if (await branchExists(gitImpl, repoCwd, branch)) {
|
|
174
|
+
throw new Error(`Branch ${branch} already exists. Pick a different name with \`-w <other>\`, or clean it up first.`);
|
|
175
|
+
}
|
|
176
|
+
mkdirSync(dirname(worktreePath), { recursive: true, mode: 0o700 });
|
|
177
|
+
// Resolve base. PR path fetches the PR head into FETCH_HEAD; default path uses
|
|
178
|
+
// the local origin/<default> ref when present, else fetches, else falls back
|
|
179
|
+
// to HEAD (a repo with no remote or no commits still works).
|
|
180
|
+
let base;
|
|
181
|
+
const fetchEnv = { ...process.env, ...GIT_NO_PROMPT_ENV };
|
|
182
|
+
if (prNumber !== null) {
|
|
183
|
+
try {
|
|
184
|
+
await gitImpl(["fetch", "origin", `pull/${prNumber}/head`], repoCwd, fetchEnv);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
throw new Error(`Failed to fetch PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
188
|
+
`The PR may not exist or this repo may not have a remote named "origin".`);
|
|
189
|
+
}
|
|
190
|
+
base = "FETCH_HEAD";
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const defaultBranch = await resolveDefaultBranch(gitImpl, repoCwd);
|
|
194
|
+
let originRef = null;
|
|
195
|
+
try {
|
|
196
|
+
await gitImpl(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${defaultBranch}`], repoCwd);
|
|
197
|
+
originRef = `origin/${defaultBranch}`;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
try {
|
|
201
|
+
await gitImpl(["fetch", "origin", defaultBranch], repoCwd, fetchEnv);
|
|
202
|
+
originRef = `origin/${defaultBranch}`;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
originRef = "HEAD"; // no remote / no commits: degrade to local HEAD
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
base = originRef;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await gitImpl(["worktree", "add", "-b", branch, worktreePath, base], repoCwd);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
throw new Error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
}
|
|
216
|
+
// Best-effort: install deps + env so a fresh worktree can actually run.
|
|
217
|
+
try {
|
|
218
|
+
await bootstrapWorktree(worktreePath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* bootstrap is best-effort; a failed install must not fail the launch */
|
|
222
|
+
}
|
|
223
|
+
return { worktreePath, branch, existed: false };
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=sessionWorktree.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session scratchpad — a permission-free directory the agent writes its working
|
|
3
|
+
* state to, so neither it nor the user has to reconstruct intermediate results.
|
|
4
|
+
*
|
|
5
|
+
* Ports Claude Code's scratchpad with one deliberate simplification: Claude
|
|
6
|
+
* resolves the tmp root and normalizes against path traversal because its
|
|
7
|
+
* scratchpad is an allow-listed path in a permission classifier. We have no
|
|
8
|
+
* such classifier (auto mode already passes write/edit unprompted, and plan
|
|
9
|
+
* mode's write gate holds scratchpad like every other write), so the path is
|
|
10
|
+
* built plainly and the only caller-facing contract is "the dir exists or the
|
|
11
|
+
* section is omitted."
|
|
12
|
+
*
|
|
13
|
+
* Path mirrors Claude Code's shape under our own owner namespace so the two
|
|
14
|
+
* never collide: <tmp>/yagni-{uid}/<sanitized-cwd>/<sessionId>/scratchpad/.
|
|
15
|
+
* - tmp root: YAGNI_CODE_TMPDIR, else os.tmpdir()
|
|
16
|
+
* - uid: process.getuid() ?? 0 (multi-user isolation; tmpdir() is already
|
|
17
|
+
* per-user on Windows)
|
|
18
|
+
* - sanitized-cwd: non-alphanumerics → "-", length-capped (cosmetic grouping
|
|
19
|
+
* only — sessionId is the real uniqueness key)
|
|
20
|
+
* - sessionId: env.YAGNI_SESSION_ID, minted by the launcher as a UUID; when
|
|
21
|
+
* absent (a bare pi run) no scratchpad is configured at all.
|
|
22
|
+
*
|
|
23
|
+
* PURE path/section builders are separated from the one impure mkdir so tests
|
|
24
|
+
* drive the former directly and the latter through an injectable fs seam.
|
|
25
|
+
*/
|
|
26
|
+
/** Env override for the scratchpad tmp root (mirrors CLAUDE_CODE_TMPDIR). */
|
|
27
|
+
export declare const SCRATCHPAD_TMPDIR_ENV = "YAGNI_CODE_TMPDIR";
|
|
28
|
+
/**
|
|
29
|
+
* PURE: sanitize an absolute cwd into a filename-safe segment. Mirrors Claude
|
|
30
|
+
* Code's sanitizePath but without the hash suffix — the cwd segment is cosmetic
|
|
31
|
+
* grouping, not a permission identity, so an identical prefix under two long
|
|
32
|
+
* cwds is disambiguated by the sessionId one level deeper.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sanitizeCwdSegment(cwd: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* PURE: the per-user scratchpad owner dir name. uid isolates multi-user systems
|
|
37
|
+
* the way Claude Code's "claude-{uid}" does, under our own prefix.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scratchpadOwnerDir(uid: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* PURE: the session scratchpad directory path. Returns null when there is no
|
|
42
|
+
* sessionId — a scratchpad is meaningless without a per-session key, and the
|
|
43
|
+
* prompt section is gated on a non-null result.
|
|
44
|
+
*/
|
|
45
|
+
export declare function scratchpadDir(opts?: {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
uid?: number;
|
|
49
|
+
tmp?: string;
|
|
50
|
+
}): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* IMPURE: ensure the scratchpad dir exists (owner-only), failing soft. Returns
|
|
53
|
+
* the path on success and null on failure — a null result means "no scratchpad
|
|
54
|
+
* this session", which the caller turns into an omitted prompt section.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ensureScratchpadDir(path: string, mkdir?: (p: string, o: {
|
|
57
|
+
mode: number;
|
|
58
|
+
recursive: boolean;
|
|
59
|
+
}) => void): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* PURE: the prompt section naming the scratchpad. Gated by the caller on the
|
|
62
|
+
* dir existing; when present, it tells the agent where to put intermediate
|
|
63
|
+
* files instead of /tmp or the user's project.
|
|
64
|
+
*/
|
|
65
|
+
export declare function scratchpadSection(dir: string): string;
|
|
66
|
+
//# sourceMappingURL=scratchpad.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session scratchpad — a permission-free directory the agent writes its working
|
|
3
|
+
* state to, so neither it nor the user has to reconstruct intermediate results.
|
|
4
|
+
*
|
|
5
|
+
* Ports Claude Code's scratchpad with one deliberate simplification: Claude
|
|
6
|
+
* resolves the tmp root and normalizes against path traversal because its
|
|
7
|
+
* scratchpad is an allow-listed path in a permission classifier. We have no
|
|
8
|
+
* such classifier (auto mode already passes write/edit unprompted, and plan
|
|
9
|
+
* mode's write gate holds scratchpad like every other write), so the path is
|
|
10
|
+
* built plainly and the only caller-facing contract is "the dir exists or the
|
|
11
|
+
* section is omitted."
|
|
12
|
+
*
|
|
13
|
+
* Path mirrors Claude Code's shape under our own owner namespace so the two
|
|
14
|
+
* never collide: <tmp>/yagni-{uid}/<sanitized-cwd>/<sessionId>/scratchpad/.
|
|
15
|
+
* - tmp root: YAGNI_CODE_TMPDIR, else os.tmpdir()
|
|
16
|
+
* - uid: process.getuid() ?? 0 (multi-user isolation; tmpdir() is already
|
|
17
|
+
* per-user on Windows)
|
|
18
|
+
* - sanitized-cwd: non-alphanumerics → "-", length-capped (cosmetic grouping
|
|
19
|
+
* only — sessionId is the real uniqueness key)
|
|
20
|
+
* - sessionId: env.YAGNI_SESSION_ID, minted by the launcher as a UUID; when
|
|
21
|
+
* absent (a bare pi run) no scratchpad is configured at all.
|
|
22
|
+
*
|
|
23
|
+
* PURE path/section builders are separated from the one impure mkdir so tests
|
|
24
|
+
* drive the former directly and the latter through an injectable fs seam.
|
|
25
|
+
*/
|
|
26
|
+
import { mkdirSync } from "node:fs";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
/** Env override for the scratchpad tmp root (mirrors CLAUDE_CODE_TMPDIR). */
|
|
30
|
+
export const SCRATCHPAD_TMPDIR_ENV = "YAGNI_CODE_TMPDIR";
|
|
31
|
+
/** Longest sanitized-cwd segment we keep; the sessionId carries uniqueness. */
|
|
32
|
+
const MAX_SANITIZED_CWD = 64;
|
|
33
|
+
/**
|
|
34
|
+
* PURE: sanitize an absolute cwd into a filename-safe segment. Mirrors Claude
|
|
35
|
+
* Code's sanitizePath but without the hash suffix — the cwd segment is cosmetic
|
|
36
|
+
* grouping, not a permission identity, so an identical prefix under two long
|
|
37
|
+
* cwds is disambiguated by the sessionId one level deeper.
|
|
38
|
+
*/
|
|
39
|
+
export function sanitizeCwdSegment(cwd) {
|
|
40
|
+
const sanitized = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/^-+|-+$/g, "");
|
|
41
|
+
return sanitized.slice(0, MAX_SANITIZED_CWD) || "root";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* PURE: the per-user scratchpad owner dir name. uid isolates multi-user systems
|
|
45
|
+
* the way Claude Code's "claude-{uid}" does, under our own prefix.
|
|
46
|
+
*/
|
|
47
|
+
export function scratchpadOwnerDir(uid) {
|
|
48
|
+
return `yagni-${uid}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* PURE: the session scratchpad directory path. Returns null when there is no
|
|
52
|
+
* sessionId — a scratchpad is meaningless without a per-session key, and the
|
|
53
|
+
* prompt section is gated on a non-null result.
|
|
54
|
+
*/
|
|
55
|
+
export function scratchpadDir(opts = {}) {
|
|
56
|
+
const sessionId = opts.sessionId?.trim();
|
|
57
|
+
if (!sessionId)
|
|
58
|
+
return null;
|
|
59
|
+
const tmp = opts.tmp ?? tmpdir();
|
|
60
|
+
const uid = opts.uid ?? (typeof process.getuid === "function" ? process.getuid() ?? 0 : 0);
|
|
61
|
+
const cwd = sanitizeCwdSegment(opts.cwd ?? ".");
|
|
62
|
+
return join(tmp, scratchpadOwnerDir(uid), cwd, sessionId, "scratchpad");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* IMPURE: ensure the scratchpad dir exists (owner-only), failing soft. Returns
|
|
66
|
+
* the path on success and null on failure — a null result means "no scratchpad
|
|
67
|
+
* this session", which the caller turns into an omitted prompt section.
|
|
68
|
+
*/
|
|
69
|
+
export function ensureScratchpadDir(path, mkdir = mkdirSync) {
|
|
70
|
+
try {
|
|
71
|
+
mkdir(path, { recursive: true, mode: 0o700 });
|
|
72
|
+
return path;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* PURE: the prompt section naming the scratchpad. Gated by the caller on the
|
|
80
|
+
* dir existing; when present, it tells the agent where to put intermediate
|
|
81
|
+
* files instead of /tmp or the user's project.
|
|
82
|
+
*/
|
|
83
|
+
export function scratchpadSection(dir) {
|
|
84
|
+
return ("# Scratchpad directory\n\n" +
|
|
85
|
+
`Use this session scratchpad directory for files that do not belong in the user's project:\n` +
|
|
86
|
+
`${dir}\n\n` +
|
|
87
|
+
"- Store intermediate results or data during multi-step tasks.\n" +
|
|
88
|
+
"- Write temporary scripts or configuration files.\n" +
|
|
89
|
+
"- Save outputs that don't belong in the user's project.\n" +
|
|
90
|
+
"- Anything that would otherwise go to /tmp.\n\n" +
|
|
91
|
+
"The directory is session-specific and isolated from the user's project.");
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=scratchpad.js.map
|
|
@@ -74,7 +74,7 @@ You are grounded in how THIS company works: call ask_yagni before inferring a co
|
|
|
74
74
|
|
|
75
75
|
Never fabricate file paths, contents, or findings. If you cannot find something, say so.
|
|
76
76
|
|
|
77
|
-
Your final message is your report back to the driving agent, which has NOT seen what you read or did
|
|
77
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done. Your final message is your report back to the driving agent, which has NOT seen what you read or did: make it a concise report of what was done and the key findings, since the caller relays it to the user and it only needs the essentials. Cover what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
|
|
78
78
|
const GENERAL_AGENT = {
|
|
79
79
|
name: GENERAL_AGENT_NAME,
|
|
80
80
|
description: "General-purpose agent for research, multi-file changes, and self-contained tasks.",
|
|
@@ -94,6 +94,8 @@ one you actually read with a tool. If you cannot find something, say "not
|
|
|
94
94
|
found" — a plausible-sounding invention is worse than no answer because the
|
|
95
95
|
driving agent trusts your report.
|
|
96
96
|
|
|
97
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
98
|
+
|
|
97
99
|
Your final message is your report back to the driving agent, which has NOT
|
|
98
100
|
seen what you read. Make it compressed and complete: exact file paths, the
|
|
99
101
|
key excerpts, and a one-paragraph map of how the pieces relate. Say what you
|
|
@@ -122,6 +124,8 @@ convention, an ownership rule, or anything organization-specific.
|
|
|
122
124
|
Never fabricate file paths or results. Report what you actually did and what
|
|
123
125
|
you actually found.
|
|
124
126
|
|
|
127
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
128
|
+
|
|
125
129
|
Your final message is your report back to the driving agent, which has NOT
|
|
126
130
|
seen what you did. List every file you touched, what changed in each, the
|
|
127
131
|
commands you ran with their outcomes, and anything you deliberately left
|
|
@@ -154,6 +158,8 @@ a convention, an ownership rule, or anything organization-specific.
|
|
|
154
158
|
Never fabricate file paths or findings. If you could not verify something,
|
|
155
159
|
say exactly what you tried and why you could not.
|
|
156
160
|
|
|
161
|
+
Complete the task fully — do not gold-plate, but do not leave it half-done.
|
|
162
|
+
|
|
157
163
|
Your final message is your verdict back to the driving agent, which has NOT
|
|
158
164
|
seen what you read. Format:
|
|
159
165
|
## Verdict
|
|
@@ -105,6 +105,7 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
|
|
|
105
105
|
label: string;
|
|
106
106
|
description: string;
|
|
107
107
|
promptSnippet: string;
|
|
108
|
+
promptGuidelines: string[];
|
|
108
109
|
parameters: Type.TObject<{
|
|
109
110
|
todos: Type.TArray<Type.TObject<{
|
|
110
111
|
text: Type.TString;
|
package/dist/extension/todos.js
CHANGED
|
@@ -204,6 +204,21 @@ export function makeTodoTool(get, set) {
|
|
|
204
204
|
"in_progress at a time, mark items completed the moment they are done, and add newly " +
|
|
205
205
|
"discovered steps as pending. Use it for any task with three or more steps, updating as you go.",
|
|
206
206
|
promptSnippet: "todo_write: keep a user-visible checklist for multi-step work (full-list replacement).",
|
|
207
|
+
promptGuidelines: [
|
|
208
|
+
"Use todo_write proactively when a task needs 3 or more distinct steps, requires careful " +
|
|
209
|
+
"planning, or the user gives you a list of things (numbered or comma-separated).",
|
|
210
|
+
"Capture new instructions as todos the moment you receive them, and mark a step in_progress " +
|
|
211
|
+
"BEFORE you start working on it.",
|
|
212
|
+
"When in doubt, use it — a visible checklist answers \"is it stuck?\" without the user having " +
|
|
213
|
+
"to interrupt.",
|
|
214
|
+
"Skip it when there is only one straightforward task, the work is trivial, or the request is " +
|
|
215
|
+
"purely conversational or informational — in those cases just do the task directly.",
|
|
216
|
+
"Pass the FULL list every call; it replaces the previous one. Keep exactly ONE item in_progress " +
|
|
217
|
+
"at a time.",
|
|
218
|
+
"Mark a step completed the moment it is done (do not batch completions), and add newly " +
|
|
219
|
+
"discovered steps as pending. Only mark a step completed when it is fully done — if tests " +
|
|
220
|
+
"fail or work is partial, leave it in_progress and add a new step for the blocker.",
|
|
221
|
+
],
|
|
207
222
|
parameters,
|
|
208
223
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
209
224
|
const normalized = normalizeTodos(params.todos);
|
package/dist/paths.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export declare function resolveExtensionPath(): string;
|
|
|
29
29
|
* it pulls in the pipeline alone, with none of pi's TUI surface.
|
|
30
30
|
*/
|
|
31
31
|
export declare function resolveHeadlessGoPath(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Absolute path to the extension's session-worktree entry
|
|
34
|
+
* (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.
|
|
35
|
+
*
|
|
36
|
+
* Mirrors `resolveHeadlessGoPath`: derived from the extension entry so the
|
|
37
|
+
* bundled-vs-workspace fallback is resolved once. Only this one module is
|
|
38
|
+
* loaded (not the whole extension): it pulls in the worktree plumbing alone,
|
|
39
|
+
* with none of pi's TUI surface.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveSessionWorktreePath(): string;
|
|
32
42
|
/**
|
|
33
43
|
* Absolute path to pi's package root — the dir whose package.json names the
|
|
34
44
|
* package. The shadow package dir is built from this (we read its package.json
|
package/dist/paths.js
CHANGED
|
@@ -43,6 +43,19 @@ export function resolveHeadlessGoPath() {
|
|
|
43
43
|
const entry = resolveExtensionPath();
|
|
44
44
|
return join(dirname(entry), "pipeline", "headlessGo.js");
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Absolute path to the extension's session-worktree entry
|
|
48
|
+
* (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors `resolveHeadlessGoPath`: derived from the extension entry so the
|
|
51
|
+
* bundled-vs-workspace fallback is resolved once. Only this one module is
|
|
52
|
+
* loaded (not the whole extension): it pulls in the worktree plumbing alone,
|
|
53
|
+
* with none of pi's TUI surface.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSessionWorktreePath() {
|
|
56
|
+
const entry = resolveExtensionPath();
|
|
57
|
+
return join(dirname(entry), "pipeline", "sessionWorktree.js");
|
|
58
|
+
}
|
|
46
59
|
/**
|
|
47
60
|
* Absolute path to pi's package root — the dir whose package.json names the
|
|
48
61
|
* package. The shadow package dir is built from this (we read its package.json
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing + validation for `-w / --worktree [name]` (YAG-594).
|
|
3
|
+
*
|
|
4
|
+
* No side effects, no I/O: `parseWorktreeFlag` extracts the flag and its
|
|
5
|
+
* optional value from argv (leaving everything else for pi unchanged), and
|
|
6
|
+
* `validateWorktreeSlug` mirrors Claude's allowlist so a hostile name is
|
|
7
|
+
* rejected before the launcher touches git in any way.
|
|
8
|
+
*/
|
|
9
|
+
export interface WorktreeFlag {
|
|
10
|
+
/** Distinguishes "-w with no name" from "-w absent" so bare `-w` is honored. */
|
|
11
|
+
requested: boolean;
|
|
12
|
+
/** The name value, when supplied (`-w foo` / `--worktree=foo`). */
|
|
13
|
+
name?: string;
|
|
14
|
+
/** Everything that isn't the worktree flag/name, passed through to pi. */
|
|
15
|
+
remainingArgs: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract `-w`/`--worktree [name]` from argv without disturbing the rest. Runs
|
|
19
|
+
* BEFORE `parseOutputFormat` so the two argv-mutators never double-handle a
|
|
20
|
+
* flag: this strips only the worktree flag, and the caller feeds `remainingArgs`
|
|
21
|
+
* to `parseOutputFormat` next.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseWorktreeFlag(argv: string[]): WorktreeFlag;
|
|
24
|
+
/**
|
|
25
|
+
* Parse a PR reference: `#N` or a GitHub-style PR URL, mirroring the extension's
|
|
26
|
+
* `parsePRReference` (the two packages stay independent; this is the launcher's
|
|
27
|
+
* copy so `-w` can recognize a PR ref before slug-validating the raw name).
|
|
28
|
+
*/
|
|
29
|
+
export declare function parsePRReference(input: string): number | null;
|
|
30
|
+
/**
|
|
31
|
+
* Validate a worktree slug before any side effect. Mirrors Claude's guard:
|
|
32
|
+
* length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously
|
|
33
|
+
* with a clear message (surfaced by the caller).
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateWorktreeSlug(slug: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Validate the `-w` name + argv before any side effect. Returns a user-facing
|
|
38
|
+
* error message when the launch should be refused, or `undefined` to proceed.
|
|
39
|
+
* PR refs (`#N` / URL) skip slug validation here — they map to `pr-<N>` in the
|
|
40
|
+
* extension, whose own validation covers the mapped slug.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateWorktreeLaunchArgs(name: string | undefined, argv: string[]): string | undefined;
|
|
43
|
+
//# sourceMappingURL=worktreeArgs.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing + validation for `-w / --worktree [name]` (YAG-594).
|
|
3
|
+
*
|
|
4
|
+
* No side effects, no I/O: `parseWorktreeFlag` extracts the flag and its
|
|
5
|
+
* optional value from argv (leaving everything else for pi unchanged), and
|
|
6
|
+
* `validateWorktreeSlug` mirrors Claude's allowlist so a hostile name is
|
|
7
|
+
* rejected before the launcher touches git in any way.
|
|
8
|
+
*/
|
|
9
|
+
/** Maximum slug characters (mirrors Claude's guard). */
|
|
10
|
+
const MAX_SLUG_LENGTH = 64;
|
|
11
|
+
/** Allowlist per `/`-separated segment. */
|
|
12
|
+
const VALID_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
13
|
+
/**
|
|
14
|
+
* Extract `-w`/`--worktree [name]` from argv without disturbing the rest. Runs
|
|
15
|
+
* BEFORE `parseOutputFormat` so the two argv-mutators never double-handle a
|
|
16
|
+
* flag: this strips only the worktree flag, and the caller feeds `remainingArgs`
|
|
17
|
+
* to `parseOutputFormat` next.
|
|
18
|
+
*/
|
|
19
|
+
export function parseWorktreeFlag(argv) {
|
|
20
|
+
const remainingArgs = [];
|
|
21
|
+
let requested = false;
|
|
22
|
+
let name;
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
if (arg === "-w" || arg === "--worktree") {
|
|
26
|
+
requested = true;
|
|
27
|
+
// Consume the next token as the name only if it isn't another flag.
|
|
28
|
+
const next = argv[i + 1];
|
|
29
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
30
|
+
name = next;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith("--worktree=")) {
|
|
36
|
+
requested = true;
|
|
37
|
+
name = arg.slice("--worktree=".length);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
remainingArgs.push(arg);
|
|
41
|
+
}
|
|
42
|
+
return { requested, name, remainingArgs };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a PR reference: `#N` or a GitHub-style PR URL, mirroring the extension's
|
|
46
|
+
* `parsePRReference` (the two packages stay independent; this is the launcher's
|
|
47
|
+
* copy so `-w` can recognize a PR ref before slug-validating the raw name).
|
|
48
|
+
*/
|
|
49
|
+
export function parsePRReference(input) {
|
|
50
|
+
const urlMatch = input.match(/^https?:\/\/[^/]+\/[^/]+\/[^/]+\/pull\/(\d+)\/?(?:[?#].*)?$/i);
|
|
51
|
+
if (urlMatch?.[1])
|
|
52
|
+
return parseInt(urlMatch[1], 10);
|
|
53
|
+
const hashMatch = input.match(/^#(\d+)$/);
|
|
54
|
+
if (hashMatch?.[1])
|
|
55
|
+
return parseInt(hashMatch[1], 10);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate a worktree slug before any side effect. Mirrors Claude's guard:
|
|
60
|
+
* length cap, per-segment allowlist, `.`/`..` rejection. Throws synchronously
|
|
61
|
+
* with a clear message (surfaced by the caller).
|
|
62
|
+
*/
|
|
63
|
+
export function validateWorktreeSlug(slug) {
|
|
64
|
+
if (slug.length > MAX_SLUG_LENGTH) {
|
|
65
|
+
throw new Error(`Invalid worktree name: must be ${MAX_SLUG_LENGTH} characters or fewer (got ${slug.length})`);
|
|
66
|
+
}
|
|
67
|
+
for (const segment of slug.split("/")) {
|
|
68
|
+
if (segment === "." || segment === "..") {
|
|
69
|
+
throw new Error(`Invalid worktree name "${slug}": must not contain "." or ".." path segments`);
|
|
70
|
+
}
|
|
71
|
+
if (!VALID_SLUG_SEGMENT.test(segment)) {
|
|
72
|
+
throw new Error(`Invalid worktree name "${slug}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate the `-w` name + argv before any side effect. Returns a user-facing
|
|
78
|
+
* error message when the launch should be refused, or `undefined` to proceed.
|
|
79
|
+
* PR refs (`#N` / URL) skip slug validation here — they map to `pr-<N>` in the
|
|
80
|
+
* extension, whose own validation covers the mapped slug.
|
|
81
|
+
*/
|
|
82
|
+
export function validateWorktreeLaunchArgs(name, argv) {
|
|
83
|
+
if (name !== undefined && parsePRReference(name) === null) {
|
|
84
|
+
try {
|
|
85
|
+
validateWorktreeSlug(name);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return err instanceof Error ? err.message : String(err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (argv.includes("-c") || argv.includes("--continue")) {
|
|
92
|
+
return "`-c`/`--continue` is not supported with `-w` yet. Use `-w <name>` then `--session <id>` (or `-r`) to resume.";
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=worktreeArgs.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.0-staging.
|
|
3
|
+
"version": "1.0.0-staging.1180.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"turndown": "^7.2.4",
|
|
41
41
|
"typebox": "^1.3.15"
|
|
42
42
|
},
|
|
43
|
-
"yagniSourceSha": "
|
|
43
|
+
"yagniSourceSha": "1db89538911e63de0e383aad36bd707e947140d9"
|
|
44
44
|
}
|