@drawbridge/drawbridge-agents 0.1.51 → 0.1.58

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,45 +1,47 @@
1
1
  ---
2
2
  name: drawbridge-add-connection
3
- description: Use when adding a new vendor connection (a new manifest in drawbridge-utils) runs the auth-type assessment FIRST so the integration is built the way the vendor's own docs recommend, then walks the build against the documented checklist and its guards.
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.
4
4
  ---
5
5
 
6
6
  # Add a connection
7
7
 
8
- A connection is one manifest file in `drawbridge-utils/lib/connections/`, and the
9
- build half is documented `drawbridge-docs/reference/connection-hooks.md`,
10
- "Adding a connection". This skill exists for what comes BEFORE the file: deciding
11
- what kind of connection to build, from the vendor's documentation rather than
12
- from whichever auth mechanism is easiest to wire.
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.
13
12
 
14
- The failure it prevents is real: Attentive was first built as pasted API keys
15
- because keys worked, and their docs turned out to frame private-app keys as
16
- "integrate your own account" the public OAuth app was the documented path for
17
- platforms like Drawbridge. The rebuild cost an afternoon; the assessment costs
18
- ten minutes.
19
-
20
- ## When to use
21
-
22
- Any new vendor manifest, and any auth-type change to an existing one (keys →
23
- OAuth is a rebuild of the auth surface, not a tweak).
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.**
24
18
 
25
19
  ## Steps
26
20
 
27
- 1. **Fetch the vendor's developer docs and answer these in writing, with the
28
- URL of the page that answers each.** Never from memory the Klaviyo consent
29
- url was once written from memory, pointed at their API host, and stalled
21
+ 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
30
23
  every merchant on a blank page.
31
-
32
- - What auth mechanisms exist, and **who does the vendor say each is for**?
33
- The distinction that matters is *your own account* versus *accounts outside
34
- your organization*. Drawbridge is always the second: merchants connect
35
- THEIR account to OUR platform.
36
- - Is there a partner / public-app / marketplace program? What does
37
- registration take, and what credentials does it issue?
38
- - For OAuth: authorize url, token url, how client credentials are sent
39
- (form fields vs Basic header), whether PKCE is required, token lifetime,
40
- whether refresh tokens exist, scope names.
41
-
42
- 2. **Pick the `auth.type` from the answers, not from convenience:**
24
+ - **API reference**: auth mechanisms and who the vendor says each is for
25
+ (your own account vs accounts outside your organization Drawbridge is
26
+ always the second), partner or public-app program, OAuth urls and
27
+ 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:**
43
45
 
44
46
  | vendor's shape | type |
45
47
  |---|---|
@@ -48,32 +50,53 @@ OAuth is a rebuild of the auth surface, not a tweak).
48
50
  | the vendor offers ONLY per-account credentials | `keys` |
49
51
  | no third party behind it at all | `generated` |
50
52
 
51
- `keys` when OAuth exists is a decision someone must make out loud usually
52
- as an explicitly-flagged interim while a partner registration is pending, and
53
- the manifest header says so.
54
-
55
- 3. **If the app is not yet registered, build it dormant.** `requires` names the
56
- client env vars that will exist after registration; until they land on a
57
- deployment, the connection is offered nowhere. Shipping the manifest complete
58
- and inert beats waiting adding the env vars becomes the launch.
59
-
60
- 4. **Record the assessment in the manifest header**: every cited fact, every
61
- contradiction between the vendor's own pages (cite both and say which one the
62
- code models), and for OAuth the questions only a live registration
63
- answers: does consent echo `state` back, what is the real token lifetime,
64
- does one app accept both environments' redirect urls.
65
-
66
- 5. **Build against the checklist** in
67
- `drawbridge-docs/reference/connection-hooks.md` "Adding a connection". Its
68
- guards are the enforcement: the directory-registration test, the
69
- feature-grant test, `build()` itself. Trust the named failures over your
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
70
73
  memory of the steps.
