@drawbridge/drawbridge-agents 0.1.59 → 0.1.61

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.
@@ -1,102 +1,246 @@
1
1
  ---
2
2
  name: drawbridge-add-connection
3
- description: Use when adding a vendor connection (a new manifest in drawbridge-utils), changing one's auth type or scopes, or adding a hook, link, or step to an existing manifest and when asked to check an existing connection against the connection checklist.
3
+ description: Use for ANY work touching connections, vendors or hooks in drawbridge-utils — adding or checking a manifest, adding a vendor or its credentials, changing auth shape or scopes, adding a hook/link/step, or moving a vendor's API calls out of another repo. Also use before answering questions about how connections work.
4
4
  ---
5
5
 
6
- # Add a connection
6
+ # Connections
7
7
 
8
- A connection is one manifest in `drawbridge-utils/lib/connections/providers/`.
9
- The manifest is the only place vendor facts live, and it carries its own
10
- proof: a `review` key the contract test refuses to accept incomplete. This
11
- skill is the order of work that fills it honestly.
8
+ **Invoke this before touching a manifest, a vendor file, or any code that calls a
9
+ vendor's API. Not after. Darren, 2026-09-13: "never make a connection without
10
+ using that skill."**
12
11
 
13
- **A manifest ships complete: every hook the vendor can serve, every link the
14
- dashboard exposes, every scope those need, copy humanized, walked on dev.
15
- Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do
16
- this right, our production launch deadline is somewhat flexible." If the work
17
- does not fit the time, the time moves.**
12
+ ## Which job is this?
13
+
14
+ | What you are doing | Go to |
15
+ |---|---|
16
+ | adding a whole new vendor connection | **A. New connection** |
17
+ | adding credentials for a vendor | **B. Vendor** |
18
+ | a repo calls a vendor's API directly | **C. Vendor code outside a manifest** |
19
+ | checking an existing connection | **D. Check** |
20
+
21
+ They share one law, and it is the reason this file exists.
22
+
23
+ ## THE LAW
24
+
25
+ **A vendor's facts live on its manifest. Nothing else.** Not a constant in
26
+ drawbridge-sync, not a client in drawbridge-api, not an env var read at a call
27
+ site. If a line of code knows a vendor's endpoint, header, payload shape, error
28
+ string or id format, it belongs in a hook.
29
+
30
+ **A vendor is NAMED by the connection that spends it.** `vendors : [ … ]` on the
31
+ manifest. That is what makes the provider screen able to say what a credential is
32
+ for, and what makes `vendorSettings()` hand a hook its fields.
33
+
34
+ **Grep before you invent.** A key, a field, a hook or a collection that already
35
+ has a shape must reuse it. Read the contract and the scaffold first, every time.
36
+
37
+ **A hook writes no prose.** (Darren, 2026-09-18, after 66 sentences came out of
38
+ the manifests.) A success answers `request` and `response` and NO `message` —
39
+ nobody is shown one. A skip or failure keeps ONE short reason, the vendor's own
40
+ error text where there is one. A missing setting, credential or input is not a
41
+ sentence: declare it with `expects({ settings : { audience : yup.string().required() } }, hook)`
42
+ from `../contract.js` and the shell answers "Audience is a required field"; a
43
+ vendor answer without an id is `yup.object({ id : yup.string().required() }).validateSync(...)`,
44
+ a fault that retries. Every health check says exactly `'Health check passed.'`.
45
+ The reconnect card is `disconnected( name )` from the contract. A field `message`
46
+ that restates its label is deleted. yup comes from `../../yup.js`, never `'yup'`.
47
+
48
+ ## STOP — the three mistakes that have actually been made
49
+
50
+ Each of these was made by someone who had read this file. They are here as
51
+ gates, not trivia.
52
+
53
+ **1. Inventing a key instead of using the mechanism.** A vendor card said nothing
54
+ about what its credentials were for, so a `usage` key was added to the vendor
55
+ scaffold and to all eight vendor files — when the answer was that the manifest
56
+ that spends it had not named it in `vendors`. The screen derives that. One list
57
+ entry, no new key. *(2026-09-13. Earlier: a flat `provider` string was invented
58
+ when `provider : { slug, id }` was already in five collections.)*
18
59
 
