pipebrain-mcp 0.49.0 → 0.50.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 (3) hide show
  1. package/README.md +14 -5
  2. package/dist/main.js +74 -97
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,16 +14,24 @@ Register it with Claude Code (no install step — `npx` fetches it), at **user s
14
14
  every repo on the machine gets the tools:
15
15
 
16
16
  ```
17
- claude mcp add pipebrain -s user -e PIPEBRAIN_API_URL=https://api.pipebrain.dev -e 'PIPEBRAIN_TOKEN=${PIPEBRAIN_TOKEN}' -- npx -y pipebrain-mcp@latest
17
+ claude mcp add pipebrain -s user -e PIPEBRAIN_API_URL=https://api.pipebrain.dev -e 'PIPEBRAIN_TOKEN=${PIPEBRAIN_TOKEN}' -e PIPEBRAIN_ORG=your-org-slug -- npx -y pipebrain-mcp@latest
18
18
  ```
19
19
 
20
- Both `-e` flags are required, not decoration. The server reads its configuration from the
21
- environment of the process that launches it, and refuses to start if either value is
22
- missing or malformed — including an unexpanded `${PIPEBRAIN_TOKEN}` placeholder, which is
23
- the failure that otherwise looks like a revoked token. There is no default API URL (see
20
+ The first two `-e` flags are required, not decoration. The server reads its configuration
21
+ from the environment of the process that launches it, and refuses to start if either value
22
+ is missing or malformed — including an unexpanded `${PIPEBRAIN_TOKEN}` placeholder, which
23
+ is the failure that otherwise looks like a revoked token. There is no default API URL (see
24
24
  the table below), so a registration that passes neither flag produces a server that never
25
25
  starts.
26
26
 
27
+ The third flag fails the other way, which is why it is easy to miss. Every call acts inside
28
+ one organization and no credential carries one, so a server registered without
29
+ `PIPEBRAIN_ORG` starts perfectly and is then refused on its first organization-scoped call.
30
+ Replace `your-org-slug` with your own slug or id; `ppb doctor` (from `pipebrain-cli`)
31
+ prints the ones you may name before any of this is wired. A call may still name a
32
+ different organization than the default, and `list_orgs` returns every organization your
33
+ credential may name — the same set the refusal lists.
34
+
27
35
  The single quotes around the token are load-bearing: they keep `${PIPEBRAIN_TOKEN}` a
28
36
  literal *reference* in Claude's config instead of expanding it as you type, so the token
29
37
  value itself never lands in a config file — the server resolves it from your environment
@@ -35,6 +43,7 @@ at each launch.
35
43
  | --- | --- |
36
44
  | `PIPEBRAIN_TOKEN` | Your `pbt_…` API token (Settings → Organization → API tokens). |
37
45
  | `PIPEBRAIN_API_URL` | API base URL, e.g. `https://api.pipebrain.dev`. Required — there is no default, so a session can always tell which environment it is bound to. |
46
+ | `PIPEBRAIN_ORG` | The organization slug or id calls act in unless a call names another. Optional, and its absence does not stop the server starting — but a server with no default is refused on every organization-scoped call, since no credential carries one. `list_orgs` returns the ones you may name. |
38
47
 
39
48
  No tool here RETURNS a secret value: the secrets tools answer with metadata only, a
40
49
  response never echoes a value, and there is deliberately no value-returning tool — no get,
package/dist/main.js CHANGED
@@ -17675,10 +17675,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17675
17675
  );
17676
17676
  }
17677
17677
  registerStrict("list_orgs", {
17678
- description: "List the organizations this credential may act in \u2014 each with its name, slug, id, kind (personal or shared) and your role there. THIS IS THE ONE TOOL THAT NEEDS NO `org`: every other call names the organization it acts in and is refused if it names none, and this is how you find out which names you may use. The answer is your live memberships at this moment, which is exactly what that check tests against, so an organization you have just joined appears here and one you have been removed from does not (\xA77)."
17679
- // NO inputSchema, and the absence is the point rather than an omission. There is
17680
- // nothing to narrow: the answer is the caller's own memberships, and a filter over a
17681
- // list this short would be a second way to ask one question.
17678
+ description: "List the organizations this credential may act in, with each name, slug, id, kind and your role. This is the one tool that needs no `org`: every other call names the organization it acts in and is refused without one, and this is how you find the names you may use. The answer is your memberships right now, which is what that check reads, so a joined organization appears and a left one does not."
17682
17679
  }, () => (
17683
17680
  // A BARE ARRAY, unlike `list_waiting`'s `{ items }`, and for the reason that tool
17684
17681
  // gives rather than against it: the envelope has already been unwrapped by
@@ -17698,7 +17695,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17698
17695
  run(() => client2().unscoped().listOrgs(), GUIDANCE_POINTER)
17699
17696
  ));
17700
17697
  registerStrict("list_projects", {
17701
- description: "List all projects with their key, environments, milestones (token, label, active, archived \u2014 an archived milestone is a finished release retired from the pickers used to schedule work, still valid on every issue that already names it), deliverable targets (slug, label, active), and open-issue counts \u2014 the project switcher and workflow entry point (\xA77).",
17698
+ description: "List every project: key, environments, milestones (an archived one leaves the pickers, staying valid on issues naming it), deliverable targets and open-issue counts. Pages with `limit` (default 200, max 500) and `offset`.",
17702
17699
  // Optional pagination (§9) — bounds mirror list_issues/list_requirements; the
17703
17700
  // server defaults limit to 200 (max 500) and passes limit/offset as query params.
17704
17701
  inputSchema: {
@@ -17707,7 +17704,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17707
17704
  }
17708
17705
  }, (args) => run(() => client2().listProjects(args), GUIDANCE_POINTER));
17709
17706
  registerStrict("create_project", {
17710
- description: "Create a new project \u2014 the workflow bootstrap entry point, since issues and requirements are all project-scoped and nothing can be filed until a project exists. Both `name` (its display label) and `key` are required; the key is 2\u20135 uppercase letters (A\u2013Z, e.g. `DWB`), unique per org, and becomes the prefix of every display id in the project. Returns the created project (\xA77).",
17707
+ description: "Create a project. Nothing can be filed until one exists. `name` is the display label; `key` is 2\u20135 uppercase letters (e.g. `DWB`), unique per organization, and prefixes every display id.",
17711
17708
  // Inline shape: @pipeline/domain has no standalone create-project schema — the
17712
17709
  // same `{ name, key }` pair lives only inside createOrgInputSchema.project, so
17713
17710
  // this mirrors it exactly (key = 2–5 A–Z via projectKeySchema) and matches
@@ -17718,12 +17715,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17718
17715
  }
17719
17716
  }, (args) => run(() => client2().createProject(args)));
17720
17717
  registerStrict("list_waiting", {
17721
- description: "Every gate awaiting a PERSON across ALL projects in the organization this call acts in \u2014 name that organization with `org`, which is refused if omitted unless this session has a default \u2014 longest wait first: a requirement revision awaiting approval, a question awaiting an answer, and work in `verify` holding at least one criterion not yet passed. Each entry names its project, the ref the action is taken on (`KEY-R{n}` for a revision, the ISSUE\u2019s `KEY-{n}` for a question or for work at verify), and `waitingSince` \u2014 the instant the wait began, not a duration. Nothing else is a gate: work in `triage` never appears, and neither does work in verify whose criteria have ALL passed. THIS TOOL is READ-ONLY and clears nothing; what clears each gate differs. Answering a question has no tool on this surface, by design. A requirement revision is approved with `approve_requirement`. A verify gate clears as its criteria pass \u2014 an `auto` one through `set_criterion_state`, a `manual` one only ever by a person in the web app. Use it to see what your work is blocked on before asking, rather than to act on it."
17722
- // NO NARROWING ARGUMENTS. The one argument this tool does take — `org`, which every
17723
- // tool takes — is not a narrowing: it says WHICH ORGANIZATION the call acts in, not
17724
- // which of that organization's gates come back, and the caller's membership there is
17725
- // checked on the way in (PIPE-410). The viewer narrowing the web queue offers is
17726
- // filter state on that view, not something an agent picks.
17718
+ description: "Every gate awaiting a person across all projects in the organization named by `org`, longest wait first: a requirement revision awaiting approval, a question awaiting an answer, and work at verify still holding a criterion that has not passed. Each entry names its project, the ref to act on, and when the wait began. Read-only, and it clears nothing. A revision clears with `approve_requirement`. Use it to see what is waiting before you ask."
17727
17719
  }, () => (
17728
17720
  // `{ items }`, mirroring the HTTP envelope rather than unwrapping to a bare array
17729
17721
  // like the sibling list tools. The reason is the envelope itself: this tool is a
@@ -17746,12 +17738,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17746
17738
  // one description loaded into EVERY session by `anthropic/alwaysLoad`. The clauses
17747
17739
  // that went were the ones arguing for the rule rather than stating it; what a caller
17748
17740
  // needs to predict a refusal all stayed.
17749
- description: `Everything a session needs before it starts work, in ONE read \u2014 assembled by the API and returned verbatim, with the BINDING GUIDANCE appended after it: the product doctrine and this organization\u2019s own writing rules, in full. This is the one call that returns them; every other read only names it. CALL IT FIRST, before you write anything. Addressed by \`issue\` OR by \`project\`, never both and never neither: an issue names its own project, so the pair is refused rather than resolved for you (\xA77). Given an ISSUE: that issue\u2019s full detail \u2014 the same payload \`get_issue\` returns \u2014 PLUS the BODY TEXT of every requirement it links, at the revision the link is pinned at. That text is the round trip this removes: \`get_issue\` names which documents bind the work and at which revision, never what they SAY. Comments come CAPPED at the newest \`commentLimit\` (default ${WORK_CONTEXT_COMMENT_LIMIT}), with \`commentCount\` \u2014 the issue\u2019s TRUE total \u2014 beside them; read the issue directly for the rest. Given a PROJECT: \`available\`, the work a session could START right now. It EXCLUDES finished and abandoned work (\`done\`, \`killed\`), work another person has claimed, anything blocked \u2014 the status guard would refuse to start that \u2014 and work an operator has reserved for a person, which no token may claim \u2014 a reservation refuses the claim and nothing else. Your OWN claims stay, and so does unscheduled work carrying no milestone. \`available\` comes CAPPED at \`availableLimit\` rows, with \`availableCount\` \u2014 the project\u2019s TRUE total of startable work \u2014 beside it. The item form carries no \`available\`; the project form carries no item. BOTH forms also carry the project half: the release currently being worked (the active milestone) and the project\u2019s cross-cutting requirements WITH their text \u2014 they bind every epic automatically \u2014 and this organization\u2019s approved conventions in full, standing guidance that binds work in EVERY project here, not just this one. READ-ONLY \u2014 this claims nothing, moves nothing and writes nothing.`,
17750
- // The WHOLE domain schema rather than its `.shape`: the exactly-one-of rule is a
17751
- // refinement on the OBJECT, and a raw shape drops it — the SDK validates against
17752
- // what it is handed, so the shape alone would quietly accept both arguments and
17753
- // neither. Handing it the schema lets its refusal reach the caller verbatim,
17754
- // naming the two arguments it refused rather than a reworded stand-in.
17741
+ description: "Everything a session needs before starting work. Call it first: it alone returns the doctrine and the organization\u2019s writing rules in full. Address it by `issue` or `project`, never both. An issue returns its detail plus the text of each requirement it links, comments capped at `commentLimit` (default " + WORK_CONTEXT_COMMENT_LIMIT + "), with `commentCount` the true total. A project returns `available` \u2014 startable work, excluding done, killed, blocked, person-reserved and others\u2019 claims \u2014 capped at `availableLimit` with `availableCount` as the true total. Read-only.",
17755
17742
  inputSchema: getWorkContextInputSchema,
17756
17743
  // THE TWO ANNOTATIONS PIPE-D30 REQUIRES, and the reason each key is spelled out
17757
17744
  // rather than built: an unknown `_meta` key is IGNORED — no error, no warning — so a
@@ -17792,7 +17779,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17792
17779
  }
17793
17780
  );
