Skip to content

feat(GLA-1114): issue-level approve gate — block approve when no visual asset attached - #5538

Closed
jqueguiner wants to merge 30 commits into
paperclipai:masterfrom
jqueguiner:gla-1114-issue-approve-gate
Closed

feat(GLA-1114): issue-level approve gate — block approve when no visual asset attached#5538
jqueguiner wants to merge 30 commits into
paperclipai:masterfrom
jqueguiner:gla-1114-issue-approve-gate

Conversation

@jqueguiner

Copy link
Copy Markdown

Thinking Path

  • Paperclip orchestrates AI agents and hosts the asset library UI where founders review and approve marketing briefs
  • The asset library has a per-document provenance gate that blocks cloud-AI assets — but no gate at the issue (brief) level
  • Founder policy (added 2026-05-09) mandates every [review-and-ship] brief must have ≥1 image or video asset before it can be marked approved
  • Without an issue-level gate, a founder could approve a text-only brief by clicking Approve on the copy doc, even if no visual exists
  • This PR adds the visual-mandatory check at the issue level: red banner + disabled Approve button when no image/video doc is attached; amber warning + enabled when a [visual-waiver] founder comment overrides; green pass when visual is present
  • The benefit is that the asset library now enforces the visual-mandatory policy structurally, not just by convention

What Changed

  • lib/asset-type.ts: add IssueApprovalGate type, evaluateIssueApprovalGate(), hasVisualAsset(), hasVisualWaiver() — detects image/video docs by extension/format/body heuristics; recognises [visual-waiver] comments as a downgrade from block to warning
  • app/components/IssueApproveBar.tsx: new client component — shows banner (red/amber/none), APPROVE BRIEF button disabled when gate blocks, POSTs to issue-level approve endpoint on click
  • app/components/AssetIssueSummaryView.tsx: accepts gate + paperclipUrl props, renders IssueApproveBar below the description section
  • app/api/issues/[issueId]/approve/route.ts: when docKey is absent (issue-level approve), fetches all docs + comments, runs evaluateIssueApprovalGate, posts approval comment + PATCHes status to todo on pass; existing per-doc path unchanged
  • app/asset/[issueId]/page.tsx: fetches comments server-side (direct Paperclip API call), computes issue gate, passes gate + paperclipUrl to AssetIssueSummaryView
  • app/asset/[issueId]/[docKey]/page.tsx: same gate computation + props for the canonical-slug fallback render path

Verification

  • Open /asset/{text-only-brief} → red banner Cannot approve — no image/video asset attached… + APPROVE BRIEF button disabled
  • Add an image IssueDocument with local-AI provenance → reload → banner clears, button enabled
  • Click APPROVE BRIEF → comment posted + issue status → todo
  • Founder posts [visual-waiver] comment on issue → reload → amber warning banner, button enabled
  • Per-doc approve at /asset/{issueId}/{docKey} still works unchanged (docKey present → existing provenance gate path)
  • npm run build passes clean (verified locally)

Risks

  • Comments fetch hits Paperclip /api/issues/{id}/comments — if that endpoint doesn't exist or returns unexpected shape, fetchComments returns [] (safe default: gate evaluates as no-waiver, blocks without visual). No hard failure.
  • evaluateIssueApprovalGate is called on every page load for issue-level routes; it's a pure local function with two O(n) array scans — negligible cost.
  • Existing per-doc approve route is unchanged for docKey-present calls; the new branch only runs when docKey is absent.

Model Used

  • Provider: Anthropic
  • Model: claude-sonnet-4-6 (Claude Sonnet 4.6)
  • Context: 200k token context window
  • Mode: agentic tool use (read/edit/write/bash), no extended thinking

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have run tests locally and they pass
  • 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
  • I have considered and documented any risks above
  • I will address all Greptile and reviewer comments before requesting merge

jqueguiner and others added 30 commits May 8, 2026 07:52
The partial unique index `issues_open_routine_execution_uq` previously
required execution_run_id IS NOT NULL, leaving a window where two
routine_execution siblings sharing (company_id, origin_id,
origin_fingerprint) could both be inserted with null execution_run_id. A
later UPDATE that populated execution_run_id (and kept the row in the
index) admitted both rows, wedging every subsequent write — including
the reaper's PATCH {status:"cancelled"} — with 23505.

- Migration 0082 hides newer duplicates and recreates the partial index
  without the execution_run_id IS NOT NULL clause so collisions fire at
  INSERT time. Rollback is documented inline.
- Schema mirrors the new predicate.
- Dispatcher coalesce path adds a fallback finder so a partial-unique
  conflict before execution_run_id is populated still resolves to the
  pre-existing sibling instead of bubbling 23505.
- Error handler lifts postgres-js error fields (code, constraint, table,
  detail) so future failures surface in app frames instead of bare
  driver stacks.
- Issue update route logs origin tuple + pg fields when svc.update
  throws an unexpected error, surfacing the failing call site.
- Regression test covers (a) duplicate sibling INSERT now fails with
  23505/issues_open_routine_execution_uq, (b) cancellation PATCH on a
  sibling whose execution_run_id is populated returns successfully, and
  (c) hidden prior siblings free the index slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…GLA-441)

Add scoped bypass in assertAgentIssueMutationAllowed: when the caller is a
security-role agent and the target issue is critical or high priority, allow
POST /comments through with an audit log row (cross_assignee_security_comment,
safeguard_role_bypass=true). Gate stays scoped to comments — PATCH issue, PUT
documents, and other mutation paths still 403.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Tighten GLA-441's role=security cross-assignee comment bypass: now also
requires the caller's agent id be in SAFEGUARD_COMMENT_ALLOWLIST (only the
SafeguardReviewer for now). Other security-role agents (e.g.
SecurityEngineer) lose the bypass and get the standard 409 path.

Stamp comment metadata.safeguardBypass=true on the bypassed insert so
reviewers can see at a glance why a non-assignee posted; widens the audit
log row from GLA-441 with a per-comment artifact. Schema gets a new
optional safeguardBypass boolean on IssueCommentMetadata.

Tests: 8 cases — allow on high+critical with metadata stamped, deny on
medium, deny non-allowlisted role=security, deny cross-company, deny PATCH,
deny engineer, plus assignee-self path unchanged (no bypass flag).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Productivity reaper now skips long_active_duration triggers when the source
issue has at least one non-terminal blocker via issue_relations, or at least
one non-terminal direct child assigned to a different agent. no_comment_streak
and high_churn triggers are never downgraded. Emits a structured
productivity_review.suppressed_long_active log line with reason, blockerCount,
and nonTerminalChildCount when the suppressor fires.

Implements Option A from GLA-597. No schema change. Rollback: git revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssor tests

- Export listUnresolvedBlockerIssueIds from server/src/services/issues.ts so
  productivity-review can reuse the cancelled-blocker-stays-unresolved semantic
  the rest of the issue service already relies on.
- Import the helper in productivity-review.ts (call site at collectEvidence
  was missing the symbol after GLA-599 landed, so the suppressor path threw
  ReferenceError on the blocker branch).
- Remove three duplicate long_active suppressor tests now covered by the
  GLA-599 test set; keep the GLA-437 multi-child + blockedBy repro test.

Verification: pnpm --filter @paperclipai/server exec vitest run productivity-review-service (17/17 green).

Rollback: git revert.
PAPERCLIP_API_KEY was captured as '' at module init (line 19) before the
env-sourcing block ran (line 34+). Moved sourcing into an IIFE-style block
immediately after imports so all process.env keys are populated before any
const reads them.

