@adrata/adrata-mcp 1.0.19 → 1.0.40

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.
package/README.md CHANGED
@@ -268,6 +268,9 @@ All free tools plus:
268
268
  | `research_person` | Composite: deep person research |
269
269
  | `discover_prospects` | Composite: find new prospects |
270
270
  | `get_next_contacts` | Composite: next best contacts to reach |
271
+ | `commit_lead_to_outbound` | Governed: commit a lead above the /targets line — Pipeline's Outbound stage |
272
+ | `release_lead_from_outbound` | Governed: take a lead back out of Outbound |
273
+ | `list_outbound_leads` | Who is committed to Outbound right now |
271
274
  | `get_competitive_intel` | Composite: competitive analysis |
272
275
  | `get_meeting_brief` | Composite: pre-meeting intelligence |
273
276
  | `get_deal_coaching` | Composite: deal coaching insights |
package/access/tiers.js CHANGED
@@ -27,6 +27,12 @@ export const TOOL_TIERS = {
27
27
  // or reach a second workspace from the CLI at all.
28
28
  list_workspaces: TIERS.ENTERPRISE,
29
29
  switch_workspace: TIERS.ENTERPRISE,
30
+ // Provisioning a customer's workspace: reading who is seated, and seating
31
+ // somebody with the onboarding link that reaches them. Enterprise because it
32
+ // is workspace administration -- the API separately requires the connected
33
+ // user to be a workspace admin and answers 403 otherwise.
34
+ list_workspace_seats: TIERS.ENTERPRISE,
35
+ create_onboarding_link: TIERS.ENTERPRISE,
30
36
  adrata_api_catalog: TIERS.FREE,
31
37
  paper_app_audit: TIERS.FREE,
32
38
  adrata_desktop_app_audit: TIERS.FREE,
@@ -220,6 +226,7 @@ export const TOOL_TIERS = {
220
226
  list_work_boards: TIERS.ENTERPRISE,
221
227
  list_source_control_connections: TIERS.ENTERPRISE,
222
228
  get_source_control_connection_events: TIERS.ENTERPRISE,
229
+ bind_repository_to_board: TIERS.ENTERPRISE,
223
230
  get_work_item_delivery_evidence: TIERS.ENTERPRISE,
224
231
  audit_work_hub: TIERS.ENTERPRISE,
225
232
  list_work_item_acceptance_criteria: TIERS.ENTERPRISE,
@@ -235,6 +242,7 @@ export const TOOL_TIERS = {
235
242
  record_work_item_qa_failure_and_release: TIERS.ENTERPRISE,
236
243
  requeue_work_item_qa_after_fix: TIERS.ENTERPRISE,
237
244
  get_work_item_history: TIERS.ENTERPRISE,
245
+ get_work_item_field_changes: TIERS.ENTERPRISE,
238
246
  get_work_item_comments: TIERS.ENTERPRISE,
239
247
  get_work_board_rollup: TIERS.ENTERPRISE,
240
248
  list_work_board_rollups: TIERS.ENTERPRISE,
@@ -243,6 +251,8 @@ export const TOOL_TIERS = {
243
251
  attach_work_item_qa_evidence: TIERS.ENTERPRISE,
244
252
  verify_work_item_qa_evidence_playback: TIERS.ENTERPRISE,
245
253
  move_work_item: TIERS.ENTERPRISE,
254
+ preview_work_item_acceptance_reaffirmation: TIERS.ENTERPRISE,
255
+ reaffirm_work_item_current_acceptance: TIERS.ENTERPRISE,
246
256
  transfer_work_item_between_boards: TIERS.ENTERPRISE,
247
257
  set_work_item_tag: TIERS.ENTERPRISE,
248
258
  block_work_item: TIERS.ENTERPRISE,
@@ -258,6 +268,11 @@ export const TOOL_TIERS = {
258
268
  unsatisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
259
269
  comment_on_work_item: TIERS.ENTERPRISE,
260
270
  flag_work_item: TIERS.ENTERPRISE,
271
+ // Retiring a card and bringing it back. Same reasoning as every board write:
272
+ // these change a real workspace record.
273
+ archive_work_item: TIERS.ENTERPRISE,
274
+ delete_work_item: TIERS.ENTERPRISE,
275
+ unarchive_work_item: TIERS.ENTERPRISE,
261
276
  // The containers above the cards, and the "add this to the roadmap" verb.
262
277
  // Same reasoning as every board tool: scopes are real workspace records.
263
278
  list_work_scopes: TIERS.ENTERPRISE,
@@ -411,6 +426,18 @@ export const TOOL_TIERS = {
411
426
  // get_speedrun_list already mapped above
412
427
  discover_prospects: TIERS.PRO,
413
428
  get_next_contacts: TIERS.PRO,
429
+ // The Outbound trio, registered in `toolsets/prospecting.js` beside `get_next_contacts` and
430
+ // named in its header alongside it. They arrived on the desktop branch while this tier map
431
+ // grew on main, so neither side could see the gap: the registry had three tools this table
432
+ // did not, and `getToolTier` falls back to ENTERPRISE for anything unlisted -- which would
433
+ // have quietly put a PRO workspace's own Outbound moves behind the top tier rather than
434
+ // erroring. The exhaustiveness test is what refuses the fallback, and it is right to.
435
+ //
436
+ // PRO, matching the toolset they ship in: committing a lead to Outbound IS the pipeline move
437
+ // for a person, so it belongs with the prospecting reads that lead to it, not above them.
438
+ commit_lead_to_outbound: TIERS.PRO,
439
+ list_outbound_leads: TIERS.PRO,
440
+ release_lead_from_outbound: TIERS.PRO,
414
441
 
415
442
  // --- COMPOSITE TOOLSETS: intelligence (pro tier) ---
416
443
  get_competitive_intel: TIERS.PRO,
package/api-bridge.js CHANGED
@@ -298,6 +298,12 @@ const PATH_WRITE_SCOPES = [
298
298
  ['/api/v1/contacts', 'write:people'],
299
299
  ['/api/v1/people', 'write:people'],
300
300
  ['/api/v1/leads', 'write:people'],
301
+ // Lead lists — including the marker-owned Outbound list that /targets and
302
+ // Pipeline share — are gated by the API on read:speedrun for reads AND
303
+ // writes (scope_guard/mod.rs: `path.starts_with("/lead-lists")`). Naming it
304
+ // here turns the preview's "unknown (no mapping for this path)" into the
305
+ // scope the server actually checks.
306
+ ['/api/v1/lead-lists', 'read:speedrun'],
301
307
  ['/api/v1/agent-tasks', 'write:tasks'],
302
308
  ['/api/v1/tasks', 'write:tasks'],
303
309
  ['/api/v1/actions', 'write:actions'],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adrata/adrata-mcp",
3
- "version": "1.0.19",
4
- "description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. 80+ tools for companies, people, deals, actions, buyer groups, warm intros, webhooks, intelligence, and more.",
3
+ "version": "1.0.40",
4
+ "description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. About 275 tools registered at startup for companies, people, deals, actions, buyer groups, warm intros, webhooks and intelligence, plus 65 more behind eight named toolsets you load with enable_toolset.",
5
5
  "type": "module",
6
6
  "main": "server.js",
7
7
  "bin": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
- "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
12
+ "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js security.cap-contract.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js toolsets/prospecting.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/field-changes.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/provisioning/onboarding-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
13
13
  },
14
14
  "keywords": [
15
15
  "mcp",
package/security.js CHANGED
@@ -143,18 +143,169 @@ export function rateLimitResponse(retryAfterMs) {
143
143
  // 2. Input Validation
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
+ /**
147
+ * Length caps for TOP-LEVEL string tool arguments, by argument name.
148
+ *
149
+ * `applySecurityLayer` wraps every tool this server registers, so these caps
150
+ * are the FIRST thing any argument meets — before Zod, before the handler,
151
+ * before the API. Until 2026-09-05 they TRUNCATED, silently, and the tool then
152
+ * returned success: a card flag ended mid-word at exactly 1000 characters
153
+ * ("Do not substitute API SHA or bounce the card merely t") and the
154
+ * prohibition was gone for every subsequent reader including its author.
155
+ *
156
+ * Every comparable cap in the Rust API refuses instead — comments 2000,
157
+ * criterion parts 600, engineering-proof fields 600, reviewer model 120,
158
+ * evidence summary 600 — several enforced twice, once in the handler and
159
+ * again as a database CHECK. So every caller already handles a refusal. This
160
+ * layer was the only one that cut, and it ran first, which is why the cut was
161
+ * invisible from both ends.
162
+ */
163
+ const DEFAULT_STRING_CAP = 1000;
164
+
165
+ /**
166
+ * The API's own cap on a card comment, and on every free-text `reason` that
167
+ * routes through `validate_body`: flag, block, dependency, sharing, QA-flow
168
+ * note. Rust calls it `MAX_COMMENT_BODY_CHARS`
169
+ * (`code/api/crates/routes-gtm/src/work_boards/mod.rs`).
170
+ *
171
+ * `security.cap-contract.test.js` reads that constant out of the Rust
172
+ * source and fails if this number ever drops below it, because THAT is the
173
+ * divergence this whole change is about: a cap here that sits below the cap
174
+ * downstream cuts a value the API would have accepted, and the API never gets
175
+ * to refuse it.
176
+ */
177
+ const API_COMMENT_BODY_CAP = 2000;
178
+
179
+ /**
180
+ * Sanity ceiling for an argument that carries a document or a data file rather
181
+ * than prose (a CSV to import, a knowledge file's markdown, a paper's HTML).
182
+ * Deliberately far above any product rule: the point is to bound the transport,
183
+ * and to let the API be the layer that says how big a document may be.
184
+ */
185
+ const BULK_PAYLOAD_CAP = 1_000_000;
186
+
187
+ const STRING_CAPS_BY_KEY = {
188
+ // Long-form prose the API caps far lower anyway (a comment body is refused
189
+ // at 2000). 10000 here is a sanity ceiling, not the product rule.
190
+ body: 10000,
191
+ message: 10000,
192
+ description: 10000,
193
+
194
+ // The reason family. These were the DANGEROUS truncations and the reason
195
+ // this cap exists as its own class:
196
+ //
197
+ // flag_work_item.reason API cap 2000 — a 1000-cut value was
198
+ // ACCEPTED truncated. This is the flag
199
+ // defect: a prohibition ended mid-word at
200
+ // exactly 1000 characters and read as
201
+ // complete to every later reader.
202
+ // block_work_item.reason API cap 2000 — same shape.
203
+ // move_work_item.reason NO API cap at all, so nothing downstream
204
+ // transfer_...between_boards could ever catch the cut. Permanently and
205
+ // silently shortened to 1000.
206
+ // every X-Adrata-Reason header NO API cap anywhere in Rust
207
+ // (api-bridge.js) (presence-only). Same silence.
208
+ //
209
+ // Raising these to the API's own 2000 is what keeps this change from
210
+ // trading a silent cut for a refusal of honest input: a 1400-character
211
+ // reason is legitimate, the API accepts it, and until now this layer cut it.
212
+ reason: API_COMMENT_BODY_CAP,
213
+
214
+ // `auditReason` is the same field family under a different name: the shared
215
+ // `moneyWrite` envelope (server.js) spreads it into EVERY money write, so it
216
+ // was the one reason-shaped argument the first version of this table missed.
217
+ auditReason: API_COMMENT_BODY_CAP,
218
+
219
+ // `record_work_item_qa_failure_and_release.details` is composed into a flag
220
+ // reason (`"QA criterion failure (<id>): " + details`) and so lands under
221
+ // the same 2000 API cap. A 1000-character cut plus that ~14-character prefix
222
+ // stayed comfortably under 2000, which is exactly why the "~1,014 characters,
223
+ // 200 OK" measurement was silent. Known rough edge: a `details` near 2000
224
+ // composes to over 2000 and the API refuses the composed value with its own
225
+ // message. That is a refusal rather than a silence, which is the property
226
+ // this change is about.
227
+ details: API_COMMENT_BODY_CAP,
228
+
229
+ // Search terms. A query longer than this is a paste, not a search.
230
+ query: 500,
231
+ search: 500,
232
+
233
+ // --- Payloads, not prose ---
234
+ //
235
+ // These carry a document or a data file, and at the old 1000 default they
236
+ // were the WORST instances of the truncation: `bulk_import`/`manage_data`
237
+ // took a CSV — any real one is past 1000 in its first few rows — cut it to
238
+ // 1000, imported the fragment, and reported success. A partial import that
239
+ // announces itself as complete is harder to notice than a lost sentence.
240
+ //
241
+ // The ceiling here is a transport sanity bound, NOT a product rule. This
242
+ // layer has no business deciding how large a knowledge file may be; the API
243
+ // owns that, and it can refuse. What this layer must never again do is
244
+ // decide it silently.
245
+ csvData: BULK_PAYLOAD_CAP,
246
+ content: BULK_PAYLOAD_CAP,
247
+ };
248
+
249
+ /** The cap that applies to a top-level string argument named `key`. */
250
+ export function maxLengthForKey(key) {
251
+ return Object.prototype.hasOwnProperty.call(STRING_CAPS_BY_KEY, key)
252
+ ? STRING_CAPS_BY_KEY[key]
253
+ : DEFAULT_STRING_CAP;
254
+ }
255
+
256
+ /**
257
+ * The refusal text. It names the actual length, the cap, and how much to cut,
258
+ * because the caller is usually an agent that can shorten and retry — and it
259
+ * says plainly that nothing was written, since the whole point of the defect
260
+ * this replaces was a caller believing a truncated write had landed.
261
+ */
262
+ export function overLengthMessage(field, length, maxLength) {
263
+ return `Parameter "${field}" is ${length} characters and the cap is ${maxLength} — `
264
+ + `remove ${length - maxLength}. Nothing was written: the value was refused whole `
265
+ + `rather than cut, so shorten it and call again.`;
266
+ }
267
+
146
268
  /**
147
269
  * Common validation schemas for tool parameters.
148
270
  * These wrap and tighten the existing Zod schemas.
149
271
  */
150
272
  export const validators = {
151
- /** Sanitize a string — strip control characters, limit length. */
152
- sanitizeString(value, maxLength = 1000) {
153
- if (typeof value !== 'string') return value;
154
- // Strip control characters except newline/tab
155
- // eslint-disable-next-line no-control-regex
156
- const cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
157
- return cleaned.slice(0, maxLength);
273
+ /**
274
+ * Strip control characters, and REFUSE a string longer than `maxLength`.
275
+ *
276
+ * Returns `{ value, error }` rather than a bare string on purpose. Until
277
+ * 2026-09-05 this ended `return cleaned.slice(0, maxLength)` — the value was
278
+ * cut, `validateToolInput` still reported `valid: true`, and the write went
279
+ * on to return 200. The object shape means a caller cannot use the result as
280
+ * a string without noticing, so the refusal cannot be dropped the way the
281
+ * silent slice was.
282
+ *
283
+ * Length is counted in CODE POINTS (`[...cleaned].length`), matching the
284
+ * Rust API's `chars().count()` downstream rather than UTF-16 code units, so
285
+ * a value this layer accepts is not one the next layer measures differently.
286
+ */
287
+ sanitizeString(value, maxLength = DEFAULT_STRING_CAP, field = 'value') {
288
+ if (typeof value !== 'string') return { value, error: null };
289
+ // Vertical tab (0x0B) and form feed (0x0C) ARE whitespace: they separate
290
+ // words. Deleting them outright fuses the tokens either side into one —
291
+ // "evidence<VT>01M1RAMY" becomes "evidence01M1RAMY" — and that is worse to
292
+ // read than a truncation, because the sentence still looks complete, so a
293
+ // reader has no tell at all. They collapse to a space. Every other control
294
+ // character in the class is not a separator, so removing it stays correct.
295
+ //
296
+ // Tab (0x09), newline (0x0A) and carriage return (0x0D) sit outside both
297
+ // classes and pass through untouched, as they always have.
298
+ const cleaned = value
299
+ // eslint-disable-next-line no-control-regex
300
+ .replace(/[\x0B\x0C]/g, ' ')
301
+ // eslint-disable-next-line no-control-regex
302
+ .replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '');
303
+ // Measured AFTER stripping: control characters must not eat the budget.
304
+ const length = [...cleaned].length;
305
+ if (length > maxLength) {
306
+ return { value: cleaned, error: overLengthMessage(field, length, maxLength) };
307
+ }
308
+ return { value: cleaned, error: null };
158
309
  },
159
310
 
160
311
  /** Validate that a string doesn't contain obvious injection patterns. */
@@ -223,13 +374,15 @@ export function validateToolInput(toolName, args) {
223
374
  continue;
224
375
  }
225
376
 
226
- // Sanitize string values
227
- const maxLen = key === 'body' || key === 'message' || key === 'description'
228
- ? 10000
229
- : key === 'query' || key === 'search'
230
- ? 500
231
- : 1000;
232
- sanitized[key] = validators.sanitizeString(value, maxLen);
377
+ // Sanitize string values. An over-long value is REFUSED, not cut:
378
+ // see the cap table above for why this layer used to truncate silently.
379
+ const { value: cleaned, error: lengthError } =
380
+ validators.sanitizeString(value, maxLengthForKey(key), key);
381
+ if (lengthError) {
382
+ errors.push(lengthError);
383
+ continue;
384
+ }
385
+ sanitized[key] = cleaned;
233
386
 
234
387
  // Validate specific field types
235
388
  if (key === 'id' || key.endsWith('Id')) {
package/server.js CHANGED
@@ -55,6 +55,7 @@ import { registerEmailTools } from './tools/email-tools.js';
55
55
  import { registerWorkBoardTools } from './tools/work-board-tools.js';
56
56
  import { registerSourceControlTools } from './tools/source-control/connection-tools.js';
57
57
  import { registerRoadmapTools } from './tools/roadmap-tools.js';
58
+ import { registerProvisioningTools } from './tools/provisioning/onboarding-tools.js';
58
59
  import { registerPaperTools } from './tools/paper-tools.js';
59
60
  import { register as registerAlwaysLoadedTools } from './toolsets/revenue/always-loaded.js';
60
61
  import { register as registerExtensibilityTools } from './toolsets/extensibility.js';
@@ -2394,7 +2395,29 @@ registerWorkBoardTools(server, {
2394
2395
  getGrantedScope: () => loadTokens()?.scope,
2395
2396
  });
2396
2397
 
2397
- registerSourceControlTools(server, { z, api, ok });
2398
+ registerSourceControlTools(server, {
2399
+ z,
2400
+ api,
2401
+ ok,
2402
+ validateApiBridgeRequest,
2403
+ buildMutationHeaders,
2404
+ getGrantedScope: () => loadTokens()?.scope,
2405
+ });
2406
+
2407
+ // Seating a customer's people and minting the onboarding link an AE hands them.
2408
+ // The first path from MCP that can actually finish: `invite_user` through
2409
+ // `adrata_ai_tool_execute` refuses every machine principal at the confirmation
2410
+ // step (ai_crm_tools/mod.rs:427), so the bridge could preview a seat forever and
2411
+ // create none. See tools/provisioning.js for why this is one tool and not two.
2412
+ registerProvisioningTools(server, {
2413
+ z,
2414
+ api,
2415
+ ok,
2416
+ validateApiBridgeRequest,
2417
+ buildMutationHeaders,
2418
+ getGrantedScope: () => loadTokens()?.scope,
2419
+ getWorkspaceId: () => loadTokens()?.workspaceId ?? AUTH?.workspaceId ?? null,
2420
+ });
2398
2421
 
2399
2422
  // The containers above the cards, and the "add this to the roadmap" verb
2400
2423
  // (company/decisions/2026-08-06-spoq-roadmap-sync.md).
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.adrata/adrata-mcp",
4
4
  "description": "Adrata revenue-intelligence MCP server: companies, people, opportunities, actions, buyer groups, enrichment, email, and workspace operations for AI agents.",
5
5
  "status": "active",
6
- "version": "1.0.19",
6
+ "version": "1.0.40",
7
7
  "websiteUrl": "https://adrata.com/developers",
8
8
  "repository": {
9
9
  "url": "https://github.com/adrata/adrata",
@@ -15,7 +15,7 @@
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "@adrata/adrata-mcp",
18
- "version": "1.0.19",
18
+ "version": "1.0.40",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  },
@@ -13,7 +13,14 @@ The workflow semantics are fixed by
13
13
  `company/decisions/2026-08-29-two-gate-ai-qa-human-production.md`; the required
14
14
  evidence loop is fixed by
15
15
  `company/decisions/2026-08-29-recorded-staging-qa-standard.md`. This skill is
16
- their operating procedure.
16
+ their operating procedure. Read the current amendment at
17
+ `company/decisions/2026-09-04-complete-quality-gates-and-instant-interactions.md`
18
+ and the card contract at
19
+ `company/playbooks/engineering/card-readiness-and-evidence-contract.md`.
20
+ Every required criterion/configuration must pass: failed, blocked, skipped,
21
+ missing, stale, unknown, or flaky results block completion. Do not use a human
22
+ acknowledgement or legacy unknown-author exception to clear an incomplete gate.
23
+ No visible UI changes are authorized by the quality audit.
17
24
 
18
25
  ## Establish the real queue
19
26
 
@@ -171,8 +178,14 @@ does not establish that the screen is visually correct.
171
178
  `ADRATA_MCP_CONFIG_DIR`. It opens the exact deployed product bundle through a
172
179
  one-use, read-only loopback session: upstream reads keep the QA OAuth identity,
173
180
  writes fail locally, and private media capabilities remain process-private.
174
- Record the returned session fingerprint and the deployed app build in the
175
- playback evidence, then close the bridge.
181
+ It defaults to the canonical deployment on both halves the canonical API for
182
+ the card and `https://adrata.com` for the bundle — and serves that origin's own
183
+ response policy, so whether a recording plays is decided by the deployed
184
+ `media-src` rather than by no policy at all. Record the returned session
185
+ fingerprint, the deployed app build, AND the `Deployed security policy served
186
+ by this bridge` line in the playback evidence, then close the bridge. If that
187
+ line is the WARNING instead, the bridge sent no policy and the playback it
188
+ showed says nothing about the deployed one.
176
189
 
177
190
  ## Manage cards as outcomes, not bug counters
178
191
 
@@ -36,22 +36,34 @@ implementation-and-submit path below.
36
36
  available here, and the reviewer's comment is usually the one thing that
37
37
  would have stopped you.
38
38
 
39
- **A card carrying a `flag` is not workable.** The flag means somebody has already
40
- found a problem with the card ITSELF unclear scope, no repro, a blocked
41
- dependency, a question waiting on an answer and its `reason` says which. It is
42
- a different claim from the tag, which is only about urgency: a flagged Critical
43
- card is still not something to start on. Read the comments for the surrounding
44
- argument and pick a different card, unless you are the person who can answer it.
45
- If you resolve it, clear the flag with `flag_work_item` (`flagged: false`) and
46
- say what resolved it. Anyone may clear one, which is exactly why every clear is
47
- recorded as a comment.
48
-
49
- If the card is not workable no repro, no expected behaviour, ambiguous scope —
50
- **flag it** with `flag_work_item` and the one question that would unblock it,
51
- then stop. Do not invent the missing half. Flagging is what turns "I could not
52
- work this" into something the next person sees before they pick it up, rather
53
- than a sentence in your report that nobody reads. An unworkable card is a triage
54
- problem, not a coding problem.
39
+ **Read the flag's reason and `blocksClaim` separately.** A flag preserves a
40
+ finding; `blocksClaim` says whether an agent can take the work. An ordinary
41
+ failed test, fixture repair or deploy prerequisite that an agent can resolve
42
+ stays claimable with `blocksClaim: false`. Keep the finding visible while
43
+ repairing it. Urgency still comes from the tag, not the flag.
44
+
45
+ Use `blocksClaim: true` only for an explicit prerequisite agents cannot satisfy,
46
+ such as owner authorization, vendor consent or a product decision. Read its
47
+ comments and take another workable card until that prerequisite is satisfied.
48
+ When the field is absent, inspect the deployed API and tool contract: older
49
+ versions treat every flag as blocking. Do not clear an unresolved finding to
50
+ work around an old claim gate; record the release dependency instead.
51
+
52
+ When the finding is resolved, use `flag_work_item` (`flagged: false`) with the
53
+ `expectedFlagGeneration` captured for that finding and a concrete resolution
54
+ reason. A stale-generation refusal means a newer finding must be read and
55
+ resolved; substituting its generation without doing that loses the newer
56
+ failure. If the generation is absent, do not invent one or claim the older
57
+ client supplied concurrent-update protection. Record the release/client
58
+ prerequisite. For a deployed QA repair, use `requeue_work_item_qa_after_fix` with the
59
+ actual failed/deployed builds and `repairedFlagGeneration`, after proving the
60
+ fix's relevant blobs are deployed. Clearing a flag is not a QA pass.
61
+
62
+ If a missing repro, expected behavior or scope decision prevents work, record
63
+ what is missing and the next action on the card. Set a blocking flag only when
64
+ the prerequisite needs someone outside the agent work loop. Do not invent the
65
+ missing half, and do not turn an ordinary engineering failure into an owner
66
+ decision.
55
67
 
56
68
  ## Claim it, in the same move, FIRST
57
69
 
@@ -77,8 +89,10 @@ Three reasons, and the third is the real one:
77
89
  Reuse the same `idempotencyKey` if the call fails and you retry. The server
78
90
  replays rather than appending a second transition.
79
91
 
80
- **If the claim is refused**, the card already belongs to somebody: pick a
81
- different one. `force: true` exists it takes the card over and demands a reason
92
+ **If the claim is refused**, read its actual reason. Another worker, missing
93
+ scope, readiness requirements or an older deployed gate can each refuse a claim.
94
+ Resolve an agent-repairable prerequisite or pick another workable card; do not
95
+ treat every refusal as proof of ownership. `force: true` exists — it takes the card over and demands a reason
82
96
  saying why — but it is for a card whose owner is genuinely gone, not for working
83
97
  around a refusal. Taking someone's card because it was convenient is the one move
84
98
  on this board that costs trust permanently. If you use it, say so in your report,
@@ -114,27 +128,22 @@ Two things to carry back to the card:
114
128
  session), so the next person at aligning can ask you what you meant; what it
115
129
  must not arrive with is a list of outcomes nobody can check.
116
130
 
117
- ## Tick what you ran
118
-
119
- Before you move the card on, tick the criteria you actually executed, with
120
- `satisfy_work_item_acceptance_criterion` and the evidence in `note` — a build
121
- number, a run link, the caveat that makes it honest. An untouched checkbox says
122
- nothing about whether the check passed; it says nobody has been near it, which
123
- is the same thing an abandoned card says.
124
-
125
- Ticking from a build column records `claimed`, not `verified`. At a QA gate, an
126
- authenticated human may verify even when they own the card or authored the
127
- criterion: the human is the accountable exception when one person operates the
128
- workspace. An agent verifies only when its credential differs from the
129
- criterion author, and QA2 requires a different agent credential from the one
130
- whose clean receipt opened QA1. One AI cannot certify both gates. Tick anyway
131
- when the result is `claimed` it is a real state that says the work is believed
132
- done and tells QA what to rerun. Never tick a box you did not execute merely to
133
- improve the count.
134
-
135
- If a tick turns out to be wrong, `unsatisfy_work_item_acceptance_criterion`
136
- takes it off. It clears the note with it, so if the evidence is worth keeping,
137
- put it on the card with `comment_on_work_item` first.
131
+ ## Record builder readiness
132
+
133
+ Before QA1 entry, record the exact deployed build, implementation and test
134
+ blobs, executed checks, failures and replay instructions in the builder
135
+ handoff. Leave acceptance criteria **unmet and unverified** for independent QA,
136
+ as required by `AGENTS.md` and the recorded staging QA standard. Readiness means
137
+ deployed, testable and free of known card-specific failure; a missing fixture,
138
+ failing relevant check or unexplained runtime failure keeps the card in its
139
+ build column.
140
+
141
+ The builder releases its build claim at handoff and does not consume a QA-pass
142
+ lease or credential. A fresh independent reviewer follows `qa-the-card`; a
143
+ fixer never certifies its own repair. If an earlier unsupported tick needs to
144
+ be cleared, preserve its evidence in a comment before using
145
+ `unsatisfy_work_item_acceptance_criterion` through the governed current-stage
146
+ claim procedure.
138
147
 
139
148
  ## Submit it
140
149
 
@@ -38,6 +38,13 @@ const DOMAIN_OVERRIDES = {
38
38
  who_am_i: 'workspace',
39
39
  get_current_user: 'workspace',
40
40
 
41
+ // Provisioning: seating a user and minting the onboarding link that reaches
42
+ // them. Categorised 'workspace' rather than given a category of their own —
43
+ // these are workspace administration, and the profile that can administer a
44
+ // workspace is the one that should see them.
45
+ list_workspace_seats: 'workspace',
46
+ create_onboarding_link: 'workspace',
47
+
41
48
  // Governed API bridge.
42
49
  // Broad backwards-compatible escape hatches stay on the unprofiled Adrata
43
50
  // server. Product profiles use the namespace-bound capability bridge below.
@@ -241,10 +248,20 @@ const IDEMPOTENT_WRITES = new Set([
241
248
  // comment, criterion, or card.
242
249
  'move_work_item', 'transfer_work_item_between_boards', 'set_work_board_column_wip_limit', 'set_work_item_tag', 'set_work_item_kind',
243
250
  'create_work_item', 'comment_on_work_item', 'flag_work_item',
251
+ // Retiring an already-retired card is REFUSED with a 409 rather than
252
+ // replayed, so the reason of the first retirement cannot be silently
253
+ // overwritten — which makes a retry with the same key land on the same state.
254
+ // The restore is a plain clear of the same five columns.
255
+ 'archive_work_item', 'unarchive_work_item',
244
256
  // A repeat block is the same edge (the pair is unique), and a repeat
245
257
  // unblock deletes a row that is already gone. Neither compounds.
246
258
  'block_work_item', 'unblock_work_item',
247
259
  'add_work_item_acceptance_criterion',
260
+ // (delete_work_item needs no entry here either: DESTRUCTIVE_PATTERN matches
261
+ // the `delete_` prefix, so a host confirmation-gates it on its NAME. That is
262
+ // the whole reason the destructive branch is a separate tool rather than a
263
+ // `kind` argument on archive_work_item — a switch inside a safe-sounding name
264
+ // would be gated by nothing.)
248
265
  // Reclassifying is a PUT of the whole routing decision, not an append:
249
266
  // replaying the same key rewrites the criterion to the same route and the
250
267
  // server's receipt table records one change, never a second one.