17794
17781
  registerStrict("create_milestone", {
17795
- description: "Create a milestone on a project. `token` is the short ref issues point at via their `milestone` field (e.g. `M1`) and is unique per project (a duplicate is rejected); `label` is its display name (e.g. `Feature Verification`). Optional `position` orders it (0-based, defaults to last); optional `active` marks it the current milestone \u2014 AT MOST ONE milestone per project is active, so `active: true` transactionally DEMOTES the previously active one rather than failing. Returns the created milestone (\xA77).",
17782
+ description: "Create a milestone. `token` is the ref issues point at (e.g. `M1`) and is unique per project; a duplicate is rejected. `label` names it; `position` orders it, 0-based, last by default. `active: true` demotes whichever was active.",
17796
17783
  inputSchema: {
17797
17784
  project: projectKeySchema,
17798
17785
  token: external_exports.string().min(1),
@@ -17802,7 +17789,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17802
17789
  }
17803
17790
  }, ({ project, ...input }) => run(() => client2().createMilestone(project, input)));
17804
17791
  registerStrict("update_milestone", {
17805
- description: "Rename, reorder, archive or restore a milestone, addressed by its token (e.g. `M1`) or uuid. Supply at least one of `token`, `label`, `position`, or `archived` \u2014 an empty edit is rejected. Renaming the `token` does NOT rewrite issues pointing at the old one (`issues.milestone` is a loose ref), so prefer changing `label`. `archived: true` retires a finished release from the everyday pickers WITHOUT deleting it \u2014 the row keeps its token, label and position, issues carrying the token are untouched, and a completion review still opens against it; `archived: false` restores it. The project\u2019s ACTIVE milestone cannot be archived (refused `milestone_active` \u2014 make another milestone active first), and an archived one cannot be activated (refused `milestone_archived` \u2014 restore it first). Use set_active_milestone to change `active` (\xA77).",
17792
+ description: "Rename, reorder, archive or restore a milestone, by token (`M1`) or uuid. Supply at least one of `token`, `label`, `position` or `archived`; an empty edit is rejected. Renaming a token does not rewrite the issues carrying it, so prefer `label`. The active milestone cannot be archived (`milestone_active`), and an archived one cannot be activated (`milestone_archived`).",
17806
17793
  inputSchema: {
17807
17794
  project: projectKeySchema,
17808
17795
  milestone: external_exports.string().min(1),
@@ -17813,14 +17800,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17813
17800
  }
17814
17801
  }, ({ project, milestone, ...fields }) => run(async () => client2().updateMilestone(project, await resolveMilestoneId(client2(), project, milestone), fields)));
17815
17802
  registerStrict("set_active_milestone", {
17816
- description: "Make a milestone the project\u2019s active one, addressed by its token (e.g. `M2`) or uuid. At most one milestone per project is active; this clears the previous one in the same transaction. Returns the activated milestone (\xA77).",
17803
+ description: "Make a milestone its project\u2019s active one, by token (e.g. `M2`) or uuid. At most one per project is active, so this clears the previous in the same transaction.",
17817
17804
  inputSchema: {
17818
17805
  project: projectKeySchema,
17819
17806
  milestone: external_exports.string().min(1)
17820
17807
  }
17821
17808
  }, ({ project, milestone }) => run(async () => client2().activateMilestone(project, await resolveMilestoneId(client2(), project, milestone))));
17822
17809
  registerStrict("delete_milestone", {
17823
- description: "Soft-delete a milestone, addressed by its token (e.g. `M3`) or uuid; its token frees up for reuse. Issues still naming it are NOT rewritten and keep the now-dangling token (`issues.milestone` is a loose ref) \u2014 re-point them first. Returns confirmation metadata only (\xA77).",
17810
+ description: "Soft-delete a milestone by token (e.g. `M3`) or uuid; the token frees up for reuse. Issues naming it are not rewritten and keep a dangling token, so re-point them first.",
17824
17811
  inputSchema: {
17825
17812
  project: projectKeySchema,
17826
17813
  milestone: external_exports.string().min(1)
@@ -17831,7 +17818,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17831
17818
  return { deleted: true, project, milestone, id };
17832
17819
  }));
17833
17820
  registerStrict("create_deliverable_target", {
17834
- description: "Add a deliverable target (platform) to a project. `slug` is the immutable lowercase-alphanumeric machine id a deliverable stores as its platform (e.g. `bsky`, `threads`) and is unique per project; `label` is its display name (e.g. `Bluesky`). Optional `position` orders it (0-based, defaults to last); optional `active` (default true) gates whether new deliverables can target it. Returns the created target.",
17821
+ description: "Add a platform a deliverable can target. `slug` is the immutable id it stores (e.g. `bsky`), unique per project. `label` names it, `position` orders it, `active` (default true) gates new deliverables.",
17835
17822
  inputSchema: {
17836
17823
  project: projectKeySchema,
17837
17824
  slug: platformSlugSchema,
@@ -17841,7 +17828,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17841
17828
  }
17842
17829
  }, ({ project, ...input }) => run(() => client2().createDeliverableTarget(project, input)));
17843
17830
  registerStrict("update_deliverable_target", {
17844
- description: "Update a deliverable target, addressed by its slug (e.g. `bsky`) or uuid. Supply at least one of `label`, `position`, or `active` \u2014 an empty edit is rejected. The `slug` is immutable (it is the value stored on existing deliverables). Set `active: false` to remove it from the add-deliverable menu while keeping existing deliverables working.",
17831
+ description: "Update a deliverable target by slug or uuid. Supply at least one of `label`, `position` or `active`; an empty edit is rejected. The slug is immutable; `active: false` hides it from new deliverables.",
17845
17832
  inputSchema: {
17846
17833
  project: projectKeySchema,
17847
17834
  target: external_exports.string().min(1),
@@ -17851,7 +17838,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17851
17838
  }
17852
17839
  }, ({ project, target, ...fields }) => run(async () => client2().updateDeliverableTarget(project, await resolveDeliverableTargetId(client2(), project, target), fields)));
17853
17840
  registerStrict("delete_deliverable_target", {
17854
- description: "Delete a deliverable target, addressed by its slug (e.g. `bsky`) or uuid. Only a target with NO deliverables can be deleted; one already in use is rejected (deactivate it with update_deliverable_target instead). Returns confirmation metadata only.",
17841
+ description: "Delete a deliverable target by slug or uuid. Only a target with no deliverables can be deleted; one in use is rejected \u2014 deactivate it with `update_deliverable_target` instead.",
17855
17842
  inputSchema: {
17856
17843
  project: projectKeySchema,
17857
17844
  target: external_exports.string().min(1)
@@ -17864,7 +17851,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17864
17851
  registerStrict(
17865
17852
  "list_issues",
17866
17853
  {
17867
- description: `List issue summaries (display id, title, type, status, terminal, blocked, \`reservedForPerson\`, priority, milestone, claimer, criteria progress, updated_at, and the header \u2014 \`summary\`, \`why\`, \`affectedSurfaces\`, \`headerStale\`, \`headerDerivedAt\` \u2014 so a list says what each issue is about without opening it) filtered by project, environment, status[], type[], claimedBy (me/unclaimed/uuid), priority[], milestone, text (title+body), parent, blocked, and needsHeader (\xA77). DEFAULT: with no status filter this returns BUILDABLE work only \u2014 ${OPEN_STATUSES.join(", ")}. Terminal work (done, killed) is excluded unless you ask for it by name: pass status explicitly, e.g. status: ["done","killed"] for finished and abandoned work, or list every value to see everything. An explicit status filter is used verbatim and the default is not merged into it. The default is the API's own, applied server-side, so an omitted status means the same thing on every surface. Every row also carries \`terminal\` (PIPE-R15 r4 rule 5). blocked: false returns only work with nothing blocking it, and every issue it returns can be moved to ready without refusal; blocked: true returns only blocked work. needsHeader: true returns exactly the issues whose header is missing or no longer describes the body it was derived from \u2014 the set \`set_issue_header\` still has to be called on \u2014 and needsHeader: false returns only those whose header still holds. Each row\u2019s \`headerStale\` is the same expression this filter selects on, so a row cannot disagree with the filter that returned it. The status default above is not lifted for it: name done and killed explicitly when the sweep is meant to reach finished work as well.`,
17854
+ description: "List issue summaries \u2014 status, priority, milestone, claimer, criteria progress and the header \u2014 filtered by project, status, type, claimer, priority, milestone, text, parent, blocked and needsHeader. With no status filter the default is buildable work only: " + OPEN_STATUSES.join(", ") + '. Terminal work is excluded until you name it, so pass status ["done","killed"] for finished and abandoned work. An explicit filter is used verbatim, never merged. needsHeader true returns the issues whose header is missing or stale.',
17868
17855
  inputSchema: listIssuesFilterSchema.shape
17869
17856
  },
17870
17857
  // The default lives in the API now, not here (PIPE-D35, Listing issues
@@ -17880,7 +17867,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17880
17867
  (args) => run(() => client2().listIssues(args), GUIDANCE_POINTER)
17881
17868
  );
17882
17869
  registerStrict("get_issue", {
17883
- description: `Full issue detail by display id or uuid: the header \u2014 \`summary\`, \`why\`, \`affectedSurfaces\`, with \`headerDerivedAt\` and \`headerStale\`, true when nobody has written a header or the body has moved since one was written, and \`set_issue_header\` is what writes it \u2014 body, criteria (states, modes, evidence), explicit requirement links (pinned revision + staleness), the project\u2019s cross-cutting requirements at their latest approved revision, this organization\u2019s approved conventions \u2014 standing guidance that BINDS work in every project here, delivered in full at the revision a person approved \u2014 children, and comments (\xA77). Carries \`terminal\`: true when the issue is finished (\`done\`) or deliberately abandoned (\`killed\`) and is therefore NOT buildable work \u2014 check it before acting on an issue you were handed by key. Carries \`projectArchived\` on the same terms: true when the project is retired, so this read still answers while lists, search and the work-moving writes do not. Reading criterion state: \`state_evidence\` is required only when a TOKEN sets one, so a \`manual\` criterion \u2014 settable only from a web session \u2014 carries none by design, and the attribution the server already stores (who set it, and when) IS its record. What this payload tells you about ANOTHER issue goes stale. Reference that issue by key and say what you need from it; never restate its status, its criteria, or whether those criteria passed. Long records read in PARTS: \`bodyOffset\`/\`bodyLimit\` window the body (UTF-16 offsets; parts concatenate back exactly), \`commentOffset\`/\`commentLimit\` window the oldest-first comment list. Unpaged, the body is capped at ${ISSUE_BODY_CHAR_LIMIT} chars and comments at the newest ${ISSUE_COMMENT_LIMIT}; a response that left anything out says so in \`omitted\` \u2014 the true totals, the omitted comment count, and in \`next\` the exact arguments for the adjacent part (body walks forward, comments toward older) \u2014 follow \`next\` until null to retrieve everything. An issue under both caps reads whole in ONE call, no notice.`,
17870
+ description: "Full issue detail by key or uuid: header, body, criteria, requirement links, cross-cutting requirements, approved conventions, children and comments. `terminal` is true when the issue is done or killed, so it is not buildable; `projectArchived` is true when the project is retired, and this read still answers while lists and writes do not. `headerStale` is true when no header was written or the body moved. Long records page with `bodyOffset` and `commentOffset`; `omitted` and `next` say what was left out.",
17884
17871
  inputSchema: getIssueInputSchema.shape,
17885
17872
  // The same result-threshold annotation `get_work_context` declares, and the same
17886
17873
  // key spelled out for the same reason (an unknown `_meta` key is silently
@@ -17908,78 +17895,66 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17908
17895
  })
17909
17896
  ), GUIDANCE_POINTER));