19
- ## Steps
60
+ > **Before adding any key, answer out loud: what already answers this? If the
61
+ > answer is "a manifest would, if it declared the vendor / hook / field it
62
+ > already can", do that instead.**
63
+
64
+ **2. "This vendor has no connection, so it has no manifest."** Bright Data was
65
+ written as a vendor nothing named, on the grounds that no merchant links a Bright
66
+ Data account. Neither does anyone link our SendGrid account. **Who a credential
67
+ belongs to is not who spends it.** Drawbridge's own vendors are spent by the
68
+ `drawbridge` manifest and named in its `vendors` list — SendGrid, Twilio, HubSpot,
69
+ Bright Data. A vendor no manifest names is a bug, not a category.
70
+
71
+ **3. Leaving the vendor's calls behind.** Bright Data became a provider row and
72
+ its API calls stayed in `drawbridge-sync/lib/scrape.js` for another day — an
73
+ endpoint constant, a bearer header and a request body inline in a 3,798-line
74
+ file. Moving the credential is half the job. **The calls are the job.**
75
+
76
+ ## A. New connection
77
+
78
+ **Copy `_scaffold.js`. Answer every key. Delete nothing.**
79
+ `drawbridge-utils/lib/connections/_scaffold.js` is three things at once: the file
80
+ you copy, the reference for what each key may hold, and **the schema `build()`
81
+ reads**. A manifest that omits a key or invents one cannot be imported — it fails
82
+ before any test runs, in every repo that pulls the package, naming the key.
83
+
84
+ So "did I miss something" is answered by the suite. What follows is judgement,
85
+ which a schema cannot check.
86
+
87
+ **A manifest ships complete: every hook the vendor can serve, every link its
88
+ dashboard exposes, every scope those need, copy humanized, walked on dev.
89
+ Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do this
90
+ right, our production launch deadline is somewhat flexible." If the work does not
91
+ fit the time, the time moves.**
20
92
 
21
93
  1. **Read the vendor twice, and write down where.** Fetch, never recall — a
22
- consent url once written from memory pointed at the api host and stalled
23
- every merchant on a blank page.
94
+ consent url written from memory pointed at the api host and stalled every
95
+ merchant on a blank page.
24
96
  - **API reference**: auth mechanisms and who the vendor says each is for
25
97
  (your own account vs accounts outside your organization — Drawbridge is
26
- always the second), partner or public-app program, OAuth urls and
98
+ always the second), partner or public-app program, OAuth endpoints and
27
99
  transport, token lifetime, refresh, every endpoint a hook will call.
28
- - **Dashboard docs** (help centre, app UI): the page url for each object
29
- we will hold an id for (list, segment, tag, contact, order), where the
30
- vendor says the id appears, what the vendor calls each thing, and what a
31
- disconnect actually does on their side. This read fills `urls.*` and the
32
- merchant copy. A vendor whose UI urls are not documented gets an
33
- observed form, marked observed, confirmed on dev before promote.
34
- `review.api` and `review.dashboard` take the urls read.
35
-
36
- 2. **Scope for the whole manifest, not today's hooks.** List every scope the
37
- vendor offers with its url in `review.scopes`, then request every scope any
38
- hook in the contract could need for this vendor contacts, segments,
39
- tags, custom fields, lists, webhooks whether or not that hook ships
40
- now. Scopes are app-level at Klaviyo and Attentive: adding one later costs
41
- every connected merchant a re-consent. "Minimum for what I built today" is
42
- the wrong minimum.
43
-
44
- 3. **Pick `auth.type` from the vendor's own framing:**
45
-
46
- | vendor's shape | type |
100
+ - **Dashboard docs**: the page url for each object we hold an id for, where
101
+ the vendor says that id appears, the vendor's own names for things, and what
102
+ a disconnect really does on their side. This read fills `urls.*` and the
103
+ merchant copy.
104
+
105
+ Both urls go on **the vendor file's `urls`**, beside its `scopes` link.
106
+ `build()` throws if a `review` key reappears on a manifest.
107
+
108
+ 2. **Scope for the whole manifest, not today's hooks.** Request every scope any
109
+ hook in the contract could need for this vendor, whether or not it ships now.
110
+ Scopes are app-level at Klaviyo and Attentive: adding one later costs every
111
+ connected merchant a re-consent. Declare them in `auth.oauth.scopes`
112
+ **without it a vendor cannot detect drift**, which is the bug this whole
113
+ rework came out of.
114
+
115
+ 3. **The auth SHAPE says which kind it is. There is no `auth.type` to pick.**
116
+ All five options are present and exactly one is non-false:
117
+
118
+ | the vendor's shape | the truthy key |
47
119
  |---|---|
