@yagni-app/code 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +11 -3
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +70 -2
- package/dist/extension/permission/execPolicy.js +47 -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/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/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/dist/extension/footer.js
CHANGED
|
@@ -42,6 +42,7 @@ import { spawnSync } from "node:child_process";
|
|
|
42
42
|
import { statSync } from "node:fs";
|
|
43
43
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
44
44
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
45
|
+
import { createDiffStatCache, formatDiffStat } from "./diffStat.js";
|
|
45
46
|
export const BRANCH_MAX_WIDTH = 60;
|
|
46
47
|
const WORKTREE_MAX_WIDTH = 30;
|
|
47
48
|
/** Section separator: single space + middle dot + single space. */
|
|
@@ -229,13 +230,18 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
229
230
|
// terminal edge. Truncation runs against the reduced content width.
|
|
230
231
|
const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
|
|
231
232
|
const contentWidth = Math.max(1, width - pad.length);
|
|
232
|
-
// Line 1: folder · [worktree] · branch
|
|
233
|
+
// Line 1: folder · [worktree] · branch [+N -M]
|
|
233
234
|
const line1Parts = [theme.fg("accent", input.git.folder)];
|
|
234
235
|
if (input.git.inRepo) {
|
|
235
236
|
if (input.git.worktree)
|
|
236
237
|
line1Parts.push(theme.fg("warning", `[${input.git.worktree}]`));
|
|
237
|
-
if (input.git.branch)
|
|
238
|
-
|
|
238
|
+
if (input.git.branch) {
|
|
239
|
+
const branchSpan = theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…")));
|
|
240
|
+
// Diff badge rides the branch segment (Kimi's `branch [ +N -M ]` shape),
|
|
241
|
+
// colored dim — the same grey as the cost stats on line 2.
|
|
242
|
+
const diffSpan = input.diff ? ` ${dim(`[${input.diff}]`)}` : "";
|
|
243
|
+
line1Parts.push(branchSpan + diffSpan);
|
|
244
|
+
}
|
|
239
245
|
}
|
|
240
246
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
241
247
|
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
@@ -269,17 +275,23 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
269
275
|
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
270
276
|
return (_tui, theme, footerData) => {
|
|
271
277
|
let gitCache;
|
|
272
|
-
|
|
278
|
+
let diffStatCache;
|
|
279
|
+
const clearCaches = () => {
|
|
273
280
|
gitCache = undefined;
|
|
274
|
-
|
|
281
|
+
diffStatCache = undefined;
|
|
282
|
+
};
|
|
283
|
+
const unsubscribeBranch = footerData.onBranchChange?.(clearCaches);
|
|
275
284
|
const gitInfo = () => {
|
|
276
285
|
if (!gitCache) {
|
|
277
|
-
|
|
286
|
+
// Read cwd once; both the git info and the diff cache key off it.
|
|
287
|
+
const dir = ctx.sessionManager.getCwd();
|
|
288
|
+
gitCache = detectGitInfo(dir, process.env.HOME || process.env.USERPROFILE);
|
|
289
|
+
diffStatCache = createDiffStatCache(dir);
|
|
278
290
|
}
|
|
279
291
|
return gitCache;
|
|
280
292
|
};
|
|
281
293
|
if (invalidateHandle) {
|
|
282
|
-
invalidateHandle.invalidateGit =
|
|
294
|
+
invalidateHandle.invalidateGit = clearCaches;
|
|
283
295
|
invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
|
|
284
296
|
}
|
|
285
297
|
return {
|
|
@@ -291,13 +303,14 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
|
291
303
|
git: gitInfo(),
|
|
292
304
|
model: ctx.model?.id ?? "no-model",
|
|
293
305
|
mode: modeHolder?.get() ?? null,
|
|
306
|
+
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
294
307
|
usage: collectUsage(ctx.sessionManager),
|
|
295
308
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
296
309
|
statuses,
|
|
297
310
|
}, theme, width, resolveFooterPadX());
|
|
298
311
|
},
|
|
299
312
|
invalidate() {
|
|
300
|
-
|
|
313
|
+
clearCaches();
|
|
301
314
|
},
|
|
302
315
|
dispose() {
|
|
303
316
|
unsubscribeBranch?.();
|
|
@@ -186,6 +186,12 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
186
186
|
export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
|
|
187
187
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
188
188
|
export type { SpoolEntry, SpoolClientOpts, FlushOutcome, JudgmentWriteOutcome } from "./spool.js";
|
|
189
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
190
|
+
export type { RegisterCondensedToolsDeps, RowStatus } from "./condensedTools.js";
|
|
191
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
192
|
+
export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
|
|
193
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
194
|
+
export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
|
|
189
195
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
190
196
|
export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
|
|
191
197
|
//# sourceMappingURL=index.d.ts.map
|
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";
|
|
@@ -48,6 +49,9 @@ import { registerChipEditor } from "./chipEditor.js";
|
|
|
48
49
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
49
50
|
import { makeFlywheelState } from "./flywheel.js";
|
|
50
51
|
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
52
|
+
import { registerCondensedTools } from "./condensedTools.js";
|
|
53
|
+
import { isDesktopSurface } from "./surface.js";
|
|
54
|
+
import { registerWorkingLine, WORKING_INDICATOR_FRAMES } from "./workingLine.js";
|
|
51
55
|
function isEvalMode(env = process.env) {
|
|
52
56
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
53
57
|
}
|
|
@@ -160,6 +164,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
160
164
|
// no-position record suggestions reach the model) and record_decision
|
|
161
165
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
162
166
|
const flywheelState = makeFlywheelState();
|
|
167
|
+
// The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
|
|
168
|
+
// Registered before the subagent/advisor tools: they publish their live
|
|
169
|
+
// progress through this handle so the elapsed/token suffix survives their
|
|
170
|
+
// overrides. TUI-gated internally (agent_start checks ctx.mode).
|
|
171
|
+
const workingLine = registerWorkingLine(pi);
|
|
163
172
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
164
173
|
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
165
174
|
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
@@ -177,7 +186,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
177
186
|
// hide it forever from a session that switched TO Advanced. One state handle
|
|
178
187
|
// per session, shared with /advise so they draw on the same cap.
|
|
179
188
|
const advisorState = makeAdvisorState();
|
|
180
|
-
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
|
|
189
|
+
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine });
|
|
181
190
|
pi.registerTool(askAdvisorTool);
|
|
182
191
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
183
192
|
// draws on the same cap rather than opening a side channel around it.
|
|
@@ -217,7 +226,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
217
226
|
// driver's delegation identity for the diamond directive (see the
|
|
218
227
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
219
228
|
const ultraHolder = createUltraHolder();
|
|
220
|
-
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
229
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine });
|
|
221
230
|
registerUltraCommand(pi, ultraHolder);
|
|
222
231
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
223
232
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -593,6 +602,51 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
593
602
|
// pointers. Computed once per activation (rules are launch-time state, like
|
|
594
603
|
// pi's own skill discovery); fail-soft to null.
|
|
595
604
|
const rulesSection = claudeRulesSection(deps.env ?? process.env);
|
|
605
|
+
// YAG-575: session scratchpad — a permission-free dir the agent writes its
|
|
606
|
+
// working state to. Computed + ensured once at activation (like rulesSection,
|
|
607
|
+
// which must exist before the first before_agent_start), not on session_start,
|
|
608
|
+
// so the very first turn already carries the section. Fails closed: no
|
|
609
|
+
// sessionId (a bare pi run) or a failed mkdir means no section, and the mkdir
|
|
610
|
+
// failure is logged so a missing scratchpad is not silent.
|
|
611
|
+
const scratchpadDirPath = scratchpadDirFor({
|
|
612
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
613
|
+
cwd: process.cwd(),
|
|
614
|
+
tmp: env[SCRATCHPAD_TMPDIR_ENV] || undefined,
|
|
615
|
+
});
|
|
616
|
+
let scratchpadSectionText;
|
|
617
|
+
if (scratchpadDirPath) {
|
|
618
|
+
const ensured = ensureScratchpadDir(scratchpadDirPath);
|
|
619
|
+
if (ensured) {
|
|
620
|
+
scratchpadSectionText = scratchpadSection(ensured);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
logEvent({
|
|
624
|
+
source: "scratchpad",
|
|
625
|
+
level: "error",
|
|
626
|
+
event: "scratchpad_mkdir_failed",
|
|
627
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// The condensed Claude Code-style transcript: the seven built-ins re-render
|
|
632
|
+
// as flat "● Tool(arg)" rows, quiet runs collapse into one "Read 2 files,
|
|
633
|
+
// ran 2 shell commands" summary line, and project writes/edits keep visible
|
|
634
|
+
// previews. Execution delegates to the same factory-made built-ins pi would
|
|
635
|
+
// have used (settings-honoring), so ONLY presentation changes — which is why
|
|
636
|
+
// eval mode keeps pi's stock registration (measured behavior stays
|
|
637
|
+
// byte-identical) and the desktop surface keeps the structured pipeline.
|
|
638
|
+
// YAGNI_CLASSIC_TOOL_ROWS=1 is the debugging escape hatch back to pi's
|
|
639
|
+
// boxed renderers.
|
|
640
|
+
if (!evalMode && !isDesktopSurface() && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
641
|
+
try {
|
|
642
|
+
registerCondensedTools(pi, {
|
|
643
|
+
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Rendering must never break activation; pi's built-ins remain.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
596
650
|
// Own the identity + inject live company context (and repo rules) on every
|
|
597
651
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
598
652
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -617,6 +671,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
617
671
|
: YAGNI_IDENTITY_DRIVER
|
|
618
672
|
: undefined,
|
|
619
673
|
rulesSection,
|
|
674
|
+
scratchpadSection: scratchpadSectionText,
|
|
620
675
|
}),
|
|
621
676
|
});
|
|
622
677
|
// Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
|
|
@@ -819,6 +874,14 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
819
874
|
catch {
|
|
820
875
|
// A label must never disrupt session start.
|
|
821
876
|
}
|
|
877
|
+
// The pulsing star indicator that pairs with the composed working line.
|
|
878
|
+
// Fails closed to pi's default spinner.
|
|
879
|
+
try {
|
|
880
|
+
ctx.ui?.setWorkingIndicator?.({ frames: [...WORKING_INDICATOR_FRAMES], intervalMs: 140 });
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// An indicator must never disrupt session start.
|
|
884
|
+
}
|
|
822
885
|
// One-time hint naming what changed now that reasoning is collapsed by
|
|
823
886
|
// default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
|
|
824
887
|
// marker so it fires only on the launch that actually seeded the key — it
|
|
@@ -1015,5 +1078,10 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
1015
1078
|
// W4 trust plumbing: the durable write-spool for the judgment-capture tools
|
|
1016
1079
|
// (idempotencyKey-replayed; server-side dedup makes flush safe).
|
|
1017
1080
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
1081
|
+
// The condensed Claude Code-style transcript: flat tool rows, run summaries,
|
|
1082
|
+
// and the composed streaming status line.
|
|
1083
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
1084
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
1085
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
1018
1086
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
1019
1087
|
//# sourceMappingURL=index.js.map
|
|
@@ -727,12 +727,59 @@ function isPipeToShell(command) {
|
|
|
727
727
|
* backticks, including inside double quotes). Matches FORBIDDEN rules only —
|
|
728
728
|
* can upgrade the classification, never relax it.
|
|
729
729
|
*/
|
|
730
|
+
const QUOTED_HEREDOC_OPEN = /^cat[ \t]+<<[ \t]*(?:'([^']+)'|"([^"]+)"|\\([A-Za-z_][A-Za-z0-9_]*))[ \t]*\r?\n/;
|
|
731
|
+
/**
|
|
732
|
+
* Recognize the `cat <<'QUOTED' … QUOTED` idiom inside a command substitution.
|
|
733
|
+
*
|
|
734
|
+
* This is the one way to pass multi-line body content to `gh`/`git` as a
|
|
735
|
+
* literal string: the quoted delimiter (`<<'EOF'`, `<<"EOF"`, or `<<\EOF`)
|
|
736
|
+
* makes the heredoc body literal — no `$`/backtick expansion — and `cat` merely
|
|
737
|
+
* prints it. A body like `rm -rf /tmp/test-db` inside such a heredoc is PROSE,
|
|
738
|
+
* not a command; sweeping it for forbidden rules is a false positive.
|
|
739
|
+
*
|
|
740
|
+
* Returns true ONLY for the exact safe shape: the whole substitution is `cat`
|
|
741
|
+
* with no arguments, followed by a heredoc whose delimiter is quoted and whose
|
|
742
|
+
* terminator is the delimiter on its own final line. Any deviation (unquoted
|
|
743
|
+
* delimiter, extra args to `cat`, a second command after the terminator)
|
|
744
|
+
* returns false and falls through to the ordinary danger scan. Safety does not
|
|
745
|
+
* hinge on this exemption: the outer command (the `gh pr create`, the `rm`) is
|
|
746
|
+
* still classified on its own merits, heredoc or not.
|
|
747
|
+
*/
|
|
748
|
+
function isLiteralQuotedCatHeredoc(inner) {
|
|
749
|
+
const text = inner.trim();
|
|
750
|
+
const m = QUOTED_HEREDOC_OPEN.exec(text);
|
|
751
|
+
if (!m)
|
|
752
|
+
return false;
|
|
753
|
+
const delim = m[1] ?? m[2] ?? m[3];
|
|
754
|
+
const bodyStart = m[0].length;
|
|
755
|
+
const lines = text.slice(bodyStart).split(/\r?\n/);
|
|
756
|
+
if (lines.length === 0)
|
|
757
|
+
return false;
|
|
758
|
+
// The final line must be the bare delimiter (the closing `)` is already
|
|
759
|
+
// stripped by extractSubstitutions).
|
|
760
|
+
if (lines[lines.length - 1].trim() !== delim)
|
|
761
|
+
return false;
|
|
762
|
+
// No line BEFORE the terminator may equal the delimiter: in real bash, a
|
|
763
|
+
// delimiter line terminates the heredoc there, turning everything after it
|
|
764
|
+
// into an executed command (e.g. a body with a bare `EOF` line followed by
|
|
765
|
+
// `rm -rf /x` would run the `rm`). Reject that shape and fall to the scan.
|
|
766
|
+
for (const line of lines.slice(0, -1)) {
|
|
767
|
+
if (line.trim() === delim)
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
730
772
|
function dangerScanSubstitutions(command, policy, depth) {
|
|
731
773
|
if (depth > MAX_SCAN_DEPTH)
|
|
732
774
|
return null;
|
|
733
775
|
for (const inner of extractSubstitutions(command)) {
|
|
734
776
|
if (inner.trim().length === 0)
|
|
735
777
|
continue;
|
|
778
|
+
// Quoted-heredoc cat: the body is literal data, not an executable
|
|
779
|
+
// command — skipping it avoids false "forbidden" hits on prose that happens
|
|
780
|
+
// to contain a forbidden-looking token. The outer command still classifies.
|
|
781
|
+
if (isLiteralQuotedCatHeredoc(inner))
|
|
782
|
+
continue;
|
|
736
783
|
if (isPipeToShell(inner)) {
|
|
737
784
|
return {
|
|
738
785
|
decision: "forbidden",
|
|
@@ -65,9 +65,16 @@ export declare function buildStageInvocation(stage: PipelineStage, ctx: {
|
|
|
65
65
|
* `yagni` provider) before a stage passthrough. Mirrors buildLaunch's
|
|
66
66
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
67
67
|
* left untouched.
|
|
68
|
+
*
|
|
69
|
+
* `otelExtensionPath` (absent on a non-exporting launch) additionally loads
|
|
70
|
+
* pi-otel so child LLM spend is traced too — stage children are the bulk of a
|
|
71
|
+
* /go run's cost, and an OTel export that misses them undercounts. The path
|
|
72
|
+
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
73
|
+
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
68
74
|
*/
|
|
69
75
|
export declare function groundedChildArgv(stageArgv: string[], opts: {
|
|
70
76
|
piCli: string;
|
|
71
77
|
extensionPath: string;
|
|
78
|
+
otelExtensionPath?: string;
|
|
72
79
|
}): string[];
|
|
73
80
|
//# sourceMappingURL=invocation.d.ts.map
|
|
@@ -84,6 +84,12 @@ export function buildStageInvocation(stage, ctx) {
|
|
|
84
84
|
* `yagni` provider) before a stage passthrough. Mirrors buildLaunch's
|
|
85
85
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
86
86
|
* left untouched.
|
|
87
|
+
*
|
|
88
|
+
* `otelExtensionPath` (absent on a non-exporting launch) additionally loads
|
|
89
|
+
* pi-otel so child LLM spend is traced too — stage children are the bulk of a
|
|
90
|
+
* /go run's cost, and an OTel export that misses them undercounts. The path
|
|
91
|
+
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
92
|
+
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
87
93
|
*/
|
|
88
94
|
export function groundedChildArgv(stageArgv, opts) {
|
|
89
95
|
const userChoseProvider = stageArgv.includes("--provider");
|
|
@@ -91,6 +97,7 @@ export function groundedChildArgv(stageArgv, opts) {
|
|
|
91
97
|
opts.piCli,
|
|
92
98
|
"-e",
|
|
93
99
|
opts.extensionPath,
|
|
100
|
+
...(opts.otelExtensionPath ? ["-e", opts.otelExtensionPath] : []),
|
|
94
101
|
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
95
102
|
...stageArgv,
|
|
96
103
|
];
|
|
@@ -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
|
|
@@ -61,11 +61,19 @@ async function defaultWritePrompt(body) {
|
|
|
61
61
|
/**
|
|
62
62
|
* Default child resolution: the parent process IS pi (so `process.argv[1]` is
|
|
63
63
|
* pi's cli), and our compiled extension entry sits one dir up from this module.
|
|
64
|
+
*
|
|
65
|
+
* `otelExtensionPath` comes from `YAGNI_OTEL_EXTENSION_PATH`, set by the
|
|
66
|
+
* launcher only when an OTLP endpoint is configured (see the CLI's otel.ts) —
|
|
67
|
+
* children then load pi-otel so their LLM spend is traced. Guarded with
|
|
68
|
+
* existsSync so a stale env value degrades to an untraced child, never a
|
|
69
|
+
* child that fails to boot.
|
|
64
70
|
*/
|
|
65
71
|
function defaultResolveChild() {
|
|
66
72
|
const piCli = process.argv[1] ?? "pi";
|
|
67
73
|
const extensionPath = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
68
|
-
|
|
74
|
+
const otelPath = process.env.YAGNI_OTEL_EXTENSION_PATH;
|
|
75
|
+
const otelExtensionPath = otelPath && fs.existsSync(otelPath) ? otelPath : undefined;
|
|
76
|
+
return { piCli, extensionPath, ...(otelExtensionPath ? { otelExtensionPath } : {}) };
|
|
69
77
|
}
|
|
70
78
|
const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
71
79
|
/** Truncate to PER_TASK_OUTPUT_CAP bytes, never exceeding the cap. */
|
|
@@ -116,8 +124,12 @@ export async function runStage(stage, ctx, deps) {
|
|
|
116
124
|
lens: ctx.lens,
|
|
117
125
|
...(tierCap ? { tierCap } : {}),
|
|
118
126
|
});
|
|
119
|
-
const { piCli, extensionPath } = resolveChild();
|
|
120
|
-
const argv = groundedChildArgv(passthrough, {
|
|
127
|
+
const { piCli, extensionPath, otelExtensionPath } = resolveChild();
|
|
128
|
+
const argv = groundedChildArgv(passthrough, {
|
|
129
|
+
piCli,
|
|
130
|
+
extensionPath,
|
|
131
|
+
...(otelExtensionPath ? { otelExtensionPath } : {}),
|
|
132
|
+
});
|
|
121
133
|
// Fold events into a fixed-size accumulator as they stream — never retain
|
|
122
134
|
// the full array (that OOMs on a verbose stage; YAG-317 follow-up).
|
|
123
135
|
const acc = newEventAccumulator();
|
|
@@ -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
|