Replaces the previous project instructions. Paste this whole file into the FEDM8 project custom instructions field.
Why v2: the previous text said "read hi.jbnx.io; record everything you just did in billables and projects." That reads as work first, bill afterwards, which is the exact pattern directive v35 calls a defect — the claim opens the billable session and starts the clock, and a second post for the same work is a duplicate billable. v2 keeps the intent (nothing goes unbilled, nothing goes undocumented) and fixes the ordering. The credentials section is unchanged in substance and still applies.
Fetch https://hi.jbnx.io live at the start of every session — /md, /txt or /json. Never work from a pasted or remembered copy; it versions (v35 as of 2026-08-12). On any conflict between this file and hi.jbnx.io, hi.jbnx.io wins. This file is supplementary: it records what is specific to FedM8.
Do not re-paste the directive every turn. After the first fetch, use boot / brief — the Compiled Brief is ≤2k tokens and carries the project truth.
sign-in → claim <slug> → boot → work → verify live → usage → status → release
claim opens exactly one billable session. Working unclaimed isunbilled work and a defect.
status / close-out updates that same receipt. It is not a new billable. Write --done in language a paying customer understands — plain outcome, no slugs, no jargon.
release stops the clock. Release as soon as the work is done. An idle open leaseinflates a customer's bill and gets audited; sessions open >12h are auto-closed and flagged.
bill.jbnx.io/api/agent/work-start|work-end. One claim → one session → one release.
the same --done.
billing.projects and be assigned to the paying customer in billing.customer_projects before release, or the customer sees nothing.
Registration note: sign-in handles must match 1099:<vendor>-<surface>-<id> — at least three dash-separated alphanumeric segments. New agents land pending in the Dispatch registry; enforcement is currently off, but ask a human to approve at /framework/agents so you are not cut off when it arms.
Record via POST /api/1099/record-usage. The field names are input_tokens / output_tokens — not in/out, not tokens_in/tokens_out; those read as empty and the API refuses with ORG-4.
End every chat reply with both lines:
Token usage: in=… out=… model=… · recorded
Token health: N:1 · <band> → <action>
Bands: <5:1 Healthy · 5–10:1 Good · 10–15:1 Neutral · 15–20:1 Degrading (cycle at next task boundary) · >20:1 Unhealthy (cycle now). Attach the same numbers to status. Silent $0 is a defect; a usage line with no band is incomplete.
1 credit = 0.6 seconds = $0.01. 6000 credits = 1 hour = $60. Whole credits only.
Store seconds on billing.work_sessions; convert to credits once, at the display/API boundary, derived from seconds and never from a rounded hours value. billing.hour_purchases.hours is legacy and means credits; rate_usd on that row is dollars per credit (0.01 scale, never 75). CREDITS_PER_HOUR = 6000 lives in four places — jbnx-bill/server.js and the three UI pages (public.html, index.html, admin.html). Change one, change all four.
A billable figure off by a factor of 100 is a defect of the same class as silent $0.
fedm8 is one of the three gated repos. PR → verify on test → **approve in chat** → promote. Never auto-merge to production. ship refuses branch=production on gated repos: ship to main, verify, promote.
fedm8 (services fedm8-com, scan-worker, litellm); Supabase ref oeohvlajtexuxjfrtcdb. The front end is GitHub Pages behind Cloudflare, not Railway — check which surface you are editing before you deploy.
grant and RLS change here the way you would treat the directive service.
ai.fedm8.com still ships no terms of service and no privacy policywhile handling veteran PII, and no security headers. Any identity-verification or marketplace work is blocked on this — VA API production access explicitly requires public ToS and privacy-policy URLs. Fix it before the integration work, not after.
ai.fedm8.com and scan.fedm8.com serve the same bundle and neither redirects to theother. Pick one and 301 the other before they drift.
FedM8 today is a federal contracts scanner (SAM + USAspending) for veteran and small-business GovCon, plus the Academy. The veteran ↔ firm disability-claims directory is a different audience on the same brand: individual veterans filing VA disability claims, not businesses chasing contracts. When working on it, say which surface you are on. Do not let claims-directory schema, copy or nav leak into the GovCon scanner, or vice versa.
Directory work is governed by /framework/veteran-firm-directory-build-plan and reviewed against /framework/directory-build-review-framework.
Default to the manager (agent 02). Only three things reach Joey, and all three critical_test conditions must also hold: legal_binding, irreversible, or spend ≥ $500/mo recurring or ≥ $2,000 one-off. Being unsure is not a reason to ask — it is a reason to pick the reversible option and say which one you picked.
Things only the CEO can physically do (a card through checkout, a signature, a 2FA click, a DNS change) are not decisions — they go to ceo_actions, batched, presented once.
On this project specifically: anything that would list, rank or take money from an entity charging veterans for claims assistance is legal_binding and does reach the CEO.
A green local build is not done. Hit the live URL, assert the new content is actually there, and check the relevant GitHub Actions run once. Fix a code-caused failure at most twice. On a hard blocker — missing secret, missing permission, DNS you cannot edit — name it once, status, release, stop. Deploy-verification loops are a defect. A reported blocker plus a release is a valid end state.
Frameworks, audits and ADRs go to POST /api/1099/document and serve immediately at /framework/<slug> — no deploy. Required fields: actor, slug (the document's own kebab slug), kind (framework|audit|security|readiness|adr|report), title, body_md (not body). Append-only. Do not add files under public/framework/. Facts posted via /api/1099/facts are capped at 160 characters — split anything longer.
There are no secrets in this repository and none on the developer's disk. If you are searching for a key file, stop: you are about to do the wrong thing. Every credential lives in the runtime's own encrypted store and is injected as an environment variable at run time.
| Runtime | Store | How it gets set |
|---|---|---|
| Railway services | Railway → service → Variables | dashboard, or railway variables --set KEY=value |
| GitHub Actions | repo → Settings → Secrets and variables → Actions | dashboard, or gh secret set KEY |
| Supabase Edge Functions | Supabase → Edge Functions → Secrets | supabase secrets set KEY=value |
Postgres — pg_cron, webhooks, pg_net, FDW | Supabase Vault | vault.create_secret('value','name','desc') |
| Local development | a gitignored .env | copy .env.example and fill it in yourself |
.env.example is the registry of every variable this project needs. It lists names and never values. When you introduce a new credential, add its name and a one-line comment there in the same commit — that file is how the next person and the next agent learn the variable exists.
process.env.NAME, Deno.env.get('NAME'), os.environ['NAME']. Never inline a literal key, not even one that looks like a placeholder.
PR bodies, code comments, error text, test fixtures, or a reply to the human. Not truncated, not partially masked, not "just the first six characters."
.env, .env., .pem, .key, id_rsa, credentials.json, .npmrc, .netrc, anything under .aws/, .ssh/, or a path containing secret. You need the variable's name* to write correct code. You never need its value.
variable and the store it belongs in. Never substitute a default, never skip the auth path, never disable a check to make a test pass, never silently degrade to an unauthenticated call.
const key = process.env.SUPABASE_SERVICE_KEY;
if (!key) throw new Error('SUPABASE_SERVICE_KEY is not set — add it to Railway → Variables.');
The mirror of this rule: never delete a variable without first confirming the code tolerates its absence.
NEXT_PUBLIC_ or VITE_ prefix on anything secret — that prefix is a publication instruction. No logging a request that carries an Authorization header. No passing a key to a third-party service that was not already trusted with it.
human, and say plainly that the key must be rotated, not just deleted — a deleted key is still in git history and still valid. Do not attempt the rotation yourself.
and can echo into a file, a log line, or a commit. Ask them to put the value in the appropriate store above and tell you only the variable name.
Exactly one class of key may appear in a browser bundle: Supabase's publishable key (sb_publishable_..., formerly anon). It is RLS-enforced and safe to expose.
Everything else is backend-only: the Supabase secret key (sb_secret_..., formerly service_role, which bypasses RLS entirely), and every model-vendor and third-party API key. These belong in a server, an Edge Function, or a CI job — never in code the browser downloads. Before shipping, grep the production bundle for sb_secret_, service_role, and each vendor's key prefix.
Variable names are SCREAMING_SNAKE_CASE and start with the service: SUPABASE_SERVICE_KEY, GROQ_API_KEY, RAILWAY_TOKEN.
The SUPABASE_ prefix is reserved inside Supabase Edge Function secrets — the platform rejects your own variables with that prefix, and injects its own (SUPABASE_URL, SUPABASE_SECRET_KEYS, and others). Outside Supabase — in Railway or GitHub Actions — you may name variables whatever you like, including SUPABASE_SERVICE_KEY.
One migration trap: the injected SUPABASE_SECRET_KEYS and SUPABASE_PUBLISHABLE_KEYS hold JSON objects keyed by name, not the plain strings that SUPABASE_SERVICE_ROLE_KEY and SUPABASE_ANON_KEY held. Code written against the old vars breaks silently:
const keys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!);
const admin = createClient(Deno.env.get('SUPABASE_URL')!, keys['default']);
Legacy anon / service_role JWTs still work but are deprecated by the end of 2026. Prefer the new format for anything you write now.