48
- | installed from the vendor's app store, never redirects back | `install` |
49
- | registered app, merchant consents on the vendor's screen | `oauth` |
50
- | the vendor offers ONLY per-account credentials | `keys` |
51
- | no third party behind it at all | `generated` |
52
-
53
- `keys` is only for a vendor that offers nothing else to third parties.
54
- Where OAuth exists, build OAuth; a registration that has not landed ships
55
- the manifest dormant (`requires` names the client vars), never as keys.
56
-
57
- 4. **Implement every hook the vendor can serve.** The only `false` is "the
58
- vendor has no endpoint for this", with the one-line reason and the page
59
- that shows the absence. Time is never a reason. "Flip later" is how segment
60
- labels shipped as tags with no id kept. Every `urls` entry the dashboard
61
- read found is implemented too.
62
-
63
- 5. **Write the merchant copy from the two reads, then run the humanizer.**
64
- Excerpt, description, guide, confirm, error copy: the vendor's own names
65
- for things, the guide's navigation path from the help centre, and what
66
- disconnect really does. Then invoke `humanizer:humanizer` on the copy
67
- before commit and put the date in `review.content`. Not "if time".
68
-
69
- 6. **Build against the checklist** in
70
- `drawbridge-docs/reference/connection-hooks.md` → "Adding a connection".
71
- Its guards are the enforcement: directory registration, feature grant,
72
- `build()`, the `review` contract test. Trust a named failure over your
73
- memory of the steps.
74
-
75
- 7. **Walk it on dev, then date `review.verified`.** Connect a real account,
76
- pick the resource, sync one contact and one opted-out contact, open each
77
- `urls.*` link. `verified` is `null` on the publish that first carries the
78
- manifest (the walk needs it deployed) and dated on the next; a manifest
79
- still `null` at promote does not promote. A green suite is not the walk.
80
-
81
- ## Checking an existing connection
82
-
83
- Same steps, read as questions: is each `review` entry a real url or date, do
84
- the requested scopes cover every hook the contract lists, does each `urls.*`
85
- entry exist and open, was the copy humanized. A gap is a manifest fix, not a
86
- note.
120
+ | installed from the vendor's app store, never redirects back | `auth.install` |
121
+ | registered app, merchant consents on the vendor's screen | `auth.oauth` |
122
+ | the vendor offers ONLY per-account credentials | `auth.keys` |
123
+ | Drawbridge mints the credential, no third party | `auth.generated` |
124
+ | nothing to authenticate | `auth.none` |
125
+
126
+ The other four are `false` **carrying the reason**. `type` is INJECTED by
127
+ `build()` from the shape; leave it out a declared `type` that disagrees
128
+ with the shape throws.
129
+
130
+ 4. **Implement every hook the vendor can serve.** The only `false` is "the vendor
131
+ has no endpoint for this", with the reason and the page that shows the absence.
132
+ Time is never a reason.
133
+
134
+ **`lifecycle.health` is REQUIRED of every `oauth` and `install` connection** —
135
+ both end in a token somebody else can revoke, expire or narrow, and none of
136
+ those tells us.
137
+
138
+ **A hook describes its effects; it never performs them.** It receives
139
+ `( props, options )` and returns the answer envelope; `perform()` does the
140
+ writing. **Read `lib/connections/_hooks.js` before writing your first one** —
141
+ it carries a complete annotated hook and runs it.
142
+
143
+ 5. **Write the merchant copy from the two reads, then run `humanizer:humanizer`
144
+ on it before commit.** Excerpt, description, guide, confirm, error copy: the
145
+ vendor's own names, the help centre's navigation path, what disconnect really
146
+ does. `content.errors` names the vendor in both arms — a merchant connecting
147
+ two accounts in one sitting needs to know which one failed.
148
+
149
+ 6. **Let the enforcement check you.** `build()` refuses, at import: a missing or
150
+ invented top-level key, a missing hook, a hook prop its slot does not carry, an
151
+ unknown trigger, an oauth vendor declining `lifecycle.health`, two truthy auth
152
+ kinds, a step whose type disagrees with its position. Trust a named failure
153
+ over your memory of these steps.
154
+
155
+ 7. **Walk it on dev.** Connect a real account, pick the resource, sync one contact
156
+ and one opted-out contact, break the grant and confirm the error reaches the
157
+ card, the page and the checklist, reconnect and confirm it clears *immediately*,
158
+ then open each `urls.*` link. **A green suite is not the walk.** Record it in
159
+ the release doc for the change that carries the manifest — there is no
160
+ `review.verified` any more.
161
+
162
+ ## B. Vendor
163
+
164
+ One vendor, one file: `lib/connections/vendors/<vendor>.js`, carrying `fields`,
165
+ `icon`, `name`, `slug` and `urls`. However many connections spend it.
166
+
167
+ **Then name it.** Add the slug to the `vendors` list of every manifest that
168
+ spends it. **This is not optional bookkeeping** — it is what makes the provider
169
+ screen say who spends the credential, and what makes `vendorSettings({ slug })`
170
+ return the fields to that connection's hooks.
171
+
172
+ - A vendor merchants connect → named by that connection (`klaviyo`, `shopify`).
173
+ - **A vendor DRAWBRIDGE spends → named by the `drawbridge` manifest**, beside
174
+ SendGrid, Twilio, HubSpot and Bright Data. There is no such thing as a vendor
175
+ with no connection; there are vendors no MERCHANT connects.
176
+ - Each field's `credential` names the env var it falls back to. That is the only
177
+ place that name is written — `requires` derives from it, and so does the test
178
+ env fixture.
179
+
180
+ A vendor that is named by nothing renders a credential form saying nothing about
181
+ what it powers. If you find yourself wanting a key to explain a vendor, you have
182
+ found an unnamed vendor instead.
183
+
184
+ ## C. Vendor code outside a manifest
185
+
186
+ You found an endpoint, a token header, a payload shape or a vendor's error string
187
+ in drawbridge-sync, drawbridge-api, drawbridge-webhooks or anywhere else.
188
+
189
+ **It becomes a hook.** Not "later", not "it has no connection", not "it needs a
190
+ dependency the package cannot have".
191
+
192
+ 1. **Which manifest spends this vendor?** If a merchant connects it, theirs. If
193
+ Drawbridge pays for it, `drawbridge`. Name it in `vendors` if it is not there.
194
+ 2. **Which domain?** Read `HOOKS` in `contract.js` first. If nothing fits, add a
195
+ domain — with the reasoning for why it is not one of the existing ones, and
196
+ slots named for what every vendor of that kind does rather than for what this
197
+ one calls it. Every other manifest then answers the new domain `false` with a
198
+ reason; `build()` will not let you forget one.
199
+ 3. **Split vendor from Drawbridge, explicitly.** This is the judgement call and
200
+ it is the whole design. The vendor's facts move; our policy stays. From the
201
+ Bright Data move: the endpoint and the unblocking request moved; the tier
202
+ ladder that chooses between renderers, and the extraction that runs on whatever
203
+ comes back, did not.
204
+ 4. **A dependency the package must not carry is not an exemption.** Published
205
+ utils is imported by app-web and share, so it holds no browser, no BullMQ, no
206
+ socket server. The hook answers the part the vendor knows and the shell does
207
+ the rest — `scrape.browser` answers an ENDPOINT and the shell drives it. An
208
+ injected `fetcher` covers plain HTTP.
209
+ 5. **Sweep, and pin the sweep.** A test that greps this repo for the vendor's
210
+ endpoint and env names, failing on any that survive. Both
211
+ `connection-slug-neutral.test.js` (api) and `commerce-slug-neutral.test.js`
212
+ (sync) are the pattern, exemptions named with reasons and each one re-checked
213
+ for whether it is still earning its place.
214
+
215
+ ## D. Check
216
+
217
+ Same steps read as questions: do the requested scopes cover every hook the
218
+ contract lists, does each `urls.*` entry open, does the vendor file carry its
219
+ api/dashboard/scopes links, **is the vendor named by the connection that spends
220
+ it**, **is any of its wire detail still outside the manifest**, was the copy
221
+ humanized, has it been walked. A gap is a fix, not a note.
87
222
 
88
223
  ## Red flags — stop
89
224
 
90
225
  - "Minimum scopes for now" — re-consent for every merchant later.
91
- - "Help centre only for the guide" `urls.*` will be empty.
92
- - "Humanizer if time" it is a step with a date, not a polish.
93
- - "Verified in the header comment" — `review.verified` is the record.
94
- - "Flip that hook later", "skipped because of the deadline" — nothing is
95
- skipped. Say the time is short and keep going.
226
+ - "Humanizer if time" it is a step, not a polish.
227
+ - "Flip that hook later", "skipped because of the deadline" nothing is skipped.
96
228
  - "Keys as an interim, OAuth after the demo" — the OAuth build is the work.
229
+ - "I'll add `review.verified`" — the key is gone and `build()` throws on it.
230
+ - "The hook can just write it" — a hook holds no controller. Describe the write.
231
+ - "`false` because nobody has asked for it yet" — `false` is a decision about the
232
+ VENDOR, with a reason.
233
+ - **"This vendor has no connection"** — it has no MERCHANT. Name it on
234
+ `drawbridge`.
235
+ - **"I'll add a key for that"** — what already answers it? Almost always a
236
+ manifest declaring something it already can.
237
+ - **"The credentials moved, that's the migration"** — the calls are the job.
97
238
 