71
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.
87
+
88
+ ## Red flags — stop
89
+
90
+ - "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.
96
+ - "Keys as an interim, OAuth after the demo" — the OAuth build is the work.
97
+
72
98
  ## Notes
73
99
 
74
- - The manifest is the only place vendor facts live. If the assessment finds a
75
- fact that must hold across repos, record it with `drawbridge-record-contract`.
76
- - The picker/search question is part of the assessment: if the vendor's list
77
- endpoint filters by name server-side, the field must NOT declare
78
- `search : false` — Attentive was the first vendor that could, and a test pins
79
- that it stays searchable.
100
+ - 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.
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: drawbridge-asana-comment
3
+ description: Use before posting ANY comment to an Asana task — ship handoffs, answers, status notes, questions. Enforces scannable lists of actions over paragraphs, plain correctly-named things, a mandatory review before posting, and the humanizer pass.
4
+ ---
5
+
6
+ # Write an Asana comment
7
+
8
+ Every comment posted to Asana goes through this skill. No exceptions for length, urgency, or
9
+ "it's just a quick note". Posting is publishing. A teammate reads it, acts on it, and cannot
10
+ unread a wrong one.
11
+
12
+ Four rules, in order of how often they get broken.
13
+
14
+ ## 1. Lists and actions, not paragraphs
15
+
16
+ There is no word count. Length is not the problem, shape is. Structure it so the reader can
17
+ scan it and act.
18
+
19
+ **The default shape:**
20
+
21
+ - One line on what changed, in plain past tense.
22
+ - A list of what to do and what to expect. One step per line, each one something they can
23
+ actually do or check.
24
+ - Anything that would otherwise look wrong to them, as its own line.
25
+
26
+ Use prose only for something a list would distort, like a one-sentence cause. Never write three
27
+ paragraphs where four bullets would do.
28
+
29
+ **Every line earns its place.** Cut it if it is:
30
+
31
+ - The investigation narrative. They want the outcome, not how you got there.
32
+ - File names, commit SHAs, function names, branch names. They are verifying behaviour.
33
+ - Anything restating what they already told you.
34
+ - Anything that would not change what they do next.
35
+
36
+ **Completeness beats brevity.** Include every scenario to retest, any setup gotcha, and
37
+ anything in the result that would look wrong without warning. A second comment correcting or
38
+ completing the first is the real failure. Structure is what keeps a complete comment readable,
39
+ so restructure before you cut content.
40
+
41
+ **The stop test:** if it does not fit on a phone screen without scrolling, restructure it. Do
42
+ not trim facts to hit a length.
43
+
44
+ ## 2. Use normal words and name things correctly
45
+
46
+ Write for the person reading it, not for the repo.
47
+
48
+ - **No internal jargon.** If the term lives in our docs and not in their message, replace it
49
+ with a plain description. Runbook, backfill, cascade, manifest, provider row, seam, capped,
50
+ hydrate all read as noise to someone testing a button. If you must use one, say what it is
51
+ the first time.
52
+ - **Name things by their real name.** The screen, the button, the plan, the setting, exactly as
53
+ the interface labels them. Not your paraphrase, not the variable name. If the button says
54
+ Send, write Send.
55
+ - **Quote user-facing copy exactly**, so they can match it against what is on screen.
56
+ - Use their words for their bug. If they called it a prize notification, it is a prize
57
+ notification, not a lead-facing transactional send.
58
+
59
+ ## 3. Review before posting, always
60
+
61
+ Never post a first draft. Read it back against this list:
62
+
63
+ - Is it a scannable list of actions, or did it drift back into paragraphs?
64
+ - **Is every claim verified?** Each factual statement must be something you checked in this
65
+ session: a test you ran, a deploy you confirmed live, a value you read. Anything you merely
66
+ believe gets cut or marked unverified. A wrong comment is worse than a thin one and costs
67
+ them a retest.
68
+ - Any jargon they have not used themselves?
69
+ - Does it open with `<a data-asana-gid="<their gid>"/>` so they are actually notified?
70
+ - Would this need a follow-up comment? Fix that now.
71
+
72
+ Then show the user the final text and get a go-ahead before posting, unless they have already
73
+ said to proceed. After posting, read the comment back (`get_task` with comments) and confirm
74
+ the mention expanded and the formatting survived. Asana silently flattens some markup.
75
+
76
+ ## 4. Run it through `humanizer`
77
+
78
+ Required by `conventions/humanized-copy.md`, no exceptions for length. It must not touch the
79
+ mention markup, quoted interface copy, or any concrete step. If a pass makes an instruction
80
+ vaguer, keep the original for that line. If humanizer is unavailable, say so and do not post.
81
+
82
+ ## Sound like a teammate
83
+
84
+ The comment should read as though the person who did the work wrote it in a hurry, because that
85
+ is what a good one is.
86
+
87
+ - **Plain past tense about what you did.** "Fixed on dev." Not "the fix has been implemented"
88
+ or "this has now been addressed".
89
+ - **No throat-clearing.** No "Thanks for flagging this", "Great catch", "Just wanted to let you
90
+ know", "I hope this helps", "Let me know if you have questions".
91
+ - **No hedging stack.** Not "this should now potentially resolve". Either it works and you saw
92
+ it work, or you say what you did not check.
93
+ - **Own a mistake in one line and move on.** "I had this wrong, it only affects email." Not a
94
+ paragraph of explanation.
95
+ - **Say what you did not test.** Being straight about a gap is what makes the rest credible.
96
+ - **Ask a real question when you need an answer**, at the end, as a question. Do not bury it.
97
+ - **Bold almost nothing.** If everything is bold, nothing is.
98
+ - **No emoji** unless the thread already uses them.
99
+
100
+ ## Notes
101
+
102
+ - Plain prose goes in `text`. Use `html_text` for a mention or a list. Allowed tags are
103
+ `<body> <strong> <em> <code> <ol> <ul> <li> <a> <blockquote> <pre>` only.
104
+ - The `<a data-asana-gid="..."/>` form expands to a real @-mention. Reassignment alone is easy
105
+ to miss, so mention the person even when you also reassign.
106
+ - If the Asana MCP is not connected, stop and say so. Never describe a comment as posted when
107
+ it was not.
@@ -1,69 +1,44 @@
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 summary of what changed plus retest/verify steps, and sets the due date to now.
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.
4
4
  ---
5
5
 
6
6
  # Asana ship handoff
7
7
 
8
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 the work to `develop` (which deploys to dev).
9
+ need to retest. Use this right after merging to `develop` (which deploys to dev).
10
10
 
11
11
  ## Inputs
12
12
 
13
- - The Asana task URL or ID for the shipped work. Ask the user for it if it isn't obvious from
14
- the branch, PR, or conversation do not guess.
15
- - The change you shipped (from the diff / PR).
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.
15
+ - The change you shipped.
16
16
 
17
17
  ## Steps
18
18
 
19
- 1. **Load the task.** `asana_get_task` with the task gid (extract it from the URL).
20
- 2. **Find the last commenter.** `asana_get_stories_for_task`, filter to comment stories
21
- (`type` / `resource_subtype` is a comment, not a system story), take the **most recent**
22
- one's `created_by`. This is the reassignee do not hardcode a person. If there are no
23
- comments, ask the user who to assign to.
24
- 3. **Draft the comment short.** Two parts, nothing else: what changed (and, if they
25
- reported a bug, what actually caused it), then a **Retest** section with the steps and
26
- the expected result. Ground the steps in the behaviour that actually changed.
27
-
28
- **Hard limit: ~200 words, and never more than 6 short paragraphs/bullets.** These
29
- comments have been far too long — reading them costs the reader more than the fix did.
30
- Cut: the investigation narrative, why the bug shipped, code/file/commit detail, anything
31
- restating what they already reported, and any sentence that would not change what they
32
- do next.
33
-
34
- **Short is not partial.** Include everything they need in ONE comment every scenario to
35
- retest, any setup or data gotcha, and anything about the result that would otherwise look
36
- wrong (an odd amount, a delay, a cosmetic leftover). A follow-up comment correcting or
37
- completing this one is the failure this limit exists to prevent, so check the draft for
38
- omissions *before* trimming words.
39
-
40
- **Open with an @-mention of the reassignee** — use `html_text` and start the body with
41
- `<a data-asana-gid="<their user gid>"/>` (Asana expands it into a proper mention). A
42
- comment without a mention may notify no one; reassignment alone is easy to miss.
43
- 4. **Run the draft through `humanizer`.** A real teammate reads this, and generated-sounding
44
- prose reads as effort nobody spent. Invoke the `humanizer` skill on the drafted body: it
45
- strips the AI tells — inflated claims, hedging, stock transitions, "it's worth noting",
46
- restating the ask twice.
47
-
48
- It pulls in the same direction as the word limit above, so apply it *before* the final
49
- trim and re-check the result against both rules. Two things it must not touch: the
50
- `<a data-asana-gid="..."/>` mention (leave the markup exactly as-is) and any concrete
51
- detail from the Retest steps — if a pass makes the steps vaguer, keep the original wording.
52
- 5. **Confirm before writing.** This posts to an external system. Show the user the chosen
53
- assignee and the drafted comment and get a go-ahead, unless they've told you to proceed
54
- without asking.
55
- 6. **Apply** (on approval):
56
- - `asana_update_task` — set `assignee` to the last commenter's gid and `due_at` to the
57
- current date/time (ISO 8601).
58
- - `asana_create_task_story` — post the comment.
59
- 7. **Report** the task URL, who it was assigned to, and confirm the due date was updated.
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.
60
36
 
