Guidance for coding agents working on helix-opspile.
An ops inbox fed from Slack, Microsoft Teams and Discord, running on Cloudflare (Workers, R2, Queues, D1/Durable Objects). Read docs/ARCHITECTURE.md before changing anything: it defines the four piles and the on-disk layout of pile 2. Clef, in code and docs, is the decision model that labels threads and channels.
- This repository is public. Never commit message content, channel or team names, customer names, account ids, tenant ids, app ids or tokens — not in code, fixtures, docs, issues, commit messages or screenshots. Identifiers live in Worker bindings.
- Credentials are Worker secrets; plain ids are Worker vars. Both are listed in the README's "Bindings" section — keep that list current when you add one.
- Entra client-credentials token redemption only works server-side (from the Worker); a browser-origin request fails with AADSTS9002326.
- Code style is
@adobe/eslint-config-helix(npm run lint), ES modules, 2-space indent, max line length 100.
-
ui/*.htmlname their scripts (<script type="module" src="inbox.js"></script>), andsrc/page.jsinlines them where the page is served. Never write a script into a page (test/ui-scripts.test.jsfails on one).ui/app.jsis every page's script. It fetches/api/*without following redirects: a redirect is Cloudflare Access's sign-in, so the page shows "Sign in again" instead of "Failed to fetch" (POST /__dev/signoutinnpm run ui:devshows it). -
People see this product as Enterprise Support ("the inbox" in a sentence), with the Adobe icon; the bots post under their own names per platform, so the pages call them "the bot".
opspilestays the code name: the repository, identifiers, agent prompts, the MCP server, and messages about how a deployment is set up. -
The header is every page's too: its styles in
ui/spectrum.cssandui/header.js, the globalHeader: the switch between the pages, which keeps where each page was left in the browser tab, and you with your menu. Change it there, not in a page. -
The pages are Spectrum 2 (README, "Spectrum 2"), and their styles are all in
ui/spectrum.css(every page's: the base, the header, the shared look) andui/spectrum-<page>.css(the page's layout, then its look), which the pages link. A page has no<style>and the scripts write no staticstyle="": add a class and its rule there, with Spectrum 2 tokens (var(--spectrum-…)) only, no raw colors, lengths or durations (test/ui-spectrum.test.js);spectrum-tokens.cssis built from what they use. -
Generated files:
src/emoji.js(fromscripts/emoji.mjs, do not edit it by hand),src/ui-markdown.bundle.js,src/ui-editor.bundle.js(CodeMirror, the knowledge-base editor's text field) andsrc/ui-highlight.bundle.js(@pierre/highlights, the code highlighter the Worker serves at/inbox/highlight.js; all fromscripts/ui-markdown.mjs, not committed), andsrc/ui-spectrum.bundle.js(Spectrum 2's tokens, from@spectrum-css/tokens, the same way).npm run buildbuilds the bundles, and so donpm test,npm run lint,npm run ui:dev,cf deployandcf dev. Runnpm cifirst in a fresh checkout. -
src/agent/stub.jsreaches theThreadAgentDurable Objects without importing the Agents SDK, so the paths that use it stay testable in Node. Keep SDK imports out of everything butsrc/agent/agent.js. -
ui/rank.jsis the team order's model; itsMODELblock is written bynpm run rank -- train(README, "Train the team order"), not by hand..rank/holds production threads: never commit it. -
scripts/dev-ui-fixtures.mjsis the fake data ofnpm run ui:dev. Add the case a UI change is about, so the preview (and its screenshot) shows it. -
Most lifecycles here are implicit: a nullable timestamp, a watermark, whether a row exists. docs/STATE-MACHINES.md lists them, with the rules they follow. Read the entry before changing one, and add one for anything new with a lifecycle.
- The inbox in a browser:
playwright-cli --cdp=9222. Chrome runs with remote debugging on port 9222 and is signed in to the inbox (Cloudflare Access), sogoto,snapshot,eval,clickandscreenshotwork on the production inbox as the user sees it.screenshotanddroptake refs fromsnapshot(e492), not CSS selectors. To check a fix before it is deployed, runnpm run ui:dev(PORT=for another port) and open it in the same browser;npm run ui:session -- <inbox origin>points it at production's API, read-only. - Screenshots for pull requests come from
npm run ui:dev, never from production. To attach one,dropthe files onto the PR's comment box. GitHub writes theiruser-attachmentsURLs into the box: read them witheval, clear the box, and put them in the PR body. - The Worker:
cf, the Cloudflare CLI (readscloudflare.config.ts;--mode prodor--mode stage). Find a command withcf cli search "<task>", then<command> --help, and keep the search anonymous.cf o11y telemetry querysearches the Worker's logs,cf o11y telemetry live-tailfollows them, andcf d1 query <database id>reads D1. npx wranglerwherecfcannot do something yet:wrangler tail helix-opspile-prodstreams live logs,wrangler secret putsets one secret, andwrangler d1 execute helix-opspile --remote --command "SELECT …"queries D1 (helix-opspile-stagefor stage).- Production is live. Read from it freely, but get the user's go-ahead before anything that writes: D1 changes, secrets, deploys, posts.
/diag/*(theDIAG_TOKENsecret in thex-opspile-diagheader; see the README) shows the Teams side, the Discord poller and single thread agents as the Worker sees them.