Compare commits

...

6 Commits

Author SHA1 Message Date
Devin Foley
347f38019f docs: clarify all PR template sections are required, not just thinking path
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 23:47:08 -07:00
Devin Foley
25615407a4 docs: update CONTRIBUTING.md to require PR template, Greptile 5/5, and passing tests
Add explicit PR Requirements section referencing .github/PULL_REQUEST_TEMPLATE.md.
Clarify that all PRs must use the template, achieve a 5/5 Greptile score with
all comments addressed, and have passing tests/CI before merge.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 23:46:58 -07:00
Octasoft Ltd
f843a45a84 fix: use sh instead of /bin/sh as shell fallback on Windows (#891)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Agents run shell commands during workspace provisioning (git
worktree creation, runtime services)
> - When `process.env.SHELL` is unset, the code falls back to `/bin/sh`
> - But on Windows with Git Bash, `/bin/sh` doesn't exist as an absolute
path — Git Bash provides `sh` on PATH instead
> - This causes `child_process.spawn` to throw `ENOENT`, crashing
workspace provisioning on Windows
> - This PR extracts a `resolveShell()` helper that uses `$SHELL` when
set, falls back to `sh` (bare) on Windows or `/bin/sh` on Unix
> - The benefit is that agents running on Windows via Git Bash can
provision workspaces without shell resolution errors
## Summary
- `workspace-runtime.ts` falls back to `/bin/sh` when
`process.env.SHELL` is unset
- On Windows, `/bin/sh` doesn't exist → `spawn /bin/sh ENOENT`
- Fix: extract `resolveShell()` helper that uses `$SHELL` when set,
falls back to `sh` on Windows (Git Bash PATH lookup) or `/bin/sh` on
Unix

Three call sites updated to use the new helper.

Fixes #892

## Root cause

When Paperclip spawns shell commands in workspace operations (e.g., git
worktree creation), it uses `process.env.SHELL` if set, otherwise
defaults to `/bin/sh`. On Windows with Git Bash, `$SHELL` is typically
unset and `/bin/sh` is not a valid path — Git Bash provides `sh` on PATH
but not at the absolute `/bin/sh` location. This causes
`child_process.spawn` to throw `ENOENT`.

## Approach

Rather than hard-coding a Windows-specific absolute path (e.g.,
`C:\Program Files\Git\bin\sh.exe`), we use the bare `"sh"` command which
relies on PATH resolution. This works because:
1. Git Bash adds its `usr/bin` directory to PATH, making `sh` resolvable
2. On Unix/macOS, `/bin/sh` remains the correct default (it's the POSIX
standard location)
3. `process.env.SHELL` takes priority when set, so this only affects the
fallback

## Test plan

- [x] 7 unit tests for `resolveShell()`: SHELL set, trimmed, empty,
whitespace-only, linux/darwin/win32 fallbacks
- [x] Run a workspace provision command on Windows with `git_worktree`
strategy
- [x] Verify Unix/macOS is unaffected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@devinfoley.com>
2026-04-02 17:34:26 -07:00
Dotta
36049beeea Merge pull request #2552 from paperclipai/PAPA-42-add-model-used-to-pr-template-and-checklist
feat: add Model Used section to PR template and checklist
2026-04-02 13:47:46 -05:00
Devin Foley
c041fee6fc feat: add Model Used section to PR template and checklist
Add a required "Model Used" section to the PR template so contributors
document which AI model (with version, context window, reasoning mode,
and other capability details) was used for each change. Also adds a
corresponding checklist item.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:32:22 -07:00
Dotta
82290451d4 Merge pull request #2541 from paperclipai/pap-1078-qol-fixes
fix(ui): polish issue detail timelines and attachments
2026-04-02 13:31:12 -05:00
4 changed files with 101 additions and 8 deletions

View File

