mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
fix(pr): restore upstream dev and skill behavior
This commit is contained in:
1 parent
299fb9f603
commit
a362f6bf0b
5 files changed
+184
-10
No files matched your search
+43
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user