61
37
  ## Notes
62
38
 
63
- - If the Asana MCP isn't connected, stop and tell the user to authorize it don't fabricate a
64
- handoff.
65
- - Keep the comment user/QA-facing: what to test and expect, not internal implementation detail.
66
- The reader is verifying behaviour, not reviewing the fix if a sentence only makes sense to
67
- someone who has read the diff, it does not belong in the comment.
68
- - Consider running `drawbridge-ship-feature` instead if docs/graph updates for this ship
69
- haven't been done yet — this skill is the final step of that flow.
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.
package/claude/CLAUDE.md CHANGED
@@ -2,6 +2,7 @@
2
2
  @../conventions/cross-repo-contracts.md
3
3
  @../conventions/karpathy-guidelines.md
4
4
  @../conventions/rules.md
5
+ @../conventions/verification.md
5
6
  @../conventions/javascript-formatting.md
6
7
  @../conventions/nested-objects.md
7
8
  @../conventions/jsx-fragments.md
@@ -19,3 +20,4 @@
19
20
  @../conventions/git-branching.md
20
21
  @../conventions/graphify.md
21
22
  @../conventions/testing.md
23
+ @../conventions/workflow-step-naming.md
@@ -68,13 +68,27 @@ skill) so it stops living only in tribal memory.
68
68
  `lib/suppression.js canSend()` — a new lead-facing send path must call it, or opted-out
69
69
  recipients get contacted (TCPA/CAN-SPAM exposure). Lead-facing = the notification carries
70
70
  `lead`/`workflow`/`connection`; those sends also REQUIRE an org connection (no system-sender
71
- fallback), while org-less system mail (OTC codes, member invites, security notices) keeps the
72
- system sender and is never suppressed. Inbound STOP flows webhooks
71
+ fallback), while system mail (OTC codes, member invites, security notices) keeps the system
72
+ sender and is never suppressed. **System mail is identified by `audience`, not by whether an
73
+ organization is present** — `queue/identity.js` sends member mail WITH an org, and
74
+ `stream/transaction.js` sends AI-balance notices with none. Inbound STOP flows webhooks
73
75
  (`POST /connection/:id/twilio`, signature-verified against the URL built from
74
76
  `APP_CLIENT_WEBHOOKS_URI`) → `buffer` doc → sync drain → platform-global suppression row;
