From ff022208904750ba2e07908b54453dd151f8b118 Mon Sep 17 00:00:00 2001 From: Alaa Alghazouli Date: Thu, 12 Mar 2026 23:03:44 +0100 Subject: [PATCH 01/11] fix: add initdbFlags to embedded postgres ctor types --- cli/src/commands/worktree.ts | 1 + packages/db/src/migration-runtime.ts | 1 + server/src/index.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index 7311793b..e807dafb 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -83,6 +83,7 @@ type EmbeddedPostgresCtor = new (opts: { password: string; port: number; persistent: boolean; + initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; diff --git a/packages/db/src/migration-runtime.ts b/packages/db/src/migration-runtime.ts index 10b7b9b1..e07bdf04 100644 --- a/packages/db/src/migration-runtime.ts +++ b/packages/db/src/migration-runtime.ts @@ -17,6 +17,7 @@ type EmbeddedPostgresCtor = new (opts: { password: string; port: number; persistent: boolean; + initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; diff --git a/server/src/index.ts b/server/src/index.ts index 50c6a7b2..0479f878 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -53,6 +53,7 @@ type EmbeddedPostgresCtor = new (opts: { password: string; port: number; persistent: boolean; + initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; From 3d2abbde7223801edea65c15799c6f060a04012b Mon Sep 17 00:00:00 2001 From: Sigmabrogz Date: Fri, 13 Mar 2026 00:42:28 +0000 Subject: [PATCH 02/11] fix(openclaw-gateway): catch challengePromise rejection to prevent unhandled rejection process crash Resolves #727 Signed-off-by: Sigmabrogz --- packages/adapters/openclaw-gateway/src/server/execute.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adapters/openclaw-gateway/src/server/execute.ts b/packages/adapters/openclaw-gateway/src/server/execute.ts index eaacbd33..f1c85c11 100644 --- a/packages/adapters/openclaw-gateway/src/server/execute.ts +++ b/packages/adapters/openclaw-gateway/src/server/execute.ts @@ -605,6 +605,7 @@ class GatewayWsClient { this.resolveChallenge = resolve; this.rejectChallenge = reject; }); + this.challengePromise.catch(() => {}); } async connect( From 71e1bc260d18a8ec00a18f3f6d2bab1b33134114 Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Tue, 17 Mar 2026 09:35:57 -0400 Subject: [PATCH 03/11] Clarify linked ticket references in Paperclip skill Co-Authored-By: Paperclip --- skills/paperclip/SKILL.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 0e6044e6..eb098c49 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -71,6 +71,8 @@ Read enough ancestor/comment context to understand _why_ the task exists and wha **Step 8 — Update status and communicate.** Always include the run ID header. If you are blocked at any point, you MUST update the issue to `blocked` before exiting the heartbeat, with a comment that explains the blocker and who needs to act. +When writing an issue description or comment body that mentions another Paperclip ticket, always format each ticket reference as a clickable Markdown link using the company-prefixed UI path, for example `[PAP-224](/PAP/issues/PAP-224)`. + ```json PATCH /api/issues/{issueId} Headers: X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID @@ -144,12 +146,19 @@ Access control: ## Comment Style (Required) -When posting issue comments, use concise markdown with: +When posting issue comments or writing issue descriptions, use concise markdown with: - a short status line - bullets for what changed / what is blocked - links to related entities when available +**Ticket references are links (required):** If you mention another issue identifier such as `PAP-224`, `ZED-24`, or any `{PREFIX}-{NUMBER}` ticket id inside a comment body or issue description, wrap it in a Markdown link: + +- `[PAP-224](/PAP/issues/PAP-224)` +- `[ZED-24](/ZED/issues/ZED-24)` + +Never leave bare ticket ids in issue descriptions or comments when a clickable internal link can be provided. + **Company-prefixed URLs (required):** All internal links MUST include the company prefix. Derive the prefix from any issue identifier you have (e.g., `PAP-315` → prefix is `PAP`). Use this prefix in all UI links: - Issues: `//issues/` (e.g., `/PAP/issues/PAP-224`) @@ -172,6 +181,7 @@ Submitted CTO hire request and linked it for board review. - Approval: [ca6ba09d](/PAP/approvals/ca6ba09d-b558-4a53-a552-e7ef87e54a1b) - Pending agent: [CTO draft](/PAP/agents/cto) - Source issue: [PC-142](/PAP/issues/PC-142) +- Depends on: [PAP-224](/PAP/issues/PAP-224) ``` ## Planning (Required when planning requested) From 7a08fbd37001702b448fa3225b5308d412ab74ac Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Tue, 17 Mar 2026 09:43:47 -0400 Subject: [PATCH 04/11] Reduce duplicate ticket-link guidance Co-Authored-By: Paperclip --- skills/paperclip/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index eb098c49..1b895f16 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -71,7 +71,7 @@ Read enough ancestor/comment context to understand _why_ the task exists and wha **Step 8 — Update status and communicate.** Always include the run ID header. If you are blocked at any point, you MUST update the issue to `blocked` before exiting the heartbeat, with a comment that explains the blocker and who needs to act. -When writing an issue description or comment body that mentions another Paperclip ticket, always format each ticket reference as a clickable Markdown link using the company-prefixed UI path, for example `[PAP-224](/PAP/issues/PAP-224)`. +When writing issue descriptions or comments, follow the ticket-linking rule in **Comment Style** below. ```json PATCH /api/issues/{issueId} From c539fcde8b73fe7abb52ac48556dfdcf665373a1 Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Tue, 17 Mar 2026 09:52:32 -0400 Subject: [PATCH 05/11] Fix stale Paperclip issue link example Co-Authored-By: Paperclip --- skills/paperclip/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 1b895f16..054c6a87 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -180,7 +180,7 @@ Submitted CTO hire request and linked it for board review. - Approval: [ca6ba09d](/PAP/approvals/ca6ba09d-b558-4a53-a552-e7ef87e54a1b) - Pending agent: [CTO draft](/PAP/agents/cto) -- Source issue: [PC-142](/PAP/issues/PC-142) +- Source issue: [PAP-142](/PAP/issues/PAP-142) - Depends on: [PAP-224](/PAP/issues/PAP-224) ``` From f37e0aa7b3ec3c2da0afb6d8ec30ab96e961991b Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 20:34:08 -0700 Subject: [PATCH 06/11] fix(pi-local): pass --skill flag for paperclip skills and enable pi_local in adapter dropdown - Add --skill ~/.pi/agent/skills to pi CLI invocation so it loads the paperclip skill - Enable pi_local in the AgentConfigForm adapter type dropdown (was showing as coming soon) --- packages/adapters/pi-local/src/server/execute.ts | 4 ++++ ui/src/components/AgentConfigForm.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 27dce547..e6c4df97 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -336,6 +336,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) args.push(...extraArgs); return args; diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 3b3d53c0..69c31919 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -939,7 +939,7 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe /* ---- Internal sub-components ---- */ -const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "cursor"]); +const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor"]); /** Display list includes all real adapter types plus UI-only coming-soon entries. */ const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean }[] = [ From 5536e6b91e9c6d872612c41e98c5e56af6297a92 Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 20:40:55 -0700 Subject: [PATCH 07/11] fix(pi-local): ensure skills directory exists before passing --skill flag - Hoist PI_AGENT_SKILLS_DIR to module-level constant to avoid duplicate path computation - Always create ~/.pi/agent/skills directory before early return in ensurePiSkillsInjected, so the path is valid when --skill flag is passed --- .../adapters/pi-local/src/server/execute.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index e6c4df97..697387cd 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -26,6 +26,7 @@ import { ensurePiModelConfiguredAndAvailable } from "./models.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips"); +const PI_AGENT_SKILLS_DIR = path.join(os.homedir(), ".pi", "agent", "skills"); function firstNonEmptyLine(text: string): string { return ( @@ -56,35 +57,35 @@ function resolvePiBiller(env: Record, provider: string | null): async function ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) { const skillsEntries = await listPaperclipSkillEntries(__moduleDir); + + await fs.mkdir(PI_AGENT_SKILLS_DIR, { recursive: true }); if (skillsEntries.length === 0) return; - const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills"); - await fs.mkdir(piSkillsHome, { recursive: true }); const removedSkills = await removeMaintainerOnlySkillSymlinks( - piSkillsHome, + PI_AGENT_SKILLS_DIR, skillsEntries.map((entry) => entry.name), ); for (const skillName of removedSkills) { await onLog( "stderr", - `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${piSkillsHome}\n`, + `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${PI_AGENT_SKILLS_DIR}\n`, ); } for (const entry of skillsEntries) { - const target = path.join(piSkillsHome, entry.name); + const target = path.join(PI_AGENT_SKILLS_DIR, entry.name); try { const result = await ensurePaperclipSkillSymlink(entry.source, target); if (result === "skipped") continue; await onLog( "stderr", - `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.name}" into ${piSkillsHome}\n`, + `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.name}" into ${PI_AGENT_SKILLS_DIR}\n`, ); } catch (err) { await onLog( "stderr", - `[paperclip] Failed to inject Pi skill "${entry.name}" into ${piSkillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + `[paperclip] Failed to inject Pi skill "${entry.name}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`, ); } } @@ -337,8 +338,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) args.push(...extraArgs); From aa854e7efed5f1b85d4019ec837940eeb94eaeca Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 20:51:59 -0700 Subject: [PATCH 08/11] fix: include toolName in tool_result transcript entries for Pi adapter When tool_result entries arrive without a matching tool_call, the transcript was showing generic 'tool' as the name. Now pl-local parses toolName from tool_execution_end events and passes it through, so the UI can display the actual tool name (e.g., 'bash', 'Read', 'Ls') instead of 'tool'. --- packages/adapter-utils/src/types.ts | 2 +- packages/adapters/pi-local/src/ui/parse-stdout.ts | 2 ++ ui/src/components/transcript/RunTranscriptView.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index f907d4b4..e7639593 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -246,7 +246,7 @@ export type TranscriptEntry = | { kind: "thinking"; ts: string; text: string; delta?: boolean } | { kind: "user"; ts: string; text: string } | { kind: "tool_call"; ts: string; name: string; input: unknown; toolUseId?: string } - | { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean } + | { kind: "tool_result"; ts: string; toolUseId: string; toolName?: string; content: string; isError: boolean } | { kind: "init"; ts: string; model: string; sessionId: string } | { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] } | { kind: "stderr"; ts: string; text: string } diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts index b80fe5f1..7ded5633 100644 --- a/packages/adapters/pi-local/src/ui/parse-stdout.ts +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -130,6 +130,7 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { if (type === "tool_execution_end") { const toolCallId = asString(parsed.toolCallId); + const toolName = asString(parsed.toolName); const result = parsed.result; const isError = parsed.isError === true; const contentStr = typeof result === "string" ? result : JSON.stringify(result); @@ -138,6 +139,7 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { kind: "tool_result", ts, toolUseId: toolCallId || "unknown", + toolName, content: contentStr, isError, }]; diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx index 5f42ec0e..cd52dbc1 100644 --- a/ui/src/components/transcript/RunTranscriptView.tsx +++ b/ui/src/components/transcript/RunTranscriptView.tsx @@ -400,7 +400,7 @@ export function normalizeTranscript(entries: TranscriptEntry[], streaming: boole type: "tool", ts: entry.ts, endTs: entry.ts, - name: "tool", + name: entry.toolName ?? "tool", toolUseId: entry.toolUseId, input: null, result: entry.content, From 47a6d86174e7be38b1154207a27971448273099f Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 20:57:32 -0700 Subject: [PATCH 09/11] fix: include toolName in tool_result entries from turn_end toolResults The turn_end event includes toolResults array with toolName. Previously the parsing only included toolCallId, now we also extract toolName so the UI can display the correct tool name even when tool_result entries arrive without a preceding tool_call. --- packages/adapters/pi-local/src/ui/parse-stdout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts index 7ded5633..f7b5b7f9 100644 --- a/packages/adapters/pi-local/src/ui/parse-stdout.ts +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -77,6 +77,7 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { kind: "tool_result", ts, toolUseId: asString(tr.toolCallId, "unknown"), + toolName: asString(tr.toolName), content: contentStr, isError, }); From 6eb9545a72be62987ce86e7a25c41ca407ddb00a Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 21:02:53 -0700 Subject: [PATCH 10/11] fix: extract text content from Pi's tool result content arrays Pi returns tool results in format: {"content": [{"type": "text", "text": "..."}]} Previously we were JSON.stringify-ing the whole object, showing as: {"content":[{"type":"text","text":"..."}]} Now we extract the actual text content for cleaner display. --- .../adapters/pi-local/src/ui/parse-stdout.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts index f7b5b7f9..57a78725 100644 --- a/packages/adapters/pi-local/src/ui/parse-stdout.ts +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -72,7 +72,17 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { for (const tr of toolResults) { const content = tr.content; const isError = tr.isError === true; - const contentStr = typeof content === "string" ? content : JSON.stringify(content); + + // Extract text from Pi's content array format + let contentStr: string; + if (typeof content === "string") { + contentStr = content; + } else if (Array.isArray(content)) { + contentStr = extractTextContent(content as Array<{ type: string; text?: string }>); + } else { + contentStr = JSON.stringify(content); + } + entries.push({ kind: "tool_result", ts, @@ -134,7 +144,21 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { const toolName = asString(parsed.toolName); const result = parsed.result; const isError = parsed.isError === true; - const contentStr = typeof result === "string" ? result : JSON.stringify(result); + + // Extract text from Pi's content array format: {"content": [{"type": "text", "text": "..."}]} + let contentStr: string; + if (typeof result === "string") { + contentStr = result; + } else if (result && typeof result === "object") { + const resultObj = result as Record; + if (Array.isArray(resultObj.content)) { + contentStr = extractTextContent(resultObj.content as Array<{ type: string; text?: string }>); + } else { + contentStr = JSON.stringify(result); + } + } else { + contentStr = JSON.stringify(result); + } return [{ kind: "tool_result", From 1cd61601f399150646c3655ed266a0d92c6bb88f Mon Sep 17 00:00:00 2001 From: Richard Anaya Date: Wed, 18 Mar 2026 21:11:03 -0700 Subject: [PATCH 11/11] fix: handle direct array format for Pi tool results Pi sometimes sends tool results as a direct array [{"type":"text","text":"..."}] rather than wrapped in {"content": [...]}. Now handles both formats to properly extract text content instead of showing raw JSON. --- packages/adapters/pi-local/src/ui/parse-stdout.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts index 57a78725..64a553e9 100644 --- a/packages/adapters/pi-local/src/ui/parse-stdout.ts +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -145,13 +145,18 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { const result = parsed.result; const isError = parsed.isError === true; - // Extract text from Pi's content array format: {"content": [{"type": "text", "text": "..."}]} + // Extract text from Pi's content array format + // Can be: {"content": [{"type": "text", "text": "..."}]} or [{"type": "text", "text": "..."}] let contentStr: string; if (typeof result === "string") { contentStr = result; + } else if (Array.isArray(result)) { + // Direct array format: result is [{"type": "text", "text": "..."}] + contentStr = extractTextContent(result as Array<{ type: string; text?: string }>); } else if (result && typeof result === "object") { const resultObj = result as Record; if (Array.isArray(resultObj.content)) { + // Wrapped format: result is {"content": [{"type": "text", "text": "..."}]} contentStr = extractTextContent(resultObj.content as Array<{ type: string; text?: string }>); } else { contentStr = JSON.stringify(result);