98
239
  ## Notes
99
240
 
100
241
  - A fact that must hold across repos goes through `drawbridge-record-contract`.
101
- - If the vendor's list endpoint filters by name server-side, the field must
102
- NOT declare `search : false`; a test pins that for Attentive.
242
+ - If the vendor's list endpoint filters by name server-side, the field must NOT
243
+ declare `search : false`; a test pins that for Attentive.
244
+ - The reference doc's tables are GENERATED from the registry
245
+ (`lib/connections/reference.js`) and a test fails if they drift. Regenerate,
246
+ never hand-edit.
@@ -19,8 +19,12 @@ scan it and act.
19
19
  **The default shape:**
20
20
 
21
21
  - One line on what changed, in plain past tense.
22
+ - What they need before the first step: a permission to grant, a setting to switch on, a
23
+ record that has to exist. One line each, before the checks.
22
24
  - A list of what to do and what to expect. One step per line, each one something they can
23
- actually do or check.
25
+ actually do or check. Every step starts with the click path from where they already are
26
+ (Admin, Connections, then the store; Shopify admin, Apps, then Drawbridge). "Open the app"
27
+ is not a path.
24
28
  - Anything that would otherwise look wrong to them, as its own line.
25
29
 
26
30
  Use prose only for something a list would distort, like a one-sentence cause. Never write three
@@ -65,6 +69,13 @@ Never post a first draft. Read it back against this list:
65
69
  session: a test you ran, a deploy you confirmed live, a value you read. Anything you merely
66
70
  believe gets cut or marked unverified. A wrong comment is worse than a thin one and costs
67
71
  them a retest.
72
+ - **Did you do every check yourself, on dev, on the record you name?** "The code writes the
73
+ badge" is not the same as "I opened Admin, Connections and saw the badge on
74
+ drawbridge-qa-jaime". A check you have not performed as the reader will perform it gets
75
+ performed now, or is written as "not tested". Naming their store and promising a state you
76
+ never looked at sends them hunting for something that is not there (2026-09-17, task
77
+ 1218569574129209: the badge was promised on a store the daily run had not touched, and the
78
+ "This is a development store" screen was described without saying which app or which page).
68
79
  - Any jargon they have not used themselves?
