Skip to main content

Chatticus

Promoting the TypeScript architecture to main

Status: proposal for owner approval. Nothing in this document has been executed. Read-only assessment taken against origin/develop at b902f5d3 and origin/main at e68b5f4f on 2026-10-13. No account ids, secret values or ARNs appear here; take them from AGENTS.local.md (absent in the assessing worktree).

1. Bottom line

Do not merge develop into main yet. A plain merge would, in one motion:

  1. Tag and publish a v1.0.0 release (semantic-release finds no reachable tag), then fail its own pre-1.0 guard after the tag already exists.
  2. Fire nine staging and production deploy workflows with no approval gate and no branch protection (by decision of the owner, 2026-10-15: friction-free, no protection). By code reading, most jobs fail fast, and one class (auth) redeploys legacy stacks from new code. None deploys the TypeScript control plane to a working staging or production, because those environments were never bootstrapped for it.
  3. Break the private marketing site's build (it pip installs node_modules/chatticus/python, which the merge deletes) and put the live marketing waitlist and contact forms at risk (their /api is the legacy Python production thin-turn Lambda that the TypeScript control plane deliberately does not replace).

Promoting is mostly a gating and sequencing problem, not a code problem. The code on develop is green. The plan in section 8 does the safe things first: protect main and the environments, change what a push to main does, fix the release baseline, decouple the website, and only then merge. Staging and production cutover to the TypeScript control plane is a separate, later, human-run phase.

2. How far apart the branches are

Evidence: git rev-list --left-right --count origin/main...origin/develop gives 52 691 (main has 52 commits develop lacks, develop has 691 main lacks). Merge base is ee27393d. The 52 main-only commits are Python-era release commits ("0.41.0" style) and merge commits; git diff --stat origin/main...origin/develop is 1723 files, +262781 -52971.

Area main develop
python/ present (191 files, the Python control plane, also the semantic-release project) deleted
conversation/ absent present (507 files, the TypeScript control plane on Pi-durable)
host-protocol/, computer/host/ absent present
.releaserc.json absent present (JS semantic-release)
Workflows 11 (ci, semantic-release, nine deploy-{web,thinturn,auth}-{env}) 16: adds deploy-controlplane-{env} x3, build-computer-image-development, verify-account-deploy-role; CI replaces the Python job with a conversation job (moto, vitest, cucumber-js)
features/ behave-era Gherkin 91 added, 73 deleted, 30 modified (cucumber-js manifest)
infra/ legacy single-account stacks adds dedicated-account stacks, ControlPlane stack, zones, certificates, management DNS, tagging; thin-turn stack drops the Python Lambdas (about -460 lines); integration-test-stack and sse-spike-stack deleted
project/ (Kanbus) older board 287 files added, 57 modified (board is carried by the promotion)
Root package.json workspaces web, infra; version 0.1.0 adds workspaces conversation, host-protocol, computer/host; adds semantic-release devDependencies

So main still ships the Python control plane (the python/ tree, behave, the Python semantic-release). Staging and production run the TypeScript stacks in the production AWS account; the README no longer carries a status section.

3. Release and version a merge would produce

Config on develop (.releaserc.json): branch main, tag format v${version}, conventionalcommits preset, releaseRules: [{ breaking: true, release: "minor" }], changelog, npm (no publish), git (commits package.json, package-lock.json, CHANGELOG.md with [skip ci]), GitHub release. The workflow runs on a successful CI run on a push to main whose actor is not a bot, and its guard-pre-1-0 job fails any release that is not 0.*.

Commits on develop not on main, by type: pm 446, feat 83, fix 47, docs 17, chore 16, test 14, spike 5, ci 4, web 2, infra 2. There are zero BREAKING CHANGE footers or !: subjects, and breaking changes map to minor anyway. Analysis alone would say a minor bump (0.44.0).

The trap: all 48 tags (latest v0.43.0) point at commits that are ancestors of neither main nor develop (git tag --merged origin/main is empty; v0.43.0 is on 4643ba29, which no remote branch contains). semantic-release only counts tags reachable from the branch, so on the first release it will see no prior release and compute 1.0.0. It publishes the tag, the release commit and the GitHub release first; guard-pre-1-0 then fails the run. Result: a public v1.0.0 tag and release that the owner then has to delete.

