fix(pr): restore upstream dev and skill behavior

This commit is contained in:
Dotta committed 2026-04-16 08:01:17 -05:00
1 parent 299fb9f603
commit a362f6bf0b
5 files changed
+184 -10

No files matched your search

+43 -1
View File
@@ -24,6 +24,8 @@ node_lines=()
pg_pids=()
pg_pidfiles=()
pg_data_dirs=()
browser_pids=()
browser_lines=()
is_pid_running() {
local pid="$1"
@@ -87,6 +89,14 @@ while IFS= read -r line; do
node_lines+=("$line")
done < <(ps aux | grep -E '/paperclip(-[^/]+)?/' | grep node | grep -v grep || true)
# --- Agent browser processes (headless Chrome from ~/.agent-browser) ---
while IFS= read -r line; do
[[ -z "$line" ]] && continue
pid=$(echo "$line" | awk '{print $2}')
browser_pids+=("$pid")
browser_lines+=("$line")
done < <(ps aux | grep -E 'agent-browser/browsers/chrome-.*/Google Chrome for Testing' | grep -v grep || true)
candidate_pidfiles=()
candidate_pidfiles+=(
"$HOME"/.paperclip/instances/*/db/postmaster.pid
@@ -107,7 +117,7 @@ for pidfile in "${candidate_pidfiles[@]:-}"; do
append_postgres_from_pidfile "$pidfile"
done
if [[ ${#node_pids[@]} -eq 0 && ${#pg_pids[@]} -eq 0 ]]; then
if [[ ${#node_pids[@]} -eq 0 && ${#pg_pids[@]} -eq 0 && ${#browser_pids[@]} -eq 0 ]]; then
echo "No Paperclip dev processes found."
exit 0
fi
@@ -144,6 +154,22 @@ if [[ ${#pg_pids[@]} -gt 0 ]]; then
echo ""
fi
if [[ ${#browser_pids[@]} -gt 0 ]]; then
echo "Found ${#browser_pids[@]} agent browser process(es):"
echo ""
for i in "${!browser_pids[@]:-}"; do
line="${browser_lines[$i]}"
pid=$(echo "$line" | awk '{print $2}')
start=$(echo "$line" | awk '{print $9}')
cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}')
cmd=$(echo "$cmd" | sed "s|$HOME/||g")
printf " PID %-7s started %-10s %s\n" "$pid" "$start" "$cmd"
done
echo ""
fi
if [[ "$DRY_RUN" == true ]]; then
echo "Dry run — re-run without --dry to kill these processes."
exit 0
@@ -158,6 +184,13 @@ if [[ ${#node_pids[@]} -gt 0 ]]; then
sleep 2
fi
if [[ ${#browser_pids[@]} -gt 0 ]]; then
echo "Sending SIGTERM to agent browser processes..."
for pid in "${browser_pids[@]}"; do
kill -TERM "$pid" 2>/dev/null && echo " signaled $pid" || echo " $pid already gone"
done
fi
leftover_pg_pids=()
leftover_pg_data_dirs=()
for i in "${!pg_pids[@]:-}"; do
@@ -203,4 +236,13 @@ if [[ ${#pg_pids[@]} -gt 0 ]]; then
done
fi
if [[ ${#browser_pids[@]} -gt 0 ]]; then
for pid in "${browser_pids[@]:-}"; do
if kill -0 "$pid" 2>/dev/null; then
echo " agent browser $pid still alive, sending SIGKILL..."
kill -KILL "$pid" 2>/dev/null || true
fi
done
fi
echo "Done."
+4 -5
View File
@@ -32,16 +32,15 @@
"skills"
],
"scripts": {
"preflight:workspace-links": "tsx ../scripts/ensure-workspace-package-links.ts",
"dev": "pnpm run preflight:workspace-links && tsx src/index.ts",
"dev:watch": "pnpm run preflight:workspace-links && cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx ./scripts/dev-watch.ts",
"dev": "tsx src/index.ts",
"dev:watch": "cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx ./scripts/dev-watch.ts",
"prepare:ui-dist": "bash ../scripts/prepare-server-ui-dist.sh",
"build": "pnpm run preflight:workspace-links && tsc && mkdir -p dist/onboarding-assets && cp -R src/onboarding-assets/. dist/onboarding-assets/",
"build": "tsc && mkdir -p dist/onboarding-assets && cp -R src/onboarding-assets/. dist/onboarding-assets/",
"prepack": "pnpm run prepare:ui-dist",
"postpack": "rm -rf ui-dist",
"clean": "rm -rf dist",
"start": "node dist/index.js",
"typecheck": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
"typecheck": "pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.888.0",
@@ -184,7 +184,31 @@ export async function findLocalServiceRegistryRecordByRuntimeServiceId(input: {
const records = await listLocalServiceRegistryRecords(
input.profileKind ? { profileKind: input.profileKind } : undefined,
);
return records.find((record) => record.runtimeServiceId === input.runtimeServiceId) ?? null;
const record = records.find((entry) => entry.runtimeServiceId === input.runtimeServiceId) ?? null;
if (!record) return null;
let candidate = record;
if (!isPidAlive(candidate.pid)) {
const ownerPid = candidate.port ? await readLocalServicePortOwner(candidate.port) : null;
if (!ownerPid) {
await removeLocalServiceRegistryRecord(candidate.serviceKey);
return null;
}
candidate = {
...candidate,
pid: ownerPid,
processGroupId: candidate.processGroupId && isPidAlive(candidate.processGroupId) ? candidate.processGroupId : ownerPid,
lastSeenAt: new Date().toISOString(),
};
await writeLocalServiceRegistryRecord(candidate);
}
if (!(await isLikelyMatchingCommand(candidate))) {
await removeLocalServiceRegistryRecord(record.serviceKey);
return null;
}
return candidate;
}
export function isPidAlive(pid: number) {
@@ -214,7 +238,10 @@ async function isLikelyMatchingCommand(record: LocalServiceRegistryRecord) {
const { stdout } = await execFileAsync("ps", ["-o", "command=", "-p", String(record.pid)]);
const commandLine = stdout.trim();
if (!commandLine) return false;
return commandLine.includes(record.command) || commandLine.includes(record.serviceName);
const normalize = (value: string) => value.replace(/["']/g, "").replace(/\s+/g, " ").trim();
const normalizedCommandLine = normalize(commandLine);
const normalizedRecordedCommand = normalize(record.command);
return normalizedCommandLine.includes(normalizedRecordedCommand) || normalizedCommandLine.includes(record.serviceName);
} catch {
return true;
}
+71 -2
View File
@@ -152,7 +152,76 @@ Practical rules:
**Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work.
**Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work.
## Issue Dependencies (Blockers)
Paperclip supports first-class blocker relationships between issues. Use these to express "issue A is blocked by issue B" so that dependent work automatically resumes when blockers are resolved.
### Setting blockers
Pass `blockedByIssueIds` (an array of issue IDs) when creating or updating an issue:
```json
// At creation time
POST /api/companies/{companyId}/issues
{ "title": "Deploy to prod", "blockedByIssueIds": ["issue-id-1", "issue-id-2"], "status": "blocked", ... }
// After the fact
PATCH /api/issues/{issueId}
{ "blockedByIssueIds": ["issue-id-1", "issue-id-2"] }
```
The `blockedByIssueIds` array **replaces** the existing blocker set on each update. To add a blocker, include the full list. To remove all blockers, send `[]`.
Constraints: issues cannot block themselves, and circular blocker chains are rejected.
### Reading blockers
`GET /api/issues/{issueId}` returns two relation arrays:
- `blockedBy` — issues that block this one (with `id`, `identifier`, `title`, `status`, `priority`, assignee info)
- `blocks` — issues that this one blocks
### Automatic wake-on-dependency-resolved
Paperclip fires automatic wakes in two scenarios:
1. **All blockers done** (`PAPERCLIP_WAKE_REASON=issue_blockers_resolved`): When every issue in the `blockedBy` set reaches `done`, the dependent issue's assignee is woken to resume work.
2. **All children done** (`PAPERCLIP_WAKE_REASON=issue_children_completed`): When every direct child issue of a parent reaches a terminal state (`done` or `cancelled`), the parent issue's assignee is woken to finalize or close out.
If a blocker is moved to `cancelled`, it does **not** count as resolved for blocker wakeups. Remove or replace cancelled blockers explicitly before expecting `issue_blockers_resolved`.
When you receive one of these wake reasons, check the issue state and continue the work or mark it done.
## Requesting Board Approval
Agents can create approval requests for arbitrary issue-linked work. Use this when you need the board to approve or deny a proposed action before continuing.
Recommended generic type:
- `request_board_approval` for open-ended approval requests like spend approval, vendor approval, launch approval, or other board decisions
Create the approval and link it to the relevant issue in one call:
```json
POST /api/companies/{companyId}/approvals
{
"type": "request_board_approval",
"requestedByAgentId": "{your-agent-id}",
"issueIds": ["{issue-id}"],
"payload": {
"title": "Approve monthly hosting spend",
"summary": "Estimated cost is $42/month for provider X.",
"recommendedAction": "Approve provider X and continue setup.",
"risks": ["Costs may increase with usage."]
}
}
```
Notes:
- `issueIds` links the approval into the issue thread/UI.
- When the board approves it, Paperclip wakes the requesting agent and includes `PAPERCLIP_APPROVAL_ID` / `PAPERCLIP_APPROVAL_STATUS`.
- Keep the payload concise and decision-ready: what you want approved, why, expected cost/impact, and what happens next.
## Project Setup Workflow (CEO/Manager Common Path)
@@ -350,7 +419,7 @@ PATCH /api/agents/{agentId}/instructions-path
| My identity | `GET /api/agents/me` |
| My compact inbox | `GET /api/agents/me/inbox-lite` |
| Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` |
| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,blocked` |
| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked` |
| Checkout task | `POST /api/issues/:issueId/checkout` |
| Get task + ancestors | `GET /api/issues/:issueId` |
| List issue documents | `GET /api/issues/:issueId/documents` |
@@ -314,6 +314,43 @@ PATCH /api/issues/issue-200
{ "comment": "Your Mine inbox has 1 unread issue: [PAP-310](/PAP/issues/PAP-310)." }
```
### Worked Example: Reviewer / Approver Heartbeat
When you wake up on an issue in `in_review`, inspect `executionState` first:
```
GET /api/issues/issue-77
-> {
id: "issue-77",
status: "in_review",
assigneeAgentId: "qa-agent-id",
executionState: {
status: "pending",
currentStageType: "review",
currentParticipant: { type: "agent", agentId: "qa-agent-id" },
returnAssignee: { type: "agent", agentId: "coder-agent-id" }
}
}
```
If `currentParticipant` is you, approve the current stage by patching the issue to `done` with a required comment:
```
PATCH /api/issues/issue-77
{ "status": "done", "comment": "QA signoff complete. Verified the regression and test coverage." }
```
Paperclip writes the execution decision automatically. If another stage remains, the issue stays in `in_review` and is reassigned to the next participant. If this was the final stage, the issue reaches actual `done`.
To request changes, use a non-`done` status with a required comment. Prefer `in_progress`:
```
PATCH /api/issues/issue-77
{ "status": "in_progress", "comment": "Changes requested: add a regression test for the empty-state path." }
```
Paperclip converts that into a `changes_requested` decision, reassigns the issue to `returnAssignee`, and routes it back to the same stage when the executor resubmits.
---
## Worked Example: Manager Heartbeat