Compare commits
18 Commits
canary/v20
...
canary/v20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1c4b2e420 | ||
|
|
1d1511e37c | ||
|
|
fff0600b1d | ||
|
|
1cd61601f3 | ||
|
|
6eb9545a72 | ||
|
|
47a6d86174 | ||
|
|
aa854e7efe | ||
|
|
5536e6b91e | ||
|
|
f37e0aa7b3 | ||
|
|
1ac85d837a | ||
|
|
731c9544b3 | ||
|
|
528f836e71 | ||
|
|
c539fcde8b | ||
|
|
7a08fbd370 | ||
|
|
71e1bc260d | ||
|
|
88df0fecb0 | ||
|
|
8a201022c0 | ||
|
|
3d2abbde72 |
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
type: string
|
||||
default: master
|
||||
stable_date:
|
||||
description: Stable release date in UTC (YYYY-MM-DD). First stable that day is .0, then .1, and so on.
|
||||
description: Enter a UTC date in YYYY-MM-DD format, for example 2026-03-18. Do not enter a version string. The workflow will resolve that date to a stable version such as 2026.318.0, then 2026.318.1 for the next same-day stable.
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
@@ -251,6 +251,7 @@ jobs:
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PUBLISH_REMOTE: origin
|
||||
run: |
|
||||
version="$(git tag --points-at HEAD | grep '^v' | head -1 | sed 's/^v//')"
|
||||
if [ -z "$version" ]; then
|
||||
|
||||
@@ -222,7 +222,8 @@ After at least one good canary exists:
|
||||
3. open `Actions` -> `Release`
|
||||
4. run it with:
|
||||
- `source_ref`: the tested commit SHA or canary tag source commit
|
||||
- `stable_date`: leave blank or set the intended UTC date
|
||||
- `stable_date`: leave blank or set the intended UTC date like `2026-03-18`
|
||||
do not enter a version like `2026.318.0`; the workflow computes that from the date
|
||||
- `dry_run`: `true`
|
||||
5. confirm the dry-run succeeds
|
||||
6. rerun with `dry_run: false`
|
||||
@@ -231,6 +232,11 @@ After at least one good canary exists:
|
||||
9. confirm git tag `vYYYY.MDD.P` exists
|
||||
10. confirm the GitHub Release was created
|
||||
|
||||
Implementation note:
|
||||
|
||||
- the GitHub Actions stable workflow calls `create-github-release.sh` with `PUBLISH_REMOTE=origin`
|
||||
- local maintainer usage can still pass `PUBLISH_REMOTE=public-gh` explicitly when needed
|
||||
|
||||
## 13. Suggested Maintainer Policy
|
||||
|
||||
Use this policy going forward:
|
||||
|
||||
@@ -77,22 +77,31 @@ npx paperclipai@canary onboard --data-dir "$(mktemp -d /tmp/paperclip-canary.XXX
|
||||
|
||||
Use [`.github/workflows/release.yml`](../.github/workflows/release.yml) from the Actions tab with the manual `workflow_dispatch` inputs.
|
||||
|
||||
[Run the action here](https://github.com/paperclipai/paperclip/actions/workflows/release.yml)
|
||||
|
||||
Inputs:
|
||||
|
||||
- `source_ref`
|
||||
- commit SHA, branch, or tag
|
||||
- `stable_date`
|
||||
- optional UTC date override in `YYYY-MM-DD`
|
||||
- enter a date like `2026-03-18`, not a version like `2026.318.0`
|
||||
- `dry_run`
|
||||
- preview only when true
|
||||
|
||||
Before running stable:
|
||||
|
||||
1. pick the canary commit or tag you trust
|
||||
2. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
|
||||
2. resolve the target stable version with `./scripts/release.sh stable --date "$(date +%F)" --print-version`
|
||||
3. create or update `releases/vYYYY.MDD.P.md` on that source ref
|
||||
4. run the stable workflow from that source ref
|
||||
|
||||
Example:
|
||||
|
||||
- `source_ref`: `master`
|
||||
- `stable_date`: `2026-03-18`
|
||||
- resulting stable version: `2026.318.0`
|
||||
|
||||
The workflow:
|
||||
|
||||
- re-verifies the exact source ref
|
||||
@@ -122,7 +131,7 @@ This is mainly for emergency/manual use. The normal path is the GitHub workflow.
|
||||
```bash
|
||||
./scripts/release.sh stable
|
||||
git push public-gh refs/tags/vYYYY.MDD.P
|
||||
./scripts/create-github-release.sh YYYY.MDD.P
|
||||
PUBLISH_REMOTE=public-gh ./scripts/create-github-release.sh YYYY.MDD.P
|
||||
```
|
||||
|
||||
## Stable Changelog Workflow
|
||||
@@ -217,7 +226,7 @@ This is a partial release. npm is already live.
|
||||
Do this immediately:
|
||||
|
||||
1. push the missing tag
|
||||
2. rerun `./scripts/create-github-release.sh YYYY.MDD.P`
|
||||
2. rerun `PUBLISH_REMOTE=public-gh ./scripts/create-github-release.sh YYYY.MDD.P`
|
||||
3. verify the GitHub Release notes point at `releases/vYYYY.MDD.P.md`
|
||||
|
||||
Do not republish the same version.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -605,6 +605,7 @@ class GatewayWsClient {
|
||||
this.resolveChallenge = resolve;
|
||||
this.rejectChallenge = reject;
|
||||
});
|
||||
this.challengePromise.catch(() => {});
|
||||
}
|
||||
|
||||
async connect(
|
||||
|
||||
@@ -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<string, string>, 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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -336,6 +337,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
args.push("--tools", "read,bash,edit,write,grep,find,ls");
|
||||
args.push("--session", sessionFile);
|
||||
|
||||
// Add Paperclip skills directory so Pi can load the paperclip skill
|
||||
args.push("--skill", PI_AGENT_SKILLS_DIR);
|
||||
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
|
||||
return args;
|
||||
|
||||
@@ -72,11 +72,22 @@ 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,
|
||||
toolUseId: asString(tr.toolCallId, "unknown"),
|
||||
toolName: asString(tr.toolName),
|
||||
content: contentStr,
|
||||
isError,
|
||||
});
|
||||
@@ -130,14 +141,35 @@ 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);
|
||||
|
||||
// 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<string, unknown>;
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
contentStr = JSON.stringify(result);
|
||||
}
|
||||
|
||||
return [{
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId: toolCallId || "unknown",
|
||||
toolName,
|
||||
content: contentStr,
|
||||
isError,
|
||||
}];
|
||||
|
||||
@@ -19,7 +19,8 @@ Examples:
|
||||
|
||||
Notes:
|
||||
- Run this after pushing the stable tag.
|
||||
- Defaults to git remote public-gh.
|
||||
- Resolves the git remote automatically.
|
||||
- In GitHub Actions, origin is used explicitly.
|
||||
- If the release already exists, this script updates its title and notes.
|
||||
EOF
|
||||
}
|
||||
@@ -54,7 +55,9 @@ fi
|
||||
|
||||
tag="v$version"
|
||||
notes_file="$REPO_ROOT/releases/${tag}.md"
|
||||
PUBLISH_REMOTE="${PUBLISH_REMOTE:-public-gh}"
|
||||
if [ "${GITHUB_ACTIONS:-}" = "true" ] && [ -z "${PUBLISH_REMOTE:-}" ] && git_remote_exists origin; then
|
||||
PUBLISH_REMOTE=origin
|
||||
fi
|
||||
PUBLISH_REMOTE="$(resolve_release_remote)"
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh CLI is required to create GitHub releases." >&2
|
||||
|
||||
@@ -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 issue descriptions or comments, follow the ticket-linking rule in **Comment Style** below.
|
||||
|
||||
```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: `/<prefix>/issues/<issue-identifier>` (e.g., `/PAP/issues/PAP-224`)
|
||||
@@ -171,7 +180,8 @@ 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)
|
||||
```
|
||||
|
||||
## Planning (Required when planning requested)
|
||||
|
||||
@@ -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 }[] = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -57,8 +57,8 @@ export function AuthPage() {
|
||||
|
||||
const canSubmit =
|
||||
email.trim().length > 0 &&
|
||||
password.trim().length >= 8 &&
|
||||
(mode === "sign_in" || name.trim().length > 0);
|
||||
password.trim().length > 0 &&
|
||||
(mode === "sign_in" || (name.trim().length > 0 && password.trim().length >= 8));
|
||||
|
||||
if (isSessionLoading) {
|
||||
return (
|
||||
@@ -91,6 +91,11 @@ export function AuthPage() {
|
||||
className="mt-6 space-y-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (mutation.isPending) return;
|
||||
if (!canSubmit) {
|
||||
setError("Please fill in all required fields.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate();
|
||||
}}
|
||||
>
|
||||
@@ -128,7 +133,12 @@ export function AuthPage() {
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
<Button type="submit" disabled={!canSubmit || mutation.isPending} className="w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
aria-disabled={!canSubmit || mutation.isPending}
|
||||
className={`w-full ${!canSubmit && !mutation.isPending ? "opacity-50" : ""}`}
|
||||
>
|
||||
{mutation.isPending
|
||||
? "Working…"
|
||||
: mode === "sign_in"
|
||||
|
||||
Reference in New Issue
Block a user