75
77
  api registers the number's SmsUrl at twilio connect (`lib/twilio.js`) and `scripts.js
76
78
  registerTwilioWebhooks` backfills — a deployed webhooks route with unregistered numbers hears
77
79
  nothing, silently.
80
+ - **`audience` picks the FROM-address; `intent` picks the opt-out machinery** (utils ↔ sync ↔
81
+ api). `resolveEmailSender` (sync `queue/notification.js`) resolves verified `identity` row →
82
+ `subAddress( leadSender, organization.shortId )` for lead-facing mail → `accountSender` for
83
+ member and platform mail. The tag is `shortId`, never `subdomain` — a rename and a custom-plan
84
+ downgrade both rewrite subdomain, and the from-address is frozen into delivered mail; api's
85
+ `identityDefaults.from` must tag with the SAME field or the settings screen shows a merchant an
86
+ address their leads never see. Drop the `leadFacing` branch and the fallback is `leadSender || accountSender`
87
+ for everything, so the moment a lead address is configured, Drawbridge's own mail leaves from
88
+ the merchant-facing lead domain — silently, with nothing raised. `subAddress` lives in utils
89
+ `lib/email.js` and is the inverse of `toCanonicalEmail`, which suppression uses to strip the
90
+ same tag; break the round-trip and consent lookups stop matching. api mirrors ONLY the
91
+ lead-facing branch (`identityDefaults.from`). Detail: drawbridge-docs `reference/providers.md`.
78
92
  - **Adding an email/SMS vendor is a two-repo change** (api ↔ sync). A connection is deliverable
79
93
  only if sync implements a sender for its slug — `lib/email.js` (`{ mailchimp, sendgrid }`) and
80
94
  `lib/sms.js` (`{ twilio }`), which `queue/notification.js` reads **directly** as its guard and
@@ -108,6 +122,14 @@ skill) so it stops living only in tribal memory.
108
122
  missing until 2026-08-13 (Shopify renames took 5+ min to reach share pages). Product scope is
109
123
  special: the doc carries no campaign/org refs, so the worker resolves featuring campaigns via
110
124
  `campaign.products[]`, per-page org (canonical product docs can span orgs).
125
+ Since 2026-09-09 share's HTML cache also stores a **404 render** under the same html key,
126
+ for the soft TTL only and never background-revalidated: every miss leaves share's single
127
+ server IP and lands in the api's global per-IP limiter, so a scanner walking a subdomain
128
+ (Sentry DRAWBRIDGE-SHARE-FQ) or a burst on an unpublished slug used to cost an api call per
129
+ request. The seam therefore MUST bust on page **insert** as well as update/delete (sync
130
+ `stream/page.js`), or a page created already published answers "not found" until the entry
131
+ expires. A slug can never carry a dot (`slugify` strips them), which is what lets share's
132
+ proxy refuse extension paths before they reach the api at all.
111
133
  - Scraped-asset file reuse keys on `organization + meta.origin + meta.element` **plus
112
134
  `meta.render` for pipeline-rendered marks** (sync `resolveAsset` writes/queries it; the field
113
135
  and its index live in drawbridge-api `schema/file.js`). `origin` identifies the source asset,
@@ -207,6 +229,14 @@ skill) so it stops living only in tribal memory.
207
229
  sync upserts the same invoice from three concurrent webhooks (finalized/payment_succeeded/voided)
208
230
  and relies on the index to reject the losing insert so the catch can redo it as an update. Drop
209
231
  the index → silent duplicate invoice rows; remove the catch → concurrent events throw + retry.
232
+ - The `buffer` collection's **unique partial index on Stripe `provider.id`** (drawbridge-api
233
+ `schema/buffer.js`, Stripe rows with a string id only) and webhooks' E11000 catch in
234
+ `route/stripe.js` are one contract: the route dedupes a Stripe redelivery with a read before its
235
+ insert, two retries arriving together both pass that read, and the index refuses the second
236
+ insert, which the route answers 200 without touching the first row. Drop the index → the race
237
+ reopens silently and sync processes one event twice; remove the catch → a concurrent retry 500s
238
+ and Stripe keeps retrying. The index is manual (the resetIndexes block is the runbook) and its
239
+ creation fails while duplicates exist — dedupe first.
210
240
  - The `contact` collection's **unique multikey index on `leads`** (drawbridge-api
211
241
  `schema/contact.js`) and sync's contact resolution (`lib/contact.js` + `queue/contact.js`) are
212
242
  one contract: a lead belongs to at most one contact. One submission enqueues two resolve jobs
@@ -455,6 +485,18 @@ sweep (`queue/identity.js`), which re-points any number whose Twilio config drif
455
485
  the path in one place and the sweep "repairs" every number to the wrong URL — inbound SMS
456
486
  (STOP included) goes dark with all rows reading healthy.
457
487
 
488
+ **Provider rows are keyed by VENDOR, and no manifest owns a vendor (utils ↔ api ↔ sync ↔
489
+ webhooks, since utils 0.0.153).** A manifest declares only the fields it spends under
490
+ `provider.vendors.<vendor>`; `mergeVendors()` in utils unions every manifest's slices into
491
+ one entry per vendor, and the `provider` collection holds one row per key of that merge —
492
+ `sendgrid`, `twilio`, `hubspot` for the private `drawbridge` connection, one `google` row for
493
+ however many Google connections come. Build refuses a vendor drawn two ways, a shared field
494
+ declared differently, and a credential name claimed twice. A connection reads its vendors
495
+ merged through `vendorSettings({ controller, slug })` (sync's `drawbridgeProvider()`, the
496
+ api's identity/organization routes); the merged keys keep the old names. Reading
497
+ `providerSettings({ slug : 'drawbridge' })` anywhere finds no row — it was split on dev
498
+ 2026-09-08 and prod is seeded per-vendor. Detail: `drawbridge-docs/reference/providers.md`.
499
+
458
500
  ## Import surfaces
459
501
 
460
502
  - Import names against a package's **actual exports** — a missing export resolves to `undefined`
@@ -18,6 +18,8 @@ descriptions — engineer-facing text follows the code conventions instead.
18
18
  Rules when running it:
19
19
 
20
20
  - Run humanizer BEFORE posting/committing the copy, not after someone flags it.
21
+ - **Asana comments go through the `drawbridge-asana-comment` skill**, which runs the humanizer
22
+ pass for you along with the brevity cap and the read-back. Do not hand-roll a comment.
21
23
  - It must not change facts, numbers, steps, or expected results — if a pass makes concrete
22
24
  instructions vaguer, keep the original wording for those parts.
23
25
  - Leave markup intact (Asana mention tags, links, placeholders like `{name}`).
@@ -12,3 +12,9 @@
12
12
  This is not a style preference, it is the difference between advice and noise. Asserting a limitation that isn't real sends the reader to check your work, and asserting two implementations are identical when one carries a field the other drops ships a regression. Both have happened.
13
13
 
14
14
  In practice: before claiming a component can't do something, read the component. Before calling two implementations equivalent, read both ends to end. Before proposing a new endpoint or field to work around a gap, confirm the gap exists in the code rather than in your sample of it.
15
+
16
+ 6. Prove absence before you claim it, and name the space you searched. "Nothing writes this field", "that isn't documented anywhere", "no caller does this" — claims of that shape cost far more to establish than they look. Reading one file proves what that file does; it can never prove what every other file doesn't. Search the whole space the claim covers — every repo, the built `dist/` as well as the source, `drawbridge-docs/user-stories/` as well as the code, and the tests — then put the space in the sentence: "no writer across the family, source and dist" rather than "nothing writes it".
17
+
18
+ Why: the reader cannot see where you stopped looking. A bare "it doesn't exist" reads as settled, and by the time the thing turns up — documented under an older name, written by a manifest in another package — work has already been built on top of it. Naming the search space turns an unfalsifiable claim into one that can be checked in seconds.
19
+
20
+ Three traps that have actually caused this. A counter incremented from a manifest in `drawbridge-utils` while the search only covered `drawbridge-sync`. A workflow step documented under a name the product had since changed, so searching for the product's label found nothing. A key assembled at runtime (`'totals.' + field`) that no search for the literal will ever match — when a claim depends on a name, check whether the name is ever built rather than written.
@@ -0,0 +1,39 @@
1
+ # Verify before you assert
2
+
3
+ Every rule here was written after a confident, wrong statement reached Darren. They share one shape: the reasoning was sound and the input was never checked. Sound reasoning on an unchecked input is the most expensive kind of wrong, because nothing downstream looks suspicious.
4
+
5
+ ## Never state a number you did not measure
6
+
7
+ A measurement in a sentence — a count, a total, "none in the last 30 days" — may only be emitted when the underlying field is actually present. Gate on `value != null` before the claim and say nothing when it is absent.
8
+
9
+ `|| 0` on an unmeasured field manufactures a fact. In drawbridge-growth it reached three surfaces at once: the task queue, profile embeddings, and outreach drafts telling paying customers they had done nothing, all from a field no connected database was filling.
10
+
11
+ The same applies to prose. "This is faster", "most callers do X", "that's a small table" are measurements. Measure them or drop them.
12
+
13
+ ## Walk the flow before saying it works
14
+
15
+ Tests passing, lint clean and a green build are not evidence that a screen works. Three bugs in a row survived all three and were found by clicking: a `.then()` chained off a react-query `mutate()` (which returns undefined), colour swatches handed `{ hex, name }` objects where CSS wanted a string, and a screen in a question flow that asked nothing.
16
+
17
+ Before reporting a feature as working, run the path a person takes through it. If the runtime makes that hard, that is a reason to fix the harness, not to skip the walk.
18
+
19
+ ## Fetch before claiming what is on a branch
20
+
21
+ Before asserting what is or isn't on `origin/develop` or `origin/main`, run `git fetch` **in that repo**. Fetching one family repo does not refresh the others, and grepping a stale `origin/develop` returns stale content with no warning.
22
+
23
+ In a multi-repo check, fetch every repo you will cite, in one batch, first. Treat an `origin/<branch>` read as evidence only if the fetch happened in this session. "Repo A ships a write that repo B's schema rejects" is exactly the shape a stale ref produces — re-fetch, or read the live database, before reporting it.
24
+
25
+ ## Quote vendor documentation, do not recall it
26
+
27
+ When writing against a vendor's API, open its documentation and quote each literal as you write it: authorize url, token url, revoke url, API base, required headers, scope names, auth scheme. Then re-read the finished file against the docs as a separate pass, paying most attention to the lines written first.
28
+
29
+ Klaviyo's `authorize` host was written from memory as `a.klaviyo.com` — the API host — while the browser-facing url is `www.klaviyo.com`. The docs were fetched five times that session for the values that felt uncertain, and never for the one already written. Confidence is not verification, and the values never doubted are the ones that reach a user unchecked.
30
+
31
+ Pin the API **version** too. Documentation defaults to `latest`; a repo pins something older, and a field that exists in `latest` may not exist in the pinned version — or may be deprecated there when it is current in `latest`.
32
+
33
+ ## Check the shape you are consuming, not the one you expect
34
+
35
+ Before importing, confirm the symbol is actually exported by that package's entry point. Before chaining off a wrapper's return value, confirm what it returns. Both have shipped inert code that compiled cleanly.
36
+
37
+ ## A build that exits 0 is not a build that ran
38
+
39
+ Confirm the build produced its real output — a route table, an emitted bundle, a written artifact — rather than trusting the exit code. `drawbridge-app-web` aborts on a missing environment variable and still exits 0, which is a "successful" build that compiled nothing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.51",
3
+ "version": "0.1.58",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {