@drawbridge/drawbridge-agents 0.1.78 → 0.1.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,52 +22,61 @@ The ten lines below are the release, as Darren confirmed them on 2026-10-22. The
22
22
 
23
23
  ## Line 1: start
24
24
 
25
- Read the release task and every subtask through `drawbridge-tests/lib/asana.js` (`api( 'GET', '/tasks/<id>?opt_fields=name,notes,memberships.section.name' )`, then `/tasks/<id>/subtasks`). Subtasks live in "DrawBridge - Application" and stay there. `gh auth status` must pass; if it does not, stop and tell the person `brew install gh && gh auth login`. Other developers start releases from their own machines, so this is never assumed.
25
+ Read the release task and every subtask through `drawbridge-tests/lib/asana.js` (`api( 'GET', '/tasks/<id>?opt_fields=name,notes,memberships.section.name' )`, then `/tasks/<id>/subtasks`). Subtasks live in "DrawBridge - Application" (or the Backlog) and stay there. A release is named with one ice cream, in alphabetical order across releases, nothing else: "Almond", "Butterscotch", "Cherry", "Dulce", "Espresso" exist (Darren, 2026-10-22: no numbers, no weeks, no dates; the alphabet carries the order). The date is the task's due field. If asked to create one, take the next letter after the latest in the Releases project. Run `npx drawbridge-agents-release-doctor` from the api checkout first: it checks git, `gh auth status`, `op` on the beta channel, the api's two env files, `scripts/op.sh`, the npm registry and both clusters, and prints one numbered list of what is missing with the fix for each. Stop on any required miss and hand that list to the person. Other developers start releases from their own machines, so nothing is assumed.
26
26
 
27
27
  ## Line 2: the one list
28
28
 
29
29
  For every subtask, find or write the story (`drawbridge-feature` step 1). Then ONE numbered list to the person who started the release, each item three plain sentences (`conventions/rules.md` rule 7), holding:
30
30
 
31
31
  - every unknown from every story;
32
- - **who on the development team takes the pull requests** (a GitHub username). Asked now so nobody needs to be present between lines 3 and 5. Never guess a reviewer;
33
- - **a package publish, if any subtask names `@drawbridge/components` or `@drawbridge/drawbridge-utils`**, in these words: this release publishes a package; the package is built, reviewed and merged to the package's `main` first; the person publishes it when asked (the skill cannot), the publish needs the one-time password until the automation token (queue task 1218041740718540) exists; the apps' pull requests cannot merge until their pin bumps resolve.
32
+ - **who on the development team takes the pull requests**: their GitHub username (for the pull requests) and their Asana name (for the task). Asked now so nobody needs to be present between lines 3 and 5. Never guess a reviewer;
33
+ - **a package publish, if any subtask names `@drawbridge/components` or `@drawbridge/drawbridge-utils`**, in these words: this release publishes a package; the package is built first and gets its own pull request to the package's `main`; once that merges the person publishes from `main` (`npm run build`, the one-time password, until the automation token, queue task 1218041740718540, exists); the skill then bumps the pin in every consumer and no app pull request opens before the registry has the version. Line 4 step 2 is the exact sequence.
34
34
 
35
- Wait for the yes. Nothing below runs before it.
35
+ Wait for the yes. Nothing below runs before it. A release that publishes a package stops once more, at the publish (line 4 step 2); everything else between lines 3 and 5 runs without anyone present.
36
36
 
37
37
  ## Line 3: Building
38
38
 
39
- 1. Move the task to Building (`POST /sections/1219601033297172/addTask { task }`).
40
- 2. `release/<id>` in every repo a subtask touches, off `develop` (components and utils off `main`; docs off `main`), each in a worktree under `/Users/darrenshea/Projects/.worktrees/<repo>-release-<id>` so no main checkout changes branch.
39
+ 1. **One release at a time.** List the project's tasks (`GET /projects/1219601041167594/tasks?opt_fields=name,memberships.section.name`). If any other release sits in Building, Review, QA or Approved, stop and say which one; only one release is being built or tested at a time (Darren, 2026-10-22). Then move this task to Building (`POST /sections/1219601033297172/addTask { task }`).
40
+ 2. `release/<id>` in every repo a subtask touches, off `develop` (components and utils off `main`), each in a worktree under `/Users/darrenshea/Projects/.worktrees/<repo>-release-<id>` so no main checkout changes branch. **drawbridge-docs is the exception**: its `release/<id>` branch is made in the main docs checkout (docs is `main`-only and single-writer), because `npx drawbridge-agents-check-stories` reads nothing from a worktree path (Asana 1218852240225835). "Repo a subtask touches" includes docs, components and utils, not only the six apps.
41
41
  3. Copy `drawbridge-docs/templates/release.md` to `releases/<id>.md`, fill Scope and Branches (reviewer from line 2), commit on the docs branch. The markdown is the one record; there is no json.
42
- 4. Write the task link into `drawbridge-api/release.json`, commit on the api's `develop` directly ("release <id>: band"), push. Dev shows the yellow band above the account once the api redeploys. This is the only develop commit before the pull requests merge.
43
- 5. If the release task has no subtask named "Marketing: What's new", create one.
42
+ 4. If `drawbridge-api/release.json` does not already carry this task's link, write it, commit on the api's `develop` directly ("release <id>: band"), push; dev shows the yellow band above the account once the api redeploys. This is the only develop commit before the pull requests merge. When the link is already there, say so and move on.
43
+ 5. Move every subtask from Application → Queue to Application → In progress (`POST /sections/1211315620607633/addTask { task }`). In the Application project, Queue means not scheduled and In progress means in a release (Darren, 2026-10-22); Complete comes at line 10. The release task itself lives in the Releases project only, never in Application or the Backlog, because a release draws its subtasks from both (Darren, 2026-10-22: "releases will include application tasks and backlog items, so their own board makes more sense"). A Backlog subtask keeps its Backlog membership; only Application subtasks get the Queue → In progress move.
44
+ 6. **No pull request yet.** No "Marketing: What's new" subtask yet either: it is created, written and completed in one go at line 10, so developers and QA never see an open marketing item among the subtasks they are building and testing (Darren, 2026-10-22: "we should not have the marketing task in a release until it's completed, as it's confusing for the developers and qa people"). Nothing is opened, drafted or assigned at this point; the developer hears nothing until line 5, when the branch is finished. A pull request opened early reads to the developer as "review this now" and there is nothing to review (Darren, 2026-10-22, after the first run).
44
45
 
45
46
  ## Line 4: build, on Local
46
47
 
47
48
  All work on the release branches against the **Local** database. Develop and its schema are never touched while a release is being built, and one release at a time is promoted to develop.
48
49
 
49
- 1. **Local up.** `npm run migrate -- refresh` in the api (copies dev into Local; `--dry` first), then `npm run local` in every app the release touches: dashboard 3000, share 3001, website 3002, api 4000, sync 4001, webhooks 4002. The scripts force the ports; nothing sets them.
50
- 2. **Package first.** A package subtask is built, reviewed and merged to the package's `main` before any app subtask that depends on it; when it is merged, ask the person to publish, then bump the pin on the apps' release branches.
50
+ 1. **Local up.** `npm run migrate -- refresh` in the api (copies dev into Local; `--dry` first), then `npm run local` in every app the release touches: dashboard 3000, share 3001, website 3002, api 4000, sync 4001, webhooks 4002; the components app (`npm run app` in drawbridge-components) is 3003. The scripts force the ports; nothing sets them.
51
+ 2. **Package first, and the publish never blocks.** Every subtask that touches `@drawbridge/drawbridge-utils` or `@drawbridge/components` is built before any app subtask, so the publish is the earliest thing the person is asked for, and it never pauses the build (Darren, 2026-10-22: "I had to wait a while for the publishing code to be written"; "the publish step includes merging each npm package to main, and bumping the pin(s)"). The whole package step, per package, utils before components (components peer-depends on utils):
52
+ 1. Build the package subtasks on `release/<id>` in that package, tests first, `npm test` and `npm run test:app` (components) green, the two review passes.
53
+ 2. Bump the package's own version in its `package.json` (patch), commit, push, and open its pull request to the package's `main` (`gh pr create --base main …`), assigned to the developer from line 2. Put the link on the release task.
54
+ 3. Ask the person for the publish, as one numbered item with the exact words: merge that pull request, then from the package checkout on `main`: `git pull && npm run build` (tsup, tests, `npm publish` with the one-time password), then confirm with `npm view <package> version`.
55
+ 4. **Keep going** on the app subtasks. For one that needs the new package code, `npm link` the local package checkout into the app worktree so it is written and tested against it now.
56
+ 5. When `npm view <package> version` shows the new version (checked between subtasks, never by stopping): bump the pin to that exact version in **every consumer in the family**, not only the release's (Darren, 2026-10-22: "Once a package is published, all consumer pins should also be updated"; pins are version-exact family-wide, `conventions/drawbridge-packages.md`). One command does it: `npx drawbridge-agents-bump <package> <version>` (from any family checkout; `--dry` lists the consumers first) finds every consumer, keeps each one's field, installs the exact version so the lockfile names it, commits "<package> <version>" and pushes each repo's current branch. Before running it, the release's own consumers must have their `release/<id>` branch checked out (the worktrees), so they take the bump there, while every other consumer takes a pin-only commit on its integration branch. Unlink first where a worktree was linked; run the tests after. A sibling package bumped this way is itself published by the same steps before the apps that depend on it.
57
+ 6. No app pull request opens while a pin still names a version the registry does not have.
51
58
  3. **Per subtask**, on the release branch: read the story and the reference page, one graph query, failing test first where a suite exists, the change, `npm test` in that repo. One commit per subtask, its message starting with the subtask id.
52
- 4. **Migration.** If any subtask moves data, ONE file, `drawbridge-api/scripts/migration-<id>.js`, exporting `({ controller }) => async ({ dryRun, organization, ...args }) => counts`, holding every data step. It uses the controller only; a dry run refuses controller writes but cannot refuse a native `collection.drop()` or `dropIndex()`, so a step that drops anything branches on `dryRun` itself and reports what it would drop. Prove it on Local: `npm run migrate -- <id> --dry --env local`, then `--env local`. The runner prints the counts and writes `runs/<env>-<id>-<time>.json`; nothing copies them anywhere.
59
+ 4. **Migration.** If any subtask moves data, ONE file, `drawbridge-api/scripts/migration-<id>.js`, exporting `({ controller }) => async ({ dryRun, organization, ...args }) => counts`, holding every data step. It uses the controller only; a dry run refuses controller writes but cannot refuse a native `collection.drop()` or `dropIndex()`, so a step that drops anything branches on `dryRun` itself and reports what it would drop. Anything a person must do around the run (export a collection and read it back before a drop, take a snapshot) is written into the release doc's Migrations section as the developer's steps, not into the file. Prove it on Local: `npm run migrate -- <id> --dry --env local`, then `--env local`. The runner prints the counts and writes `runs/<env>-<id>-<time>.json`; nothing copies them anywhere.
53
60
  5. **Walk every touched app on Local**: `npm run walk -- --local <path or url>` from drawbridge-tests (dashboard at `localhost:3000`; sync must be running for anything live). Record what was seen; a label quoted later comes from this walk.
54
- 6. **QA agent on Local.** Dispatch `drawbridge-qa` with `target : local`, once per subtask that has a story (story paths, the SUBTASK id, the organization id from `drawbridge-tests/fixtures/e2e-org.md`, present on Local because Local is a copy of dev). Fix every fail on the release branch, redispatch, until every `drawbridge-tests/results/qa/<subtask id>.json` reads `fails : 0`. The fixes are commits in the same pull request; that is why the agent runs here and not on dev (Darren: "all agent work is done locally so the work and fixes get committed as part of the PR").
61
+ 6. **QA agent on Local.** Dispatch `drawbridge-qa` with `target : local`, once per subtask that has a story (story paths, the SUBTASK id, the organization id: `APP_TEST_ORG_ID` in `drawbridge-tests/.env`, the e2e organization `fixtures/e2e-org.md` describes, present on Local because Local is a copy of dev). A subtask spanning many stories gets one report listing them all. Fix every fail on the release branch, redispatch, until every `drawbridge-tests/results/qa/<subtask id>.json` reads `fails : 0`. The fixes are commits in the same pull request; that is why the agent runs here and not on dev (Darren: "all agent work is done locally so the work and fixes get committed as part of the PR").
55
62
  **Inbound webhooks are the exception.** Every Local Environment points `APP_CLIENT_WEBHOOKS_URI` at `http://localhost:4002`, which Shopify, Stripe and Twilio cannot reach, so a change to how a webhook is received or handled is marked in the Scope table as "webhooks: tested on dev" and the QA person checks it on dev in line 7.
56
63
  7. **Review.** `superpowers:code-reviewer` against the stories, then `ponytail-review`, on each repo's diff.
57
64
  8. Push the release branches.
58
65
 
59
66
  ## Line 5: Review
60
67
 
68
+ Only when every repo's release branch is finished: every subtask committed, `npm test` green in each repo, every QA report at `fails : 0`, both review passes done, the branches pushed. A pull request is the request to review, so it exists only once there is something to review.
69
+
61
70
  1. One pull request per repo: `gh pr create --base develop --head release/<id> --title "Release <id>: <task name>" --assignee <developer> --reviewer <developer> --body <the Scope table, the per-subtask commits, the QA reports' paths, and the migration's dev command if there is one>`. Components and utils target `main`.
62
71
  2. Fill the Branches table in `releases/<id>.md` with the PR links; commit.
63
- 3. Put the PR links on the release task as ONE numbered list (`drawbridge-asana-comment`), assign the task to the developer, move it to Review (`POST /sections/1219762451512990/addTask`).
72
+ 3. Attach every QA report's screenshots to the release task (`node lib/asana-attach.js <release id> <png>` from drawbridge-tests, one per scenario, named `<subtask id>-<scenario>.png`), because the reports under `results/qa/` are gitignored and exist only on the machine that ran them. Then put the PR links on the release task as ONE numbered list (`drawbridge-asana-comment`), assign the task to the developer, move it to Review (`POST /sections/1219762451512990/addTask`).
64
73
  4. Stop. Report the PR links.
65
74
 
66
75
  ## Lines 6 to 8: the developer and the QA person
67
76
 
68
77
  The skill does none of this. For the record, so the skill can answer "what is the next step":
69
78
 
70
- - Line 6, the developer: reviews the diff, runs the branch locally, approves and merges. Dev deploys itself. If there is a migration: `npm run migrate -- <id> --dry --env dev` from the api checkout on `develop`, read the printed result, then without `--dry`. Then moves the task to QA and assigns the QA person in Asana.
79
+ - Line 6, the developer: reviews the diff, runs the branch locally, approves and merges. Dev deploys itself. If there is a migration: `npm run migrate -- <id> --dry --env dev` from the api checkout on `develop`, read the printed result, then without `--dry`. The developer may have an agent run those two dev commands on their explicit instruction in their own session (Darren, 2026-10-22); the skill never runs them on its own, and no agent ever runs the production one. Then moves the task to QA and assigns the QA person in Asana.
71
80
  - Line 7, the QA person: tests on dev. The band already names the release.
72
81
  - Line 8, the QA person: moves the task to Approved.
73
82
 
@@ -77,23 +86,23 @@ The skill does none of this. For the record, so the skill can answer "what is th
77
86
 
78
87
  1. Confirm the task sits in Approved; refuse otherwise.
79
88
  2. Hand over the preparation: the window (avoid 00:00 and 02:00 UTC, sync's scheduled jobs) and the Atlas snapshot. Wait.
80
- 3. Per app, in this order unless the release doc says otherwise (dashboard, api, sync, share, webhooks, website): hand over the fast-forward of `main` to the exact tested `develop` commit (`git push origin <sha>:refs/heads/main`), then the Deploy click in DigitalOcean (prod apps deploy from `main` with deploy-on-push off). Verify by reading: the deployment ACTIVE, `/health` reporting the commit. Note the commit in the release doc's Promote section.
81
- 4. Migration, after the api is ACTIVE and before sync: from the developer's api checkout on `main` at the promoted commit, with the api's two-line `.env.production` beside `migrate.js`: `npm run migrate -- <id> --env prod --dry --sha <promoted api commit>`, read the result, then without `--dry`. (The deployed api container can run it too, through `doctl apps console`.) Then `git rm drawbridge-api/scripts/migration-<id>.js`, committed on develop, and `main` fast-forwarded once more.
89
+ 3. Per app, in this order unless the release doc says otherwise (dashboard, api, sync, share, webhooks, website): hand over the fast-forward of `main` to the exact tested `develop` commit (`git push origin <sha>:refs/heads/main`), then the Deploy click in DigitalOcean (prod apps deploy from `main` with deploy-on-push off). Verify by reading: the deployment ACTIVE through the DigitalOcean MCP server registered in Claude Code (`apps-get-deployment-status`; the release doctor checks it is there, and no machine needs doctl), `/health` reporting the commit. Note the commit in the release doc's Promote section.
90
+ 4. Migration, after the api is ACTIVE and before sync: from the developer's api checkout on `main` at the promoted commit, with the api's two-line `.env.production` beside `migrate.js`: `npm run migrate -- <id> --env prod --dry --sha <promoted api commit>`, read the result, then without `--dry`. (The deployed api container can run it too, from the api component's Console in the DigitalOcean dashboard.) Then `git rm drawbridge-api/scripts/migration-<id>.js`, committed on develop, and `main` fast-forwarded once more.
82
91
  5. Hand over the verification list from the release doc's Promote section; the developer reports what they saw; the skill fills it in.
83
92
 
84
93
  ## Line 10: Released
85
94
 
86
- 1. **What's new.** Write the marketing breakdown into the "Marketing: What's new" subtask, in the customer's words, one item per subtask a merchant would notice (`drawbridge-asana-comment` for the shape, `humanizer` for the words).
95
+ 1. **What's new.** Create the "Marketing: What's new" subtask on the release task now (`POST /tasks/<id>/subtasks`), with the marketing breakdown as its description, in the customer's words, one item per subtask a merchant would notice (`drawbridge-asana-comment` for the shape, `humanizer` for the words), and complete it in the same step (`PUT /tasks/<subtask> { completed : true }`). It exists only finished; a release never carries an open marketing subtask.
87
96
  2. Complete every subtask (Application → Complete). Move the release task to Released (`POST /sections/1219601137919036/addTask`).
88
97
  3. Remove the worktrees; refresh the knowledge graph (`drawbridge-ship-feature` step 5). Report the promoted commits.
89
98
 
90
99
  ## What the skill never does
91
100
 
92
- - Push `main`, deploy production, run anything against the production or dev database, publish a package, merge a pull request, assign the QA person or move the task to QA, Approved. Those are a developer's or a tester's lines.
93
- - Name a reviewer, a tester, a window or a release scope that nobody told it. Each is one numbered question in line 2.
101
+ - Push `main`, deploy production, write to the production database, publish a package, merge a pull request, assign the QA person or move the task to QA or Approved. Those are a developer's or a tester's lines. On dev the skill writes nothing of its own either (the refresh only READS dev to copy it into Local); the dev migration runs on a developer's explicit instruction, by the developer or by an agent they ask.
102
+ - Name a reviewer or a release scope that nobody told it; both are asked in line 2. The tester is the developer's choice at line 6 and the window is the developer's at line 9; the skill asks for neither.
94
103
  - Write a release document, branch or migration under any name but the Asana id.
95
104
  - Build or run the QA agent against dev or develop before the pull requests merge. Local only.
96
105
 
97
106
  ## Section ids
98
107
 
99
- Planned `1219601041167595`, Building `1219601033297172`, Review `1219762451512990`, QA `1219601033155111`, Approved `1219601033299066`, Released `1219601137919036`; project `1219601041167594`, workspace `15457086713130`.
108
+ Releases project `1219601041167594` (workspace `15457086713130`): Planned `1219601041167595`, Building `1219601033297172`, Review `1219762451512990`, QA `1219601033155111`, Approved `1219601033299066`, Released `1219601137919036`. Application project `1207845659550631`, where the subtasks live: Queue `1207845659550632` (not scheduled), In progress `1211315620607633` (in a release), Complete `1213887707425532`.
package/bin/bump.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // Bump one @drawbridge package to an exact version in EVERY consumer in the family,
3
+ // lockfile included, one commit per repo on its current branch, pushed.
4
+ //
5
+ // npx drawbridge-agents-bump @drawbridge/components 0.1.6
6
+ // npx drawbridge-agents-bump @drawbridge/drawbridge-agents 0.1.80 --no-push
7
+ // npx drawbridge-agents-bump @drawbridge/drawbridge-utils 0.0.222 --only drawbridge-api,drawbridge-sync
8
+ // … --dry list the consumers and stop
9
+ //
10
+ // Pins are version-exact family-wide (conventions/drawbridge-packages.md), so a
11
+ // publish is not finished until every consumer names it: the release skill runs
12
+ // this the moment the registry has the version (Darren, 2026-10-22: "Once a
13
+ // package is published, all consumer pins should also be updated"). A consumer's
14
+ // own place for the package (dependencies, devDependencies, peerDependencies) is
15
+ // kept. Bumping the agents package also mirrors its template into .claude.
16
+ const { execFileSync } = require('child_process')
17
+ const fs = require('fs')
18
+ const path = require('path')
19
+
20
+ const FLAGS = { dependencies: '--save', devDependencies: '--save-dev', peerDependencies: '--save-peer' }
21
+
22
+ // Which repos pin `name`, and under which field. `manifests` is { repoName: packageJson }.
23
+ const consumersOf = (manifests, name) => Object.entries(manifests)
24
+ .map(([ repo, pkg ]) => {
25
+ const field = Object.keys(FLAGS).find((key) => pkg && pkg[key] && pkg[key][name])
26
+ return field ? { repo, field, current: pkg[field][name] } : null
27
+ })
28
+ .filter(Boolean)
29
+ .sort((a, b) => a.repo.localeCompare(b.repo))
30
+
31
+ const saveFlag = (field) => FLAGS[field]
32
+
33
+ // A pin lands on an integration branch or the release's own branch, never on a
34
+ // stray feature branch someone left checked out (growth-b sat on ship-main).
35
+ const takesBump = (branch) => branch === 'develop' || branch === 'main' || branch.startsWith('release/')
36
+
37
+ const readManifests = (projectsDir) => Object.fromEntries(
38
+ fs.readdirSync(projectsDir)
39
+ .filter((entry) => entry.startsWith('drawbridge-') && fs.existsSync(path.join(projectsDir, entry, 'package.json')) && fs.existsSync(path.join(projectsDir, entry, '.git')))
40
+ .map((entry) => {
41
+ try { return [ entry, JSON.parse(fs.readFileSync(path.join(projectsDir, entry, 'package.json'), 'utf8')) ] } catch (error) { return [ entry, null ] }
42
+ })
43
+ )
44
+
45
+ const run = (cwd, cmd, args) => execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'pipe' ] }).trim()
46
+
47
+ const main = () => {
48
+ const argv = process.argv.slice(2)
49
+ const [ name, version ] = argv
50
+ const dry = argv.includes('--dry')
51
+ const push = !argv.includes('--no-push')
52
+ const onlyIndex = argv.indexOf('--only')
53
+ const only = onlyIndex === -1 ? null : new Set(argv[onlyIndex + 1].split(','))
54
+
55
+ if (!name || !version || !/^\d+\.\d+\.\d+/.test(version)) {
56
+ console.error('usage: drawbridge-agents-bump <@drawbridge/package> <exact version> [--dry] [--no-push] [--only repo,repo]')
57
+ process.exit(1)
58
+ }
59
+
60
+ const projectsDir = path.resolve(process.cwd(), '..')
61
+ const consumers = consumersOf(readManifests(projectsDir), name).filter((c) => !only || only.has(c.repo))
62
+
63
+ if (!consumers.length) {
64
+ console.error('no consumer of ' + name + ' under ' + projectsDir)
65
+ process.exit(1)
66
+ }
67
+
68
+ for (const { repo, field, current } of consumers) {
69
+ const dir = path.join(projectsDir, repo)
70
+ const branch = run(dir, 'git', [ 'branch', '--show-current' ])
71
+ if (current === version) { console.log(repo + ': already ' + version + ' (' + field + ', ' + branch + ')'); continue }
72
+ if (!takesBump(branch)) { console.log(repo + ': skipped, on ' + (branch || 'a detached head') + ' (check out develop, main or release/<id> first)'); continue }
73
+ if (dry) { console.log(repo + ': ' + current + ' -> ' + version + ' (' + field + ', ' + branch + ')'); continue }
74
+ run(dir, 'npm', [ 'install', '--save-exact', saveFlag(field), name + '@' + version, '--no-audit', '--no-fund', '--loglevel=error' ])
75
+ const files = [ 'package.json', 'package-lock.json' ]
76
+ if (name === '@drawbridge/drawbridge-agents') {
77
+ try { run(dir, 'npx', [ 'drawbridge-agents-sync' ]) } catch (error) { /* the mirror is best-effort; the pin still lands */ }
78
+ files.push('.claude')
79
+ }
80
+ run(dir, 'git', [ 'add', '-A', ...files.filter((f) => fs.existsSync(path.join(dir, f))) ])
81
+ if (!run(dir, 'git', [ 'status', '--porcelain', '--', ...files ])) { console.log(repo + ': nothing to commit'); continue }
82
+ run(dir, 'git', [ 'commit', '-q', '-m', name.replace('@drawbridge/', '') + ' ' + version ])
83
+ if (push) run(dir, 'git', [ 'push', '-q', 'origin', branch ])
84
+ console.log(repo + ': ' + current + ' -> ' + version + ' on ' + branch + (push ? ', pushed' : ''))
85
+ }
86
+ }
87
+
88
+ module.exports = { consumersOf, readManifests, saveFlag, takesBump }
89
+
90
+ if (require.main === module) main()
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // Everything a release needs on this machine, checked in one go before line 1 of
3
+ // the release skill, printed as ONE numbered list of what is missing with the fix
4
+ // for each. Exit 1 when anything required is missing, so a developer on a new
5
+ // machine fails in ten seconds, not an hour in.
6
+ //
7
+ // npx drawbridge-agents-release-doctor
8
+ //
9
+ // Required: git, gh signed in, op on the beta channel (--environment), the api's
10
+ // .env.local and .env.development naming a 1Password Environment, the Local and
11
+ // dev clusters resolving, the npm registry reachable, the DigitalOcean MCP server
12
+ // registered in Claude Code. Warned, not required: graphify (the read-before-write
13
+ // gate skips it when absent).
14
+ const { execFileSync } = require('child_process')
15
+ const dns = require('dns')
16
+ const fs = require('fs')
17
+ const os = require('os')
18
+ const path = require('path')
19
+
20
+ process.env.PATH = [ path.join(os.homedir(), '.local', 'bin'), '/opt/homebrew/bin', '/usr/local/bin', process.env.PATH || '' ].join(path.delimiter)
21
+
22
+ const sh = (cmd, args) => { try { return execFileSync(cmd, args, { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'pipe' ] }) } catch (error) { return null } }
23
+
24
+ // A two-line env file: OP_SERVICE_ACCOUNT_TOKEN and OP_ENVIRONMENT_ID, both with a value.
25
+ const envFileOk = (text) => {
26
+ const keys = {}
27
+ for (const line of String(text || '').split('\n')) {
28
+ const match = line.match(/^\s*([A-Z_]+)\s*=\s*['"]?([^'"\s]*)['"]?\s*$/)
29
+ if (match) keys[match[1]] = match[2]
30
+ }
31
+ return Boolean(keys.OP_SERVICE_ACCOUNT_TOKEN && keys.OP_ENVIRONMENT_ID)
32
+ }
33
+
34
+ // Deployments are read through the DigitalOcean MCP server registered in Claude Code
35
+ // at user scope (~/.claude.json, `mcpServers.digitalocean`), the way Darren's machine
36
+ // has it; every developer's machine is set up to match (Darren, 2026-10-22). doctl is
37
+ // not used anywhere in the flow.
38
+ const readClaudeConfig = () => { try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8')) } catch (error) { return null } }
39
+ const hasDigitalOcean = (config) => Boolean(config && config.mcpServers && config.mcpServers.digitalocean && config.mcpServers.digitalocean.url)
40
+
41
+ // The printed list: one numbered line per failure, each with its fix; nothing when all pass.
42
+ const report = (results) => {
43
+ const failed = results.filter((r) => !r.ok)
44
+ if (!failed.length) return 'release doctor: everything present (' + results.length + ' checks).'
45
+ return 'release doctor: ' + failed.length + ' of ' + results.length + ' missing.\n' + failed.map((r, i) => (i + 1) + '. ' + r.name + (r.required ? '' : ' (optional)') + ': ' + r.fix).join('\n')
46
+ }
47
+
48
+ const checks = (projectsDir) => {
49
+ const api = path.join(projectsDir, 'drawbridge-api')
50
+ const read = (file) => { try { return fs.readFileSync(path.join(api, file), 'utf8') } catch (error) { return null } }
51
+ return [
52
+ { name: 'git', required: true, ok: Boolean(sh('git', [ '--version' ])), fix: 'install Xcode command line tools' },
53
+ { name: 'gh signed in', required: true, ok: Boolean(sh('gh', [ 'auth', 'status' ])), fix: 'brew install gh && gh auth login' },
54
+ { name: 'op with --environment (beta channel)', required: true, ok: /--environment/.test(sh('op', [ 'run', '--help' ]) || ''), fix: 'install the 1Password CLI beta: see drawbridge-growth/scripts/op.sh for the curl line' },
55
+ { name: 'drawbridge-api/.env.local', required: true, ok: envFileOk(read('.env.local')), fix: 'two lines, OP_SERVICE_ACCOUNT_TOKEN and OP_ENVIRONMENT_ID, naming the Local Environment' },
56
+ { name: 'drawbridge-api/.env.development', required: true, ok: envFileOk(read('.env.development')), fix: 'two lines naming the Development Environment' },
57
+ { name: 'drawbridge-api/scripts/op.sh', required: true, ok: fs.existsSync(path.join(api, 'scripts', 'op.sh')), fix: 'pull develop in drawbridge-api' },
58
+ { name: 'npm registry', required: true, ok: Boolean(sh('npm', [ 'view', '@drawbridge/drawbridge-agents', 'version' ])), fix: 'check the network or npm login' },
59
+ { name: 'DigitalOcean MCP server in Claude Code', required: true, ok: hasDigitalOcean(readClaudeConfig()), fix: 'claude mcp add --scope user --transport http digitalocean https://apps.mcp.digitalocean.com/mcp --header "Authorization: Bearer <your DigitalOcean API token>" (the skill reads deployments through it; no doctl)' },
60
+ { name: 'graphify', required: false, ok: Boolean(sh('graphify', [ '--version' ])), fix: 'npm run sync installs it; or DRAWBRIDGE_SKIP_GRAPHIFY=1' }
61
+ ]
62
+ }
63
+
64
+ const resolves = (host) => dns.promises.resolveSrv('_mongodb._tcp.' + host).then(() => true, () => false)
65
+
66
+ const main = async () => {
67
+ const projectsDir = path.resolve(process.cwd(), '..')
68
+ const results = checks(projectsDir)
69
+ results.push({ name: 'Local cluster resolves', required: true, ok: await resolves('cluster0.sbftndf.mongodb.net'), fix: 'the Local Atlas cluster is paused or gone; resume it in Atlas' })
70
+ results.push({ name: 'dev cluster resolves', required: true, ok: await resolves('cluster0.1iourxk.mongodb.net'), fix: 'check the network' })
71
+ console.log(report(results))
72
+ process.exit(results.some((r) => r.required && !r.ok) ? 1 : 0)
73
+ }
74
+
75
+ module.exports = { checks, envFileOk, hasDigitalOcean, report }
76
+
77
+ if (require.main === module) main()
@@ -20,12 +20,14 @@ skill) so it stops living only in tribal memory.
20
20
  (fails closed). Deploy order matters: ship app-web **before** api, or the api blocks every
21
21
  signin that arrives without a token. See `drawbridge-docs/user-stories/03-auth`.
22
22
  - `BASIC_AUTH_USERNAME` + `BASIC_AUTH_PASSWORD` is the shared basic-auth staging-gate pair read
23
- by **drawbridge-website** (`middleware.js`) and **drawbridge-app-web** (`proxy.js`) — same
24
- names in both so one credential pair gates both dev deployments. The gate arms only when
25
- **both** are set (fails open: a missing/misnamed var silently leaves the deployment public —
26
- unset both in prod, set both in dev). Exempt: app-web `/api/health` (platform health checks)
27
- and website Builder preview traffic. Not related to OAuth/session auth, which runs after the
28
- gate unchanged.
23
+ by **drawbridge-website** (`proxy.js`), **drawbridge-app-web** (`proxy.js`) and, since
24
+ 2026-10-22, **drawbridge-components** (`server.js`, the components app at
25
+ components.trydrawbridge.com, where the pair is set directly on the DO app rather than
26
+ through a 1Password Environment) — same names everywhere so one credential pair gates
27
+ all three. The gate arms only when **both** are set (fails open: a missing/misnamed var
28
+ silently leaves the deployment public — unset both in prod, set both in dev). Exempt:
29
+ app-web `/api/health`, components `/health` (platform health checks) and website Builder
30
+ preview traffic. Not related to OAuth/session auth, which runs after the gate unchanged.
29
31
 
30
32
  ## Sync owns reactive side effects
31
33
 
@@ -1,6 +1,6 @@
1
1
  # Migrations live in the api
2
2
 
3
- Every one-off migration and backfill goes in `drawbridge-api/scripts.js`.
3
+ Every one-off migration and backfill is one file in `drawbridge-api/scripts/`, `migration-<release asana id>.js`, run through `drawbridge-api/migrate.js` and nothing else.
4
4
 
5
5
  **The only exception is media processing** — work that needs ffmpeg or sharp to
6
6
  open an image or a video. Those dependencies exist in `drawbridge-sync` and
@@ -46,23 +46,31 @@ quietly that dev served stale code the whole time.
46
46
  return (`{ found, updated }` or whatever the counts are). The first run of a
47
47
  migration is a read.
48
48
  - Run it only through the runner. On a laptop, from the api checkout:
49
- `npm run migrate -- <asana id> --dry --env dev`, then
50
- `--organization <id> --env dev --release <slug>`, then `--env dev --release <slug>`.
49
+ `npm run migrate -- <release asana id> --dry --env dev`, then
50
+ `--organization <org id> --env dev`, then `--env dev`. There is no `--release`
51
+ flag: the migration id IS the release id.
51
52
  No env file holds a value: the runner re-runs itself through `scripts/op.sh`
52
53
  with the file `--env` names (`.env.local` / `.env.development`, two lines naming
53
54
  a 1Password Environment), so the Environment is injected the way the servers
54
55
  get it. It refuses the wrong cluster, hands a dry run a controller that throws
55
56
  on any write, and writes the result to `runs/<env>-<fn>-<time>.json` (read the
56
57
  file, never the console: `op run` conceals anything that looks like a secret).
57
- - **Production runs inside the deployed api container**, never from a laptop,
58
- so no laptop ever holds a production file: `doctl apps console <prod api app
59
- id> drawbridge-api`, then `op run --environment "$OP_ENVIRONMENT_ID" -- node
60
- migrate.js <release asana id> --env prod --dry --sha <promoted commit>`, then the
61
- same without `--dry`. The image carries `migrate.js` and `scripts/`. There is no git there, so the runner proves `--sha`
62
- against `APP_COMMIT_HASH` (the deployed commit, injected by the app spec); in
63
- a checkout it still refuses production off `main`, at the wrong commit, or with
64
- `scripts/`, `lib/` or `migrate.js` modified. `cat` the counts file from the
65
- console shell afterwards, outside `op run`, to read it unconcealed.
58
+ - **Production is run by a developer, by hand, from their api checkout** on
59
+ `main` at the promoted commit with a clean tree, with the api's two-line
60
+ `.env.production` beside `migrate.js` (Darren, 2026-10-22: "the api is the
61
+ only place that needs this, and locally it's there"):
62
+ `npm run migrate -- <release asana id> --env prod --dry --sha <promoted commit>`,
63
+ then the same without `--dry`. The runner refuses production off `main`, at
64
+ the wrong commit, or with `scripts/`, `lib/` or `migrate.js` modified. The
65
+ deployed api container can run it too (the api component's Console in the
66
+ DigitalOcean dashboard, then the `op run --environment "$OP_ENVIRONMENT_ID" --
67
+ node migrate.js …` form; nobody installs doctl for this; the image carries `migrate.js` and `scripts/`, there is no
68
+ git, so `--sha` is proven against `APP_COMMIT_HASH`). An agent never runs it:
69
+ it hands the developer the command and verifies by reading afterwards. The
70
+ dev run is different: a developer may ask an agent in their own session to run
71
+ the two `--env dev` commands and show the result, since the guard admits only
72
+ the dev cluster whoever types it (Darren, 2026-10-22). Never unasked, and
73
+ never production.
66
74
  - **The runner records nothing but its counts file**, and nothing else records
67
75
  a run either: a developer runs the migration on dev after the pull request
68
76
  merges and reads the printed result, and runs it on production from their
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -16,7 +16,9 @@
16
16
  "drawbridge-agents-graph": "bin/graph.js",
17
17
  "drawbridge-agents-check-docs": "scripts/check-doc-links.js",
18
18
  "drawbridge-agents-check-stories": "scripts/check-stories.js",
19
- "drawbridge-agents-check-streams": "scripts/check-streams.js"
19
+ "drawbridge-agents-check-streams": "scripts/check-streams.js",
20
+ "drawbridge-agents-bump": "bin/bump.js",
21
+ "drawbridge-agents-release-doctor": "bin/release-doctor.js"
20
22
  },
21
23
  "files": [
22
24
  "claude",