17910
17897
  registerStrict("create_issue", {
17911
- description: "Create an issue (epic/task/bug/feedback). Omitted status defaults to triage (still subject to \xA76 guards); omitted environment resolves explicit \u2192 parent\u2019s environment \u2192 project default (prod). Optional parent (a parent already `done` or `killed` is rejected `parent_terminal`, unless the child is itself terminal; a new issue is a leaf, so no cycle is possible here), priority, milestone, requirement_links (pins each at its approved revision; a target with no approved revision is rejected), deliverable_links (content deliverables like DWB-C3/bsky \u2014 pins each at its current revision; the promotion path), and ordered {text, verification, howTo} criteria (\xA77). A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9): work past triage belongs to a release, and the create is refused (`milestone_required`) without one. An open child moves the parent to `in_progress` (rule 10). A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. When a bug is filed against a defined acceptance criterion, pass that criterion\u2019s uuid as `criterionId` and the bug records which criterion it was filed against \u2014 what has already been reported arrives as `filedBugs` on every criteria read. Filing a second bug against one criterion is supported: one row can fail for two different reasons at once. Bugs only: any other type naming a criterion is refused (`criterion_requires_bug`). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
17898
+ description: "Create an epic, task, bug or feedback. Status defaults to triage; anything but triage or killed needs a milestone, else refused `milestone_required`. Optional parent, priority, milestone, links and ordered criteria. An open issue under a done or killed parent is refused `parent_terminal`. Epics, tasks and bugs carry criteria; feedback is refused `criteria_not_supported`. A `manual` criterion needs `howTo`, else refused `how_to_required`. A bug may name the criterion it was filed against. See the doctrine `get_work_context` returns, under \u201CSearch before you create\u201D.",
17912
17899
  inputSchema: createIssueInputSchema.shape
17913
17900
  }, (args) => run(() => client2().createIssue(args)));
17914
17901
  registerStrict("promote_feedback", {
17915
- description: "Promote a FEEDBACK issue into a NEW epic or task (PIPE-143): creates the issue in the feedback\u2019s project (`title` + `type` required; omitted environment inherits the FEEDBACK\u2019s environment, not the project default; optional status/body/priority/milestone/requirement_links/criteria as in create_issue), records the feedback as its permanent source (`promotedFrom`/`promotedTo` on both details), closes the feedback as done, and comments the new key on it. The feedback\u2019s own title/body are never touched \u2014 write the new issue\u2019s body as real work. Rejected (409) for non-feedback, terminal, or already-promoted sources, and refused (422 `status_guard`, naming each offending child and its status) when the feedback carries a child issue that is not yet `done` or `killed` \u2014 closing the source would strand that child under a finished parent, so finish or kill the child first. Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue; the SOURCE feedback needs none to be closed by the promotion. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; this tool is what retypes it into work before anything is built. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
17902
+ description: "Retype a feedback report as real work \u2014 an epic or a task \u2014 so it can carry criteria and be built. Feedback itself is refused `criteria_not_supported`, which is what this call resolves. Closing the source feedback is refused `status_guard` when anything beneath it, at any depth, is not done or killed, and the refusal names it. A `manual` criterion needs `howTo`, or it is refused `how_to_required`. See the doctrine `get_work_context` returns, under \u201CThe bar for filing a bug\u201D.",
17916
17903
  inputSchema: {
17917
17904
  issue: external_exports.string().min(1),
17918
17905
  ...promoteFeedbackInputSchema.shape
17919
17906
  }
17920
17907
  }, ({ issue: issue2, ...fields }) => run(() => client2().promoteFeedback(issue2, fields)));
17921
17908
  registerStrict("open_review", {
17922
- description: "Open the COMPLETION REVIEW \u2014 the reading of an epic\u2019s WHOLE accumulated change as one body of work, by a third fresh session that neither built it nor evaluated it. It runs at one moment: once the epic\u2019s contract is green and before anyone sets it `done`. It creates one ORDINARY `task` at `ready` carrying the review checklist as its body. The epic form (`epic`) titles it `Completion review \u2014 <KEY-n> <epic title>`, files it as a CHILD of the epic and inherits the epic\u2019s milestone; the milestone form (`project` + `milestone`) titles it `Completion review \u2014 milestone <token>`, carries that token and has no parent. Pass EXACTLY ONE of `epic` or `project` + `milestone`: both, or neither, is refused. One review at a time per scope \u2014 a second is refused (`review_already_open`) NAMING the standing one, so read, finish or kill that task instead. The scopes overlap ONE way: the epic form\u2019s review also stands for that same project and milestone, so a later milestone-form open is refused; a milestone-form review blocks no epic-form open. A review opens only on a non-terminal epic \u2014 `review_target_not_epic` otherwise, `review_target_terminal` on a `done`/`killed` epic (a re-opened epic reviews again). NO NEW GATE was added to `done`: file each finding as ordinary work \u2014 a CHILD OF THE EPIC, a sibling of this review \u2014 and the children clause already on `done` holds the epic open until every finding is done or killed. A finding filed UNDER the review gates nothing, because that gate reads one level deep. Killing the review, or any finding, is the recorded waiver: deliberately ungated, and written to the status history with who did it and through which token. THIS CALL MOVES NOTHING. Attaching an open child otherwise starts its parent (PIPE-R10 rule 10); this one is exempt, because the review is the gate \u2014 starting the work again would drop it out of `list_waiting`. Writing guidance for the checklist and for each finding lives in the product doctrine, which `get_work_context` returns.",
17923
- // The WHOLE domain schema rather than its `.shape`, for `get_work_context`'s
17924
- // reason: the exactly-one-of rule is a refinement on the OBJECT, and a raw
17925
- // shape drops it — the refusal naming the two forms would never reach the
17926
- // caller, and both arguments (or neither) would be accepted here.
17909
+ description: "Open the completion review of an epic\u2019s whole change, by a session that neither built nor evaluated it. It creates one task at ready carrying the checklist. Pass exactly one of `epic`, or `project` plus `milestone`. One review stands per scope; a second is refused `review_already_open`. An epic-form review also holds that milestone scope; a milestone-form review blocks no epic. File findings as children of the epic, not the review. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
17927
17910
  inputSchema: openReviewInputSchema
17928
17911
  }, (args) => run(() => client2().openReview(args)));
17929
17912
  registerStrict("update_issue", {
17930
- description: "Update any subset of an issue\u2019s title, body, type, priority, milestone, environment, parent (self/cycle rejected, and moving an OPEN issue onto a parent already `done` or `killed` is rejected with `parent_terminal` \u2014 clearing the parent with null is always allowed), requirement_links (add/remove; adding a link to an unapproved requirement is rejected), deliverable_links (add/remove content-deliverable links, add pins at the current revision), or decision_links (add/remove decision records this work sits under \u2014 `KEY-D{n}`, from `list_decisions`). Changing type to `feedback` is rejected while the issue has criteria (\xA77), since feedback carries none; epic, task and bug interchange freely and the criteria ride along, keeping their AC-{n} numbers and their states. Clearing `milestone` (passing null) is rejected while the issue sits past `triage` \u2014 work already scheduled keeps a milestone (PIPE-R10 rule 9); swap it for a different one, or move the issue back to `triage` first. Setting `parent` ATTACHES the issue, and attaching an OPEN issue starts its new parent: a parent of any type moves to `in_progress`, recorded in that parent\u2019s status history against you and the credential you called with (PIPE-R10 rule 10). Nothing moves it back. Three attachments start nothing: an issue that is already `done` or `killed`; re-sending the parent an issue already has; and a parent that cannot legally hold `in_progress` \u2014 one that is blocked, or carries no milestone \u2014 which KEEPS the status it has and still takes the child, so a finding stays fileable against exactly that work. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CLink by key, never paste\u201D and \u201CFile decisions as questions\u201D.",
17913
+ description: "Change an issue\u2019s fields, parent and links. Header fields are refused here; criteria are set with `set_criteria`, which epics, tasks and bugs all carry. Moving an open issue onto a done or killed parent is refused `parent_terminal`; one with open work beneath it is refused `parent_terminal_subtree`. A cycle-closing re-parent is refused `parent_cycle`. Attaching open work starts its new parent unless that parent is blocked, unscheduled or under a finished parent. See the doctrine `get_work_context` returns, under \u201CMost choices are yours\u201D.",
17931
17914
  inputSchema: updateIssueInputSchema.shape
17932
17915
  }, ({ issue: issue2, ...fields }) => run(() => client2().updateIssue(issue2, fields)));
17933
17916
  registerStrict("set_issue_header", {
17934
- description: "Write an issue\u2019s HEADER \u2014 the three lines an operator reads before deciding anything: `summary` (the change, in one sentence), `why` (the user, business, cost or operational reason rather than the technical one; when nothing is user-visible, the cost of leaving it alone) and `affectedSurfaces` (which parts of the product this touches, and who feels it). All three are REQUIRED and the write is whole-value: the header is replaced entire and never merged, so a partial write cannot leave the stored provenance vouching for lines it did not touch. Returns the full issue detail. WRITE ALL THREE IN PLAIN WORDS: no identifiers, no file paths, no type names, no jargon. Their reader is deciding, not building, and the depth belongs in the body underneath them. A `summary` shaped like code \u2014 a repository path, a line number, a symbol written as a call or in backticks \u2014 is refused (400) naming what was found. `summary` is capped at 350 characters; over it the write is refused naming both the cap and the length received. THE HEADER CAN BE WRITTEN NO OTHER WAY: `summary`, `why` or `affectedSurfaces` passed to `create_issue`, `update_issue` or `promote_feedback` is refused (400) naming the field, rather than silently dropped \u2014 create the issue first, then write its header here. Every read of the issue carries `headerStale`, true when the body has changed since these three lines were derived from it and true when no header was ever written; writing here clears it. Writing guidance for these three lines is stated above; the organization\u2019s own writing rules, which `get_work_context` returns, govern the body beneath them.",
17917
+ description: "Write an issue\u2019s header \u2014 `summary` (the change in one sentence), `why` (the user, business or cost reason, not the technical one) and `affectedSurfaces` (what this touches and who feels it). All three are required and replace the header whole. Write them in plain words: no identifiers, no file paths, no jargon. A summary shaped like code is refused; `summary` caps at 350 characters. Create and update refuse these fields. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
17935
17918
  inputSchema: setIssueHeaderInputSchema.shape
17936
17919
  }, ({ issue: issue2, ...fields }) => run(() => client2().setIssueHeader(issue2, fields)));
17937
17920
  registerStrict("set_status", {
17938
- description: "Set an issue\u2019s status; the \xA76 guards apply and a guard failure returns the specific reason, naming the rows it refused on (\xA77). The criteria gates read whatever criteria the issue carries, whatever its type: verify needs every auto criterion passed, done needs every criterion passed. ONE guard remains EPIC-ONLY, and the asymmetry is the rule rather than an oversight \u2014 an epic that has stated NO criteria is refused at both gates, because an empty list would clear them having committed to nothing, while a task or a bug carrying none moves through both untouched (most work is stated adequately by its body). `done` additionally requires every child of the issue being closed to be terminal \u2014 an epic, a task and a bug alike \u2014 read from each child\u2019s STATUS and never from its criteria. A BLOCKED issue \u2014 one carrying an explicit mark or waiting on a blocker that is not yet `done`/`killed` \u2014 cannot move to `ready` or `in_progress`, and the refusal names the reason and every unresolved blocker, so clear the mark (`clear_blocked`) or finish, kill or `unlink_blocker` each blocker first; every other target still moves while blocked. An issue with NO milestone cannot move to any status but `triage` and `killed` (PIPE-R10 rule 9) \u2014 set one with `update_issue` first; `killed` is exempt, so work nobody scheduled can still be abandoned.",
17921
+ description: "Move an issue to a new status. verify needs every auto criterion passed; done needs every criterion passed, and every issue beneath it, at any depth, done or killed \u2014 epic, task or bug alike \u2014 read from status. An epic with no criteria is refused at both gates; a task or bug with none moves through. A blocked issue cannot reach ready or in_progress until its blockers clear. An issue with no milestone can only be triage or killed.",
17939
17922
  inputSchema: setStatusInputSchema.shape
17940
17923
  }, (args) => run(() => client2().setStatus(args.issue, args.status)));
17941
17924
  registerStrict("claim_issue", {
17942
- description: "Atomically claim an issue (\xA76 rule 1): succeeds only if unclaimed; under a race exactly one wins and the loser\u2019s error names the current claimer. Claim is orthogonal to status (\xA77). REFUSED with `reserved_for_person` when an operator has reserved the work for a person, and THE CLAIM IS THE ONLY CALL A RESERVATION REFUSES \u2014 reading it, commenting on it, moving its status and releasing a claim all still succeed. The mark stops you taking the work, not touching it, so do not read it as a general instruction to leave the item alone. Clearing the mark is a browser-session act, so no call here lifts it: say in a comment why the work looks agent-eligible and leave the choice to the operator.",
17925
+ description: "Claim an issue for this session. It succeeds only if unclaimed; under a race one caller wins and the loser is told who holds it. Claiming is separate from status. It is refused `reserved_for_person` when an operator has reserved the work for a person \u2014 that mark refuses the claim and nothing else, so reading, commenting and moving status all still work. Only a person in the browser can lift it; say in a comment why the work looks agent-ready.",
17943
17926
  inputSchema: { issue: issueRefSchema }
17944
17927
  }, (args) => run(() => client2().claimIssue(args.issue)));
17945
17928
  registerStrict("release_issue", {
17946
- description: "Release an issue\u2019s claim, clearing claimed_by (\xA77).",
17929
+ description: "Release your claim on an issue, leaving it unclaimed and its status unchanged.",
17947
17930
  inputSchema: { issue: issueRefSchema }
17948
17931
  }, (args) => run(() => client2().releaseIssue(args.issue)));
17949
17932
  registerStrict("add_comment", {
17950
- description: "Add a markdown comment to an issue, attributed to the operator (and token label over MCP). A comment EXPLAINS a blockage \u2014 the full account of what you hit and why you stopped \u2014 but never records one: `set_blocked` (with a reason) and `link_blocker` (when you are waiting on another issue) are what put an issue in the blocked state (\xA77/WORKFLOW). Writing guidance for a comment lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CHow to write here\u201D.",
17933
+ description: "Add a markdown comment to an issue, attributed to the operator and token label. A comment explains a blockage; `set_blocked` and `link_blocker` are what record one. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
17951
17934
  inputSchema: commentInputSchema.shape
17952
17935
  }, (args) => run(() => client2().addComment(args.issue, args.body)));
17953
17936
  registerStrict("set_blocked", {
17954
- description: "Record an issue as BLOCKED with a required human-readable `reason` \u2014 state on the issue, not a comment: it shows on the issue and counts on the project\u2019s Work page, and only clearing it (`clear_blocked`) ends it. Setting it again on an already-blocked issue replaces the standing reason and keeps the old one as history, attributed to whoever set it. The reason is one or two lines saying what the work is waiting on; the full account of what you tried still belongs in a comment. Returns the updated issue detail, whose `blocked` carries the reason and who set it. Writing guidance for the reason lives in the product doctrine, which `get_work_context` returns.",
17937
+ description: "Mark an issue blocked with a required `reason` \u2014 state on the issue, not a comment, and only `clear_blocked` ends it. Setting it again replaces the reason and keeps the old one as history. The reason is a line or two on what the work waits on; what you tried goes in a comment. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
17955
17938
  inputSchema: setBlockedInputSchema.shape
17956
17939
  }, (args) => run(() => client2().setBlocked(args.issue, args.reason)));
17957
17940
  registerStrict("clear_blocked", {
17958
- description: "Clear an issue\u2019s blocked mark \u2014 the blockage is over. Idempotent: clearing an issue that is not blocked is a no-op, not an error. The cleared mark is kept as history with who ended it, so a past blockage stays readable. Returns the updated issue detail (`blocked` is then null).",
17941
+ description: "Clear an issue\u2019s blocked mark. Clearing one that is not blocked is a no-op, not an error. The mark stays as history, and `blocked` on the returned issue is null.",
17959
17942
  inputSchema: { issue: issueRefSchema }
17960
17943
  }, (args) => run(() => client2().clearBlocked(args.issue)));
17961
17944
  registerStrict("link_blocker", {
17962
- description: "Record that `issue` is WAITING ON `blocker` \u2014 the dependency between two issues, written from the waiting issue\u2019s side. One call records both directions: `get_issue` on the waiting issue lists it under `blockedBy` (with the blocker\u2019s display id, title, and whether it is already `done`/`killed`), and `get_issue` on the blocker lists it under `blocks`, so finishing work shows what it frees. Both issues must be in the SAME project. An issue cannot block itself, and an edge that would close a cycle (directly or through other issues) is refused with nothing recorded. Adding the same blocker twice records no second edge. Returns the waiting issue\u2019s updated detail.",
17945
+ description: "Record that `issue` is waiting on `blocker`. One call writes both sides: the waiting issue lists it under `blockedBy`, the blocker under `blocks`. Both must be in the same project. An issue cannot block itself, and an edge that would close a cycle is refused with nothing recorded.",
17963
17946
  inputSchema: linkBlockerInputSchema.shape
17964
17947
  }, (args) => run(() => client2().addBlocker(args.issue, args.blocker)));
17965
17948
  registerStrict("unlink_blocker", {
17966
- description: "Remove the blocked-by edge between `issue` and `blocker` \u2014 the dependency is gone, and one call clears it from BOTH issues\u2019 views. Idempotent: unlinking a pair that is not linked is a no-op, not an error. Returns the waiting issue\u2019s updated detail.",
17949
+ description: "Remove the blocked-by edge between `issue` and `blocker`; one call clears it from both sides. Unlinking a pair that is not linked is a no-op, not an error.",
17967
17950
  inputSchema: linkBlockerInputSchema.shape
17968
17951
  }, (args) => run(() => client2().removeBlocker(args.issue, args.blocker)));
