feat(sdk): scratchDir/scratchFile — organized temp file management (#790)#793
feat(sdk): scratchDir/scratchFile — organized temp file management (#790)#793tamirdresher merged 11 commits intodevfrom
Conversation
…les (#790) - Add scratchDir() and scratchFile() to resolution.ts - Export from SDK barrel - Create .squad/.scratch/ during init, add to .gitignore - 8 new tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🛫 PR Readiness Check
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 11 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 0 active Copilot thread(s) resolved (5 outdated skipped) |
| ❌ | CI passing | 15 check(s) still running |
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
Pull request overview
Adds first-class “scratch” utilities to the Squad SDK so temporary artifacts are written under .squad/.scratch/ (and gitignored) instead of cluttering the repo root.
Changes:
- Introduces
scratchDir()andscratchFile()utilities in the SDK (and exports them publicly). - Updates
initSquad()scaffolding to create.squad/.scratch/and append it to.gitignore. - Adds Vitest coverage for scratch directory/file behavior and a changeset entry.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/scratch-dir.test.ts | Adds tests validating scratch dir creation and scratch file naming/content behavior. |
| packages/squad-sdk/src/resolution.ts | Implements scratchDir() / scratchFile() in the SDK resolution module. |
| packages/squad-sdk/src/index.ts | Exposes scratchDir / scratchFile from the public SDK barrel export. |
| packages/squad-sdk/src/config/init.ts | Ensures .squad/.scratch/ is scaffolded and added to .gitignore during init. |
| .changeset/scratch-dir-utility.md | Declares a minor SDK release for the new scratch utilities. |
| export function scratchFile(squadRoot: string, prefix: string, ext: string = '.tmp', content?: string): string { | ||
| const dir = scratchDir(squadRoot); | ||
| const filename = `${prefix}-${Date.now()}${ext}`; | ||
| const filePath = path.join(dir, filename); | ||
| if (content !== undefined) { | ||
| storage.writeSync(filePath, content); | ||
| } |
There was a problem hiding this comment.
scratchFile() builds the target path using path.join(dir, filename) where filename includes user-controlled prefix/ext. If prefix is absolute (starts with / or a drive letter) or contains ../, path.join can escape the scratch directory and write outside .squad/. Please constrain prefix/ext to safe basename-like values and/or run the resulting filePath through the existing ensureSquadPath(filePath, squadRoot) guard before writing.
| const dir = scratchDir(squadRoot); | ||
| const filename = `${prefix}-${Date.now()}${ext}`; | ||
| const filePath = path.join(dir, filename); | ||
| if (content !== undefined) { | ||
| storage.writeSync(filePath, content); | ||
| } |
There was a problem hiding this comment.
scratchFile() uses only Date.now() for uniqueness. Two calls in the same millisecond with the same prefix/ext will generate identical paths and storage.writeSync() will overwrite the first file. Please incorporate a stronger uniquifier (e.g., randomUUID() or a monotonic counter) so collisions are not possible under fast successive calls.
| * Create a temporary file inside the scratch directory. | ||
| * | ||
| * Returns the absolute path to the file. The caller is responsible for | ||
| * deleting the file when done (or relying on the cleanup capability). | ||
| * | ||
| * @param squadRoot - Absolute path to the `.squad/` directory. | ||
| * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). | ||
| * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. | ||
| * @param content - Optional content to write immediately. | ||
| * @returns Absolute path to the created temp file. |
There was a problem hiding this comment.
The scratchFile() JSDoc says it "Create[s] a temporary file" and "returns ... the created temp file", but the implementation only writes a file when content is provided; otherwise it just returns a path. Please make the behavior and documentation consistent (either always create the file, or clarify that it only returns a path unless content is provided).
| * Create a temporary file inside the scratch directory. | |
| * | |
| * Returns the absolute path to the file. The caller is responsible for | |
| * deleting the file when done (or relying on the cleanup capability). | |
| * | |
| * @param squadRoot - Absolute path to the `.squad/` directory. | |
| * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). | |
| * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. | |
| * @param content - Optional content to write immediately. | |
| * @returns Absolute path to the created temp file. | |
| * Generate a temporary file path inside the scratch directory. | |
| * | |
| * Returns the absolute path to the file. If `content` is provided, the file is | |
| * created immediately with that content. Otherwise, this function only returns | |
| * a unique path and does not create the file. | |
| * | |
| * The caller is responsible for deleting the file when done (or relying on the | |
| * cleanup capability). | |
| * | |
| * @param squadRoot - Absolute path to the `.squad/` directory. | |
| * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). | |
| * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. | |
| * @param content - Optional content; when provided, the file is written immediately. | |
| * @returns Absolute path to the temp file, whether or not it has been created yet. |
| it('generates unique filenames on successive calls', () => { | ||
| const a = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'a'); | ||
| // tiny delay to ensure different timestamp | ||
| const b = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'b'); | ||
| expect(a).not.toBe(b); | ||
| }); |
There was a problem hiding this comment.
This test asserts successive calls return different filenames, but there is no delay despite the comment and scratchFile() currently keys uniqueness to Date.now(). This can be flaky if both calls occur in the same millisecond. Consider making the test deterministic (e.g., by mocking time) and/or relying on a non-time-based uniquifier in scratchFile().
| @@ -0,0 +1,71 @@ | |||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | |||
| import { scratchDir, scratchFile } from '@bradygaster/squad-sdk'; | |||
| import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs'; | |||
There was a problem hiding this comment.
readdirSync is imported but never used in this test file. Please remove the unused import to keep the test code clean and avoid unnecessary lint noise.
| import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs'; | |
| import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'; |
Date.now() can return identical values on rapid successive calls. Added monotonic counter suffix to guarantee unique filenames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sanitize prefix/ext in scratchFile to prevent path traversal - Proper monotonic counter for sub-millisecond uniqueness - JSDoc clarifies caller writes content (not always auto-written) - Remove unused readdirSync import from test - Add comment explaining monotonic counter in uniqueness test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ensureSquadPath guard after path construction to prevent traversal - Add crypto.randomUUID() for stronger uniqueness alongside monotonic counter - Update JSDoc to clarify file-creation vs path-only behavior per Copilot suggestion - Update test regex to match new filename format with UUID segment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🟡 Impact Analysis — PR #793Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
squad-sdk (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
# Conflicts: # .changeset/scratch-dir-utility.md # packages/squad-sdk/src/resolution.ts # test/scratch-dir.test.ts
🏗️ Dina Review: APPROVE WITH FOLLOW-UPscratchDir/scratchFile — temp file management All 5 Copilot review comments have been addressed in the current code (path traversal sanitized, uniqueness via counter+UUID, JSDoc updated, unused imports removed). Implementation is secure and well-tested (8 new tests). Follow-up items (non-blocking):
✅ Ready to merge. |
… JSDoc + test - Use path.basename(prefix) to prevent path traversal via ../ or absolute paths - Replace monotonic counter with crypto.randomBytes(4) hex suffix for collision-free filenames - Update JSDoc to accurately state: writes content if provided, otherwise returns path only - Update test regex patterns and comments to reflect new hex-based uniqueness mechanism Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
Adds a dedicated scratch directory (
.squad/.scratch/) withscratchDir()andscratchFile()utilities for organized temp file management.Why
Squad and agents create temporary files (prompt files, commit drafts, processing artifacts) in the repo root, causing git status noise and accidental commit risk. Closes #790
How
scratchDir(squadRoot, create?)— resolve/create.squad/.scratch/scratchFile(squadRoot, prefix, ext?, content?)— create named temp files with monotonic counter for uniqueness.squad/.scratch/and adds it to.gitignoreTesting
npm run buildpassesnpm testpasses (8 new tests — dir creation, idempotency, file content, uniqueness, auto-create)Docs
Exports
scratchDirandscratchFileadded to SDK barrel exportsBreaking Changes
None
Waivers
None