@vercel/board 0.0.0 → 0.16.0

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.
Files changed (126) hide show
  1. package/README.md +180 -0
  2. package/dist/a2-C41hD8SF.mjs +3045 -0
  3. package/dist/a2-D24cvwpN.d.mts +3359 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +30 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-DjMeZpnU.mjs +224 -0
  9. package/dist/attempts-Bxp9KgPf.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-CU56Q2Xh.d.mts +204 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-0roEmjl0.mjs +613 -0
  35. package/dist/backend-http-BVRno-ca.mjs +469 -0
  36. package/dist/backend-http-Bid7RIpC.d.mts +630 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-Bir5s8ts.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-DLiE7fLY.d.mts +4 -0
  56. package/dist/exports-Dvc1o52I.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-Bkg77Pxn.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-BsOOc-6_.mjs +170 -0
  76. package/dist/server-jwPY7_wi.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-yM5JBUgr.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-CLjsa6od.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +264 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +501 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +168 -1
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,233 @@
1
+ ---
2
+ title: A triage agent
3
+ description: Acknowledge, label, and answer every new report in a channel, first from a script you run, then as a watcher the board runs for you.
4
+ ---
5
+
6
+ ## What you'll build
7
+
8
+ An agent for `firehose`, a channel that mirrors a Slack channel where people
9
+ report problems. For each new report it reacts 👀, labels it, replies in the
10
+ thread (which lands in Slack), and waits for a person to answer.
11
+
12
+ You'll write it twice. First outside the board, with the CLI, which you can
13
+ run from a laptop or any agent loop. Then on the board, as a
14
+ [watcher](../01-board/06-watchers.md), with nothing kept open between
15
+ messages.
16
+
17
+ ## 1. See what the agent sees
18
+
19
+ ```bash
20
+ # in a terminal:
21
+ vb tail firehose --from slack --json
22
+ ```
23
+
24
+ `tail` prints one line of JSON per event, live, until Ctrl-C. `--from slack`
25
+ keeps messages that came from Slack. Each line carries the event and the
26
+ message it concerns, as it is after the event:
27
+
28
+ ```json
29
+ { "cursor": "812", "type": "message.posted", "createdAt": "2026-10-08T14:02:11.000Z", "payload": { "channel": "firehose", "text": "deploy failed on www@a1b2c3: ENOTFOUND db.internal" }, "message": { "id": "812", "re": null, "channel": "firehose", "text": "deploy failed on www@a1b2c3: ENOTFOUND db.internal", "labels": {} } }
30
+ ```
31
+
32
+ (Trimmed: events carry the whole message, its author, and more.) A new
33
+ report is a `message.posted` whose message has `re: null`: a thread root.
34
+
35
+ ## 2. Triage from a script
36
+
37
+ ### A loop that stays open
38
+
39
+ ```bash
40
+ # triage.sh: runs until you stop it
41
+ vb tail firehose --from slack --json | while read -r event; do
42
+ [ "$(jq -r '.type' <<<"$event")" = "message.posted" ] || continue
43
+ [ "$(jq -r '.message.re' <<<"$event")" = "null" ] || continue
44
+ id=$(jq -r '.message.id' <<<"$event")
45
+
46
+ vb react "$id" eyes
47
+ # your judgment goes here, e.g. ask a model whether it's a duplicate of a triaged thread:
48
+ # vb read firehose --label status=triaged --json | your-agent "$id"
49
+ vb label "$id" status=triaged
50
+ vb reply "$id" "Thanks! We're looking at it."
51
+ done
52
+ ```
53
+
54
+ The reply is synced to the report's Slack thread. A person's answer in Slack
55
+ comes back to the board as a reply.
56
+
57
+ ### One turn at a time
58
+
59
+ A turn-based agent can't keep a pipe open. `wait` blocks for the next match,
60
+ prints it, and exits, so each turn handles one event. Every event carries a
61
+ `cursor`, so the last one you handled is your place on the board:
62
+
63
+ ```bash
64
+ # triage-turn.sh: one event per run
65
+ last=$(cat .triage-cursor 2>/dev/null)
66
+ next=$(vb wait firehose --from slack ${last:+--after "$last"} --json --timeout 1h) || exit 0
67
+ jq -r '.cursor' <<<"$next" > .triage-cursor
68
+
69
+ [ "$(jq -r '.type' <<<"$next")" = "message.posted" ] || exit 0
70
+ [ "$(jq -r '.message.re' <<<"$next")" = "null" ] || exit 0
71
+ id=$(jq -r '.message.id' <<<"$next")
72
+
73
+ vb react "$id" eyes
74
+ vb label "$id" status=triaged dup-of=640
75
+ reply=$(vb reply "$id" "Thanks! Tracking this with #640.")
76
+ ```
77
+
78
+ `wait` starts at the newest event unless you pass `--after`. The first run
79
+ starts there; every later run passes the last cursor it handled, so nothing
80
+ that arrived between turns is missed.
81
+ It exits 124 when the timeout passes; `|| exit 0` treats that as a quiet
82
+ turn.
83
+
84
+ ### Wait for a person
85
+
86
+ Writes print the new message's ID, so the agent can block on the thread
87
+ right after its reply:
88
+
89
+ ```bash
90
+ # continuing triage-turn.sh:
91
+ vb wait --re "$id" --after "$reply" --timeout 30m --json
92
+ ```
93
+
94
+ `--after "$reply"` counts an answer that lands before the wait starts. Check
95
+ the event's `type` and author: thread activity also includes reactions,
96
+ edits, and labels. A timeout (exit 124) is silence, not an answer.
97
+
98
+ ## 3. Triage on the board
99
+
100
+ The script only works while it runs. A watcher is a `vb tail` the board runs
101
+ for you: the same expression, and `run` gets each event the tail would
102
+ print, durably, with retries.
103
+
104
+ ```ts app/api/board/[[...path]]/route.ts
105
+ import { board } from '@vercel/board'
106
+ import { a2 } from '@vercel/board/backend-a2'
107
+ import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
108
+ import { postgres } from '@vercel/board/backend-a2/store-postgres'
109
+
110
+ const backend = a2({
111
+ store: postgres({ connectionString: process.env.DATABASE_URL! }),
112
+ scheduler: vercelQueues({ topic: 'board' }),
113
+ })
114
+
115
+ const team = board({
116
+ backend,
117
+ // sync: { slack: slack({ … }) }, mirroring Slack's #firehose into `firehose`
118
+ watch: {
119
+ triage: {
120
+ tail: 'firehose --from slack',
121
+ run: async ({ event, board }) => {
122
+ const message = event.message
123
+ if (event.type !== 'message.posted' || !message || message.re !== null) return
124
+ await board.react(message.id, 'eyes')
125
+ // your judgment, e.g. a model call over the thread:
126
+ // const kind = await classify(message.text)
127
+ const kind = 'bug'
128
+ await board.label(message.id, { set: { kind } })
129
+ await board.reply(message.id, 'Thanks! Tracking it.')
130
+ },
131
+ },
132
+ },
133
+ })
134
+ export const GET = team.fetch, POST = team.fetch
135
+ export const { scheduler } = backend
136
+ ```
137
+
138
+ - **`event` is exactly what `vb tail --json` prints.** Debug a watcher by
139
+ running its tail in a terminal.
140
+ - **`board` writes as the watcher**, `board:watch:triage`. Its writes sync
141
+ like anyone's, so the reply lands in Slack.
142
+ - **A retried `run` doesn't post twice,** as long as it writes in the same
143
+ order: write IDs come from the match.
144
+ - **A watcher's own writes never wake it.** Writes by other watchers do, so
145
+ two watchers that answer each other loop.
146
+
147
+ Without a scheduler, a watcher runs in the request that wrote, and one that
148
+ fails waits for the next write to try again (see
149
+ [Without a scheduler](../01-board/01-mounting.md#without-a-scheduler)). A
150
+ production board takes a scheduler, mounted in
151
+ [its own route](../01-board/01-mounting.md#the-scheduler-route).
152
+
153
+ ## 4. Hand reports to an agent
154
+
155
+ Labels make good routing. A second watcher starts your coding agent once a
156
+ thread is labeled a bug:
157
+
158
+ ```ts
159
+ // app/api/board/[[...path]]/route.ts, next to triage, in `watch`:
160
+ fix: {
161
+ tail: 'firehose --label kind=bug',
162
+ when: 'enters',
163
+ run: async ({ event }) => {
164
+ // your agent, your compute:
165
+ // await startAgent({ thread: event.message!.id })
166
+ },
167
+ },
168
+ ```
169
+
170
+ `when: 'enters'` runs once each time a thread comes into view: here, when
171
+ `kind=bug` is set. Without it, the watcher also runs on every reply in a bug
172
+ thread, since labels match a message or its thread's root. That's how an
173
+ agent hears that someone answered it.
174
+
175
+ ## 5. Let agents ask to be woken
176
+
177
+ A watcher runs code you deployed, not code a message names. So an agent asks
178
+ for a wake-up with data: a label.
179
+
180
+ ```bash
181
+ # in the agent's shell, before its turn ends:
182
+ vb label 812 wake="$CLAUDE_CODE_SESSION_ID"
183
+ ```
184
+
185
+ ```ts
186
+ // app/api/board/[[...path]]/route.ts, in `watch`:
187
+ wake: {
188
+ tail: '--label wake',
189
+ run: async ({ event }) => {
190
+ if (event.type !== 'message.posted') return
191
+ // your code resumes the session the label names:
192
+ // await resumeSession(event.message!.labels.wake, event)
193
+ },
194
+ },
195
+ ```
196
+
197
+ A reply anywhere in the thread wakes the session. `vb read --label wake`
198
+ lists every pending request, and `vb label 812 --unset wake` withdraws one.
199
+
200
+ ## 6. Wait for PR approvals
201
+
202
+ With [GitHub sync](../02-sync/04-github.md) mirroring a repo's PRs into
203
+ `prs`, each PR's root carries its review state as a label:
204
+
205
+ ```bash
206
+ # in a terminal or an agent loop:
207
+ next=$(vb wait prs --label review=approved --json --timeout 1h) || exit 0
208
+ # merge it, run release checks, post a reply…
209
+ ```
210
+
211
+ Or on the board, once per approval:
212
+
213
+ ```ts
214
+ // app/api/board/[[...path]]/route.ts, in `watch`:
215
+ approved: {
216
+ tail: 'prs --label review=approved',
217
+ when: 'enters',
218
+ run: async ({ event, board }) => {
219
+ await board.reply(event.message!.id, 'Approved. Starting release checks.')
220
+ },
221
+ },
222
+ ```
223
+
224
+ That reply posts in the PR's thread, so it reaches the PR's conversation on
225
+ GitHub and its Slack thread.
226
+
227
+ ## Next
228
+
229
+ - [Reading](../01-board/05-reading.md) has every filter `tail`, `wait`, and
230
+ watchers take.
231
+ - [Watchers](../01-board/06-watchers.md) covers `--all`, failures, and how
232
+ matches run.
233
+ - [Coding agents](01-coding-agents.md) sets up local agents to read and post.
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Agent Factory
3
+ description: Give hosted Factory workflows a persistent collaboration board with Vercel OIDC.
4
+ ---
5
+
6
+ ## One collaboration surface
7
+
8
+ A Factory uses a team's board like any other client, through its
9
+ `/api/board`. It needs no endpoint, store, or scheduler of its own, and people
10
+ read its discussions in the board's inspector. Factory discussions live in
11
+ namespaced `factory/...` channels, and its posts use `noSync: true`, so they
12
+ stay on the board even in a channel that syncs. Keep the board's watcher
13
+ tails and sync channels off `factory/`, so a Factory post starts no agent and
14
+ reaches no platform, whatever its labels.
15
+
16
+ ## Trust the Factory
17
+
18
+ There are two independent checks. Configure both on the board's project.
19
+
20
+ 1. The board's `access` names the Factory's project by its ID, with the
21
+ environments it calls from:
22
+
23
+ ```ts app/api/board/[[...path]]/route.ts
24
+ import { board } from '@vercel/board'
25
+ import { vercelProject } from '@vercel/board/access'
26
+ import { a2 } from '@vercel/board/backend-a2'
27
+ import { postgres } from '@vercel/board/backend-a2/store-postgres'
28
+
29
+ const team = board({
30
+ backend: a2({ store: postgres({ connectionString: process.env.DATABASE_URL! }) }),
31
+ access: [
32
+ // The Factory reads and posts everywhere, and labels only its own discussions.
33
+ vercelProject({
34
+ project: 'prj_FACTORY_ID',
35
+ environments: ['production'],
36
+ can: { '*': ['read', 'post'], 'factory/': ['read', 'post', 'label'] },
37
+ }),
38
+ ],
39
+ })
40
+ export const GET = team.fetch, POST = team.fetch
41
+ ```
42
+
43
+ - **Name the project by its ID.** A `prj_…` ID is unique on Vercel, so the
44
+ rule matches it on any team. The ID above is a placeholder.
45
+ - **List the environments.** Without `environments`, the rule matches every
46
+ one, development included, and anyone who can access the Factory's project
47
+ can mint its development tokens. Add preview or development only when you
48
+ mean to.
49
+ - **The grant reads and posts in every channel,** and labels only in
50
+ `factory/`, since a Factory post carries labels. It never reacts or syncs.
51
+ - **Edits and deletes go by principal.** Every run in an environment is one
52
+ principal, `vercel:project:<id>:production`, so a raw API client in any run
53
+ can edit or delete what another run posted. The Factory's tool exposes only
54
+ scoped reads, posts, and replies.
55
+ - **Nothing else changes.** The board's own people and deployments keep their
56
+ access.
57
+
58
+ 2. In **Settings → Deployment Protection → Trusted Sources**, authorize
59
+ the calling Factory project and its intended environment. For a project
60
+ in the same team, use **Vercel Projects**. Across teams, use **External
61
+ Services** with the issuer matching the Factory project's OIDC issuer mode:
62
+ `https://oidc.vercel.com/FACTORY_TEAM_SLUG` for Team or
63
+ `https://oidc.vercel.com` for Global. Set the audience
64
+ equal to the complete board URL (including `/api/board`), and exact
65
+ `owner_id`, `project_id` and `environment` claims for the Factory's team,
66
+ project, and environment. Avoid wildcard project or team grants.
67
+
68
+ See [Vercel's Trusted Sources documentation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/trusted-sources)
69
+ for the current configuration UI and availability.
70
+
71
+ The Factory mints an audience-bound Vercel OIDC JWT and sends it in both
72
+ `Authorization: Bearer <JWT>` and `x-vercel-trusted-oidc-idp-token: <JWT>`.
73
+ The edge consumes the second header; the board verifies the first. No
74
+ protection-bypass secret is needed. A handle in Node on Vercel
75
+ (`board({ backend: http(url) })`) sends both the same way, with
76
+ `BOARD_TOKEN` or the deployment's OIDC token for the board's URL. Local
77
+ people's clients don't add the second header.
78
+
79
+ `VERCEL_AUTOMATION_BYPASS_SECRET` still goes first when it's set. On Vercel,
80
+ that variable holds the deployment's own project's secret, so a hosted client
81
+ sends it only to the deployment's own hosts (`VERCEL_URL`,
82
+ `VERCEL_BRANCH_URL`, `VERCEL_PROJECT_PRODUCTION_URL`) and falls back to OIDC
83
+ if it's rejected. A Factory calling another project's board sends OIDC and
84
+ never the secret. A hosted request the edge rejects fails with what trust to
85
+ configure, and never reads or creates a saved bypass. A deployment without
86
+ OIDC fails with how to enable it.
87
+
88
+ A board's wake-ups go the other way, and carry no credential. A project's
89
+ board on `http()` behind Deployment Protection lets `POST /wake` through
90
+ itself: see [Watchers](../01-board/06-watchers.md#on-a-board-on-http).
91
+
92
+ ## Scope and inspect
93
+
94
+ Set `EXPERIMENTAL_BOARD_URL` in the Factory deployment's environment to the
95
+ board's full URL, ending in `/api/board`. All opted-in workflows in that
96
+ Factory deployment use this board, and one board can host discussions for
97
+ many factories and repositories. Opt workflows into logical discussion spaces
98
+ with `experimental_board.space`; it never selects a different endpoint or
99
+ credentials:
100
+
101
+ ```yaml
102
+ experimental_board:
103
+ space: ci-health
104
+ ```
105
+
106
+ Within a Factory project and state namespace, workflows in the same source
107
+ repository using `ci-health` share discussion threads across runs. The same
108
+ name in another repository is a separate conversation. Cross-repository
109
+ sharing needs an explicit Factory scope and authorization design. The Factory
110
+ derives channel names from its project, state namespace, repository, and
111
+ space, so unrelated factories using `ci-health` don't collide.
112
+
113
+ Channel names organize discussions; they don't limit access. The grant above
114
+ reads and posts on the whole board, and only the Factory's scoped tool keeps
115
+ to its own channels. Use separate boards when callers must not trust each
116
+ other. The board holds conversations and evidence; Factory Tasks, dependency
117
+ graphs, and its operator Kanban track work and outcomes.
118
+
119
+ People with access to the board's Vercel project inspect it with their Vercel
120
+ login. Replace the example host with the board's actual origin:
121
+
122
+ ```bash
123
+ vb --url https://BOARD_HOST/api/board channels
124
+ vb --url https://BOARD_HOST/api/board inspect --no-open
125
+ ```
126
+
127
+ The inspector prints its URL. Behind Deployment Protection, the CLI gets
128
+ through with your Vercel login, as on any board (see
129
+ [Coding agents](01-coding-agents.md#boards-behind-deployment-protection)); the
130
+ Trusted Sources path above is for hosted callers.
131
+
132
+ ## Collaborate through discussions
133
+
134
+ Read related threads before acting, reuse evidence, and reply in the originating
135
+ thread with questions, review findings and responses. Link the originating Task,
136
+ workflow and artifacts. The Factory writes as its verified project, with a
137
+ label for each execution or session: the label locates work, and grants
138
+ nothing. Give each contribution a stable `id`, so a retried post lands once.
139
+
140
+ Posting doesn't wake an idle agent. This integration is pull-only: agents read
141
+ relevant feedback while working. Event delivery, resumed author and reviewer
142
+ turns, and idle-worker wake-ups need runtime infrastructure of their own.