Also added PAPERCLIP_API_KEY to notifier.env so launchd/cron invocations
(which don't inherit the interactive shell env) pick it up automatically.

Fixes slot-1650 failure reported on GLA-77.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add plugin-github-sync: manifest with events.subscribe, plugin.state.read/write,
secrets.read-ref, http.outbound, goals.read capabilities; instanceConfigSchema
for repo/host/secretRef/syncedGoalIds/dryRun; no-op handlers for issue.created,
issue.updated, goal.updated that log sanitised payload and resolve goal-ancestor
chain. 4/4 unit tests pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ated test

- Add tests/github-client.spec.ts: 19 tests covering PAT redaction,
  rate-limit parsing, backoff on 429/403, createIssue/closeIssue/
  reopenIssue/addLabel methods, redirect:error enforcement.
- Fix existing plugin.spec.ts goal.updated assertion: the handler logs
  "github-sync: goal.updated — cache invalidated", not the generic no-op
  message. Test now checks the correct message for each event type.

All 23 tests pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ADME depth docs

- Add goal-subtree-cache.spec.ts: 12 tests covering 3-level tree
  (in-scope branch syncs, out-of-scope does not, root-less skipped),
  cache hit/miss, TTL expiry, invalidation scoped per company,
  5-level depth fixture, and cycle-guard termination
- Document real-world goal tree depth (2–3 typical, 5 max) in README

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… sanitisation

- Add src/github-client.ts (missed from prior commits)
- Add src/goal-subtree-cache.ts (missed from prior commits)
- Add src/sync-engine.ts: sanitiseBody, buildTitle, mapIssueStatus,
  createSyncEngine with doSync + scheduleSync (debounce 2 s per issue)
- Update src/manifest.ts: add issues.read capability
- Refactor src/worker.ts: replace no-op handler with engine.scheduleSync
  via routeEvent; keep company/goal-subtree filter chain intact
- Add tests/sync-engine.spec.ts: 23 tests covering sanitiser unit
  tests, idempotency (create + 3 replays = 1 POST), audit log, status
  transitions (done/cancelled/open), dry-run gate, debounce
- Update tests/plugin.spec.ts: replace no-op assertions with new
  dry-run log assertions, out-of-scope short-circuit, goal.updated cache

60/60 tests pass; typecheck clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…tart

W1A of GLA-927 — unblock the marketing freeze (GLA-964) by exposing a
local UI for the Paperclip [review-and-ship] queue.

- scripts/asset-library: Next 14.2.18 + React 18.3.1 + Tailwind 3 app router
- /: placeholder ("Asset Library — coming soon")
- /api/issues: server-side proxy to PAPERCLIP_API_URL companies issues,
  filtered to titles starting with [review-and-ship]
- ecosystem.config.js: pm2 wrapper, pinned to /opt/homebrew/opt/node@20,
  loads notifier.env for PAPERCLIP_API_KEY etc.
- launchd/io.gladia.asset-library.plist: autostart on login via
  `pm2 resurrect || pm2 start`
- README documents run/install/verify

Verified: curl http://127.0.0.1:7700/ returns 200; /api/issues returns
the [review-and-ship] subset.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ration

- POST /api/companies/:id/integrations/github — set/replace config (repo, host,
  secretRef, syncedGoalIds[], dryRun). Validates repo shape, secretRef resolves
  to a secret in the company, syncedGoalIds exist in the company.
- GET /api/companies/:id/integrations/github — read config. secretRef returned
  (UUID ref only), PAT value never surfaced.
- DELETE /api/companies/:id/integrations/github — disable integration; existing
  GH issues stay in place.
- POST /api/issues/:id/sync-to-github — synchronous reconcile for one issue.
  dryRun:true returns planned payload with no GH API call. Live runs resolve the
  secret, upsert the GH issue, and update plugin state + lastError.

Config stored in pluginCompanySettings.settingsJson for
paperclipai.plugin-github-sync. Board-only auth, company-access checked on
every route. logActivity calls record configure/disable events.

UI: adds "GitHub sync (one-way, beta)" section to CompanySettings with repo,
host, write-only PAT (existing secret picker or new PAT entry), goal multi-
select, dryRun toggle, status row (last error + last sync timestamp), and
disable button.

API client added at ui/src/api/github-integration.ts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Founder hit ENOENT loop after `pm2 resurrect` because a fresh checkout has
no `.next/` and `next start` cannot run without it. Manual `npm run build`
unblocked it. This wraps the start path so pm2/launchd handle the build
themselves.

- start.sh: pin node@20, run `npm run build` if `.next/BUILD_ID` is missing
  or any source under `app/` / config files are newer than the build,
  otherwise exec `next start`. Soft-fail: if rebuild fails but a previous
  `.next/` exists, log + serve the stale artifact instead of pm2-looping;
  refresh BUILD_ID mtime so we don't retry on every restart.
- ecosystem.config.js: route pm2 through start.sh (`interpreter: bash`),
  drop the explicit node interpreter — start.sh sets PATH itself.

Verified: pm2 restart picks up start.sh, logs "build is fresh — skipping
rebuild" when artifact is current; with the current broken
`lib/forbidden-source-scan.ts` TS error in W1B work, start.sh logs the
fallback warning and still serves :7700 → 200.

Re GLA-981 post-mortem.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds upload-time scan rejecting Shutterstock / Unsplash / Getty / Adobe Stock
assets before they hit Paperclip storage. Filename regex + EXIF URL inspection
via exifr. New /api/upload route forwards clean files to the Paperclip
attachments endpoint; matches return HTTP 400 with forbidden-source body.

Acceptance smoke (scripts/scan-smoke.ts, scripts/upload-route-smoke.ts):
- shutterstock_123456.jpg -> 400 forbidden-source
- AdobeStock_98765.png -> 400 forbidden-source
- GettyImages-99999_final.jpg -> 400 forbidden-source
- clean local-flux-output.png -> not forbidden

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…brary

board-report and email-notifier now detect [review-and-ship] /
[marketing-asset] issues and emit asset-library detail-page links
(http://127.0.0.1:7700/asset/{issueId}/{docKey}) instead of plain
Paperclip issue links.

- email-notifier: when a blocked issue is asset-review, build an
  [asset-review]-prefixed email with one CTA per document, inline
  thumbnail (PNG/JPG via base64 data URI, capped at
  ASSET_LIBRARY_THUMBNAIL_MAX_BYTES=200KB), "▶ Watch video" link for
  video attachments, and a secondary issue-thread link.
- board-report: per-row primary anchor swaps to the asset library URL
  for asset-review issues; deliverables list becomes per-doc CTA links;
  rows are tagged [asset-review].
- ASSET_LIBRARY_URL env (default http://127.0.0.1:7700) drives both.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…pulates

The GET /integrations/github endpoint reads lastSyncAt from the pluginLogs
table, but POST /issues/:id/sync-to-github never inserted rows there —
neither in dryRun mode nor on live create/update/error paths.

Insert an info-level log row before returning in all three paths so that
the status row in Company Settings shows the last sync timestamp.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Current owner can now PATCH assigneeAgentId to transfer a routine to
another agent in the same company. Non-owners remain blocked.

Covers all four matrix cases from GLA-1043 spec.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
…rary

Wires the W2B approve/reject flow on the marketing asset library detail
page. Hard-blocks approval when provenance is missing, names a cloud-API
tool with no valid waiver, or matches a stock-library filename pattern.
Mirrors the gate server-side so the API rejects 422 even if the client
is bypassed.

- lib/asset-type.ts: parseProvenance reads metadata.provenance / YAML
  frontmatter / "Provenance:" block; evaluateApprovalGate returns
  {allowed,status,banner,bannerTone,...} per AC matrix (missing | cloud
  | cloud-with-exception | exception-pending | exception-invalid |
  stock | ok). CLOUD_TOOL_PATTERNS matches Runway/Pika/Sora/Heygen/
  Synthesia/Midjourney/DALL-E/ElevenLabs/Suno/Udio (case-insensitive,
  partial). STOCK_FILENAME_PATTERNS catches shutterstock_<n>/
  GettyImages-<n>/AdobeStock_<n>.
- app/api/exception-check/route.ts: server-side waiver lookup —
  exception is valid only when status==done AND title contains
  [tool-exception].
- app/api/issues/[id]/approve/route.ts: re-evaluates gate against
  upstream document, posts approval comment, PATCHes status:"todo".
- app/api/issues/[id]/reject/route.ts: requires note, posts rejection
  comment, PATCHes status:"in_progress". 4000-char note cap.
- app/asset/[id]/[doc]/ApproveRejectBar.tsx: button row + reject
  modal; APPROVE disabled when !gate.allowed; banner toned ok/warn/
  block; uses /api proxy routes (server holds PAPERCLIP_API_KEY).
- scripts/gate-check.ts: AC harness — all 7 cases pass
  (missing/cloud-no-waiver/cloud-with-valid-waiver/cloud-with-invalid-
  waiver/local-ai-ok/stock-blocks-even-with-local-ai/founder-original).

Detail page integration (page.tsx) and ProvenancePanel/AssetRenderer/
MarkdownView are W2A (GLA-984) deliverables — left untracked here for
that owner to land. Live verified against pm2 asset-library on
http://127.0.0.1:7700/asset/GLA-917/post-body — gate banner "Cannot
approve — missing provenance metadata" renders, APPROVE button
disabled.
Acceptance criterion for GLA-1026 was: pm2/launchd restart returns HTTP 200
with zero manual `npm run build`. start.sh already enforces this (commit
3580c7f). What was missing was a CI/local-runnable smoke that exercises
the same chain end-to-end without touching the running pm2 process.

- scripts/build-start-smoke.sh: runs `next build` then `next start -p 17700`
  (separate port from prod :7700), polls 30s for HTTP 200, kills the spawned
  server on exit. Exits non-zero on timeout.
- package.json: `npm run smoke` calls the helper.

Verified locally: `npm run smoke` → "[smoke] HTTP 200 on :17700 — ok".

Re GLA-1026.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…issues

A routine's owning agent (e.g. OpsReaper) may now PATCH `status=cancelled`
on its own routine_execution issues even when the issue is checked out by
another assignee (e.g. SafeguardReviewer leaking executions during a
crash loop).

Scope is narrow:
- only when `originKind=routine_execution`
- only when the request body sets `status=cancelled` and nothing else
  (besides an optional `comment`)
- only when the actor agent equals the parent routine's `assigneeAgentId`
- routine company must match issue company

Every use writes a `cross_assignee_routine_owner_cancel` activity log row
with routineId, prior assignee, and prior status, so the bypass leaves
an audit trail.

Unblocks the safeguard reaper (GLA-1045) and closes the API permission
gap surfaced by GLA-1029.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l leaked routine_execution issues

Adds a permission-grant carve-out alongside the existing routine-owner
carve-out (GLA-1048) so an agent holding the new
`tasks:cancel_routine_execution` permission can PATCH status=cancelled
on a routine_execution issue assigned to a different agent. Spec §11.a.

- New permission key: `tasks:cancel_routine_execution`.
- `assertAgentIssueMutationAllowed` now accepts a
  `routineExecutionCancelByPermission` option and bypasses the
  foreign-issue 403 only when the issue origin is `routine_execution`,
  the requested status is `cancelled`, and the actor agent has the
  permission grant. Other fields beyond status / comment are rejected
  with 403 by the PATCH handler, matching the routine-owner carve-out.
- Logs `cross_assignee_routine_execution_cancel` activity with the
  caller, prior assignee, prior status, and origin metadata.
- Migration 0083 grants the permission to OpsReaper
  (`16d0232f-1249-4db9-82d6-ab237c926e59`) at the Gladia company
  (`050de589-23d3-40bb-b227-efea13164d01`). Conditional INSERT on
  company + agent existence; idempotent via the unique index. Rollback
  in the migration footer (single DELETE).

Verified:
- pnpm --filter @paperclipai/server typecheck (clean)
- pnpm --filter @paperclipai/shared typecheck (clean)
- vitest run on
  server/src/__tests__/issue-routine-execution-cancel-permission-route.test.ts (5 tests),
  issue-routine-owner-cancel-carveout.test.ts (5 tests),
  issue-agent-mutation-ownership-routes.test.ts (14 tests) — all green.

Manual smoke deferred to CTO per acceptance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…very runbook

- README: full user-facing docs covering what the plugin does/does not do,
  config fields, dry-run workflow, disabling, status mapping, goal-subtree
  filtering, and PAT setup.
- CHANGELOG: initial 0.1.0 entry covering all features and security invariants.
- Recovery runbook stored as GLA-816 issue document: audit-log queries,
  manual GitHub close/delete steps, plugin_state mapping removal, and
  syncedGoalIds fix workflow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…tbeat injection

Adds native company-level shared_instructions to the Paperclip control plane
so founder-policy lives in one canonical record and is prepended to every
agent's resolved instructions file at heartbeat time, replacing the
filesystem-walk-and-patch model.

Schema (migration 0084_shared_instructions.sql):
- companies.shared_instructions (nullable text)
- agents.shared_instructions_opt_out (boolean, default false)
- company_shared_instructions_history (append-only audit log:
  previous_value, new_value, actor_kind, actor_user_id, request_id, created_at)
Rollback statements documented in the migration header.

API:
- PATCH /api/companies/:id/shared_instructions — board-only write, S1 gate
  rejects every agent JWT with 403 forbidden_actor_kind. Each successful PATCH
  appends a history row capturing previous + new values + actor (S2).
- GET /api/companies/:id/shared_instructions/history — cursor pagination,
  reverse-chronological.
- PATCH /api/agents/:id — extended to handle sharedInstructionsOptOut with
  the same S1 board-only gate (S3).
- GET /api/agents/:id — includes sharedInstructionsOptOut via the existing
  full-row select.

Heartbeat injection (server/src/services/heartbeat.ts):
After the final runtimeConfig is assembled and before adapter.execute, the
heartbeat resolves the company's shared_instructions and the agent's opt-out,
then calls resolveSharedInstructions to materialize a per-run merged temp
file (policy + "\n\n---\n\n" + original AGENTS.md content) under os.tmpdir
and swaps runtimeConfig.instructionsFilePath to that path. The temp file is
cleaned up in the outer finally block alongside other per-run cleanup.

Tests (server/src/__tests__/company-shared-instructions.test.ts):
Covers all six acceptance criteria from GLA-873:
1. Policy prepended ahead of role-specific instructions in the merged file.
2. Latest policy used on subsequent runs without restart.
3. Null/empty policy leaves the agent prompt unchanged.
4. Agent JWT (general/cto/cmo/ceo) PATCH returns 403 forbidden_actor_kind.
5. Successful write inserts a history row with previous + new + actor.
6. shared_instructions_opt_out=true skips the prepend for that agent only.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ation config

When the `repo` field changes, issue a one-time GET /repos/{owner}/{repo}
with the configured PAT before writing the new settings.  401/403/404
responses throw unprocessable(422); the token is redacted from the error
body before it is surfaced.  Unchanged repo skips the handshake entirely.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…stError, or plugin_logs

Verifies the redact() fix (landed in GLA-1076) covers all three error-path
leak surfaces: the 502 JSON response, plugin_company_settings.lastError, and
plugin_logs.meta.error.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Guard against accidental full-company mirror: POST /integrations/github
now returns 422 when dryRun is disabled but syncedGoalIds is empty.
Matches the safe-default requirement from the GLA-38 threat model.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… CI smoke

- watch-and-build.mjs: fs.watch over app/lib/components/pages, 5s debounce,
  npm run build + pm2 restart asset-library; runs as asset-library-watcher pm2 process
- ecosystem.config.js: register asset-library-watcher alongside asset-library
- ChunkErrorBanner.tsx: window error listener catches ChunkLoadError, shows
  "Library is rebuilding — refresh in 10s" overlay with auto-reload countdown
- layout.tsx: mount ChunkErrorBanner in root layout
- asset-library-smoke.yml: GitHub Actions job triggered on asset-library/** changes;
  npm ci → npm run build → next start → curl / (200) → curl /asset/:id (200)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… paths

When an issue has 0 IssueDocuments but its description references
workspace paths (`.planning/`, `_default/`, `produced/`, `workspaces/<uuid>/`,
`/Users/.../.paperclip-worktrees/...`), surface a red banner offering
one-click auto-resolve. The endpoint walks each matched path on local
disk, uploads it as an IssueDocument (text formats only, ≤512KB),
rewrites the description to use `paperclip-doc:<key>` references,
and inline-warns paths that fail to resolve.

E2E verified against sandbox GLA-1104 (cancelled): 2 paths resolved
to documents (brief-27-yt-long-eval-recipe.md, evidence-log.md),
1 missing path inline-warned with `⚠️(file not found on server)`.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…al attached

Implements the visual-mandatory check at the [review-and-ship] issue level:

- lib/asset-type.ts: add IssueApprovalGate type, evaluateIssueApprovalGate(),
  hasVisualAsset(), hasVisualWaiver() — detects image/video docs; recognises
  [visual-waiver] comments as a founder override (warning, not hard block)
- app/components/IssueApproveBar.tsx: new client component — red banner +
  disabled Approve button when gate blocked; amber banner + enabled when
  visual-waiver; approve POSTs to the issue-level approve endpoint
- app/components/AssetIssueSummaryView.tsx: accepts gate + paperclipUrl props;
  renders IssueApproveBar below the description section
- app/api/issues/[issueId]/approve/route.ts: handles issue-level approve when
  docKey is absent — fetches docs + comments, runs evaluateIssueApprovalGate,
  posts approval comment + PATCHes status to todo on pass; per-doc path unchanged
- app/asset/[issueId]/page.tsx: fetches comments server-side, computes issue
  gate, passes gate + paperclipUrl to AssetIssueSummaryView
- app/asset/[issueId]/[docKey]/page.tsx: same gate computation for the
  canonical-slug fallback render path

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@greptile-apps

greptile-apps Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (104 files found, 100 file limit)

@jqueguiner

Copy link
Copy Markdown
Author

Moving all my contributions to my private fork. Closing this PR.

@jqueguiner jqueguiner closed this May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant