@yagni-app/code-staging 1.1.4-staging.1416.1 → 1.1.4-staging.1419.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.
@@ -17,7 +17,7 @@ import { makeRecordDecisionTool } from "./recordDecisionTool.js";
17
17
  import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
18
18
  import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE } from "./branding.js";
19
19
  import { claudeRulesSection } from "./claudeRules.js";
20
- import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadSection } from "./scratchpad.js";
20
+ import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadOwnerRootFor, scratchpadSection } from "./scratchpad.js";
21
21
  import { registerCostCommand } from "./costHud.js";
22
22
  import { isDebug } from "./diagnostics.js";
23
23
  import { logEvent } from "./errorSink.js";
@@ -1339,24 +1339,69 @@ export async function registerYagni(pi, deps = {}) {
1339
1339
  // so the very first turn already carries the section. Fails closed: no
1340
1340
  // sessionId (a bare pi run) or a failed mkdir means no section, and the mkdir
1341
1341
  // failure is logged so a missing scratchpad is not silent.
1342
+ const scratchpadTmpRoot = env[SCRATCHPAD_TMPDIR_ENV] || undefined;
1342
1343
  const scratchpadDirPath = scratchpadDirFor({
1343
1344
  sessionId: env.YAGNI_SESSION_ID ?? undefined,
1344
1345
  cwd: process.cwd(),
1345
- tmp: env[SCRATCHPAD_TMPDIR_ENV] || undefined,
1346
+ tmp: scratchpadTmpRoot,
1346
1347
  });
1347
1348
  let scratchpadSectionText;
1348
1349
  if (scratchpadDirPath) {
1349
- const ensured = ensureScratchpadDir(scratchpadDirPath);
1350
+ const ensured = ensureScratchpadDir(scratchpadDirPath, undefined, {
1351
+ onError: (errorCode) => {
1352
+ logEvent({
1353
+ source: "scratchpad",
1354
+ level: "error",
1355
+ event: "scratchpad_mkdir_failed",
1356
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
1357
+ fields: { path: scratchpadDirPath, errorCode },
1358
+ });
1359
+ },
1360
+ });
1350
1361
  if (ensured) {
1351
1362
  scratchpadSectionText = scratchpadSection(ensured);
1352
- }
1353
- else {
1354
- logEvent({
1355
- source: "scratchpad",
1356
- level: "error",
1357
- event: "scratchpad_mkdir_failed",
1358
- sessionId: env.YAGNI_SESSION_ID ?? undefined,
1359
- });
1363
+ // Claude Code parity: srt reads CLAUDE_CODE_TMPDIR from THIS process's
1364
+ // env when wrapping a sandboxed bash command and forces the child's
1365
+ // TMPDIR onto it. Point it at the per-user scratchpad OWNER ROOT (not
1366
+ // the scratchpad itself) so sandboxed-bash temp files land in the same
1367
+ // per-user tree the scratchpad lives in, under a path SHORT enough for
1368
+ // named unix-socket binds (the test-runner IPC pattern — a deep
1369
+ // scratchpad TMPDIR trips macOS's 104-char sun_path limit with EINVAL).
1370
+ // The scratchpad remains the sanctioned staging place by PRIORITY (the
1371
+ // prompt section outranks $TMPDIR), not by path convergence.
1372
+ //
1373
+ // Production note: children (subagents, /go stages) inherit this env
1374
+ // because `env` IS process.env there and the runner spawns with it. A
1375
+ // harness injecting deps.env sees the write only in that object —
1376
+ // spawned children of such a harness do not, by construction of the
1377
+ // runner's own env seam.
1378
+ //
1379
+ // Unset when there is no scratchpad (no session id, or the mkdir
1380
+ // failed) — srt keeps its /tmp/claude default and nothing changes for
1381
+ // those sessions.
1382
+ if (!env.CLAUDE_CODE_TMPDIR) {
1383
+ const ownerRoot = scratchpadOwnerRootFor({ tmp: scratchpadTmpRoot });
1384
+ env.CLAUDE_CODE_TMPDIR = ownerRoot;
1385
+ logEvent({
1386
+ source: "scratchpad",
1387
+ level: "info",
1388
+ event: "scratchpad_tmpdir_pinned",
1389
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
1390
+ fields: { path: ownerRoot },
1391
+ });
1392
+ }
1393
+ else {
1394
+ // The trail must distinguish "pinned" from "respected an operator
1395
+ // override" — no path value here (the explicit value is the
1396
+ // operator's, not ours to echo).
1397
+ logEvent({
1398
+ source: "scratchpad",
1399
+ level: "info",
1400
+ event: "scratchpad_tmpdir_pin_skipped",
1401
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
1402
+ fields: {},
1403
+ });
1404
+ }
1360
1405
  }
1361
1406
  }
1362
1407
  // The condensed Claude Code-style transcript: the seven built-ins re-render
@@ -118,8 +118,10 @@ export function makeBashComposition(manager, settings, cwd, onShellResolutionRet
118
118
  const heredocNote = "Caveat: heredocs (<<EOF) write temp files relative to the current directory on macOS bash 3.2 — from an unwritable cwd they fail; prefer printf or redirect from a file in the working directory.";
119
119
  // $TMPDIR guidance (Claude Code parity, always-on): the sandbox sets
120
120
  // TMPDIR to the per-user sandbox-writable temp dir; /tmp itself is NOT
121
- // writable.
122
- const tmpdirNote = "For temporary files, always use the $TMPDIR environment variable. TMPDIR is automatically set to the correct sandbox-writable directory in sandbox mode. Do NOT use /tmp directly - use $TMPDIR instead.";
121
+ // writable. When the system prompt names a scratchpad directory, that
122
+ // directory takes priority for temporary files stated here so the two
123
+ // guidance lines never read as mutually exclusive always-rules.
124
+ const tmpdirNote = "For temporary files, use the $TMPDIR environment variable unless your system prompt names a scratchpad directory — the scratchpad takes priority for temporary files. TMPDIR is automatically set to the correct sandbox-writable directory in sandbox mode. Do NOT use /tmp directly - use $TMPDIR instead.";
123
125
  const wrapped = {
124
126
  ...def,
125
127
  description: `${def.description}\n\n## Command sandbox\nCommands run inside an OS sandbox: ${restrictions.join("; ")}. ${strictNote}\n${heredocNote}\n${tmpdirNote}`,
@@ -48,6 +48,18 @@ export declare function scratchpadDir(opts?: {
48
48
  uid?: number;
49
49
  tmp?: string;
50
50
  }): string | null;
51
+ /**
52
+ * PURE: the per-user scratchpad OWNER ROOT, recomputed from the SAME inputs
53
+ * `scratchpadDir` resolves from (never derived by stripping segments off a
54
+ * built path — a layout change would silently shift a stripped spelling).
55
+ * The value the sandbox forces TMPDIR onto (Claude parity): per-user, shared
56
+ * across sessions, and short enough for named unix-socket binds where the
57
+ * deep scratchpad path would trip macOS's 104-char sun_path limit.
58
+ */
59
+ export declare function scratchpadOwnerRootFor(opts?: {
60
+ uid?: number;
61
+ tmp?: string;
62
+ }): string;
51
63
  /**
52
64
  * IMPURE: ensure the scratchpad dir exists (owner-only), failing soft. Returns
53
65
  * the path on success and null on failure — a null result means "no scratchpad
@@ -56,7 +68,9 @@ export declare function scratchpadDir(opts?: {
56
68
  export declare function ensureScratchpadDir(path: string, mkdir?: (p: string, o: {
57
69
  mode: number;
58
70
  recursive: boolean;
59
- }) => void): string | null;
71
+ }) => void, opts?: {
72
+ onError?: (errorCode: string) => void;
73
+ }): string | null;
60
74
  /**
61
75
  * PURE: the prompt section naming the scratchpad. Gated by the caller on the
62
76
  * dir existing; when present, it tells the agent where to put intermediate
@@ -61,17 +61,39 @@ export function scratchpadDir(opts = {}) {
61
61
  const cwd = sanitizeCwdSegment(opts.cwd ?? ".");
62
62
  return join(tmp, scratchpadOwnerDir(uid), cwd, sessionId, "scratchpad");
63
63
  }
64
+ /**
65
+ * PURE: the per-user scratchpad OWNER ROOT, recomputed from the SAME inputs
66
+ * `scratchpadDir` resolves from (never derived by stripping segments off a
67
+ * built path — a layout change would silently shift a stripped spelling).
68
+ * The value the sandbox forces TMPDIR onto (Claude parity): per-user, shared
69
+ * across sessions, and short enough for named unix-socket binds where the
70
+ * deep scratchpad path would trip macOS's 104-char sun_path limit.
71
+ */
72
+ export function scratchpadOwnerRootFor(opts = {}) {
73
+ const tmp = opts.tmp ?? tmpdir();
74
+ const uid = opts.uid ?? (typeof process.getuid === "function" ? process.getuid() ?? 0 : 0);
75
+ return join(tmp, scratchpadOwnerDir(uid));
76
+ }
64
77
  /**
65
78
  * IMPURE: ensure the scratchpad dir exists (owner-only), failing soft. Returns
66
79
  * the path on success and null on failure — a null result means "no scratchpad
67
80
  * this session", which the caller turns into an omitted prompt section.
68
81
  */
69
- export function ensureScratchpadDir(path, mkdir = mkdirSync) {
82
+ export function ensureScratchpadDir(path, mkdir = mkdirSync, opts = {}) {
70
83
  try {
71
84
  mkdir(path, { recursive: true, mode: 0o700 });
72
85
  return path;
73
86
  }
74
- catch {
87
+ catch (err) {
88
+ // The errno CODE (never the thrown message — the message can carry a
89
+ // path or system detail, and the caller forwards this to the sink).
90
+ // fs errors are plain Error instances whose distinguishing value is
91
+ // .code (ENOTDIR vs EACCES vs EPERM...); the constructor name would
92
+ // always read "Error" and distinguish nothing. The fallback keeps the
93
+ // class name for codeless throws (a non-fs throw), so the callback's
94
+ // value is "errno code, or class name when there is no code".
95
+ const code = err?.code;
96
+ opts.onError?.(code ?? (err instanceof Error ? err.constructor.name : typeof err));
75
97
  return null;
76
98
  }
77
99
  }
@@ -82,12 +104,13 @@ export function ensureScratchpadDir(path, mkdir = mkdirSync) {
82
104
  */
83
105
  export function scratchpadSection(dir) {
84
106
  return ("# Scratchpad directory\n\n" +
85
- `Use this session scratchpad directory for files that do not belong in the user's project:\n` +
107
+ `IMPORTANT: Always use this scratchpad directory for temporary files instead of /tmp or other system temp directories (including $TMPDIR in sandboxed bash):\n` +
86
108
  `${dir}\n\n` +
109
+ "Use this directory for ALL temporary file needs:\n" +
87
110
  "- Store intermediate results or data during multi-step tasks.\n" +
88
111
  "- Write temporary scripts or configuration files.\n" +
89
112
  "- Save outputs that don't belong in the user's project.\n" +
90
- "- Anything that would otherwise go to /tmp.\n\n" +
113
+ "- Staging files for subagents: paths in this directory are readable by every subagent of this session, so hand subagents scratchpad paths rather than $TMPDIR or /tmp paths.\n\n" +
91
114
  "The directory is session-specific and isolated from the user's project.");
92
115
  }
93
116
  //# sourceMappingURL=scratchpad.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.1.4-staging.1416.1",
3
+ "version": "1.1.4-staging.1419.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)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "f17aa2f61573701f8a9b62da1d99ca022b96a7bd"
61
+ "yagniSourceSha": "72326f4909f8b24a2657ae8ddd9caa4f4ad2e614"
62
62
  }