17969
17952
  registerStrict("set_criteria", {
17970
- description: "Define or replace the ordered acceptance criteria list ({text, verification, howTo}) on a piece of work. Matched rows keep their stable AC-{n} number; new items get fresh never-reused numbers; removed items are soft-deleted. A matched row keeps its state only when BOTH its text and verification mode are unchanged \u2014 a mode flip resets it to pending (\xA77). A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. Raising a criterion from `manual` to `auto` both resets its state and clears the stored `howTo`, so a criterion moved to `auto` has lost its checking instructions \u2014 moving it back to `manual` does not restore them, and is refused (`how_to_required`) until you write new ones. Writing guidance for a criterion, and for choosing its mode, lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CChoosing auto or manual\u201D.",
17953
+ description: "Define or replace the acceptance criteria on an issue. A criterion is one sentence: a concrete situation and what must be true afterward. Epics, tasks and bugs all carry criteria; feedback is refused `criteria_not_supported` \u2014 promote it first. Matched rows keep their AC number; changing text or mode resets state. A `manual` criterion needs `howTo`, else refused `how_to_required`; `howTo` on an `auto` criterion is refused by the schema. See the doctrine `get_work_context` returns, under \u201CChoosing auto or manual\u201D.",
17971
17954
  inputSchema: setCriteriaInputSchema.shape
17972
17955
  }, (args) => run(() => client2().setCriteria(args.issue, args.criteria)));
17973
17956
  registerStrict("set_criterion_state", {
17974
- description: "Set a criterion\u2019s state (pending/passed/failed) on whatever work carries it \u2014 an epic, a task and a bug alike \u2014 addressed by criterionId ALONE, or by issue + criterionNumber (the stable AC-{n}); exactly one form. Feedback never carries criteria, so neither address resolves to one. A token-authenticated call is REJECTED on a manual criterion (manual state is web-session-only, \xA74); evidence is required when a token marks passed/failed (\xA77). Writing guidance for that evidence lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CChoosing auto or manual\u201D.",
17975
- // `issue` is OPTIONAL: the criterionId-only form needs no issue ref. The
17976
- // exactly-one-of rule can't be expressed in a raw shape, so the handler
17977
- // enforces it via the shared `@pipeline/domain` refine (mirror of §7).
17978
- //
17979
- // `criterionNumber` takes `int4Schema` so the ADVERTISED JSON Schema carries
17980
- // the real ceiling (PIPE-149) — hand-rolling it from `positiveIntSchema` told
17981
- // clients the maximum was 2^53-1 while the column and the API both stop at
17982
- // 2,147,483,647, so a client refusal that should happen never did.
17957
+ description: "Set a criterion\u2019s state \u2014 pending, passed or failed \u2014 on an epic, a task or a bug alike. Address it by `criterionId` alone, or by issue plus `criterionNumber`; exactly one form. Feedback never carries criteria, so neither address resolves to one. A token is refused on a manual criterion, which only a person in the browser may set, and a token marking passed or failed must supply evidence. See the doctrine `get_work_context` returns, under \u201CChoosing auto or manual\u201D.",
17983
17958
  inputSchema: {
17984
17959
  issue: issueRefSchema.optional(),
17985
17960
  criterionId: uuidSchema.optional(),
@@ -18002,27 +17977,27 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18002
17977
  });
18003
17978
  }));
18004
17979
  registerStrict("ask_questions", {
18005
- description: "Ask the operator a BATCH of questions on an issue \u2014 `questions` takes 1 or more {text, options?}, and a session with one open decision usually has two or three, so ask them in ONE call rather than one at a time (a half-asked set costs the operator a second pass). `options` are the choices you propose, in the order you offer them; omit it to ask in open words. Each question gets a stable per-issue `Q-{n}` number that is NEVER reused. Returns the updated issue detail, and every issue read from then on carries `questions` with their answers \u2014 so read the answer in the `get_issue` you were going to make anyway, and READ the existing questions and answers BEFORE asking again rather than re-asking a decision already taken. Answering is reserved for a person in the web UI: there is deliberately no tool for it on this surface (PIPE-R22 rule 4). A question is withdrawn with `withdraw_question`. Writing guidance for a question lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CFile decisions as questions\u201D.",
17980
+ description: "Ask the operator one or more questions on an issue. Send the batch in one call \u2014 a half-asked set costs a second pass. Each question gets a stable `Q-{n}` number that is never reused, and `options` are the choices you propose. Answers come back on every later read of the issue, so read them before asking again rather than re-asking a settled decision. Withdraw one with `withdraw_question`. See the doctrine `get_work_context` returns, under \u201CFile decisions as questions\u201D.",
18006
17981
  inputSchema: askQuestionsInputSchema.shape
18007
17982
  }, (args) => run(() => client2().askQuestions(args.issue, args.questions)));
18008
17983
  registerStrict("withdraw_question", {
18009
- description: "Withdraw a question by its stable `Q-{n}` number \u2014 asker only: the asker is the PERSON your credential belongs to, whichever organization the call names in `org`, so another agent\u2019s question is not yours to clear. Deliberately NOT human-gated, because the point is to clear a question you no longer need \u2014 the work answered it, or it was wrong to ask \u2014 instead of leaving the operator to answer something nobody is waiting on. It is recorded, never an answer: the question is soft-deleted (kept for the audit trail), its number retires and is not reissued, and the next issue read no longer carries it in `questions`. Returns the updated issue detail.",
17984
+ description: "Withdraw a question by its `Q-{n}` number, in the organization named by `org`. Only the asker may withdraw, and the asker is the person your credential belongs to, so another agent\u2019s question is not yours to clear. Use it when your own work settled the question or it was wrong to ask, rather than leaving the operator to answer something nobody awaits. The question is kept for the audit trail, its number retires, and later reads no longer carry it.",
18010
17985
  inputSchema: withdrawQuestionInputSchema.shape
18011
17986
  }, (args) => run(() => client2().withdrawQuestion(args.issue, args.number)));
18012
17987
  registerStrict("list_requirements", {
18013
- description: "List requirements filtered by project and text: display id, title, summary (the latest revision\u2019s one/two-line outline description), area (product-area label), cross_cutting flag, lifecycle (draft/approved/amended), latest + approved revision numbers, and linked-epic count (\xA77).",
17988
+ description: "List requirements filtered by project and text: lifecycle (draft/approved/amended), the latest and approved revision numbers, the cross-cutting flag and the linked-epic count.",
18014
17989
  inputSchema: listRequirementsFilterSchema.shape
18015
17990
  }, (args) => run(() => client2().listRequirements(args), GUIDANCE_POINTER));
18016
17991
  registerStrict("get_requirement", {
18017
- description: "Get a requirement by display id or uuid. Defaults to the approved revision (latest if never approved) unless `revision` is given; includes lifecycle, revision list, approvals, and linked issues (\xA77).",
17992
+ description: "Get a requirement by display id or uuid: the approved revision by default, the latest when none is approved, or the `revision` you name. Includes lifecycle, approvals and linked issues.",
18018
17993
  inputSchema: { requirement: requirementRefSchema, revision: positiveIntSchema.optional() }
18019
17994
  }, (args) => run(() => client2().getRequirement(args.requirement, args.revision), GUIDANCE_POINTER));
18020
17995
  registerStrict("create_requirement", {
18021
- description: 'Create a project-scoped requirement document with revision 1 (title, body). Optional summary is a short one/two-line outline description (versioned on the revision, like title/body); optional area is a free-text product-area label ("Backup", "Billing") used to group/filter the outline (metadata, like cross_cutting). Optional cross_cutting flags it as binding every epic in the project automatically \u2014 never link cross-cutting documents (\xA75 rule 6, \xA77). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CLink by key, never paste\u201D.',
17996
+ description: "Create a project-scoped requirement at revision 1 from `title` and `body`. Optional `summary` is a short outline, versioned with them; `area` is a free-text product-area label. The cross-cutting flag makes it bind every epic in the project on its own; never link one explicitly. See the doctrine `get_work_context` returns, under \u201CSearch before you create\u201D.",
18022
17997
  inputSchema: createRequirementInputSchema.shape
18023
17998
  }, (args) => run(() => client2().createRequirement(args)));
18024
17999
  registerStrict("update_requirement", {
18025
- description: "Update a requirement: title, body, and/or summary APPENDS a new revision (nothing is edited in place; summary versions like title/body); optional note records why. Optional area (product-area label), cross_cutting, and decision_links (add/remove decision records this rule sits under \u2014 `KEY-D{n}`, from `list_decisions`) are metadata changes (no revision) \u2014 toggling cross_cutting ON is rejected while explicit links exist; unlink first. Supplying none of title/body/summary/area/cross_cutting/decision_links is an empty edit (400) (\xA77). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CLink by key, never paste\u201D.",
18000
+ description: "Update a requirement. `title`, `body` and `summary` append a new revision \u2014 nothing is edited in place \u2014 and `note` records why. Area, cross-cutting and decision links are metadata, with no revision. Turning cross-cutting on is rejected while explicit links exist; unlink first. An empty edit is refused (400). See the doctrine `get_work_context` returns, under \u201CLink by key, never paste\u201D.",
18026
18001
  // Inline shape (updateRequirementInputSchema is a refine, no .shape) — add
18027
18002
  // summary/area here explicitly (§210) and decisionLinks (PIPE-84 §6); the
18028
18003
  // empty-edit refine is enforced by the API, not this shape.
@@ -18038,23 +18013,23 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18038
18013
  }
18039
18014
  }, ({ requirement, ...fields }) => run(() => client2().updateRequirement(requirement, fields)));
18040
18015
  registerStrict("approve_requirement", {
18041
- description: "Approve (pin) a requirement\u2019s latest revision, or a specified `revision`, recording it in approvals history and advancing the approved revision that downstream links and reads build against (\xA75, \xA77).",
18016
+ description: "Approve a requirement\u2019s latest revision, or the `revision` you name. It enters the approval history and becomes the approved revision reads and links build against.",
18042
18017
  inputSchema: approveRequirementInputSchema.shape
18043
18018
  }, (args) => run(() => client2().approveRequirement(args.requirement, args.revision)));
18044
18019
  registerStrict("list_conventions", {
18045
- description: "List this organization\u2019s conventions \u2014 standing guidance that binds work in EVERY project here, unlike a requirement, which binds one project. Returns display id (`CONV-{n}`), title, the standing text, lifecycle (draft/approved/amended), latest + approved revision numbers, and whether it has been superseded and by which convention. Optional `text` matches over title and body. Retired conventions are INCLUDED and marked, so you can see what the organization used to require rather than proposing it again. Only APPROVED, unretired conventions are actually delivered to agents \u2014 every `get_issue` and `get_work_context` read carries them.",
18020
+ description: "List this organization\u2019s conventions \u2014 standing guidance that binds every project here. Returns display id (`CONV-{n}`), title, the standing text, lifecycle and revision numbers. Retired ones are included and marked. `text` matches title and body. Only approved, unretired conventions are delivered to agents.",
18046
18021
  inputSchema: listConventionsFilterSchema.shape
18047
18022
  }, (args) => run(() => client2().listConventions(args), GUIDANCE_POINTER));
18048
18023
  registerStrict("get_convention", {
18049
- description: "Get one convention by display id (`CONV-{n}`) or uuid: the standing text at the approved revision, its full revision history with who wrote each and when, its approval history, and \u2014 when it has been retired \u2014 which convention replaced it. A convention that was never approved is readable here and is delivered to nobody.",
18024
+ description: "Get one convention by display id (`CONV-{n}`) or uuid: its approved text, the revision and approval history, and what replaced it once retired. One never approved reads here and binds nobody.",
18050
18025
  inputSchema: { convention: conventionRefSchema }
18051
18026
  }, (args) => run(() => client2().getConvention(args.convention), GUIDANCE_POINTER));
18052
18027
  registerStrict("create_convention", {
18053
- description: "Write a convention: organization-wide standing guidance, with no project attached. THE GOVERNING TEST \u2014 if it stops being true when this project ends it is a cross-cutting requirement; if it would still be true on the next project it is a convention. You must assert that test by passing `outlivesAnyProject: true`; the write is refused without it. A statement naming one specific project \u2014 its key, a display id built on it, a repository path, or a database object \u2014 is also refused (422 `convention_names_project_scope`), and the message says to file it as a cross-cutting requirement on that project instead. IT BINDS NOBODY UNTIL IT IS APPROVED, and approval is NOT available on this surface: it requires a signed-in person in the browser, so there is no tool for it here and asking for one is asking for a tool that deliberately does not exist. A new convention starts as a draft; write it, then ask a person to review and approve it. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CLink by key, never paste\u201D.",
18028
+ description: "Write a convention: standing guidance for the whole organization, with no project attached. Pass `outlivesAnyProject: true` or the write is refused. Text naming one project is refused `convention_names_project_scope`; file that as a cross-cutting requirement instead. It is a draft that binds nobody until a person approves it in a browser.",
18054
18029
  inputSchema: createConventionInputSchema.shape
18055
18030
  }, (args) => run(() => client2().createConvention(args)));
18056
18031
  registerStrict("update_convention", {
18057
- description: "Amend a convention: `title` and/or `body` APPEND a new revision \u2014 nothing is ever edited in place \u2014 and optional `note` records why. Supplying neither is an empty edit (400); a note alone amends nothing. The same project-scope refusal applies as on create, over the amended text. THE AMENDMENT BINDS NOBODY UNTIL IT IS ITSELF APPROVED: until then agents keep receiving the approved revision, so amending is not a way to change what is being delivered \u2014 a signed-in person approving the new revision is. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CConventions need a person\u2019s approval\u201D.",
18032
+ description: "Amend a convention. `title` and `body` append a new revision \u2014 nothing is edited in place \u2014 and `note` records why. Supplying neither is an empty edit (400). Amended text naming one project is refused as on create. The amendment binds nobody until a person approves it. See the doctrine `get_work_context` returns, under \u201CConventions need a person\u2019s approval\u201D.",
18058
18033
  // Inline shape (updateConventionInputSchema is a refine, so it has no `.shape`);
18059
18034
  // the empty-edit rule is enforced by the API, not by this shape.
18060
18035
  inputSchema: {
@@ -18065,19 +18040,19 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18065
18040
  }
18066
18041
  }, ({ convention, ...fields }) => run(() => client2().updateConvention(convention, fields)));
18067
18042
  registerStrict("list_prompts", {
18068
- description: "List this organization\u2019s prompt library \u2014 reusable prompt text, addressed `PROMPT-{n}`. A prompt belongs to the ORGANIZATION rather than to a project, unlike a requirement or a content draft, so with no filter this returns the whole library. Returns display id, title, the current text, the latest revision number, and the attached project when it has one. `project` narrows to the prompts attached to that project \u2014 that attachment is metadata saying what a prompt is ABOUT, never its scope, so a prompt attached elsewhere is still one you may read and run. `text` matches over TITLE AND BODY, because someone looking for a prompt remembers what it says rather than what somebody called it.",
18043
+ description: "List the organization\u2019s prompt library (`PROMPT-{n}`): display id, title, current text, latest revision and any attached project. With no filter this returns the whole library. `project` narrows to the ones attached there \u2014 an attachment says what a prompt is about, not who may run it. `text` matches title and body.",
18069
18044
  inputSchema: listPromptsFilterSchema.shape
18070
18045
  }, (args) => run(() => client2().listPrompts(args), GUIDANCE_POINTER));
18071
18046
  registerStrict("get_prompt", {
18072
- description: "Get one prompt by display id (`PROMPT-{n}`) or uuid: its current text \u2014 the LATEST revision, which is simply the prompt, since nothing approves a prompt and nothing pins to an older wording \u2014 plus the full revision history with who wrote each save, through which token, when, and the note saying why. Nothing here was edited in place, so a wording that has been replaced is still readable rather than lost; read the history before rewriting a prompt that already works.",
18047
+ description: "Get one prompt by display id (`PROMPT-{n}`) or uuid: its current text, which is simply the latest revision, plus every earlier one with who saved it, when and why.",
18073
18048
  inputSchema: { prompt: promptRefSchema }
18074
18049
  }, (args) => run(() => client2().getPrompt(args.prompt), GUIDANCE_POINTER));
18075
18050
  registerStrict("create_prompt", {
18076
- description: "Write a prompt: reusable prompt text kept ORGANIZATION-WIDE (`PROMPT-{n}`) and available in every project here \u2014 unlike a requirement or a content draft, which belong to the one project they are filed under. `body` is the prompt itself and must not be empty; `title` names it; optional `note` records why it was written. Optional `project` ATTACHES the prompt to a project, which says what it is about and nothing more \u2014 the prompt is still the organization\u2019s, and a prompt about no project in particular simply omits it. There is no approval step: it is usable the moment it is written. Nothing deletes a prompt either \u2014 later wording is added as a revision, so what is here now stays readable. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D.",
18051
+ description: "Write a prompt: reusable text held by the organization (`PROMPT-{n}`). `body` is the prompt and must not be empty; `title` names it; `note` says why. Optional `project` records what the prompt is about, not who may use it. Nothing approves or deletes a prompt; later wording is a new revision.",
18077
18052
  inputSchema: createPromptInputSchema.shape
18078
18053
  }, (args) => run(() => client2().createPrompt(args)));
18079
18054
  registerStrict("update_prompt", {
18080
- description: "Amend a prompt: `title` and/or `body` APPEND a new revision \u2014 nothing is ever edited in place \u2014 and optional `note` records why. Amending is therefore safe rather than destructive: the earlier text stays readable, the revision chain is the record of how this prompt got to where it is, and the new wording answers immediately because there is nothing to approve. `project` moves the attachment and `project: null` DETACHES it, leaving a prompt the whole organization still has; either way NO revision is written, because the revision chain is the history of the TEXT and moving the attachment changes no text. Supplying none of `title`, `body` or `project` is an empty edit (400) \u2014 a note alone amends nothing. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns.",
18055
+ description: "Amend a prompt. `title` and `body` append a new revision \u2014 nothing is edited in place \u2014 and `note` records why. `project` moves the attachment and `project: null` detaches it; neither writes a revision. Naming none of the three is an empty edit (400). See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18081
18056
  // Inline shape (updatePromptInputSchema is a refine, so it has no `.shape`),
18082
18057
  // mirroring that schema's fields — it is the wording this copy follows, `.min(1)`
18083
18058
  // bounds included, so a caller learns a blank body is refused from the advertised
@@ -18092,51 +18067,53 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18092
18067
  }
18093
18068
  }, ({ prompt, ...fields }) => run(() => client2().updatePrompt(prompt, fields)));
18094
18069
  registerStrict("create_decision", {
18095
- description: "Record ONE decision as a project-scoped decision record (`KEY-D{n}`): `title` names the decision and `body` says what was decided and why. A new record starts `proposed`; moving it to `accepted` IS the acceptance \u2014 there is no separate approval step. Returns the created record. Title and body can be corrected only while the record is `proposed`; from `accepted` onward it is frozen, and the way to change a decision is to write a NEW record and `supersede_decision` the old one \u2014 which leaves the original readable and says where the current decision went. Nothing on any surface deletes one. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CA decision record is written once\u201D and \u201CSearch before you create\u201D.",
18070
+ description: "Record one decision as a project-scoped record (`KEY-D{n}`): `title` names it, `body` says what was decided and why. It starts `proposed` and is frozen once accepted; changing it then means a new record plus `supersede_decision`. See the doctrine `get_work_context` returns, under \u201CA decision record is written once\u201D.",
18096
18071
  inputSchema: createDecisionInputSchema.shape
18097
18072
  }, (args) => run(() => client2().createDecision(args)));
18098
18073
  registerStrict("get_decision", {
18099
- description: "Get a decision record by display id (`KEY-D{n}`) or uuid: title, body, lifecycle state (`proposed`/`accepted`/`superseded`), who accepted it and when, the record that replaced it, where the CURRENT decision lives when this one has been superseded, and everything that references it \u2014 every requirement and every work item, gathered from both directions.",
18074
+ description: "Get a decision record by display id (`KEY-D{n}`) or uuid: its text, its state (`proposed`/`accepted`/`superseded`), who accepted it, what replaced it, and everything citing it.",
18100
18075
  inputSchema: getDecisionInputSchema.shape
18101
18076
  }, (args) => run(() => client2().getDecision(args.decision), GUIDANCE_POINTER));
18102
18077
  registerStrict("list_decisions", {
18103
- description: "List decision records filtered by `project` and `text`: display id, title, lifecycle state, the replacement when superseded, and how many requirements and work items cite each one. `text` matches over TITLE AND BODY, so search on what a decision was about rather than on what someone chose to call it.",
18078
+ description: "List decision records filtered by `project` and `text`: each one\u2019s state, its replacement when superseded, and how many requirements and work items cite it. `text` matches title and body.",
18104
18079
  inputSchema: listDecisionsFilterSchema.shape
18105
18080
  }, (args) => run(() => client2().listDecisions(args), GUIDANCE_POINTER));
18106
18081
  registerStrict("supersede_decision", {
18107
- description: "Replace an ACCEPTED decision record with another one: `decision` moves to `superseded` and names `replacedBy` as where the current decision now lives, so whoever opens the old record is sent to the standing one. This is the only way a decision changes \u2014 the original is never edited and never removed, and the whole chain stays readable. Refused when the record is still `proposed` (nothing has been decided yet, so correct it instead), when a record is named as its own replacement, when the replacement belongs to another project, or when the replacement\u2019s own chain already leads back to this record. Returns the superseded record.",
18082
+ description: "Replace an accepted decision record with another: `decision` becomes `superseded` and points at `replacedBy`, where the current decision lives. Refused while the record is still `proposed` (correct it instead), when a record replaces itself, when the replacement is in another project, or when its chain already leads back to this record.",
18108
18083
  inputSchema: supersedeDecisionInputSchema.shape
18109
18084
  }, (args) => run(() => client2().supersedeDecision(args.decision, args.replacedBy)));
18110
18085
  registerStrict("list_content", {
18111
- description: "List content drafts filtered by project, text, area, and author: display id (KEY-C{n}), title, summary (latest revision), area, author, latest revision number, deliverable count, and linked-issue count. Content is a project-scoped sibling of requirements \u2014 raw material that gets promoted into work, not an issue type.",
18086
+ description: "List content drafts (`KEY-C{n}`) filtered by project, text, area and author. Content is a project-scoped sibling of requirements, not an issue type.",
18112
18087
  inputSchema: listContentFilterSchema.shape
18113
18088
  }, (args) => run(() => client2().listContent(args), GUIDANCE_POINTER));
18114
18089
  registerStrict("get_content", {
18115
- description: "Get a content draft by display id (KEY-C{n}) or uuid: its revision history (default latest, or `revision` to pin) plus every per-platform deliverable with its own revisions, posted metadata (url/postedAt), linked issues (with staleness), and linked assets.",
18090
+ description: "Get a content draft by display id (`KEY-C{n}`) or uuid: its revisions, latest or the `revision` you name, plus every deliverable, linked issue and linked asset.",
18116
18091
  inputSchema: { content: contentRefSchema, revision: positiveIntSchema.optional() }
18117
18092
  }, (args) => run(() => client2().getContent(args.content, args.revision), GUIDANCE_POINTER));
18118
18093
  registerStrict("create_content", {
18119
- description: 'Create a project-scoped content draft with revision 1 (title, body). Optional summary is a short outline description (versioned on the revision, like title/body); optional area is a free-text product-area label (metadata). Optional author is the editorial byline the draft is credited to (free text, e.g. "jason") \u2014 distinct from the acting operator, and the field the list filters by. Optional metadata is an open bag for capture provenance (tags, score, sensitivity, source refs) \u2014 stored passively, never acted on. This is the capture-sink target: a captured topic becomes a draft here. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D.',
18094
+ description: "Create a project-scoped content draft at revision 1 from `title` and `body`. Optional `summary` is versioned with them; `area` is a free-text label; `author` is the byline credited on the draft, not the operator calling; `metadata` is stored passively. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18120
18095
  inputSchema: createContentInputSchema.shape
18121
18096
  }, (args) => run(() => client2().createContent(args)));
18122
18097
  registerStrict("update_content", {
18123
- description: "Update a content draft: title, body, and/or summary APPENDS a new revision (nothing is edited in place); optional note records why. Optional area, author, and metadata are metadata changes (no revision). Supplying none of title/body/summary/area/author/metadata is an empty edit (400). Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns.",
18098
+ description: "Update a content draft. `title`, `body` or `summary` appends a revision and `note` says why; `area`, `author` and `metadata` are metadata only. An empty edit is refused (400). See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18124
18099
  inputSchema: updateContentInputSchema.shape
18125
18100
  }, ({ content, ...fields }) => run(() => client2().updateContent(content, fields)));
18126
18101
  registerStrict("create_deliverable", {
18127
- description: "Add a per-platform deliverable to a draft with revision 1 (the post text). `platform` is a slug configured for the project \u2014 one of its ACTIVE deliverable targets (see list_projects \u2192 deliverableTargets; defaults are bsky/x/reddit/hn/blog, but projects can add/remove them); an unconfigured slug is rejected. Optional body seeds revision 1. There is one deliverable per platform per draft \u2014 a variant is a revision (update_deliverable), not a second deliverable. Rendered id is DRAFT-KEY/platform (e.g. DWB-C3/bsky). Writing guidance for the post text lives in the product doctrine, which `get_work_context` returns.",
18102
+ description: "Add a per-platform deliverable to a content draft at revision 1, addressed `DRAFT-KEY/platform`; optional `body` seeds it. `platform` must be one of the project\u2019s active deliverable targets (see `list_projects`); any other slug is rejected. One deliverable per platform per draft; another wording is a revision (`update_deliverable`). See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18128
18103
  inputSchema: createDeliverableInputSchema.shape
18129
18104
  }, ({ content, ...fields }) => run(() => client2().createDeliverable(content, fields)));
18130
18105
  registerStrict("update_deliverable", {
18131
- description: "Append a new revision (post text) to a deliverable, addressed as DRAFT-KEY/platform (e.g. DWB-C3/bsky); optional note records why. Platform identity is immutable \u2014 this only versions the post text. Writing guidance for the post text lives in the product doctrine, which `get_work_context` returns.",
18106
+ description: "Append a revision of the post text to a deliverable, addressed `DRAFT-KEY/platform`; `note` says why. The platform itself is immutable. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18132
18107
  inputSchema: updateDeliverableInputSchema.shape
18133
18108
  }, ({ deliverable, ...fields }) => run(() => client2().updateDeliverable(deliverable, fields)));
18134
18109
  registerStrict("record_post", {
18135
- description: "Record that a deliverable (DRAFT-KEY/platform) was posted: its public url and when (postedAt defaults to now), pinning the deliverable\u2019s current revision as the posted one. Pipebrain never posts \u2014 a human posts, then records the url here. Re-calling overwrites (a correction).",
18110
+ description: "Record that a deliverable (`DRAFT-KEY/platform`) was posted: its public url and `postedAt` (now by default), pinning its current revision as posted. Pipebrain never posts; a person does, then records it here.",
18136
18111
  inputSchema: recordPostInputSchema.shape
18137
18112
  }, ({ deliverable, ...fields }) => run(() => client2().recordPost(deliverable, fields)));
18138
18113
  registerStrict("promote_deliverable", {
18139
- description: "Promote a deliverable (DRAFT-KEY/platform, e.g. DWB-C3/bsky) to a NEW issue: creates the issue (`title` + `type` required; optional `status` defaults to triage, optional `milestone` schedules it, optional `body` seeds it \u2014 typically the current post text) AND a PROTECTED link pinning the deliverable\u2019s current revision. That link CANNOT be unlinked \u2014 the issue exists to ship this deliverable. (To link an EXISTING issue instead, use update_issue.deliverable_links, which stays removable.) Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue: work past triage belongs to a release, and the promotion is refused (`milestone_required`) without one. Seeding the body with the deliverable\u2019s own text is the exception to linking rather than pasting \u2014 it is the point of this tool, and the protected link pins the revision that text came from. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSearch before you create\u201D and \u201CLink by key, never paste\u201D.",
18114
+ // The paste exception is stated because without it an agent holding the doctrine
18115
+ // would refuse this tool's whole purpose.
18116
+ description: "Create a NEW issue from a deliverable (`DRAFT-KEY/platform`) with a protected link pinning its current revision, which cannot be removed. Needs `title` and `type`; `status` defaults to triage, `body` seeds the issue. A `status` past triage or killed needs `milestone`, or it is refused `milestone_required`. Seeding `body` with the post text is the exception to linking rather than pasting. See the doctrine `get_work_context` returns, under \u201CLink by key, never paste\u201D.",
18140
18117
  inputSchema: {
18141
18118
  deliverable: external_exports.string().min(1),
18142
18119
  title: external_exports.string().min(1),
@@ -18160,33 +18137,33 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18160
18137
  }
18161
18138
  }, ({ deliverable, ...fields }) => run(() => client2().promoteDeliverable(deliverable, fields)));
18162
18139
  registerStrict("list_assets", {
18163
- description: "List project assets (S3-backed files of any type) filtered by project, status (pending/active/rejected), category (image/video/document), and filename text, sorted by date or size (asc/desc): id, filename, contentType, size, sha256, status, hasThumbnail, and linkCount (live references). Metadata only \u2014 the bytes move by path, never through this result.",
18140
+ description: "List project assets, filtered by status (pending/active/rejected), category (image/video/document) and filename text, sorted by date or size. No bytes cross this result.",
18164
18141
  inputSchema: listAssetsFilterSchema.shape
18165
18142
  }, (args) => run(() => client2().listAssets(args), GUIDANCE_POINTER));
18166
18143
  registerStrict("get_asset", {
18167
- description: "Get an asset\u2019s metadata by uuid: filename, contentType, size, sha256 (client-computed, server-verified by read-back), status, hasThumbnail, linkCount (live references), and timestamps. Metadata only \u2014 no value/URL is returned over MCP. To get the bytes, use download_asset (a LOCAL server only \u2014 it writes to a path on the machine running the server) or `ppb assets download`.",
18144
+ description: "Get an asset\u2019s metadata by uuid: filename, contentType, size, sha256, status and linkCount. No bytes and no URL come back; use `download_asset` or `ppb assets download`.",
18168
18145
  inputSchema: { asset: uuidSchema }
18169
18146
  }, (args) => run(() => client2().getAsset(args.asset), GUIDANCE_POINTER));
18170
18147
  if (surface === "local") {
18171
18148
  registerStrict("upload_asset", {
18172
- description: "Upload a LOCAL FILE (by path) as a project asset and activate it: reads and SHA-256-hashes the file locally, registers the asset, PUTs the bytes directly to storage, and completes (server verifies the digest by read-back). Returns the asset metadata (status active on success). Only the file PATH crosses the wire \u2014 the bytes never do; do NOT pass base64 or file contents. contentType is guessed from the extension when omitted. Then use link_asset to attach the returned asset to a deliverable revision or a content draft.",
18149
+ description: "Upload a local file as a project asset and activate it. Only the `path` crosses the wire \u2014 never base64 or file contents. `contentType` is guessed from the extension when omitted. Returns the asset metadata; `link_asset` then attaches it to a draft or a deliverable revision.",
18173
18150
  inputSchema: uploadAssetInputSchema.shape
18174
18151
  }, (args) => run(() => uploadAssetFromPath(client2(), apiBaseUrl, args)));
18175
18152
  registerStrict("download_asset", {
18176
- description: "Download an ACTIVE asset\u2019s bytes to a LOCAL FILE at `path` (created/overwritten), so you can read or view an uploaded image, mockup, or document. The bytes are written to disk and re-verified against the recorded sha256; the RESULT is metadata + the written path \u2014 never the bytes or a URL. Addressed by asset uuid (from list_assets, get_asset, or get_content).",
18153
+ description: "Write an ACTIVE asset\u2019s bytes to a local file at `path`, created or overwritten, so you can read an uploaded image or document. Addressed by asset uuid. The bytes are checked against the recorded sha256; the result is metadata and the path.",
18177
18154
  inputSchema: downloadAssetInputSchema.shape
18178
18155
  }, (args) => run(() => downloadAssetToPath(client2(), args)));
18179
18156
  }
