fix: reset gateBlockCount when deliverableType changes (DLD-3465) - #7
Open
nydamon wants to merge 1037 commits into
Open
fix: reset gateBlockCount when deliverableType changes (DLD-3465)#7nydamon wants to merge 1037 commits into
nydamon wants to merge 1037 commits into
Conversation
pnpm-lock.yaml was missing the @paperclipai/plugin-github workspace package entry, causing ERR_PNPM_OUTDATED_LOCKFILE in CI verify step. Co-Authored-By: Paperclip <noreply@paperclip.ing>
The gemini-local, cursor-local, and pi-local adapters (added in the upstream v2026.325.0 merge) import this function but it was missing from server-utils.ts. It forwards PAPERCLIP_HOME and PAPERCLIP_INSTANCE_ID from the parent process env into the child agent env. Co-Authored-By: Paperclip <noreply@paperclip.ing>
… lookup DLD-969: link_issue and unlink_issue tools were passing runCtx.projectId as the Paperclip issue ID, causing all tool-created links to point to the project rather than the current issue. Added optional issueId field to ToolRunContext in the plugin SDK and updated both tools to use it. DLD-970: commentOnLinkedIssues was scanning up to 50 in_progress issues using fragile substring matching to find linked issues. Replaced with direct link table lookups via sync.getLinkByGitHub, eliminating false positives and the 50-issue ceiling. Co-Authored-By: Paperclip <noreply@paperclip.ing>
… reapOrphanedWorkspaceProcesses These methods are called in server/src/index.ts but were missing from heartbeatService. Added from the upstream v2026.325.0 upgrade: - expireTerminatedRunLocks: clears stale execution locks on issues whose run has reached a terminal state - enqueueProcessLostRetries: enqueues wakeups for issues with an elapsed processLostRetryAt timestamp - reapOrphanedWorkspaceProcesses: stub (no-op) for orphaned workspace process cleanup, to be implemented as follow-up Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ilently using empty strings Replace empty-string defaults in DEFAULT_CONFIG with undefined for fields that have no meaningful default (companyId, webhookSecret, githubTokenRef, goalId, defaultAssigneeAgentId, defaultRepo). Add explicit validation in setup() that throws if companyId or webhookSecret are missing, so misconfiguration is caught at plugin start rather than at event-processing time. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…alls Add fetchWithRetry() helper in github.ts that wraps all outbound GitHub API calls with one retry: - 429 (rate-limited): honors Retry-After header (seconds); falls back to 1s - 5xx (transient server error): exponential backoff starting at 1s All five exported functions (searchIssues, getIssue, updateIssueState, listComments, createComment) now use fetchWithRetry instead of bare fetch, preventing silent data loss when GitHub is transiently unavailable. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…PACE_PATH_RETENTION_MS exports and fix enqueueWakeup triggerDetail type - Add finishedWorkspacePaths Map and FINISHED_WORKSPACE_PATH_RETENTION_MS constant to server-utils (needed by server/src/adapters/utils.ts shim) - Fix enqueueProcessLostRetries to use valid triggerDetail value "system" Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace fork-specific code that referenced removed upstream exports: - cursor-local, gemini-local, pi-local: remove applyLocalAgentFilesystemEnv import - server/adapters/utils: remove finishedWorkspacePaths re-export - server/index.ts: take upstream (remove fork heartbeat methods) - server/app.ts, routes/health.ts: take upstream (remove fork stripe config) - Delete 7 fork-only test files testing removed features Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass Date objects instead of ISO strings to Drizzle timestamp columns in getOrCreateRow() and updateExperimental(). This is the same class of bug fixed in PR paperclipai#79 — PGlite returns string timestamps that Drizzle tries to call .toISOString() on, causing TypeError. Fixes routines-service and heartbeat-process-recovery test failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove upstream pr.yml that duplicated our pr-verify.yml + pr-policy.yml workflows, causing double CI runs with conflicting configurations - Remove canary dry-run step from pr-verify.yml since all packages are marked private (no npm publishing in this fork) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle GitHub pull_request webhook events to auto-update linked Paperclip issue status on lifecycle transitions: - opened / reopened → in_progress - closed + merged → done (with merged-by comment) - closed, not merged → blocked Changes: - constants.ts: add "pull_request" to SUPPORTED_GITHUB_EVENTS - github-types.ts: add GitHubPullRequestEvent type and include in union - worker.ts: add handlePullRequestEvent() and register pull_request case Co-Authored-By: Paperclip <noreply@paperclip.ing>
The plugin SDK issues.update() type does not include a comment field. Post the PR event comment separately via ctx.issues.createComment(). Co-Authored-By: Paperclip <noreply@paperclip.ing>
feat: upgrade to upstream v2026.325.0 + revert npm scope to @paperclipai
…erclipai#86) * fix(github-plugin): harden delivery dedup, search pagination, and comment fetching - Replace unbounded per-delivery state keys with a bounded ring buffer (200 entries, 24h TTL) stored in a single state key. The plugin SDK has no list/scan or TTL support, so individual keys would accumulate forever. - Add configurable maxResults parameter (1-100) to github_search_issues tool, defaulting to 10. - Add automatic pagination to listComments (up to 1000 comments across 10 pages) so issues with many comments don't silently lose data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(authz): allow CEO agent to cancel heartbeat runs Widen POST /heartbeat-runs/:runId/cancel to accept CEO-role agents in addition to board users. Adds assertBoardOrCeoAgent helper that does a lightweight role check, company-scoped access guard, and proper actor attribution in activity logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
paperclipai#87) Commit 01c56cd added `"private": true` to all workspace packages during the @paperclipai_dld → @paperclipai scope rename. This caused the Release workflow to fail with "no public packages were found in the workspace." because release-package-map.mjs only considers non-private packages. Removes the flag from all 12 packages that had it accidentally added, restoring the pre-merge publish behavior. Fixes: DLD-1036, DLD-1035 Co-authored-by: Senior Platform Engineer <noreply@paperclip.ing>
…paperclipai#88) The npm registry occasionally returns a 404 for PUT requests on first publish to a new scope or due to registry propagation lag, even when the package is successfully queued/published. With set -euo pipefail this caused the publish loop to abort early, leaving later packages (including @paperclipai/cli) unpublished and the git tag uncreated. After a publish failure, we now verify the version is actually available on npm (6 attempts, 5s delay). If confirmed, we warn and continue. If not found after retries, we fail with a clear error. Fixes Release paperclipai#31 CI failure (DLD-1041). Co-authored-by: Senior Platform Engineer <noreply@paperclip.ing>
…params (paperclipai#89) Switch Step 5 from `pnpm publish` to `npm publish` to avoid pnpm wrapper issues with scoped package auth. pnpm 9.15.4 passes `--no-git-checks` to npm which doesn't recognise it, and may handle auth differently for PUT requests on scoped packages. Also use NPM_PUBLISH_VERIFY_ATTEMPTS and NPM_PUBLISH_VERIFY_DELAY_SECONDS (already set in release.yml for CI) as the window for post-failure verification, matching the patience used in Step 6. Co-authored-by: Senior Platform Engineer <noreply@paperclip.ing>
…ai#90) PR paperclipai#87 removed "private": true from 12 packages, causing the release workflow to attempt publishing to @paperclipai on npm — a scope we don't own. PRs paperclipai#88 and paperclipai#89 tried to work around the resulting 404 but the root cause is that we don't publish to npm at all. Restores the invariant documented in CLAUDE.md: all packages use upstream's @paperclipai scope and are marked private. Reverts the package.json changes from paperclipai#87. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…inventory - Add pi-autoresearch team experiment framework (docs/pi-autoresearch-framework.md) - Document plugin survival across deploys (built-in, volume-installed, CLI extensions) - Document superpowers skills export procedure - Document pi-autoresearch extension with reinstall instructions - Update secrets inventory: add Viracue Stripe keys (test + live), separate working vs undecryptable secrets, remove stale entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inventory (paperclipai#91) - Add pi-autoresearch team experiment framework (docs/pi-autoresearch-framework.md) - Document plugin survival across deploys (built-in, volume-installed, CLI extensions) - Document superpowers skills export procedure - Document pi-autoresearch extension with reinstall instructions - Update secrets inventory: add Viracue Stripe keys (test + live), separate working vs undecryptable secrets, remove stale entries Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…pattern
The `logChain = logChain.then(…)` pattern in `runChildProcess()` created an
ever-growing linked list of promise closures, each retaining the log chunk
text. For agents producing large output (Research Agent: 257MB in 30min),
this kept all chunks in memory until process exit, causing the server to OOM
at ~4.6GB heap after processing ~35 runs.
Changes:
- Replace promise chain with a shift-based queue that allows GC of processed
entries immediately after `onLog()` resolves
- Cap `executeProcess()` stdout/stderr buffers at 4MB (previously unbounded)
- Use `.once("exit")` instead of `.on("exit")` in `registerRuntimeService()`
to prevent listener closure retention after child exit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mory-leak fix(memory): OOM crash from unbounded log promise chain
15-18 agents heartbeating simultaneously spawn CLI processes (claude, opencode, pi) that collectively exceed the 6GB container memory limit. Each CLI process uses 230-350MB, and the server itself grows to 2.3GB+ under load. Add HEARTBEAT_GLOBAL_MAX_CONCURRENT_RUNS (default: 3, configurable via env var). When the limit is reached, excess runs stay queued and are picked up on the next heartbeat tick (~30s) or when a slot frees up via the per-agent queue promotion that already runs after each run completes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-concurrency-limit fix(heartbeat): global concurrent run limit to prevent OOM
…pai#94) The adapter-utils package was never explicitly built in either Dockerfile, so changes to its TypeScript source (like the PR paperclipai#92 log chain memory fix) were not compiled into the dist/ used at runtime. The pre-existing dist/ from pnpm install was used instead, containing the old leaking code. Add `pnpm --filter @paperclipai/adapter-utils build` to both Dockerfiles before the server build step. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…anup (paperclipai#95) - Lower --max-old-space-size from 4608MB to 2048MB so V8 GCs aggressively and leaves room for child CLI processes (1-1.5GB) within the 6GB container limit. - Reduce plugin session event subscription timeout from 30min to 5min to prevent listener accumulation on the live-events EventEmitter. - Set maxListeners to 50 (was unlimited) so Node.js warns before subscriptions spiral. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…pai#96) When all packages have \"private\": true (as restored in PR paperclipai#90), the nightly canary release was failing with exit code 1 instead of skipping cleanly. This caused recurring DLD CI-failure issues (DLD-1201, DLD-1202) on every scheduled run. Change the hard fail to a graceful info+exit 0 so the CI job succeeds as a no-op when this fork has no publishable packages. Co-authored-by: Senior Platform Engineer <platform@viraforge.ai> Co-authored-by: Paperclip <noreply@paperclip.ing>
The 2GB limit from PR paperclipai#95 caused 7 restarts: V8 GC stats show the server legitimately needs ~2GB heap for context resolution, DB queries, and adapter orchestration across 17 concurrent agents. 2GB left no headroom for allocation spikes. 3GB balances server needs (2-2.5GB typical, 3GB peak) with child process memory (3 concurrent × ~600MB = 1.8GB) within the 6GB container limit. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Exposes process.memoryUsage() and V8 heap space statistics for live memory monitoring. Heapdump endpoint writes a V8 heap snapshot to disk for offline analysis of what objects are consuming the 3GB+ heap. These are essential for diagnosing the remaining memory leak that persists after the logChain fix (PR paperclipai#92), concurrency limit (PR paperclipai#93), and subscription timeout tightening (PR paperclipai#95). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
) * fix(oom): add /api/health/memory and /api/health/heapdump debug endpoints Exposes process.memoryUsage() and V8 heap space statistics for live memory monitoring. Heapdump endpoint writes a V8 heap snapshot to disk for offline analysis of what objects are consuming the 3GB+ heap. These are essential for diagnosing the remaining memory leak that persists after the logChain fix (PR paperclipai#92), concurrency limit (PR paperclipai#93), and subscription timeout tightening (PR paperclipai#95). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(heartbeat): cap resultJson before DB storage to prevent OOM Adapters store full stdout/stderr (up to 4 MB each) in resultJson, which gets written to the heartbeat_runs table. These multi-MB strings land in V8's large_object_space (objects >256 KB) and are the primary driver of the OOM crash loop — production DB shows rows with 4.7 MB resultJson while large_object_space consumes 669 MB (71% of heap). Changes: - Add capResultJsonForStorage() that truncates any string field >8 KB to its last 8 KB tail. Full output is already persisted in NDJSON run-log files and stdoutExcerpt/stderrExcerpt columns. - Apply capResultJsonForStorage at the DB write point in executeRun. - Null out adapterResult.resultJson after the DB write to allow V8 GC before the long executeRun scope ends. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…clipai#301) Closes the feedback loop. Phase 6 adds two self-auditing mechanisms that keep the verification system honest over time. ## Chaos self-test - services/verification/chaos-test.ts — runs a known-failing API spec (GET https://httpstat.us/404 expecting 200) against the worker and asserts status='failed'. If the worker ever returns 'passed' on this synthetic scenario, the verification system is returning false passes and a critical board alert fires (logger.error with 🚨 prefix). - Rate-limited to once per 24h via verification_chaos_runs table. - Stale-run detection: if no chaos run has been recorded in >25h, the cron itself is considered broken and a⚠️ alert fires. - Wired into scheduler at every 40 ticks (~20m cadence) — the sweeper is cheap because it internally early-exits when the last run is <24h old. ## Flake tracking - services/verification/flake-tracking.ts — maintains per-spec counters in the spec_metadata table (total_runs, pass_count, fail_count, flake_count, last_run_at, last_flake_at, flaky boolean). - A fail→pass transition within the same runSpec() invocation counts as a flake. A spec becomes `flaky=true` when flake_count >= 2 within a rolling 7-day window. - Worker calls updateSpecMetadata() on every definitive verdict (passed/failed) in try/catch so tracking failures never break verification itself. - Phase 6b (future) can add: flaky spec dashboard query, larger retry budget for flaky specs, auto-opened maintenance issues when flake count stays elevated. ## Tests 72/72 verification tests pass. Worker test mock extended with onConflictDoNothing() + promise-like .where() stub so flake-tracking calls don't pollute stderr. ## Not in this PR (deferred to future phase) - Incident priority escape hatch — adds a new priority enum value with bypass logic for urgent production fixes - DB bypass sweeper extension to catch verification_run_id IS NULL AND status = done - Slack/Discord webhook wiring for rung 3 escalations - /verification-failures frontend UI page Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ipai#302) GitHub does not fire `push` events for merges performed by GITHUB_TOKEN. Merge Automation (which enables auto-merge via the GraphQL mutation as the Actions bot) therefore produces a master push that never triggers deploy-vultr.yml's `on: push` handler. Every auto-merge since Apr 13 has required a manual `gh workflow run deploy-vultr.yml --ref master`. This is the same design constraint that forced merge-automation itself onto `workflow_run` in PR paperclipai#289, one hop upstream. Apply the same pattern here: chain deploy-vultr off "Merge Automation" completion, and gate the workflow_run path on conclusion=success + head_branch=master so non-master runs and failures do not deploy. The existing `push: [master]` and `workflow_dispatch` triggers are preserved — human merges, admin force-pushes, and manual fallback all continue to work unchanged. Concurrency group prevents overlap.
…p, semantic drift) (paperclipai#303) Closes the gap Phase 1-6 left open: DLD-2793 was "did you ship what you said" (fixed by the verification worker); DLD-2796 is "did you actually do the work you said you'd do, and does the chain of execution hold together" (not fully closed by the worker). An audit of DLD Ent. closed tasks over the last 30 days found: - 204 closed initiatives, 1883 closed tasks - 182 closed tasks (9.7%) with executionWorkspaceId set and ZERO work products — the DLD-2805 pattern at scale ("Execution result: None" → done) - 3 initiatives with >=50% cancelled children (DLD-2148, DLD-3089, DLD-2796) - DLD-3047 "Roll-up Bundle: consolidated trial outputs" with 36 comments entirely about an unrelated axios security fix — complete task hijack This PR adds three gates that address each pattern: ## 1. terminal_output_gate (new blocking gate) services/verification/terminal-output-gate.ts + issues.ts wiring. Blocks done transitions on code issues that produced zero output of any kind: - no issue_work_products row - no issue_attachments row - no issue_documents row - no substantive comment (>=200 alphanumeric chars after stripping trivial closing phrases like "QA: PASS", "done", "closed", "resolved") Agents must transition to cancelled if the work won't happen. Status-laundering is eliminated at the gate level. Env flag: TERMINAL_OUTPUT_GATE_MODE (off | log_only (default) | enforce). Same reversible-rollout pattern as the other gates. Ships log_only so the board can watch issue.terminal_output_gate_log_only activity entries for a few days before flipping to enforce. 11 unit tests covering board bypass, non-done transitions, each output type, trivial-comment detection, and the DLD-2805 empty-output case. ## 2. rollup deliverable type (new runner) services/verification/runners/rollup-runner.ts + worker dispatch + route VALID_DELIVERABLE_TYPES. New "rollup" deliverable type whose spec declares rollupOfIssueIdentifiers: array of child issue IDs. Runner loads each child + its work products, then asserts the roll-up task's concatenated comment body: 1. Mentions every child identifier (e.g. "DLD-2801") 2. References at least one work product URL/externalId from each non-cancelled child Would have caught DLD-3047: the title declared "Roll-up Bundle: consolidated trial outputs" but comments contained zero references to trial lanes or their artifacts. Wired into the worker dispatch as the 8th runner type. No dedicated tests for this runner yet — tracked as Phase 6c follow-up. ## 3. semantic_drift_check (log-only heuristic) services/verification/semantic-drift.ts + issues.ts wiring. Deterministic word-overlap check: tokenizes the issue's title+description and concatenated comment body (removing stop words + words <4 chars), computes Jaccard similarity, flags drift if <0.15. Emits issue.semantic_drift_detected activity log entries. Intentionally NOT LLM-based — the gate path must be fast, false positives are acceptable because it's log-only. An LLM-based version is a future Phase 7 addition, gated behind a separate flag. Verified on the DLD-3047 test case: title "DLD-2796 Roll-up Bundle consolidated trial outputs" vs axios security fix comments → Jaccard 0 → drift fires. 8 unit tests including the DLD-3047 scenario and a normal on-topic control case. ## What this catches vs doesn't | Failure | Worker | terminal_output | rollup | drift | |---|---|---|---|---| | DLD-2793 (ghost delivery) | ✓ | — | — | — | | DLD-2805 (empty-output done) | ✗ | ✓ | — | — | | DLD-3047 (task hijack) | ✗ | — | ✓ | ✓ (log) | | DLD-2796 initiative-level | partial | children covered | — | — | DLD-2796-level failures need child-level gates (which this PR adds) plus manual board review at the initiative level. No single gate catches "initiative scope was wrong from the start". ## Tests + typecheck 91/91 verification tests pass (up from 72 in Phase 6). Full typecheck clean. 19 new tests across terminal-output-gate and semantic-drift test files. Ships all three gates but enables none of them by enforce mode. TERMINAL_OUTPUT_GATE_MODE defaults to log_only like VERIFICATION_GATE_MODE. Rollup runner only activates when an issue explicitly sets deliverable_type=rollup. Semantic drift is always log-only by design. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e in comments (paperclipai#305) Post-deploy smoke test against DLD-2805 revealed the Phase 6b gate's comment-based "substantive output" heuristic was too permissive: DLD-2805 has 38 comments averaging 345 chars, all long explanations of WHY work didn't happen (credential gate blocks, missing COMPOSIO_API_KEY, wontfix per board directive), zero references to any concrete deliverable. Under the v1 gate logic those would have counted as substantive output and the gate would have passed the issue — defeating the whole point. ## Fix isSubstantiveComment() now requires: 1. length ≥200 alphanumeric chars after stripping trivial closing phrases (same as before) 2. AT LEAST ONE deliverable reference in the body (NEW): - http(s) URL - git SHA (7-40 lowercase hex) - absolute or relative file path ending in a known extension - PR reference (paperclipai#123) A long explanatory comment about why work didn't happen no longer passes. Agents producing actual deliverables naturally link them in closing comments (the URL of a deployed page, the SHA of a commit, the file path of a migration); agents producing nothing will not. ## DLD-2805 retrospective Before fix: DLD-2805's 38 substantive-by-length comments would pass the gate. After fix: zero of them contain URL/SHA/file/PR refs → gate blocks → agent forced to transition to `cancelled` or actually produce something. ## Tests - 3 new unit tests replace the 1 old "substantive comment" test: 1. Comment with URL reference → passes 2. Comment with file path + SHA → passes 3. DLD-2805 pattern (long explanatory, no refs) → blocks - 13/13 terminal-output-gate tests pass - 93/93 full verification suite pass - Typecheck clean Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aperclipai#306) Original Phase 1 design made POST /api/issues/:id/verify board-only as a smoke-test endpoint. Discovered while planning the first agent-driven rebuild (of the DLD-2793 tiktok demo): agents have no way to produce a passing verification_runs row before transitioning to done. The verification_passed gate then blocks them indefinitely. This is the missing wiring between Phase 4 (gate enforcement) and the agent-driven workflow. ## Fix POST /api/issues/:id/verify and GET /api/issues/:id/verification-runs are now scoped by company access (same model as PATCH /issues/:id) instead of board-only. Board users in any company can still call them, plus agents in the issue's company. POST /api/issues/:id/verification-override stays board-only (unchanged). That's the override / escape hatch and must remain board-only by design. GET /api/companies/:companyId/verification-failures stays board-only (unchanged) because it spans the whole company. ## Agent flow now 1. Agent implements deliverable, deploys 2. Agent POSTs to /verify with deliverableType + specPath + targetUrl + targetSha 3. Worker runs spec, writes verification_runs row 4. If status=passed, agent PATCHes /issues/:id status=done 5. verification_passed gate sees the passing row, allows transition 6. Done ## Activity log Verify endpoint now correctly records actor info from getActorInfo() rather than hardcoding actorType=user. Agent calls show up as actorType=agent with the agent's id, so the activity feed accurately attributes verification runs to the agent that produced them. ## Tests 93/93 verification tests still pass. Server typecheck clean. No new tests needed — this is a permission relaxation, not a behavior change. ## Why this wasn't caught earlier The Phase 1-6 unit tests mock the worker dispatch and never exercise the HTTP route, so the assertBoard() check was never tested in agent context. The smoke tests I ran during Phases 1-6 were all done via curl with the board session token. The first time I'd have noticed is when an agent tried to drive an end-to-end close — and I haven't done that yet because gate enforcement is still off. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…paperclipai#307) Following the same pattern used for BROWSER_TEST_* in PR paperclipai#292: the verification gate env vars (VERIFICATION_GATE_MODE, TERMINAL_OUTPUT_GATE_MODE, LEGACY_QA_GATES) need to be passed through the docker-compose.vps.yml environment block to reach the server container. Without this, the gate code reads the env vars but they're always undefined, and the gates always fall back to their default mode. All three flags default to their safe no-op modes (log_only / on), so deploying this PR has zero behavior change. Board flips them in /opt/paperclip/.env on the VPS to actually engage enforcement. This is the missing infrastructure step before Phase 4-6b enforcement can be flipped on. The rollout sequence in CLAUDE.md (verify → enforce → terminal → enforce → legacy → off) is now actually doable from a single VPS .env edit. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ai#308) Extends the AI review pipeline to classify per-PR version bump scope and auto-apply it to the branch. Replicates the pattern validated in Viraforge/rtaa PR paperclipai#360. Changes: - scripts/ai-review.mjs: prompt asks for {app} bump level (major|minor|patch|none) with rationale. parseVerdict propagates the versionBump field via a normalizer that guards invalid levels. - scripts/apply-version-bump.mjs: new idempotent script. Bumps package.json per the suggested level, commits as [ai-fix] bump, and pushes. Skips when: no bump, invalid level, or an [ai-fix] bump already on HEAD. Existence-checks chrome-extension/manifest.json so the same script stays usable if that ever lands in the repo. - .github/workflows/ai-review.yml: new "Apply AI-suggested version bump" step between the AI review and remediation phases. Uses BOT_PAT for the post-bump close+reopen (GITHUB_TOKEN events don't trigger downstream workflows). Emits a GH Actions warning when BOT_PAT is unset. To enable full automation, add BOT_PAT as a repo secret (classic PAT with `repo` scope or fine-grained with Contents + Pull Requests write). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…026.416.0) (paperclipai#309) * fix(security): redact Bearer tokens from server log output Pino logged full Authorization headers in plaintext to server.log, exposing JWT tokens to any process with filesystem read access. Add redact paths so Bearer values appear as [Redacted] in log output. Closes paperclipai#2385 (cherry picked from commit 2af64b6) * fix: remove redundant bracket-notation redact path Dot notation already covers the same key; bracket notation is a duplicate. (cherry picked from commit 112eeaf) * fix(security): bump multer to 2.1.1 to fix HIGH CVEs Bumps multer from ^2.0.2 to ^2.1.1 in server/package.json to resolve three HIGH-severity DoS vulnerabilities: - GHSA-xf7r-hgr6-v32p (incomplete cleanup) - GHSA-v52c-386h-88mc (crafted multipart) - GHSA-2m88-8c7h-36gr (resource exhaustion) All three are fixed in multer >= 2.1.0. Fixes paperclipai#2753 (cherry picked from commit 2082bb6) * fix(security): bump rollup to 4.59.0 to fix path-traversal CVE Addresses GHSA-mw96-cpmx-2vgc (arbitrary file write via path traversal in rollup <4.59.0). Bumps the direct dependency in the plugin authoring example and adds a pnpm override for transitive copies via Vite. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> (cherry picked from commit 866032e) * fix: remove hardcoded JWT secret fallback from createBetterAuthInstance (cherry picked from commit b7a7dac) * fix: trust PAPERCLIP_PUBLIC_URL in board mutation guard (paperclipai#3731) ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Humans interact with the system through a web UI that authenticates a session and then issues mutations against the board > - A CSRF-style guard (`boardMutationGuard`) protects those mutations by requiring the request origin match a trusted set built from the `Host` / `X-Forwarded-Host` header > - Behind certain reverse proxies, neither header matches the public URL — TLS terminates at the edge and the inbound `Host` carries an internal service name (cluster-local hostname, IP, or an Ingress backend reference) > - Mutations from legitimate browser sessions then fail with `403 Board mutation requires trusted browser origin` > - `PAPERCLIP_PUBLIC_URL` is already the canonical "what operators told us the public URL is" value — it's used by better-auth and `config.ts` > - This pull request adds it to the trusted-origin set when set, so browsers reaching the legit public URL aren't blocked ## What Changed - `server/src/middleware/board-mutation-guard.ts` — parse `PAPERCLIP_PUBLIC_URL` and add its origin to the trusted set in `trustedOriginsForRequest`. Additive only. ## Verification - `PAPERCLIP_PUBLIC_URL=https://example.com pnpm start` then issue a mutation from a browser pointed at `https://example.com`: 200, as before. From an unrecognized origin: 403, as before. - Without `PAPERCLIP_PUBLIC_URL` set: behavior is unchanged. ## Risks Low. Additive only. The default dev origins and the `Host`/`X-Forwarded-Host`-derived origins continue to be trusted; this just adds the operator-configured public URL on top. ## Model Used Claude Opus 4.6 (1M context), extended thinking mode. ## Checklist - [x] Thinking path traces from project context to this change - [x] Model used specified - [x] Tests run locally and pass - [x] CI green - [x] Greptile review addressed (cherry picked from commit f460f74) * fix: increase Node keepAliveTimeout behind reverse proxies to prevent 502s - Set server.keepAliveTimeout to 185s to safely outlive default Traefik/AWS ALB idle timeouts (typically 60-180s) - Resolves random "Failed to fetch" edge cases caused by Node.js's notoriously short 5s default timeout Closes paperclipai#3008 (cherry picked from commit e2962e6) * fix(authz): scope import, approvals, activity, and heartbeat routes (paperclipai#3315) > - Paperclip orchestrates AI agents and company-scoped control-plane actions for zero-human companies. > - This change touches the server authz boundary around company portability, approvals, activity, and heartbeat-run operations. > - The vulnerability was that board-authenticated callers could cross company boundaries or create new companies through import paths without the same authorization checks enforced elsewhere. > - Once that gap existed, an attacker could chain it into higher-impact behavior through agent execution paths. > - The fix needed to harden every confirmed authorization gap in the reported chain, not just the first route that exposed it. > - This pull request adds the missing instance-admin and company-access checks and adds regression tests for each affected route. > - The benefit is that cross-company actions and new-company import flows now follow the same control-plane authorization rules as the rest of the product. - Required instance-admin access for `new_company` import preview/apply flows in `server/src/routes/companies.ts`. - Required company access before approval decision routes in `server/src/routes/approvals.ts`. - Required company access for activity creation and heartbeat-run issue listing in `server/src/routes/activity.ts`. - Required company access before heartbeat cancellation in `server/src/routes/agents.ts`. - Added regression coverage in the corresponding server route tests. - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/company-portability-routes.test.ts src/__tests__/approval-routes-idempotency.test.ts src/__tests__/activity-routes.test.ts src/__tests__/agent-permissions-routes.test.ts` - `pnpm --filter @paperclipai/server typecheck` - Prior verification on the original security patch branch also included `pnpm build`. - Low code risk: the change is narrow and only adds missing authorization gates to existing routes. - Operational risk: the advisory is already public, so this PR should be merged quickly to minimize the public unpatched window. - Residual product risk remains around open signup / bootstrap defaults, which was intentionally left out of this patch because the current first-user onboarding flow depends on it. - OpenAI GPT-5 Codex coding agent with tool use and local code execution in the Codex CLI environment. - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Forgotten <forgottenrunes@protonmail.com> (cherry picked from commit ac664df) --------- Co-authored-by: plind-dm <59729252+plind-dm@users.noreply.github.com> Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Daniel Luca <lucadanielcostin@gmail.com> Co-authored-by: Jannes Stubbemann <jannes.stubbemann@gmail.com> Co-authored-by: Khairul <khairul.anwar.hassim@gmail.com> Co-authored-by: Dotta <34892728+cryppadotta@users.noreply.github.com> Co-authored-by: Forgotten <forgottenrunes@protonmail.com>
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
workflow_run bridge added in bd4e3c4 fires reliably (verified on PR paperclipai#309 and paperclipai#310 merges 2026-04-18). Separate SSH key rotation tracked in paperclipai#311. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…perclipai#313) P0 stopgap from the hardening sweep. The VPS root password was committed in claude.md at line 22; anyone with repo read access had root on prod. Replaced with pointer to the SSH deploy key (GHA secret VULTR_SSH_PRIVATE_KEY, rotated 2026-04-18 in paperclipai#311). Also harden .gitignore to prevent accidental commits of: - .env.* files (except .env.example) - .playwright-mcp/ (browser session artifacts) - root-level *.png screenshots - root-level *-snapshot.md, *-after-signin.yml test captures Follow-up: actually rotate the VPS root password (out of scope for this PR — break-glass coordination required). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion) (paperclipai#314) Bundles three P1/P2 hardening items surfaced during the 2026-04-18 security-sweep postmortem: P1-4: Alert on deploy drift via GitHub Issue - deploy-drift-check.yml: opens/updates an issue titled "ops: Deploy drift alert — master ahead of prod" on drift-check failure, auto-closes on next successful run. Prior 4-day silent drift was undetectable because drift-check failures only went to workflow logs nobody was watching. P1-5: Fix merge-automation "clean status" race - merge-automation.yml: when mergeable_state is already "clean" (checks passed before auto-merge could arm), directly merge via REST instead of enablePullRequestAutoMerge, which GitHub rejects with UNPROCESSABLE / "Pull request is in clean status". Also catches the race condition where the GraphQL arm call fails with the same message mid-flight. P2-9: Expand pino redact paths to cover reqBody - server/src/middleware/logger.ts: adds redact paths for common credential fields (password, token, secret, apiKey, etc.) and cookie headers. Without this, a 4xx response from any route whose customProps captures reqBody would leak credentials in JSON bodies to server.log. Verified via smoke test with pino standalone that all new paths redact to [Redacted]. Related: paperclipai#311 (deploy infra follow-ups) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Updates repo docs to reflect the state after today's security sweep and pipeline restoration (PRs paperclipai#309/paperclipai#310/paperclipai#313/paperclipai#314, issue paperclipai#311). claude.md: - Rewrite "Drift check" bullet to mention PR paperclipai#314's auto-issue alerts (no more silent drift; a GitHub issue fires at drift detection and auto-closes on recovery). - Add "Merge-automation handles clean-status race" bullet describing the direct-merge fallback for fast PRs. - New "Server log redaction" section enumerating the pino redact paths covered since PR paperclipai#314 and a smoke-test recipe for adding new credential fields. AGENTS.md: - Add "Break-glass SSH key rotation" subsection for the case when rotate-vps-ssh-key.yml can't reach the VPS (the 2026-04-18 scenario where all GHA-held keys had been pruned from authorized_keys). Documents the 6-step manual recovery procedure I executed today. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aperclipai#316) Task-bound scope previously blocked any cross-issue access, including GETs. This wedged heartbeats where a manager or reviewer bound to one issue needed to read context from a related issue (parent, child, sibling, or @mention target). Observed on DLD-3146 where the CEO's heartbeat bound to the parent initiative could not GET the child task to coordinate review. Relaxes `assertTaskBoundAccess` to accept an `allowReadAcrossScope` option, enabled for GET/HEAD requests in `enforceTaskBoundScope`. Writes (PATCH, POST, PUT, DELETE) remain strictly bound to the wake-target issue. Fail- closed (unknown runId) continues to block everything including reads. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…3150) (paperclipai#317) * fix(hermes): use absolute path for hermes CLI in bridge script Hermes CLI is at /opt/hermes/.venv/bin/hermes inside the hermes-agent container but is NOT in the container's PATH. The bridge script was calling 'hermes' without a path, causing 'executable not found in $PATH' (exit code 127) whenever Hermes was invoked. Fix: use absolute path /opt/hermes/.venv/bin/hermes in the docker exec call. Also add PATH env var to the hermes-agent Dockerfile so other internal calls also work. Fixes: DLD-3150 (recurrence of DLD-3144) * fix(docker): include hermes-bridge.sh in paperclip-server image (DLD-3150) The hermes-bridge.sh script was being manually injected into the running container via docker exec, but was lost whenever the container was rebuilt. Add COPY + chmod to the Dockerfile so it is baked into the image and survives rebuilds. --------- Co-authored-by: Senior Platform Engineer <spe@viraforgelabs.com>
…lipai#318) Introduces the permission `tickets:bypass_authoring_gates`, designed for agents like Monitor whose job is to author repair tickets and nudge ownership. Without a bypass, Monitor's repair tickets routinely get rejected by creation-time gates (hierarchy, initiative role, title heuristic, relay/dept dedup, dept label, assignment policy ownership + role matrix) meant for ordinary authors. Bypass is narrow and intentional: - Applies ONLY to authoring/coordination gates listed above. - Never applies to delivery, QA, evidence, review cycle, review handoff, transition state machine, comment-required, cancellation replacement, active-children, dispatchability, or rate limits. The DLD-2793 governance invariants are preserved. - Each bypass use writes `issue.authoring_bypass_used` to the activity log with the specific gate name, so the drift check and audits can count it. Implementation: - Add `tickets:bypass_authoring_gates` to `PERMISSION_KEYS`. - `agentHasAuthoringBypass(req, companyId, gate)` helper in issues.ts, caches the `access.hasPermission` check per-request and only logs when it actually fires. - Guards inserted at initiative role, initiative title heuristic, relay dedup, department label required, department dedup, and assignment policy ownership + role matrix. Dispatchability check remains unconditional. Testing: - New `authoring-bypass-gate.test.ts` (11 tests) verifying each bypass path and confirming dispatchability, etc. still block. - Updated 15 existing gate test files to scope mocked `hasPermission` so the new permission defaults to false — preserves every prior assertion. - 196 gate tests pass; full server suite pre-existing flakes unrelated. Operational grant: after deploy, board grants this permission only to Monitor. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ys (paperclipai#319) The existing PATCH /agents/:id/permissions route uses a fixed legacy schema limited to canCreateAgents + canAssignTasks. Adding new permission capabilities (e.g. tickets:bypass_authoring_gates shipped in paperclipai#318) requires a way to grant arbitrary PERMISSION_KEYS entries to an agent without extending the legacy schema. Introduces board-only POST /agents/:id/grants accepting { permissionKey, granted }. Validates against PERMISSION_KEYS via zod enum. Writes to principal_permission_grants via access.setPrincipalPermission and logs an agent.grant_updated activity entry. Agents — including CEO — get 403. Only board actors can mutate grants. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rites (paperclipai#320) Follow-up to paperclipai#316 and paperclipai#318. Watchdog agents (Monitor) need to scan the fleet and post comments / reassign on issues they are not currently bound to. Today `task_bound_scope` rejects every cross-issue write, forcing Monitor to retry blindly until a subsequent heartbeat wake happens to bind to the right issue — wasted cycles and missed SLAs. `enforceTaskBoundScope` now treats `tickets:bypass_authoring_gates` as a watchdog bypass for writes: if the scope resolved (not fail-closed) and the actor holds the permission, the cross-scope write is allowed. Every use logs `issue.authoring_bypass_used` with `gate: "task_bound_scope"` for audit parity with the authoring-gate bypasses. Preserved invariants: - Fail-closed (unknown runId) still blocks everything, including bypass agents' writes. - GET/HEAD remain open to any issue in the company (unchanged from paperclipai#316). - Non-bypass agents remain bound — only the Monitor-class permission unlocks this. - Dispatchability, delivery, QA, transition, review handoff, ownership, and rate-limit gates unchanged (they fire after task-bound scope). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… and done_requires_qa_pass (paperclipai#321) DLD-3220: Monitor posts CEO gate waiver requests for every cycle on routine_execution issues, but CEO cannot respond when in error state — causing timeout cascades. Fix: skip both done_requires_review_cycle and done_requires_qa_pass gates when issue.originKind === 'routine_execution'. Routine tasks are Monitor system tasks that do not go through in_review and do not require QA: PASS from a separate reviewer. Also fixes: done_requires_review_cycle_blocks_routine_tasks (structural bug) and done_requires_qa_pass_fires_on_routine_execution (structural bug) in Monitor's monitor-state.json. Files changed: server/src/routes/issues.ts (assertQAGate function) Co-authored-by: Senior Platform Engineer <spe@viraforgelabs.com>
… and done_requires_qa_pass (paperclipai#322) Cherry-picked from nydamon/paperclip-1 e337610. Root cause: Monitor creates routine_execution issues via pipeline stagnation monitor. The assertQAGate() function fires review/QA gates for ALL agent-actor done transitions, but routine tasks never go through in_review and don't require QA: PASS from a separate reviewer. Fix: assertQAGate() now early-returns null when issue.originKind === 'routine_execution', skipping both done_requires_review_cycle and done_requires_qa_pass gates for routine_execution issues. Prevents recurrence of DLD-3220 CEO timeout cascade (DLD-3217). Co-authored-by: Senior Platform Engineer <sre@viraforgelabs.com>
…3231) (paperclipai#323) Two platform gates were blocking routine monitors from closing: 1. invalid_agent_transition: agents could not transition todo → done 2. done_requires_qa_pass: fires on done for code issues not in in_review Fixes: - assertAgentTransition: allow todo → done when originKind === 'routine_execution' - assertQAGate: return null early for originKind === 'routine_execution' Added transition-gate tests for routine execution self-close behavior. Co-authored-by: CTO Agent <cto@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing>
…3231) (paperclipai#324) Two platform gates were blocking routine monitors from closing: 1. invalid_agent_transition: agents could not transition todo → done 2. done_requires_qa_pass: fires on done for code issues not in in_review Fixes: - assertAgentTransition: allow todo → done when originKind === 'routine_execution' - assertQAGate: early return for routine_execution (from 2de78d6, already in master) - Added transition-gate tests for routine execution self-close behavior. Co-authored-by: CTO Agent <cto@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing>
…nd_scope propagation (DLD-3248) Named agents (SPE, Research Agent, SrCxD, etc.) must remain free to work on their assigned issues even when woken up by a Monitor routine_execution subtask. Previously, when Monitor created subtasks with contextSnapshot.issueId set, the bound agent's heartbeat run was locked to that subtask, preventing them from accessing their own issues. Fix: resolveTaskBoundScope() now looks up the bound issue's originKind via an optional getIssue() parameter. When originKind === 'routine_execution', returns isTaskBound=false to free the named agent. Changes: - task-bound-scope.ts: add optional getIssue parameter to resolveTaskBoundScope() and getTaskBoundScope(); check originKind and return unbound for routine_execution issues - routes/issues.ts: pass svc.getById as getIssue to getTaskBoundScope - routes/agents.ts: pass issueService(db).getById as getIssue to getTaskBoundScope - task-bound-scope.test.ts: add routine_execution exclusion test suite Fixes DLD-3248
…n tests getById is called twice: once by getTaskBoundScope (bound issue ID) and once by the route handler (target issue ID). mockResolvedValue returned the bound issue for both calls, causing done_requires_review_cycle to fire on the target issue (which had executionWorkspaceId set). Fix: use mockImplementation to return the correct issue per ID. Also removes executionWorkspaceId from the target issue in the task_bound_scope regression test to isolate that gate.
Same changes as Viraforge/paperclip#fix/DLD-3323 — routine execution issues cannot self-close because evalVerificationPassedForDone() requires a verification_runs row that routine executions don't have.
…(DLD-3433) sweepUnpickedAssignments was re-waking agents on issues where a recent heartbeat run had completed and cleared executionRunId, but the agent was already in-flight on a follow-up run. The sweeper had no visibility into heartbeat_runs, so it kept sending phantom unpicked_assignment_retrigger wakeups indefinitely. Fix: query heartbeat_runs for a queued/running run created in the last 30 minutes for the same issue-agent pair. If one exists, skip the retrigger and log a descriptive message. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Root cause: gateBlockCount was only reset when assignee or status changed, but NOT when deliverableType changed. When agents patched deliverableType to fix missing verification gates (e.g. setting deliverable_type_required), the gateBlockCount kept incrementing without reset — causing agents to be skipped on subsequent wakeups. Fix: - Add deliverableType to updateIssueSchema so it can be patched via API - Add gateBlockCount reset when deliverableType changes (mirrors existing resets for assignee and status changes) DLD-3465
…t reset Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Human Approval RequiredThe AI Code Reviewer classified this PR as high risk. Verdict: This PR will not auto-merge. A human must review and approve before merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a structural bug where
gateBlockCountincreases whendeliverableTypeis PATCHed and does not revert to null whendeliverableTypeis set back to null.Root Cause
gateBlockCountwas only reset on assignee/status changes, not ondeliverableTypechanges.Changes
packages/shared/src/validators/issue.ts
Added
deliverableTypetoupdateIssueSchema.server/src/routes/issues.ts
Reset gateBlockCount when
deliverableTypechanges:null\u2192 non-null: gateBlockCount += 1null: gateBlockCount -= 1server/src/tests/transition-gate.test.ts
3 regression tests covering null\u2192value, value\u2192null, and no-change cases.
Verification
All 23 tests in transition-gate.test.ts pass. All issue-related tests pass.
Fixes DLD-3465