Mitigation, required before the merge: put a reachable baseline tag on main's current head (for example v0.43.1 on e68b5f4f, "last Python release"). Then the analyzer sees only develop's commits since the baseline and proposes v0.44.0. Also note the release job will commit a large generated CHANGELOG.md to main.

4. What a push to main triggers

All of these are on: push: branches: [main] plus workflow_dispatch, taken from develop's versions (the merge replaces main's). Staging jobs share concurrency group deploy-staging, production jobs deploy-production; they queue serially, none cancels.

Workflow Environment Steps run
ci none infra tests and synth, web test/lint/typecheck/build, conversation typecheck, vitest, cucumber
semantic-release none tag, changelog commit, GitHub release (see section 3)
deploy-controlplane-staging / -production staging / production dedicated-account.sh ENV control-plane
deploy-web-staging / -production same dedicated-account.sh ENV control-plane, then deploy-chatticus-web-ENV.sh
deploy-thinturn-staging / -production same dedicated-account.sh ENV control-plane, then deploy-chatticus-web-ENV.sh, then deploy-chatticus-thinturn-ENV.sh
deploy-auth-staging / -production same deploy-chatticus-auth-ENV.sh only

Inputs they need: environment secret AWS_DEPLOY_ROLE_ARN (exists on all three GitHub environments), repo variable CHATTICUS_INSTALLATION_NAME and repo secret CHATTICUS_BUDGETS_NOTIFICATION_EMAIL (both exist at repo level), environment variable CHATTICUS_BUDGETS_MONTHLY_LIMIT_USD (exists on staging and production). AWS_NEW_ACCOUNT_DEPLOY_ROLE_ARN exists per environment but is used only by verify-account-deploy-role.yml.

What each does, by code reading (I cannot read the secret to confirm which account AWS_DEPLOY_ROLE_ARN targets; Kanbus chatticus-7ebd57 and infra/README.md say it is deliberately still the legacy account until a cutover card, and the Oct 3 main deploys that succeeded used legacy scripts):

5. Bootstrap state of staging and production

From Kanbus (chatticus-7ebd57, chatticus-d6997c, chatticus-2e4d79, chatticus-c649bf), the infra README and the repo:

Item development staging production
AWS account exists yes yes (the production account, shared with production; see section 11) yes
Budgets, Snapshots, Computers, thin-turn data stack in the new account yes no: staging is added to production's account, which already holds them (one 100 USD budget for both; section 11) yes (100 USD placeholder)
Zones, certificates, DNS delegation yes (develop.chattic.us) no no
Auth (Cognito) and web (CloudFront) in the new account yes no no
Per-environment secrets (Google OAuth, OpenAI key, model-gateway signing key, invoke key) yes no no
ControlPlane stack (TypeScript) yes no no
GitHub AWS_DEPLOY_ROLE_ARN points at new account legacy (inferred) legacy (inferred)
GitHub environment protection (reviewers, branch policy) none branch policy main only, no reviewers branch policy main only, no reviewers

Blockers named on the board: hostname ownership (CloudFront alternate domain names and Cognito custom domains are globally unique, and legacy still holds staging.chattic.us and hey.chattic.us, so legacy must release them first), Google OAuth secrets, per-environment ECR image with a real tag instead of the hand-pushed :dev, the owner-computer path's development-only gate, and the integration-test-principal decision for staging (chatticus-80b17c). Live staging and production are now the TypeScript stacks in the production AWS account: both are deployed, and production CloudFront is enabled (README "Deployed across environments"). The legacy account's Python stacks no longer serve them.

Repo settings found with gh api (read-only): main has no branch protection (HTTP 404) and no rulesets, and the staging and production GitHub environments have no required reviewers and only a deployment branch policy limiting deploys to main. This is the owner's decision (2026-10-15), not a gap.

6. What changes for users

7. Website dependency

Chattic.us-web/package.json has "chatticus": "https://github.com/AnthusAI/Chatticus/archive/refs/heads/main.tar.gz" with transpilePackages: ["chatticus"], unpinned.

Imports used by the website (counted in its app, components, lib, scripts): chatticus/lib/ogImage, components/ui/{badge,button,card,input,avatar,accordion}, lib/utils, components/workspace/{WorkspacePanel,types}, tailwind-preset, styles/shared.css. I checked every path in the root exports map against develop: all exist. Between main and develop the only changes under those paths are four new workspace components and ui/sheet.tsx (additions); the shared files are unchanged, and the exports map in package.json is identical (develop only adds workspaces and devDependencies, which are not installed for a tarball dependency). The UI kit contract is compatible.

What breaks: the website's scripts/generate-wiki.sh (its prebuild) runs python3 -m chatticus.wiki_publish from node_modules/chatticus/python/src, its CI and deploy-marketing-production.yml run pip install -e "node_modules/chatticus/python[wiki]", and its Gherkin runs behave from the Python tree. python/ is deleted on develop, so pip install fails and wiki generation falls back to a possibly absent generated/wiki/pages.json (the script exits 1 if it is missing). A merge to main therefore breaks the website's next build and deploy. Note the same marketing deploy workflow's SSM lookup to the legacy production thin-turn is separate (section 6).

8. Open PRs, branches, CI

Principle: separate three things the current setup fuses: (a) making main carry the TypeScript code, (b) cutting a release, (c) deploying staging and production. The merge (a) should be boring and safe; (c) is a deliberate, human-gated project, as chatticus-89a222 already states ("Production is a separate deploy of a staging-proven release, not a promotion that happens because main moved").

Nothing below runs without the owner's yes. Steps 1 to 6 are reversible and touch no AWS resource.

  1. No branch protection, no environment reviewers (decision, owner, 2026-10-15). The owner chose friction-free: main has no branch protection and the staging and production environments have no required reviewers. The environments keep a deployment branch policy that allows main only. This step is closed; do not add protection or reviewers.
  2. Neutralize push-to-main deploys in a PR into develop (product PR with sub-agent review, then merge): change the staging and production deploy workflows (deploy-web-*, deploy-thinturn-*, deploy-controlplane-*) from on: push: branches: [main] to workflow_dispatch only, and make deploy-auth-* dispatch-only too. Also reconcile the account model: web, auth and thin-turn staging and production should all go through deploy-chatticus-dedicated-account.sh, or the legacy scripts should be removed, so a workflow can never mix them; remove the duplicated overlapping deploys (thinturn, web, controlplane all redeploy control-plane). Cost: staging stops auto-deploying on merge until re-enabled; one dispatch click replaces it. When staging is bootstrapped and stable, re-enable push for staging only (production stays dispatch-only).
  3. Make semantic-release safe. Tag main's current head e68b5f4f as v0.43.1 (baseline; a plain tag push to a new name, owner action, not a main push). Verify with npx semantic-release --dry-run --no-ci on a scratch checkout of the merged result that it proposes 0.44.0. Optionally raise the guard so it checks before publishing (move the 0.* check ahead of the tag push, for example via a --dry-run job whose output gates the real run). Cost: one extra tag; one small workflow change.
  4. Decouple the marketing site (private repo, separate PR there). Before the merge, pin its dependency to the exact Python-era tarball (.../archive/e68b5f4f....tar.gz) so its builds are unchanged, then port or vendor wiki_publish (the anthus-markus wiki publisher) into the website repo and drop pip install -e node_modules/chatticus/python[wiki] and the behave step; then return the dependency to main. Cost: the website stops tracking main until the vendoring lands; the UI kit is unchanged so no feature is lost. Pick the pin or an equivalent on the owner's word (decision D3).
  5. Decide the marketing intake backend (gates production cutover, not the merge). The waitlist and contact endpoints must exist somewhere other than legacy production thin-turn before that stack is replaced. Options: a small Lambda in the website repo that reads and writes the existing WAITLIST and CONTACT items in the Messaging table, or keep legacy production thin-turn alive only for those routes (this violates "no dual paths" if it lingers, so only as a bounded bridge).
  6. Land or close the stragglers: merge #394 (docs), close #347 and #348 with a comment, finish or defer TS-69. Run kbs housekeeping on develop (no PR).
  7. The merge PR. Open develop into main, titled "Promote develop to main: TypeScript control plane" (conventional subject, no breaking marker), with the sub-agent review addressed and required checks green. Body: the section 2 table, the release expectation (v0.44.0), and a statement that no staging or production deploy runs. Merge with a merge commit (main's history so far is merge commits and the v0.43.1 baseline must stay an ancestor); do not squash 691 commits. Expected effects, to verify within minutes: CI green; semantic-release creates v0.44.0 and a changelog commit; no deploy workflow starts.
  8. Bootstrap staging, human-run, one stack at a time, never --all, using the allowed per-stack script with the production account's SSO session (staging and production share it; section 11) (sh infra/deploy-chatticus-dedicated-account.sh staging STACK): the suffixed staging snapshots, computers, thin-turn first (the account's budget is production's, already there); zones; delegate from the management zone and verify with dig (this needs legacy to release staging.chattic.us first); certificates; seed the Google OAuth secret and the OpenAI parameter by hand; control-plane; auth; web. Repoint the staging environment's AWS_DEPLOY_ROLE_ARN to the dedicated account only now (verify with verify-account-deploy-role.yml). Run the migration runbook against staging (migrate-transcripts.ts copy --dry-run, copy, latest-turns, gate close, delta, verify, gate open), then conversation/bin/acceptance.ts --environment staging and a signed-in smoke turn. Cost: this is the real work; it is blocked on hostname release, Google secrets and the OpenAI key.
  9. Production, only after staging is proven, as a separate decision. Same stack order, then migration, then DNS and CloudFront cutover with the marketing intake decision (D4) already implemented. Deploy by manual dispatch. Run acceptance, keep the legacy Python stacks and the old Messaging items untouched for the 14-day window, then the purge.

Rollback

Risks

10. Decisions the owner must make

11. Account topology: development, staging and production share one AWS account

Decision by the project owner (supersedes the "account per environment" wording above): there is no separate development or staging AWS account. Development, staging and production are separate environments in one account, the production account, in us-east-1 (profile chatticus-production). The former separate development account is retired. No account ids appear here; take them from AGENTS.local.md.

The production account is already live: it holds the stacks below under the names they were deployed with, and serves hey.chattic.us and auth.chattic.us from them. Staging and development are the environments added to that account.

Isolation by name: production keeps its live names, development and staging are suffixed

Production keeps every stack id, logical id and resource name it already has. Renaming a live stack would replace its resources (a snapshot bucket, ECR repository, delegated Route 53 zones whose name servers the management zone delegates to, ACM certificates) and change what the management account points at, so nothing in production is renamed. Development and staging, the newcomers, take the suffixed ids, so the three never collide on a CloudFormation stack, an ECR repository, an ECS task definition family, an IAM role or a Cognito pool. One set of id maps in infra/lib/environments.ts and one code path in infra/lib/dedicated-account-stacks.ts build all three; there is no development-only branch. Tests compare the production templates and the development templates of the changed stacks to committed copies (infra/test/fixtures/production-templates, infra/test/fixtures/development-templates), and a pairwise test shows no two environments claim the same account-wide name.

Kind Development Staging Production (live)
Snapshots ChatticusSnapshotsDevelopment ChatticusSnapshotsStaging ChatticusSnapshots
Computers (ECR, ECS, task definition family) ChatticusComputersDevelopment (ChatticusComputerOwnerDevelopment) ChatticusComputersStaging (ChatticusComputerOwnerStaging) ChatticusComputers (ChatticusComputerOwner)
Delegated zones ChatticusEnvironmentZonesDevelopment ChatticusEnvironmentZonesStaging ChatticusEnvironmentZones
Certificates ChatticusEnvironmentCertificatesDevelopment ChatticusEnvironmentCertificatesStaging ChatticusEnvironmentCertificates
Thin-turn, control-plane, web, auth ...Development ...Staging ...Production
Deploy role stack ChatticusAccountDeployDevelopment ChatticusAccountDeployStaging ChatticusAccountDeploy
Deploy role name chatticus-github-actions-deploy-development chatticus-github-actions-deploy-staging chatticus-github-actions-deploy
Integration test role chatticus-github-actions-integration-test-development chatticus-github-actions-integration-test-staging none
GitHub OIDC provider imported by ARN from production's imported by ARN from production's created by ChatticusAccountDeploy
Owner task image tag dev staging production
Hostnames develop.chattic.us, auth-develop.chattic.us staging.chattic.us, auth-staging.chattic.us hey.chattic.us, auth.chattic.us

The production owner task definition and its image tag do not exist in the account yet (the deployed Computers stack predates them), so naming its tag production replaces nothing that runs.

Account-level, one copy in the production account: ChatticusBudgets (its monthly limit is the combined limit of all three environments; per-environment spend comes from the chatticus:environment tag). It already exists, is tagged production, and only production deploys it; the development and staging deploy scripts refuse it, and the development and staging thin-turn and auth stacks read the alerts topic from the production copy. The single GitHub OIDC provider is owned by production's ChatticusAccountDeploy, which has always created it; the development and staging deploy stacks import it. Deleting ChatticusAccountDeploy would delete the provider and lock the other two environments out. CDK bootstrap (CDKToolkit) already exists in the account.

Adding development and staging to the live production account

Production is not bootstrapped again and its ChatticusAccountDeploy stack is not redeployed as part of adding development (owner rule: the production deploy stack is never touched). The repository template for it is ahead of the deployed stack by additive explicit denies of the development stacks, SSM paths and development-tagged resources on production's deploy role (the production fixture ChatticusAccountDeploy.template.json changes by additions only). If someone deploys it later, that adds those denies to production's role and changes nothing else. Each step below is one stack, run by a person with the production account's SSO session, never --all.

  1. For each added environment, sh infra/deploy-chatticus-dedicated-account.sh ENV STACK in this order: snapshots, computers, zones, (delegate both names from the management zone, wait for dig; for development this is the follow-up PR that replaces the retired account's name servers in infra/lib/dns-delegations.ts), certificates, thin-turn, (a person seeds the Google OAuth secret and the OpenAI parameter). There is no budgets step: the account's budget is production's. These first stacks are deployed by an admin session; CI deploys take over once step 2 and the GitHub environment secrets exist.
  2. sh infra/deploy-chatticus-account-deploy.sh development (and staging): the deploy role (chatticus-github-actions-deploy-development) and the integration test role. This comes after thin-turn: createIntegrationTestRole resolves /chatticus/ENV/thin-turn/invoke-key-secret-arn as a CloudFormation SSM parameter at deploy time, and that parameter exists only once ChatticusThinTurn<Env> is deployed. The script also refuses an account without CDKToolkit and production's ChatticusAccountDeploy (which owns the OIDC provider these stacks import). Redeploy ChatticusAccountDeployStaging too: its peer list now includes development (an in-place IAM change). Set the GitHub environment's AWS_NEW_ACCOUNT_DEPLOY_ROLE_ARN and dispatch verify-account-deploy-role.yml.
  3. control-plane (with CHATTICUS_INTEGRATION_TEST_ALLOWED_ROLE_ARN set to step 2's integration test role ARN), auth, web. For development, the retired account's ChatticusWeb and ChatticusAuth stacks must be deleted before auth and web: CloudFront alternate domain names and Cognito custom domains are globally unique.

Production's live stacks are behind the code

cdk diff of the production stacks against the account (template comparison, read-only) shows no replacement and no deletion caused by the naming, and the same differences before and after the staging work: code asset hashes for the Lambda functions and website, the invoke-key origin header, a snapshot bucket lifecycle rule. The deployed ChatticusComputers is older than the code: deploying it removes the old ComputerTask and FargateHost service (desired count 0, no image in the repository) and adds the owner task definition, and deploying ChatticusSnapshots removes an export that the deployed ChatticusComputers still imports. Deploy ChatticusComputers before ChatticusSnapshots, and read both diffs first.

What the shared account does not isolate (known boundary)

Not built here (follow-ups)

Decision D10

Accept the shared bootstrap roles (above: the peer denies are a bound on the OIDC roles, not isolation), or bootstrap the production account twice with distinct qualifiers and give each environment's stacks its own synthesizer.