@@ -38,9 +38,25 @@
- -
## Model Used
<!--
Required. Specify which AI model was used to produce or assist with
this change. Be as descriptive as possible — include:
• Provider and model name (e.g., Claude, GPT, Gemini, Codex)
• Exact model ID or version (e.g., claude-opus-4-6, gpt-4-turbo-2024-04-09)
• Context window size if relevant (e.g., 1M context)
• Reasoning/thinking mode if applicable (e.g., extended thinking, chain-of-thought)
• Any other relevant capability details (e.g., tool use, code execution)
If no AI model was used, write "None — human-authored".
-->
-
## Checklist ## Checklist
- [ ] I have included a thinking path that traces from project context to this change - [ ] I have included a thinking path that traces from project context to this change
- [ ] I have specified the model used (with version and capability details)
- [ ] I have run tests locally and they pass - [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable - [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots - [ ] If this change affects the UI, I have included before/after screenshots

View File

@@ -11,8 +11,9 @@ We really appreciate both small fixes and thoughtful larger changes.
- Pick **one** clear thing to fix/improve - Pick **one** clear thing to fix/improve
- Touch the **smallest possible number of files** - Touch the **smallest possible number of files**
- Make sure the change is very targeted and easy to review - Make sure the change is very targeted and easy to review
- All automated checks pass (including Greptile comments) - All tests pass and CI is green
- No new lint/test failures - Greptile score is 5/5 with all comments addressed
- Use the [PR template](.github/PULL_REQUEST_TEMPLATE.md)
These almost always get merged quickly when they're clean. These almost always get merged quickly when they're clean.
@@ -26,11 +27,26 @@ These almost always get merged quickly when they're clean.
- Before / After screenshots (or short video if UI/behavior change) - Before / After screenshots (or short video if UI/behavior change)
- Clear description of what & why - Clear description of what & why
- Proof it works (manual testing notes) - Proof it works (manual testing notes)
- All tests passing - All tests passing and CI green
- All Greptile + other PR comments addressed - Greptile score 5/5 with all comments addressed
- [PR template](.github/PULL_REQUEST_TEMPLATE.md) fully filled out
PRs that follow this path are **much** more likely to be accepted, even when they're large. PRs that follow this path are **much** more likely to be accepted, even when they're large.
## PR Requirements (all PRs)
### Use the PR Template
Every pull request **must** follow the PR template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). If you create a PR via the GitHub API or other tooling that bypasses the template, copy its contents into your PR description manually. The template includes required sections: Thinking Path, What Changed, Verification, Risks, and a Checklist.
### Tests Must Pass
All tests must pass before a PR can be merged. Run them locally first and verify CI is green after pushing.
### Greptile Review
We use [Greptile](https://greptile.com) for automated code review. Your PR must achieve a **5/5 Greptile score** with **all Greptile comments addressed** before it can be merged. If Greptile leaves comments, fix or respond to each one and request a re-review.
## General Rules (both paths) ## General Rules (both paths)
- Write clear commit messages - Write clear commit messages
@@ -41,7 +57,7 @@ PRs that follow this path are **much** more likely to be accepted, even when the
## Writing a Good PR message ## Writing a Good PR message
Please include a "thinking path" at the top of your PR message that explains from the top of the project down to what you fixed. E.g.: Your PR description must follow the [PR template](.github/PULL_REQUEST_TEMPLATE.md). All sections are required. The "thinking path" at the top explains from the top of the project down to what you fixed. E.g.:
### Thinking Path Example 1: ### Thinking Path Example 1:

View File

@@ -24,6 +24,7 @@ import {
realizeExecutionWorkspace, realizeExecutionWorkspace,
releaseRuntimeServicesForRun, releaseRuntimeServicesForRun,
resetRuntimeServicesForTests, resetRuntimeServicesForTests,
resolveShell,
sanitizeRuntimeServiceBaseEnv, sanitizeRuntimeServiceBaseEnv,
stopRuntimeServicesForExecutionWorkspace, stopRuntimeServicesForExecutionWorkspace,
type RealizedExecutionWorkspace, type RealizedExecutionWorkspace,
@@ -1345,6 +1346,60 @@ describe("ensureRuntimeServicesForRun", () => {
}); });
}); });
describe("resolveShell (shell fallback)", () => {
const originalShell = process.env.SHELL;
const originalPlatform = process.platform;
afterEach(() => {
if (originalShell !== undefined) {
process.env.SHELL = originalShell;
} else {
delete process.env.SHELL;
}
Object.defineProperty(process, "platform", { value: originalPlatform });
});
it("returns process.env.SHELL when set", () => {
process.env.SHELL = "/usr/bin/zsh";
expect(resolveShell()).toBe("/usr/bin/zsh");
});
it("trims whitespace from SHELL env var", () => {
process.env.SHELL = " /usr/bin/fish ";
expect(resolveShell()).toBe("/usr/bin/fish");
});
it("falls back to /bin/sh on non-Windows when SHELL is unset", () => {
delete process.env.SHELL;
Object.defineProperty(process, "platform", { value: "linux" });
expect(resolveShell()).toBe("/bin/sh");
});
it("falls back to sh (bare) on Windows when SHELL is unset", () => {
delete process.env.SHELL;
Object.defineProperty(process, "platform", { value: "win32" });
expect(resolveShell()).toBe("sh");
});
it("falls back to /bin/sh on darwin when SHELL is unset", () => {
delete process.env.SHELL;
Object.defineProperty(process, "platform", { value: "darwin" });
expect(resolveShell()).toBe("/bin/sh");
});
it("treats empty SHELL as unset and uses platform fallback", () => {
process.env.SHELL = "";
Object.defineProperty(process, "platform", { value: "linux" });
expect(resolveShell()).toBe("/bin/sh");
});
it("treats whitespace-only SHELL as unset and uses platform fallback", () => {
process.env.SHELL = " ";
Object.defineProperty(process, "platform", { value: "win32" });
expect(resolveShell()).toBe("sh");
});
});
describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { describeEmbeddedPostgres("workspace runtime startup reconciliation", () => {
let db!: ReturnType<typeof createDb>; let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null; let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;

View File

@@ -24,6 +24,10 @@ import type { WorkspaceOperationRecorder } from "./workspace-operations.js";
import { readExecutionWorkspaceConfig } from "./execution-workspaces.js"; import { readExecutionWorkspaceConfig } from "./execution-workspaces.js";
import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js"; import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
export function resolveShell(): string {
return process.env.SHELL?.trim() || (process.platform === "win32" ? "sh" : "/bin/sh");
}
export interface ExecutionWorkspaceInput { export interface ExecutionWorkspaceInput {
baseCwd: string; baseCwd: string;
source: "project_primary" | "task_session" | "agent_home"; source: "project_primary" | "task_session" | "agent_home";
@@ -379,7 +383,7 @@ async function runWorkspaceCommand(input: {
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
label: string; label: string;
}) { }) {
const shell = process.env.SHELL?.trim() || "/bin/sh"; const shell = resolveShell();
const proc = await executeProcess({ const proc = await executeProcess({
command: shell, command: shell,
args: ["-c", input.command], args: ["-c", input.command],
@@ -475,7 +479,7 @@ async function recordWorkspaceCommandOperation(
cwd: input.cwd, cwd: input.cwd,
metadata: input.metadata ?? null, metadata: input.metadata ?? null,
run: async () => { run: async () => {
const shell = process.env.SHELL?.trim() || "/bin/sh"; const shell = resolveShell();
const result = await executeProcess({ const result = await executeProcess({
command: shell, command: shell,
args: ["-c", input.command], args: ["-c", input.command],
@@ -1285,6 +1289,7 @@ async function startLocalRuntimeService(input: {
const portEnvKey = asString(portConfig.envKey, "PORT"); const portEnvKey = asString(portConfig.envKey, "PORT");
env[portEnvKey] = String(port); env[portEnvKey] = String(port);
} }
const expose = parseObject(input.service.expose); const expose = parseObject(input.service.expose);
const readiness = parseObject(input.service.readiness); const readiness = parseObject(input.service.readiness);
const urlTemplate = const urlTemplate =
@@ -1359,7 +1364,8 @@ async function startLocalRuntimeService(input: {
); );
} }
} }
const shell = process.env.SHELL?.trim() || "/bin/sh";
const shell = resolveShell();
const child = spawn(shell, ["-lc", command], { const child = spawn(shell, ["-lc", command], {
cwd: serviceCwd, cwd: serviceCwd,
env, env,