Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/assistant-contracts-package-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant-contracts": patch
---

Build the package to `dist/` (tsup CJS/ESM/types) so Node runtimes such as Vercel can require it; TypeScript source under `src/` is no longer the package entrypoint
5 changes: 5 additions & 0 deletions .changeset/assistant-contracts-support-intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant-contracts": minor
---

Add the support-chat contracts: chat request/message schemas, collected-fields data part (required fields: summary and description — email is optional), issue create request/response, file confirm/delete requests and upload response, shared error shape, hard limits and the pinned Phase-2 docs-search result shape
5 changes: 5 additions & 0 deletions .changeset/assistant-support-intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant": minor
---

Add the support-chat intake feature: streaming /chat pipeline (intent classification, field extraction, live collected-fields summary, optional email), idempotent /issues creation in Linear with attachment transfer at creation time, client-direct file uploads to Vercel Blob (token/confirm/delete endpoints, server-side magic-byte validation at confirm, daily orphan-cleanup cron), per-IP rate limiting via Upstash, structured step logs with Sentry, runtime env delivery for Vercel deployments and preview CORS narrowed to the aragon-app scope
3 changes: 3 additions & 0 deletions .github/workflows/app-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# Serverless functions only see runtime env vars (the .env file is build-time only).
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true

deploy:
needs: [changes, deploy-assistant]
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/assistant-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# dev.assistant is an aliased preview deployment, so VERCEL_ENV reports 'preview' at
# runtime — ASSISTANT_ENV pins the actual environment for src/lib/env.ts.
runtime-env: "ASSISTANT_ENV=development"
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true
42 changes: 42 additions & 0 deletions .github/workflows/assistant-llm-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The "Assistant LLM Smoke" workflow runs a few real conversations (real models, real Linear
# team of the target environment) against a deployed assistant instance. It is deliberately
# NON-BLOCKING: it is not a required check anywhere — a red run is a signal to look at prompt or
# pipeline drift, not a merge gate. Tickets land in the Linear team configured for the target
# environment (a test team everywhere except production).
#
# Triggers: nightly, after every development deploy, and manually against any URL.

name: Assistant LLM Smoke

on:
schedule:
- cron: "0 3 * * *"
workflow_run:
workflows: ["Assistant Development"]
types: [completed]
workflow_dispatch:
inputs:
url:
description: "Assistant base URL to smoke"
required: false
default: "https://dev.assistant.aragon.org"

permissions:
contents: read

jobs:
smoke:
# For post-deploy runs only fire when the deploy actually succeeded.
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0

- name: Setup node
uses: actions/setup-node@v4.2.0
with:
node-version-file: .nvmrc

- name: Run LLM smoke scenarios
run: node apps/assistant/scripts/llmSmoke.mjs "${{ inputs.url || 'https://dev.assistant.aragon.org' }}"
3 changes: 3 additions & 0 deletions .github/workflows/assistant-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# VERCEL_ENV=production maps correctly at runtime; only the secrets need lifting.
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true
72 changes: 69 additions & 3 deletions .github/workflows/shared-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,22 @@ on:
type: boolean
default: true
env-overrides:
description: "Extra env lines appended to the workspace .env after secrets (e.g. per-run URL overrides)"
description: "Extra env lines appended to the workspace .env.local after setup (e.g. per-run URL overrides)"
required: false
type: string
runtime-env:
description: "Runtime env vars for the deployment (multiline KEY=VALUE). The .env file only exists at build time; serverless functions need runtime vars passed via `vercel deploy -e`"
required: false
type: string
runtime-env-keys:
description: "Names of keys to lift from the prepared workspace .env into deployment runtime env vars — secrets stay in 1Password and never appear in workflow files"
required: false
type: string
upload-sentry-source-maps:
description: "Upload source maps using SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT from the infrastructure vault"
required: false
type: boolean
default: false
outputs:
deploymentUrl:
description: "The URL of the Vercel deployment"
Expand Down Expand Up @@ -89,11 +102,24 @@ jobs:
secret-filepath: "${{ inputs.workspace }}/.env"
mode: alltofile

# Overrides must land in .env.local, not .env: `setup` writes the per-env config (including the
# default NEXT_PUBLIC_ASSISTANT_URL) to .env.local, which Next.js loads at higher precedence than
# .env. Appending an override for the same key to .env would be silently shadowed by .env.local;
# appending it to .env.local (last occurrence wins within the file) lets the per-run value take
# effect — e.g. pointing the app preview at the chained assistant preview.
- name: Apply env overrides
if: inputs.env-overrides != ''
env:
ENV_OVERRIDES: ${{ inputs.env-overrides }}
run: printf '%s\n' "$ENV_OVERRIDES" >> ${{ inputs.workspace }}/.env
run: printf '%s\n' "$ENV_OVERRIDES" >> ${{ inputs.workspace }}/.env.local

# Workspace packages that ship a built `dist/` (e.g. @aragon/assistant-contracts) must be
# compiled before `vercel build`: the lambda resolves them via package.json exports, and
# Node cannot execute the TypeScript source under packages/*/src. The `{dir}^...` filter
# selects only the dependencies of the workspace (never the workspace itself) and is a
# no-op for workspaces without buildable deps.
- name: Build workspace dependencies
run: pnpm --filter "{${{ inputs.workspace }}}^..." build

# Vercel never builds from git here: the artifact is built in this job and uploaded as-is
# (deploy --prebuilt). All vercel commands run from the repo root so Turbopack/Next treat the
Expand All @@ -118,8 +144,48 @@ jobs:
- name: Bundle Vercel application
run: pnpm vercel build ${{ env.VERCEL_ARGS }} --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }}

- name: Upload source maps to Sentry
if: inputs.upload-sentry-source-maps
run: |
: "${SENTRY_AUTH_TOKEN:?SENTRY_AUTH_TOKEN is required in the infrastructure vault}"
: "${SENTRY_ORG:?SENTRY_ORG is required in the infrastructure vault}"
: "${SENTRY_PROJECT:?SENTRY_PROJECT is required in the infrastructure vault}"
release=$(git rev-parse HEAD)
pnpm --dir "${{ inputs.workspace }}" exec sentry-cli sourcemaps inject "${GITHUB_WORKSPACE}/.vercel/output"
pnpm --dir "${{ inputs.workspace }}" exec sentry-cli sourcemaps upload --release "$release" --validate --wait "${GITHUB_WORKSPACE}/.vercel/output"

# The runtime `-e KEY=VALUE` args are built as a bash array in the same step (arrays don't
# survive GITHUB_ENV) so values containing spaces stay single arguments: the .env file
# prepared above only exists at build time, so anything a serverless function needs at
# runtime must be set as deployment env vars. Literal values come from `runtime-env`;
# `runtime-env-keys` lifts the named keys (1Password secrets) from the prepared .env,
# keeping values out of workflow files.
- name: Deploy to Vercel
run: pnpm vercel deploy --prebuilt ${{ env.VERCEL_ARGS }} ${{ inputs.env == 'production' && '--skip-domain' || '' }} --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }} > deployment-url.txt
env:
RUNTIME_ENV: ${{ inputs.runtime-env }}
RUNTIME_ENV_KEYS: ${{ inputs.runtime-env-keys }}
UPLOAD_SENTRY_SOURCE_MAPS: ${{ inputs.upload-sentry-source-maps }}
run: |
runtime_args=()
if [ "$UPLOAD_SENTRY_SOURCE_MAPS" = "true" ]; then
runtime_args+=(-e "SENTRY_RELEASE=$(git rev-parse HEAD)")
fi
while IFS= read -r line; do
[ -z "$line" ] && continue
runtime_args+=(-e "$line")
done <<< "$RUNTIME_ENV"
for key in $RUNTIME_ENV_KEYS; do
# Tolerate optional whitespace around '=' and at the end of the line — dotenv parsers
# accept `KEY = value`, so a hand-edited config entry must not be silently skipped by
# the runtime lift (and trailing whitespace must not defeat the quote stripping below).
value=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "${{ inputs.workspace }}/.env" | head -1 | cut -d= -f2- | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
value="${value%\"}"
value="${value#\"}"
if [ -n "$value" ]; then
runtime_args+=(-e "${key}=${value}")
fi
done
pnpm vercel deploy --prebuilt ${{ env.VERCEL_ARGS }} ${{ inputs.env == 'production' && '--skip-domain' || '' }} "${runtime_args[@]}" --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }} > deployment-url.txt

- name: Set output
id: set-deployment-url
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ out/

# production
build/
dist/

# misc
.DS_Store
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file is the team-shared agent entry point. `CLAUDE.md` imports it via `@AGE

- `apps/app/` — the Aragon App (app.aragon.org): all app source, configs (`next.config.mjs`, `tsconfig.json`, `jest.config.js`), `e2e/`, `docs/`, `scripts/`, `CHANGELOG.md`. Package name stays `@aragon/app`.
- `apps/assistant/` — the assistant service (assistant.aragon.org): Hono API behind the in-app support chat. Own Vercel project, continuous dev deploys from `main`, production released through its Version-PR flow (see `apps/assistant/README.md`).
- `packages/assistant-contracts/` — shared zod contracts between the assistant service and the assistant-chat widget. Domain-scoped by design, not a catch-all contracts package.
- `packages/assistant-contracts/` — shared zod contracts between the assistant service and the assistant-chat widget. Domain-scoped by design, not a catch-all contracts package. Built with `tsup` to `dist/` (CJS + ESM + `.d.ts`); Turbo `^build` and the shared-deploy workflow compile it before dependents type-check/test/deploy.
- `apps/*`, `packages/*` — reserved for future workspaces.
- Root — workspace infra only: `pnpm-workspace.yaml`, `turbo.json`, `biome.json`, `.github/`, `.husky/`, `.changeset/`, agent infra (`.agents/`, `.claude/`). Root `package.json` has no version; each workspace is versioned independently via changesets with per-package tags (`@aragon/app@1.17.0`) and release branches (`release/app/…`). See `apps/app/docs/projectDocs/release-process.md`.
- CI: workflows in `.github/workflows/` are grouped per app (`app-*.yml` for `apps/app`, `assistant-*.yml` for `apps/assistant`, `shared-*.yml` reusable). Root scripts proxy through `turbo run <task>`, so `pnpm type-check` etc. work from the repo root.
Expand Down Expand Up @@ -94,6 +94,6 @@ workspace — run them there: `cd apps/app && pnpm test:watch` (or `pnpm --filte
There are two deliberate execution paths in CI, and they must stay that way:

- **Graph tasks** (`type-check`, `lint:check`, `test:coverage`) run **from the repo root** via `turbo run …`. Turbo owns their cache/ordering, so they benefit from local caching and (later) the package graph.
- **`vercel build` and Playwright** run **inside `apps/app`** (`working-directory: apps/app`), because they need the app as the working directory — Vercel builds the artifact on the GH runner and uploads it with `deploy --prebuilt` (Vercel never builds from git here; see the comment in `.github/workflows/shared-deploy.yml`). This is why `turbo run build` is a non-caching passthrough (`cache: false`, no outputs): the real build is `vercel build`, not Turbo.
- **`vercel build` and Playwright** run from the repo root via `shared-deploy.yml` (workspace selected by input); Vercel builds the artifact on the GH runner and uploads it with `deploy --prebuilt` (Vercel never builds from git here). `turbo run build` stays a non-caching passthrough at the root (`cache: false`) because app/assistant artifacts are produced by `vercel build`, not Turbo — except workspace libraries that ship `dist/` (e.g. `@aragon/assistant-contracts`), which override `build` in their package `turbo.json` with `outputs: ["dist/**"]` and `cache: true`. `type-check` / `dev` depend on `^build` so those libraries are compiled before dependents run. `shared-deploy.yml` also runs `pnpm --filter "{<workspace>}^..." build` before `vercel build`.

Turbo caching is **local only** right now — no remote cache is wired (no `TURBO_TOKEN`/`TURBO_TEAM`). With a single package and a build that doesn't go through Turbo, a remote cache would buy nothing; revisit it once a second interdependent package (e.g. the assistant service) lands and Turbo's graph actually has work to memoize across machines. Task inputs are intentionally left at Turbo's default (hash all package files) rather than hand-narrowed globs — correctness over cache-hit-rate — and the root `biome.json` is listed in `globalDependencies` so lint caches invalidate when lint rules change.
Turbo caching is **local only** right now — no remote cache is wired (no `TURBO_TOKEN`/`TURBO_TEAM`). Task inputs are intentionally left at Turbo's default (hash all package files) rather than hand-narrowed globs — correctness over cache-hit-rate — and the root `biome.json` is listed in `globalDependencies` so lint caches invalidate when lint rules change.
33 changes: 26 additions & 7 deletions apps/assistant/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,44 @@
# `pnpm run setup <env>` (`pnpm dev` runs `setup local` automatically).
# - Secrets are NEVER checked in and NEVER set manually in the Vercel dashboard: 1Password
# (kv_assistant_<env> vaults) is the source of truth; CI propagates them on deploy.
# For local development, copy the values you need from 1Password into .env.local manually.
# For local development, copy the values you need from 1Password into `.env` (not
# `.env.local` — setup overwrites that file on every `pnpm dev`).

# Application environment: local | development | preview | production
ASSISTANT_ENV=local

# Port for the local dev server
PORT=4000

# --- Secrets (1Password: kv_assistant_<env>) — required from the feature milestone onwards ---
# --- Optional overrides ---

# Per-IP rate limits (defaults live in src/lib/config.ts)
# ASSISTANT_RATE_LIMIT_RPM=10
# ASSISTANT_RATE_LIMIT_SESSIONS_PER_DAY=20

# --- Secrets (1Password: kv_assistant_<env>) ---

# Vercel AI Gateway key (dedicated key with a spend budget)
# AI_GATEWAY_API_KEY=
AI_GATEWAY_API_KEY=

# Linear service-bot API key (scoped to a single team)
# LINEAR_API_KEY=
LINEAR_API_KEY=

# Linear team the intake creates issues in: the dev/preview vaults carry the id of the
# dedicated test team, the production vault the real support team.
LINEAR_TEAM_ID=

# Upstash Redis (Vercel Marketplace integration)
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Vercel Blob store of the assistant project (intermediate storage for chat attachments;
# files move to Linear only when the ticket is created)
BLOB_READ_WRITE_TOKEN=

# Shared secret of the cron maintenance endpoints (/internal/*); Vercel Cron sends it
# automatically as a Bearer token when the env var is set on the project
CRON_SECRET=

# Sentry DSN of the assistant project
# SENTRY_DSN=
SENTRY_DSN=
Loading
Loading