@yagni-app/code-staging 1.0.0-staging.1179.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.
@@ -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) ===";
@@ -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,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. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
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;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.0.0-staging.1179.1",
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": "0a0e2cda58c070bc25dbc00fe76d89269fb43c6f"
43
+ "yagniSourceSha": "1db89538911e63de0e383aad36bd707e947140d9"
44
44
  }