Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
ConductFlow는 고객 통화에서 약속을 찾아 추적 메일을 초안으로 작성하고, 사용자가 승인하기 전까지는 전송하지 않습니다
ConductFlow finds the promises in your client calls and drafts the follow-ups. Nothing sends without you.
비즈니스TypeScript · PLpgSQLGitHub @joinconductflow-ship-it온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기ConductFlow: turns client conversations into approved tasks and follow-up drafts. Nothing sends without you.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
ConductFlow turns conversations from small client-service businesses into approved
tasks and follow-up drafts — nothing sends without you.
Recording a demo? See DEMO.md for a tested, step-by-step script against
the live deployment.
Phase 1 built the skeleton: seeded transcript → mock extraction → commitment queue →
draft review → promise-risk dashboard, with org isolation (RLS), an approval-gated
action chokepoint, and an append-only audit log.README 전문 보기 ↗ConductFlow turns conversations from small client-service businesses into approved tasks and follow-up drafts — nothing sends without you.
Recording a demo? See DEMO.md for a tested, step-by-step script against the live deployment.
Phase 1 built the skeleton: seeded transcript → mock extraction → commitment queue → draft review → promise-risk dashboard, with org isolation (RLS), an approval-gated action chokepoint, and an append-only audit log.
Phase 2 replaces the mock with a real pipeline: paste or upload a transcript at
/ingest, and a schema-constrained model call extracts commitments (owner, deadline,
type, confidence, verbatim source span) and drafts a follow-up per commitment. Every
source span is verified verbatim against the transcript; an unverifiable span drops the
commitment to low confidence. Ingested text passes through injection sanitising and is
wrapped as data, never instructions — matches are flagged on the transcript and surfaced
in the queue and review screen.
Phase 3B closes the loop: approved commitments become cards on a task board with a real lifecycle and a completion record, and a promise that passes its date raises an in-app reminder.
Phase 3A/3C/3D add Google. Sign-in is Google OAuth through Supabase Auth, asking for
identity scopes only; Drive, Calendar, and Gmail are granted separately from /settings,
one capability at a time. Refresh tokens are sealed with envelope encryption and stored in
a table authenticated cannot read. Approving a commitment writes the follow-up into the
connected account's Gmail drafts — never sends it.
Phase 4 turns the loop into something an owner can steer. /operations maps how the business
actually runs — lead times, promise types, owners, delivery, client load — once there are twenty
commitments to reason about. /settings/blueprint is where an owner decides, per action, whether
the agent may act unattended, must ask first, or is switched off; every edit writes a new version
rather than overwriting the old one, so "what was this agent allowed to do when it did that?" stays
answerable. Conversations that mention a complaint or a legal concern, or that produce a promise
nobody owns, raise an escalation on /queue. Repeating promises become proposals on /tasks, and
exception checks flag a conversation that does not look like how this business normally works.
Phase 5 makes the blueprint real. It was advisory: a non-owner could rewrite it through PostgREST, two action paths ignored it and used the shipped defaults, and a failed read silently substituted a broader contract. Now writing a blueprint is an owner's privilege enforced by row-level security, not by an application check; actions that reach someone outside the team are demoted to approval-gated when the stored row is read, so a forged row grants nothing; every action resolves the org's own contract; and a contract that cannot be read denies the action instead of widening it.
Nothing sends, and that is enforced in code, not by scope. No Google scope permits
creating a draft without also permitting send: gmail.compose authorizes drafts.send. So
send_external_email sits in the contract's prohibitedActions — denied even with
approval — lib/gmail/client.ts exposes exactly two endpoints, and a test
(tests/gmail/push.test.ts, "the no-send guarantee") fails if the word send appears
anywhere in that module. npm run build itself only runs next build and does not run
Vitest — the guard is enforced by npm test, not by the production build. Verify with
rg -i "messages/send|drafts/send" lib/.
Live at https://conductflow.tech, the canonical origin named by SITE_ORIGIN. The
generated conductflow-woad.vercel.app alias still resolves and now 308s here, because a
sign-in started on the alias leaves its PKCE verifier cookie on that host and the callback
arrives on this one. Hosted on the joinconductflow-8385 Vercel
account and the fauqimhboonrmjzrnkzl Supabase project (both under
[email protected]). Google OAuth runs through a Cloud project named
conductflow-auth, also under that account. All 20 migrations are applied. This section
exists so nobody re-discovers these gaps from scratch — update it as items get resolved.
Works right now, verified end-to-end with real clicks against the live deployment:
Google sign-in, every module including AI-drafting (ingest's commitment extraction, lead
triage, review-response drafting), Gmail draft push, invoice math, payment risk scanning,
retainers, documents, scheduling, scope of work, reports, and the daily cron sweep. See
DEMO.md for a step-by-step walkthrough of the live product.
Resolved:
vercel ai-gateway budgets set team --limit 5 --refresh-period monthly) so nothing can
overspend. The extraction/drafting model was switched from the always-free
openai/gpt-oss-120b (rate-limited hard enough to fail on two calls back to back) to
openai/gpt-4o-mini (lib/agent/schema.ts), which draws against that $5 credit instead
— fractions of a cent per call. Watch usage at Vercel → joinconductflow-8385 → AI
Gateway → Budgets & Spend; raise the cap there if $5/month stops being enough.rejectCommitment
logged an approval_event but never updated commitment.status, so a discarded item sat
at status = 'proposed' and kept reappearing under "awaiting review" forever. Migration
0020 adds 'rejected' to the status check constraint; rejectCommitment now sets it,
and listCommitments/loadOperationsData exclude it. Fixed 2026-09-11 while preparing
the demo — nobody had actually clicked Discard on a real deployment before.Invoice 9bda9579-d2e8-...). Both now show an 8-character uppercase label derived from the UUID
(lib/billing/invoicing.ts, components/billing/BillingPanel.tsx) instead.Blocked or unfinished — pick one up if you're able to:
conductflow-auth project → Google
Auth Platform → Audience) on accounts granting the two unapproved restricted scopes
(gmail.readonly, gmail.compose); currently 3/100 used, and the cap cannot be reset.
Every user hitting that flow also sees Google's "unverified app" warning screen, since
branding shows a "needs to be verified" status too (Branding page) — until that's
verified, users see a raw Supabase-URL fallback instead of the "ConductFlow" name
already configured there. Before onboarding past ~100 total Gmail-scope users, or
removing the unverified-app warning, this needs Google's restricted-scope verification
(published estimate: ~6 weeks for a complete submission) plus an annual third-party CASA
security assessment once verified (cost varies by assessor — get quotes, budget it as a
recurring line item, not a one-time fee). Calendar/Drive scopes are sensitive, not
restricted, and are not subject to the CASA requirement.billing_rate. The table (hourly rate per client, or one
org-wide default) has no settings page anywhere — it's currently set by hand via SQL.
Draft invoice fails with "no billing rate configured for this client or organization"
for any org that hasn't had one inserted manually. Worth a small settings page.extension/) has never been tested in a
real browser. It builds cleanly and passes every static check, but nobody has done an
actual load-unpacked + live tab-capture pass yet — see the warning banner at the top of
extension/README.md for exactly what to verify, and update that banner once it's done./privacy and /terms
exist and describe what the code actually does (see lib/legal/pending.ts for the
shared unresolved values: legal entity, governing jurisdiction, effective date). Per
research done 2026-09-11 (Perplexity, 30 sources) into what a solo founder needs before
real signups:
lib/legal/pending.ts names the responsible
party as pending. Until an LLC/corp exists, the founder is personally the contracting
party; form an entity before real money or real client data from strangers flows
through this product.ai and @supabase/supabase-js versions locked in package-lock.json
declare engines.node: >=22; Node 20 will pull the same packages but is not what they
were tested against.docker info succeeds first.cp .env.local.example .env.local and fill in the values printed by supabase start
(API_URL → NEXT_PUBLIC_SUPABASE_URL, ANON_KEY, SERVICE_ROLE_KEY).AI_GATEWAY_API_KEY in .env.local for extraction — it resolves
openai/gpt-4o-mini through the Vercel AI Gateway. vercel env pull also
works: the VERCEL_OIDC_TOKEN it writes authenticates the gateway on its own, but
it expires every 12 hours. npm test does not need either; tests inject a mock model.npx supabase start then npm run db:reset (applies migrations 0001–0020 + seed).npm run dev → http://localhost:3000/onboarding. Google OAuth sign-in shipped in Phase 3A and works once the
credentials in Google setup below are in place. Continue as demo owner is the
local shortcut: a dev-only button that mints a session for the seeded [email protected]
through the admin API. There is no password field, and it renders only when
NODE_ENV is not production and the Supabase URL is loopback.Postgres, Auth, and Storage run on Supabase's infrastructure instead of in local containers, so nothing here shells out to Docker.
xxxxxxxx in https://xxxxxxxx.supabase.co).npx supabase link --project-ref <ref> — prompts for the database password you set
when creating the project. This does not start any local service.npm run db:push — applies supabase/migrations/0001–0020 directly to the hosted
database over its Postgres connection. No shadow database, no containers.psql "<connection-string>" -f supabase/seed.sql.cp .env.local.example .env.local and fill it from Project Settings → API:
NEXT_PUBLIC_SUPABASE_URL (Project URL), NEXT_PUBLIC_SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY. Add AI_GATEWAY_API_KEY as in Option A step 2 if you
need extraction; npm test doesn't.npm run dev → http://localhost:3000.app/actions/dev-auth.ts
refuses to mint a session unless NEXT_PUBLIC_SUPABASE_URL is loopback, specifically so
a typo can't mint free admin sessions against a real project. Use Google OAuth sign-in
instead (Google setup below), pointing the Google Cloud Console redirect URI at
https://<ref>.supabase.co/auth/v1/callback and enabling the Google provider under
Authentication → Providers in the dashboard — supabase/config.toml's
[auth.external.google] block only applies to supabase start, not a linked project.
Separately, add http://localhost:3000/auth/callback (or your deployed origin's
/auth/callback) to Authentication → URL Configuration → Redirect URLs in the same
dashboard — app/auth/signin/route.ts sends Supabase Auth's signInWithOAuth back to
that app-side URL after Google redirects to Supabase, and it's a separate allowlist from
the Google Cloud Console one; missing it fails the sign-in with a redirect error after
Google's own consent screen succeeds.db:reset (supabase db reset) is Docker-only — it rebuilds the local stack from
scratch and doesn't apply to a linked hosted project. On Option B, re-running
npm run db:push picks up new migrations; there's no destructive one-command reset for a
hosted database, which is the point.
| Route | What it shows |
|---|---|
/ | Marketing hero |
/onboarding | Sign-in: Google OAuth, email sign-in (/auth/email), and the dev-only demo session |
/ingest | Paste or upload a transcript; extraction produces reviewable commitments |
/queue | Commitment queue — confidence chip + status dot per promise, needs-attention strip |
/queue/[commitmentId] | Draft review: draft surface, provenance, flagged-source banner, write/rewrite draft, approval bar |
/tasks | Task board: open / in progress / delivered, plus the overdue reminder strip |
/dashboard | Promise risk: overdue, owner+deadline coverage, approved share |
/operations | Operations map: lead times, promise types, owners, delivery, client load, weekly volume — needs 20 commitments |
/settings | Google connections — grant one capability at a time; disconnecting revokes the whole connected account, not a single capability |
/settings/blueprint | The agent blueprint: per-action unattended / ask-first / off. Owner-only, append-only versions |
Approving writes an approval_event, a task, and an audit_event, and flips the
commitment to tasked. Discarding writes a rejected approval event plus its audit row.
Drafts are written during ingest, one model call per commitment. A draft call can fail on its own — free-tier rate limits do it routinely — so the review screen carries Write the draft for a commitment that has none and Rewrite draft to replace one. Generation happens before the write, so a failed rewrite leaves the existing draft untouched.
Marking a task delivered stamps who completed it and when, flips its commitment to done,
and resolves the open reminder. Reopening returns the commitment to tasked. Nothing here
notifies anyone outside the app — a reminder is an in-app nudge, never an email.
The overdue sweep raises at most one open reminder per task, so running it repeatedly is
safe. It runs daily from /api/cron/reminders (declared in vercel.json), and on demand
from Check for overdue on the board. The cron route refuses every request unless
Authorization: Bearer $CRON_SECRET matches, and refuses all of them when CRON_SECRET is
unset — set it in Vercel's project env before relying on the schedule.
Uploads accept .txt, .md, and .vtt up to 250,000 characters; VTT keeps speaker
labels because owner attribution depends on them. Files are parsed in the action and
never stored. A transcript is persisted before the model runs, so a failed extraction
keeps what was said — /queue lists it under Needs attention with a Retry that
re-runs extraction against the saved text.
Screens compose tokens and primitives; they do not invent colours, type sizes, or spacing.
app/globals.css — the tokens. Three surface levels (--canvas, --surface, --raised),
a 1.25 type scale capped at 30px, spacing and radius scales, motion timing, and the global
:focus-visible ring. That ring lives here because inline styles cannot express focus
states, which is how it went missing everywhere before.components/ui/primitives.tsx — PageHeader, Card, CardTitle, Badge, StatusPill,
EmptyState, Skeleton, buttonStyle, fieldStyle, labelStyle, proseStyle.Rules that hold across every screen:
aria-busy, so a label swapping to
"Saving…" cannot shift the layout under a cursor.npm test — no API key and no Google credentials required anywhere in the suite. Google
clients are injected, so Drive, Calendar, and Gmail are tested against fakes. Most of the
suite needs no live service at all, but tests/rls.test.ts, tests/ingest/*,
tests/drafts/*, tests/reminders/sweep.test.ts, tests/tasks/update.test.ts,
tests/agent/blueprint-store.test.ts, tests/auth/bootstrap.test.ts, and
tests/google/tokens.test.ts talk to the running local (or linked hosted, see Option B
above) Supabase stack over the network, so supabase start and npm run db:reset — or a
db:push against a hosted project — must have succeeded first; without either, those files
fail with fetch failed rather than a useful assertion. The suite covers
cross-org denial, anonymous denial, audit append-only enforcement, the deny-by-default
chokepoint, injection flagging, schema validation, span verification, deadline resolution, the
extraction retry, transcript parsing, the ingest write sequence, task transitions, the idempotent
overdue sweep, and metrics.
Phase 5 adds the enforcement tests: a seeded member is refused an agent_blueprint insert
(42501), an owner is refused one granting an unattended external action (23514), a forged row
granting push_email_draft unattended still resolves to an approval-gated contract, a blueprint
that cannot be read denies rather than widening, and two source assertions — one failing if the
SQL action lists drift from lib/agent/blueprint.ts, one failing if firstAgentContract is
imported anywhere under app/ or lib/ outside lib/agent/contract.ts.
Those stack-backed tests write rows and never delete them — nothing in this product may
delete a task. They claim the next free blueprint version rather than a literal one, so the
suite can run twice without a reset. Run npm run db:reset when the local board gets noisy
with fixtures.
npm run eval scores extraction against five labelled transcripts (tutoring,
consulting, coaching, agency, injection) using the real model. It needs a gateway
credential — AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN, read from .env.local — and
costs money. Without either, every case skips, so it is safe in CI but scores nothing.
Run it whenever you change a prompt in lib/agent/prompts.ts, and investigate a FAIL
row before editing the prompt further. Last full run: 5/5 pass, every source span
verbatim, injection fixture flagged.
EXTRACTION_MODEL is openai/gpt-4o-mini, drawing against the team's $5/month AI
Gateway budget (see "Current deployment status" above) rather than the always-free
openai/gpt-oss-120b, which rate-limits hard enough to fail on two calls back to back.
anthropic/claude-sonnet-5 is a stronger model for this job but needs a higher spend cap
to reach reliably — the eval paces itself (EVAL_PACE_MS, default 45s) as a safety
margin against any gateway model's rate limit, not just the free one.
Everything except live Google calls works without any of this — local dev keeps the demo sign-in button, and an org that has connected nothing simply gets plainer drafts.
http://localhost:54321/auth/v1/callback for Supabase Auth sign-in, and
http://localhost:3000/auth/google/connect/callback for capability grants.GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in .env.local, set
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET to the same secret, and flip
[auth.external.google] enabled = true in supabase/config.toml.DATA_SOURCE_KEK:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))".
Without it, connecting a data source fails and everything else keeps working.NEXT_PUBLIC_GOOGLE_PICKER_API_KEY in .env.local to that key. Restrict the key
to Google Picker API and Websites: your site's origins (for example,
http://localhost:3000/* and https://your-site.example/*) plus https://docs.google.com/*
for the Picker iframe. This browser key is public; it is separate from the OAuth secret.
Set NEXT_PUBLIC_GOOGLE_PICKER_APP_ID to the project's numeric Project number from
the Cloud Console dashboard. Picker requires it to grant access under drive.file.
Restart local dev or rebuild your deployment after setting these public variables.
See Google's Picker setup guide.template in the name. Picker grants
access to those files; the existing template matching uses them on the next draft.
Without NEXT_PUBLIC_GOOGLE_PICKER_API_KEY (or the project number), the button shows an
inline setup error instead of crashing.exec format error from a Supabase container (Option A only). A cached image layer
is corrupt. docker image rm -f <image> and re-run supabase start; the CLI names the
offending image in its error. Studio and postgres-meta are dashboard-only — if they stay
broken, npx supabase start -x studio,postgres-meta runs everything the app and tests
need. Doesn't apply to Option B — there's no local container to go stale.permission denied for table … (SQLSTATE 42501). The role lacks a GRANT, which
is checked before RLS. Grants live at the end of supabase/migrations/0001_schema_rls.sql.db:reset. The reset recreated
auth.users, so the browser session is stale. Sign in again at /onboarding./queue shows it under Needs attention with a Retry button.customer_verification_required (HTTP 403) from the gateway. No payment method on
file — AI Gateway refuses every request until a card is added under the team's AI
settings. Resolved on this deployment; if it recurs, check the card is still valid.RestrictedModelsError (HTTP 403). The model is paid-credit only. Either raise the
team's AI Gateway budget (vercel ai-gateway budgets set team --limit <N>) or point
EXTRACTION_MODEL at a model the current budget allows.GatewayRateLimitError saying "Free tier requests on this model are rate-limited."
This is not about which model you call — it means the team has no AI Gateway budget
set at all, so the account is treated as free-tier regardless of card-on-file or model.
Fix: vercel ai-gateway budgets set team --limit 5 --refresh-period monthly (or check
vercel ai-gateway budgets list first — an existing $0 or missing budget is the tell).
This is what broke the demo prep on 2026-09-11: the card alone did not unlock paid
routing, the explicit budget did. Ingest retries twice and then leaves the transcript in
Needs attention, so nothing is lost either way.new row violates check constraint "agent_blueprint_no_unattended_external". The
blueprint tried to grant push_email_draft or edit_crm unattended. Those reach someone
outside the team and always need a human click — set them to instead. The
editor refuses this before the database does; seeing the constraint name means something
wrote the row directly.Vercel project + Supabase hosted project with migrations 0001–0020 applied
(npx supabase db push, or apply each file's SQL directly through the Supabase
Management API's database/query endpoint if the CLI's own auth token lacks project-admin
scope — see git history around 2026-09-10/11 for how this deployment's migrations were
applied that way). SUPABASE_SERVICE_ROLE_KEY is server-only — it is never imported into
a client component. The hosted database has no seed data, so /ingest starts with no
clients there — use Add a new client.
Env vars actually required in production, beyond the three Supabase ones already named throughout this file:
SITE_ORIGIN — this deployment's own origin, e.g. https://your-app.vercel.app. See
the comment above siteOrigin() in lib/http/site-origin.ts: without it, redirect
targets and OAuth callbacks trust the Host/X-Forwarded-Host headers instead, which
is an open-redirect risk. Set this in every deployed environment.DATA_SOURCE_KEK — generate with
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))". Without
it, connecting Gmail/Drive/Calendar fails outright (DATA_SOURCE_KEK is not set);
everything else keeps working.GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET — from the Google Cloud OAuth client used
for the app's own Drive/Gmail/Calendar connect flow (/auth/google/connect/callback).
This is separate from Supabase's own Google provider config (which takes the same
client's ID/secret, registered at <project>.supabase.co/auth/v1/callback — a second,
different redirect URI on the same OAuth client covers both).CRON_SECRET — required for the scheduled sweep in vercel.json (/api/cron/reminders,
daily) to authenticate; without it every scheduled run gets a 401 and nothing runs.
Vercel's Cron feature sends this automatically once the env var exists — generate any
random string, e.g. node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))".AI_GATEWAY_API_KEY — only needed if not deploying on Vercel itself (Vercel's own AI
Gateway auto-authenticates deployed projects via OIDC with no key required). See
"Current deployment status" above for the payment-method gate this hits regardless./terms or /privacy.npm testexport SUPABASE_URL=https://<ref>.supabase.co
export NEXT_PUBLIC_SUPABASE_URL=https://<ref>.supabase.co
export SUPABASE_ANON_KEY=<anon key from Project Settings → API>
export SUPABASE_SERVICE_ROLE_KEY=<service role key, same place>
export SUPABASE_JWT_SECRET=<Project Settings → API → JWT Settings → Legacy JWT secret>
vitest.config.tshttp://127.0.0.1:5432142501 inserting an agent_blueprint row. Only an owner may change the blueprint.
Members read it and see the editor read-only.audit_event with a :contract_unavailable target. Check the database connection and retry;
nothing was written.