18180
18157
  registerStrict("link_asset", {
18181
- description: "Reference an active asset FROM a target. Two targets are wired: `deliverable_revision` (targetId is a content_deliverable_revision uuid, e.g. from get_content) and `content_draft` (targetId is a content_draft uuid \u2014 the asset attaches to the topic doc and is snapshot-copied onto deliverables when they are created). Rejects a non-active asset or a cross-project link.",
18158
+ description: "Reference an active asset from a `deliverable_revision` (a content_deliverable_revision uuid) or a `content_draft` (a content_draft uuid, copied onto later deliverables). A non-active asset or a cross-project link is rejected.",
18182
18159
  inputSchema: linkAssetInputSchema.shape
18183
18160
  }, (args) => run(() => client2().linkAsset(args)));
18184
18161
  registerStrict("unlink_asset", {
18185
- description: "Remove an asset reference by its link id (soft-delete). The asset itself is untouched \u2014 this only drops the reference from its target. A link whose target is a CHAT MESSAGE is author-only: anyone but the person who wrote that message is refused with `not_message_author` (403), so an attachment you did not post is not yours to detach. Every other target type is removable by any member.",
18162
+ description: "Remove an asset reference by its link id. This drops the reference only; the asset itself is untouched. A link on a chat message is author-only \u2014 anyone else is refused `not_message_author`. Every other target type any member can remove.",
18186
18163
  inputSchema: unlinkAssetInputSchema.shape
18187
18164
  }, (args) => run(() => client2().unlinkAsset(args.linkId)));
18188
18165
  registerStrict("set_secret", {
18189
- description: "Create a secret or add a new version under a project + environment. THE VALUE CROSSES INBOUND ONLY \u2014 the response NEVER echoes it \u2014 but inbound is a direction, not a safe one: for a sensitive value prefer the CLI (`ppb secrets set` via stdin/`--from-file`), because a value typed here lands in the chat transcript before the server can encrypt it. `name` is UPPER_SNAKE_CASE (e.g. API_KEY); `environment` is the environment slug (dev/prod); optional `description`. Writing guidance for that description lives in the product doctrine, which `get_work_context` returns.",
18166
+ description: "Create a secret or add a new version under a project and environment. Do not send a sensitive value here \u2014 it lands in the chat transcript before the server can encrypt it; use `ppb secrets set` from stdin or `--from-file`. `name` is UPPER_SNAKE_CASE, `environment` is a slug (dev/prod). See the doctrine `get_work_context` returns, under \u201CA secret value never travels in a tool call\u201D.",
18190
18167
  inputSchema: {
18191
18168
  project: projectKeySchema,
18192
18169
  environment: external_exports.string().min(1),
@@ -18196,14 +18173,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18196
18173
  }
18197
18174
  }, ({ project, environment, name, value, description }) => run(() => client2().setSecret(project, { env: environment, name, value, description })));
18198
18175
  registerStrict("list_secrets", {
18199
- description: "List a project's secrets as METADATA ONLY (id, name, environment, description, current version, timestamps) \u2014 never any value. Optional `environment` slug filter.",
18176
+ description: "List a project\u2019s secrets: id, name, environment, description, current version, timestamps. No value comes back. Optional `environment` slug narrows it.",
18200
18177
  inputSchema: {
18201
18178
  project: projectKeySchema,
18202
18179
  environment: external_exports.string().min(1).optional()
18203
18180
  }
18204
18181
  }, ({ project, environment }) => run(() => client2().listSecrets(project, { env: environment }), GUIDANCE_POINTER));
18205
18182
  registerStrict("describe_secret", {
18206
- description: "Describe one secret: its metadata plus its version history (version, created_at, created_by, and whether each version has been crypto-shredded). METADATA ONLY \u2014 no value is ever returned.",
18183
+ description: "Describe one secret: its metadata plus each version \u2014 when, by whom, and whether it was crypto-shredded. Nothing here returns a value.",
18207
18184
  inputSchema: {
18208
18185
  project: projectKeySchema,
18209
18186
  environment: external_exports.string().min(1),
@@ -18211,7 +18188,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18211
18188
  }
18212
18189
  }, ({ project, environment, name }) => run(() => client2().describeSecret(project, name, { env: environment }), GUIDANCE_POINTER));
18213
18190
  registerStrict("rotate_secret", {
18214
- description: "Rotate an EXISTING secret: store a NEW version and advance the pointer. Same value rules as `set_secret`: the value crosses INBOUND ONLY and the response NEVER echoes it, and for a sensitive value prefer the CLI (`ppb secrets set` adds a new version from stdin/`--from-file`), because a value typed here lands in the chat transcript before the server can encrypt it.",
18191
+ description: "Add a new version to an existing secret and advance the pointer. Same value rules as `set_secret`: a value crosses inbound only and nothing here returns one, so set a real credential with `ppb secrets set`, from a file or stdin.",
18215
18192
  inputSchema: {
18216
18193
  project: projectKeySchema,
18217
18194
  environment: external_exports.string().min(1),
@@ -18220,7 +18197,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18220
18197
  }
18221
18198
  }, ({ project, environment, name, value }) => run(() => client2().rotateSecret(project, name, { env: environment, value })));
18222
18199
  registerStrict("update_secret_description", {
18223
- description: "Update a secret's description (metadata-only edit \u2014 does not change the value or add a version). Writing guidance for that description lives in the product doctrine, which `get_work_context` returns.",
18200
+ description: "Change a secret\u2019s description \u2014 metadata only: no new version, and nothing here returns a value. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
18224
18201
  inputSchema: {
18225
18202
  project: projectKeySchema,
18226
18203
  environment: external_exports.string().min(1),
@@ -18229,7 +18206,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18229
18206
  }
18230
18207
  }, ({ project, environment, name, description }) => run(() => client2().updateSecretDescription(project, name, { env: environment, description })));
18231
18208
  registerStrict("delete_secret", {
18232
- description: "Delete a secret: soft-delete the entry and crypto-shred all its versions (the value becomes unrecoverable). Returns confirmation metadata only.",
18209
+ description: "Delete a secret: soft-delete the entry and crypto-shred every version, so the value is unrecoverable. Returns confirmation metadata only.",
18233
18210
  inputSchema: {
18234
18211
  project: projectKeySchema,
18235
18212
  environment: external_exports.string().min(1),
@@ -18240,14 +18217,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18240
18217
  return { deleted: true, project, environment, name };
18241
18218
  }));
18242
18219
  registerStrict("list_org_instructions", {
18243
- description: "List the org\u2019s agent instructions \u2014 the per-scope texts this server composes into the guidance `get_work_context` returns, each under a heading naming its scope. Edits apply to NEW sessions only; use this to see the CURRENT saved values mid-session (at most one row per scope)."
18220
+ description: "List this organization\u2019s agent instructions, one per scope \u2014 the texts composed into the guidance `get_work_context` returns. What is saved here may be newer than what your session composed at startup."
18244
18221
  }, () => run(() => client2().listOrgInstructions(), GUIDANCE_POINTER));
18245
18222
  registerStrict("set_org_instruction", {
18246
- description: "Set (create or replace) one scope\u2019s org-level agent instruction text, max 4000 characters \u2014 over-cap is rejected, never truncated. Scopes: `general` (server-wide), `requirements`, `content`, and `work.<type>` (one per issue type, e.g. `work.epic`). WHICH ORGANIZATION IS WRITTEN is whichever one this call acts in: pass `org` and you are rewriting THAT organization\u2019s standing instructions to every agent working there, not your own. It requires the ADMIN role in the organization named, checked against your live membership of it, so a call naming an organization you are only a CONTRIBUTOR in is refused. Takes effect at the NEXT session start \u2014 running sessions keep the text they launched with. Returns the saved instruction. Writing guidance for an instruction body lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CSetting an instruction replaces the whole body\u201D.",
18223
+ description: "Set one scope\u2019s organization-level agent instruction, up to 4000 characters; over that is refused, not truncated. Scopes are `general`, `requirements`, `content` and `work.<type>`. It writes the organization this call acts in, so passing `org` rewrites that organization\u2019s standing instructions to every agent working there. It needs the ADMIN role there, so a contributor is refused. New sessions pick it up; running ones keep what they started with. See the doctrine `get_work_context` returns, under \u201CSetting an instruction replaces the whole body\u201D.",
18247
18224
  inputSchema: setOrgInstructionInputSchema.shape
18248
18225
  }, (args) => run(() => client2().setOrgInstruction(args.scope, args.body)));
18249
18226
  registerStrict("delete_org_instruction", {
18250
- description: "Clear one scope\u2019s org-level agent instruction (soft delete; an unset scope is an error naming it). WHICH ORGANIZATION IS CLEARED is whichever one this call acts in: pass `org` and you are removing THAT organization\u2019s standing instruction to every agent working there, not your own. It requires the ADMIN role in the organization named, checked against your live membership of it, so a call naming an organization you are only a CONTRIBUTOR in is refused. Takes effect at the NEXT session start. Returns the cleared instruction.",
18227
+ description: "Clear one scope\u2019s organization-level agent instruction, in the organization named by `org` \u2014 so this removes that organization\u2019s standing instruction to every agent working there. Clearing a scope that is already unset is an error naming it. It needs the ADMIN role in that organization, so a contributor is refused. New sessions pick up the change; running ones keep what they started with.",
18251
18228
  inputSchema: deleteOrgInstructionInputSchema.shape
18252
18229
  }, (args) => run(() => client2().deleteOrgInstruction(args.scope)));
18253
18230
  return server2;
@@ -18316,7 +18293,7 @@ function loadMcpConfig() {
18316
18293
  }
18317
18294
 
18318
18295
  // src/version.ts
18319
- var VERSION = "0.49.0";
18296
+ var VERSION = "0.50.0";
18320
18297
 
18321
18298
  // src/main.ts
18322
18299
  async function scopedClient(base, org) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipebrain-mcp",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Pipebrain MCP server for Claude Code and Claude Desktop — issues, requirements, decision records, conventions, prompts, content, assets, metadata-only secrets, and more of the platform's modules over the Pipebrain API.",
5
5
  "type": "module",
6
6
  "license": "MIT",