69
80
  - Does it open with `<a data-asana-gid="<their gid>"/>` so they are actually notified?
70
81
  - Would this need a follow-up comment? Fix that now.
@@ -1,44 +1,68 @@
1
1
  ---
2
2
  name: drawbridge-asana-ship-handoff
3
- description: Use when a feature has shipped (merged to develop) against an Asana task — reassigns the task back to its last commenter, posts a concise retest summary, and sets the due date to today.
3
+ description: Use when a feature has shipped (merged to develop) against an Asana task — waits for the deploy to finish, walks the flow on dev, then hands the task back to its last commenter with a numbered browser retest and today's due date.
4
4
  ---
5
5
 
6
6
  # Asana ship handoff
7
7
 
8
- Hand a shipped feature back to whoever last commented on its Asana task, with everything they
9
- need to retest. Use this right after merging to `develop` (which deploys to dev).
8
+ Hand shipped work back to whoever last commented on the task, with a retest they can run in a
9
+ browser. Use it right after merging to `develop`.
10
+
11
+ The comment is a QA script, not a changelog. They open a browser, follow the numbered steps, and
12
+ see what you said they would see.
10
13
 
11
14
  ## Inputs
12
15
 
13
- - The Asana task URL or ID. Ask for it if it isn't obvious from the branch, PR, or
14
- conversation. Do not guess.
16
+ - The Asana task URL or ID. Ask if it isn't obvious from the branch, PR, or conversation. Do not
17
+ guess.
15
18
  - The change you shipped.
16
19
 
17
- ## Steps
18
-
19
- 1. **Load the task.** `get_task` with the gid from the URL.
20
- 2. **Find the reassignee.** `get_task_stories`, filter to stories whose `resource_subtype` is
21
- `comment_added`, take the most recent one's `created_by`. Do not hardcode a person. If there
22
- are no comments, the task's creator is the fallback, but say so and confirm it.
23
- 3. **Write the comment with `drawbridge-asana-comment`.** That skill owns the shape, the naming
24
- rules, the humanizer pass, claim verification and the read-back. Do not restate its rules
25
- here or work around them.
26
-
27
- What this comment specifically must carry: what changed, and if they reported a bug, what
28
- actually caused it; then the steps to retest and the expected result. Ground the steps in
29
- the behaviour that changed, not in the diff.
30
- 4. **Apply**, once the user has approved the text:
31
- - `update_tasks` with `assignee` set to their gid and `due_on` set to today (`YYYY-MM-DD`).
32
- The MCP cannot set a due *time*, only a date, so do not try to pass `due_at`.
33
- - `add_comment` with `html_text`.
34
- 5. **Report** the task URL, who it went to, and the due date. Confirm from the tool result, not
35
- from intent.
20
+ ## Before you write a word
21
+
22
+ 1. **Wait for the deploy.** Every app the change touched has to be live on dev — check the
23
+ deployment is ACTIVE, don't assume the push landed. Sync keeps running the old build for
24
+ about ten minutes after ACTIVE, so wait that out before testing anything that runs a job. A
25
+ comment posted mid-deploy sends them to test the old code.
26
+ 2. **Walk it in a browser yourself, on dev**, the way they will: same click path, same record,
27
+ same screens. Only what you saw goes in the comment.
28
+ 3. **Gather all of it now.** Every scenario to retest, every prerequisite (a permission to
29
+ grant, a setting to switch on, a record that has to exist), and anything on screen that will
30
+ look wrong without warning. Go back a second time to complete or correct the first comment
31
+ and you have wasted their retest.
32
+ 4. **Find who it goes to.** `get_task_stories`, keep the stories whose `resource_subtype` is
33
+ `comment_added`, take the most recent one's `created_by`. Never hardcode a person. With no
34
+ comments, the task creator is the fallback say so and confirm it.
35
+
36
+ ## What the comment says
37
+
38
+ Write it with `drawbridge-asana-comment`. That skill owns the shape, the naming rules, the
39
+ humanizer pass, claim verification and the read-back. Don't restate or work around its rules.
40
+
41
+ On top of those:
42
+
43
+ - **One numbered list**, one action per line, so they can reply "3 is wrong" and you both know
44
+ what they mean. Prerequisites numbered first, then the steps, each with what they should see.
45
+ - **Browser only.** Every step is something they click, type, or look at. Nothing about
46
+ branches, commits, files, functions, deploys, packages, or how it was built.
47
+ - **One line on what changed**, plain past tense, and the cause if they reported a bug.
48
+ - **Name screens and buttons exactly as the interface labels them.** If you didn't see the
49
+ label, don't name it. No invented terms, no paraphrased labels.
50
+ - **Say what won't change**, when something they expect to move stays put.
51
+ - **Say what you didn't test.**
52
+
53
+ ## Then apply
54
+
55
+ Once the user has approved the text:
56
+
57
+ 1. `update_tasks` with `assignee` set to their gid and `due_on` set to today (`YYYY-MM-DD`). The
58
+ MCP can't set a due *time*, so don't pass `due_at`.
59
+ 2. `add_comment` with `html_text`.
60
+ 3. Report the task URL, who it went to, and the due date — from the tool result, not from
61
+ intent.
36
62
 
37
63
  ## Notes
38
64
 
39
- - Keep it QA-facing: what to test and expect. If a sentence only makes sense to someone who has
40
- read the diff, it does not belong in the comment.
41
- - Run `drawbridge-ship-feature` instead if the docs and graph updates for this ship have not
42
- been done yet. This skill is the last step of that flow.
43
- - If the Asana MCP isn't connected, stop and say so. Never describe a handoff that did not
44
- happen.
65
+ - Run `drawbridge-ship-feature` first if the docs and graph updates for this ship aren't done.
66
+ This skill is the last step of that flow.
67
+ - One comment per ship. Don't post again unless they reply.
68
+ - If the Asana MCP isn't connected, stop and say so. Never describe a handoff that didn't happen.
@@ -519,6 +519,12 @@ api's identity/organization routes); the merged keys keep the old names. Reading
519
519
  `providerSettings({ slug : 'drawbridge' })` anywhere finds no row — it was split on dev
520
520
  2026-09-08 and prod is seeded per-vendor. Detail: `drawbridge-docs/reference/providers.md`.
521
521
 
522
+ - **The embedded app's install paths ARE `INSTALL_ENDPOINTS`** (utils `lib/connections/contract.js`
523
+ ↔ drawbridge-shopify-app `app/drawbridge.server.ts`). The api's `POST /connection/:vendor/*`
524
+ allows exactly those thirteen and 404s anything else; shopify-app does not depend on utils, so
525
+ `app/drawbridge.server.test.ts` carries the list verbatim and fails on drift. Rename one side,
526
+ rename both and the test — a mismatch is an embedded app whose organization list silently 404s.
527
+
522
528
  ## Import surfaces
523
529
 
524
530
  - Import names against a package's **actual exports** — a missing export resolves to `undefined`
@@ -527,10 +533,11 @@ api's identity/organization routes); the merged keys keep the old names. Reading
527
533
 
528
534
  ## Shopify sales channel
529
535
 
530
- - `@drawbridge/shopify` storefront `getProduct`/`getProductVariantsPage` throw the **exact
531
- message `'Shopify product not found'`** when the Storefront API can't see a product
536
+ - The Shopify storefront client (`@drawbridge/drawbridge-utils/shopify/storefront`, formerly its
537
+ own `@drawbridge/shopify` package) throws the **exact message `'Shopify product not found'`**
538
+ from `getProduct`/`getProductVariantsPage` when the Storefront API can't see a product
532
539
  (unpublished from the Drawbridge channel / deleted); transport and GraphQL failures throw
533
- `'Shopify storefront error: …'`. drawbridge-sync's product worker (`queue/product.js`)
540
+ `'Shopify storefront error: …'`. The Shopify manifest's `commerce.product` hook
534
541
  string-matches the not-found message to deactivate the product and send ResourceFeedback
535
542
  instead of retrying. Reword the message → unpublished products retry-loop forever and
536
543
  merchants get no feedback; drawbridge-api's shopifyProduct route relies on the not-found
@@ -546,7 +553,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
546
553
  - **Feedback must forward the product's own `updatedAt`, never a fresh timestamp.** Shopify
547
554
  versions feedback on `productUpdatedAt` and refuses a payload older than the version it holds,
548
555
  so stamping `new Date()` records a version ahead of the product and every later send comes back
549
- `'Feedback for a later version of this resource was already accepted'`. `@drawbridge/shopify`
556
+ `'Feedback for a later version of this resource was already accepted'`. The admin client
550
557
  reads it (costing one extra `read_products` query per send) so callers cannot get this wrong.
551
558
  - **ResourceFeedback messages are format-validated by Shopify, not by us**: one message, ≤ 100
552
559
  characters, leading capital, trailing period, not all caps. A breach fails the whole send.
@@ -570,7 +577,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
570
577
  throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
571
578
  string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
572
579
  turns an auth failure into a silent success.
573
- - `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/shopify` `lib/constants.js`) must stay in lockstep with
580
+ - `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/drawbridge-utils` `lib/shopify/constants.js`) must stay in lockstep with
574
581
  `[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
575
582
  diffs each store's granted scopes against it to flag "update permissions" on the connections
576
583
  page. A scope added to the toml but not the constant silently escapes the check; one added to
@@ -27,8 +27,9 @@ worked fine.
27
27
  ## Coordinating peer dependencies on bump
28
28
 
29
29
  When bumping any `@drawbridge/*` package, audit every sibling package's
30
- `peerDependencies` for a stale pin. Peer pins are version-exact (e.g.
31
- `"@drawbridge/shopify": "0.0.3"`), so bumping the package in consumer
30
+ `peerDependencies` for a stale pin. Consumer pins are version-exact (e.g.
31
+ `"@drawbridge/drawbridge-utils": "0.0.183"`; components' peer on utils is the
32
+ one range, `>=0.0.118`), so bumping the package in consumer
32
33
  apps while leaving a sibling's peer dep on the old version triggers
33
34
  `ERESOLVE` at install time.
34
35
 
@@ -37,9 +37,32 @@ quietly that dev served stale code the whole time.
37
37
  - Add it to the release document in `drawbridge-docs/releases/` for the launch it
38
38
  belongs to, with the ordering constraint stated: what has to be deployed before
39
39
  it can run, and what breaks if it runs early.
40
- - If the work needs a worker, enqueue the job the live path enqueues, with the
41
- same job id shape, so a migration-queued run and a live run coalesce rather
42
- than doubling.
40
+ - If the work needs a worker, enqueue the job the live path enqueues, through the
41
+ same helper the live path uses rather than a copy of it, so the payload and the
42
+ job id shape cannot drift.
43
+
44
+ ## Do not reach for a coalescing job id
45
+
46
+ A static job id looks like coalescing and is usually suppression. BullMQ drops an
47
+ add whose id still exists, and a queue that retains completed jobs will refuse a
48
+ re-add for as long as it keeps them — a week on the connection queue, a month for
49
+ failed ones. Worse, an id can be reused a layer down: `drawbridge-sync`'s
50
+ `queue/workflow.js` derives each step's job id from the orchestrator job's id, so
51
+ a static orchestrator id produced a static step id even though the orchestrator
52
+ job itself carried `removeOnComplete`.
53
+
54
+ That is not theoretical. It is what made a renamed segment never reach Mailchimp
55
+ or Klaviyo: the first register ran, and every later one for that segment was
56
+ dropped in silence for seven days.
57
+
58
+ **Make the id unique per dispatch and make the work idempotent instead.** A
59
+ register that writes the same row twice costs nothing. A register that is dropped
60
+ loses a change the merchant can see. Where a timestamp is the unique part, add a
61
+ counter too, because a loop dispatches many jobs inside one millisecond.
62
+
63
+ Reserve a static id for the case where running twice is genuinely harmful AND the
64
+ queue clears the id on both outcomes, and say in a comment which of the two you
65
+ are relying on.
43
66
 
44
67
  ## Media migrations, the one exception
45
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {