pipebrain-mcp 0.49.0 → 0.51.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 +101 -104
  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
@@ -383,9 +383,9 @@ var AGENT_CONFIG_ENTRIES = [
383
383
  { target: "skills/capture-correction/SKILL.md", sha256: "907ee8fc8e7ea15c8296acd23a816e1f9c6454edcf37a93f18af79b1c542fb75" },
384
384
  { target: "skills/pipebrain-header-pass/SKILL.md", sha256: "10545e3c1c5215b031e1c76281e65cd2ecf7420b3f07a4221824591a8263c5fc" },
385
385
  { target: "skills/pipebrain-secrets/SKILL.md", sha256: "52c2136a3cca5612981d50b926195eae3e804911e1e9723dd5eba76e66b52d6f" },
386
- { target: "skills/pipebrain-work-session/SKILL.md", sha256: "09d5741761b43fc13a12a04117edd8b194aac18974bfedb42dfc76c14030ba87" },
386
+ { target: "skills/pipebrain-work-session/SKILL.md", sha256: "1272d0a403b5826a52616cdbf2dc706f69746c21ac6c930e22df87750a2509b7" },
387
387
  { target: "skills/pipebrain-writing/SKILL.md", sha256: "8e5ed4636afcca0cacc220d8fa5310364d2d6854a9fc68ffa03e4c428989f8b8" },
388
- { target: "skills/pipebrain/SKILL.md", sha256: "e53a1cb13d4e2e396c638d8f99055f4082edef030a40ffc8806b1f52f0824b57" },
388
+ { target: "skills/pipebrain/SKILL.md", sha256: "9fdad1b78bb506e9cd91ba55f4baee0adc5f4d2c5d9d418d0e8c922554e8df74" },
389
389
  { target: "skills/requirements-altitude/SKILL.md", sha256: "60d1916da22a52138c046fcf4e5cd4cc420b7a5679a6652eb76573b7a69a1cf4" }
390
390
  ];
391
391
 
@@ -505,7 +505,7 @@ function apiBindingLabel(apiBaseUrl) {
505
505
  }
506
506
 
507
507
  // ../../libs/agent-shared/dist/doctrine.js
508
- var DOCTRINE = "# Pipebrain doctrine\n\nHow an agent works inside Pipebrain. These rules ship with the product and are the same in every organization. Each one protects something the product guarantees.\n\n## How to write here\n\nThe person reading what you write is the operator, deciding something. They have about two minutes. Put the point first. Use short sentences and ordinary words. Headings and bullets are fine. Explain why once, not under every bullet.\n\nA criterion is one sentence: a concrete situation, and what must be true afterward. If you need a general rule, write two or three criteria instead.\n\nEvidence on a criterion opens with the verdict, then how you drove it, then what it does not cover. Keep it to one screen; name the test or command, not every file you read.\n\nA closing comment says what landed, what did not and why, what you filed by key, and whether the tests are green.\n\nAnything else you notice goes to a question, when the operator has to choose; to a new issue at triage, when it is real work; or, most often, nowhere. Never add a notes, caveats, or next-steps section. An item filed and repeated in prose gets triaged twice.\n\nYour organization may add its own writing rules below this document. Most have none.\n\n## Search before you create\n\nLook for an existing record before you add one. A duplicate splits one record's history across two keys, and nothing merges them later. Update or link the record you find.\n\n## Link by key, never paste\n\nRefer to a requirement, decision, or issue by its key. Never paste its text into a body. When the source changes the copy stays stale, and the stale copy is what gets built.\n\n## Most choices are yours\n\nInside an approved requirement you choose the design, the libraries, and the order of work. Ask only when a choice changes what a user sees, costs money, or contradicts a requirement; file that one as a question. Asking about everything makes real questions wait behind choices that were yours. A technical choice worth keeping goes in a decision record.\n\n## File decisions as questions\n\nAn open decision goes in a question on the issue, filed with `ask_questions` and carrying the options you propose. It does not belong in a body, under a heading or in a field. Prose asks nobody and records no answer, so a later session cannot tell whether it was ever decided. Withdraw a question your own work has settled.\n\n## Cross-cutting requirements arrive on their own\n\nThey bind every epic and come back with a work-item read, at their latest approved revision. Never restate them in a body and never add them as explicit requirement links. A copy drifts, and an explicit link pins one revision where the read delivers the newest.\n\n## Conventions need a person's approval\n\nApproved conventions come back with the same work-item read and bind every project in the organization. Never restate one in a body and never add one as an explicit link; a copy goes stale.\n\nA convention you create or amend over the agent surface is a proposal. It is not in force until a person approves it in a browser. Until then, do not work from your own unapproved wording.\n\nA statement that stops being true when this project ends is a cross-cutting requirement. One still true on the next project is a convention. File one when an evaluator rejects work or a criterion fails, and rarely otherwise: each reaches every read, after a person reviews it.\n\n## Choosing auto or manual\n\nFirst find out what this project can drive end to end, and with which command. A criterion is `auto` when an evaluator can drive the input and read the result on the running system, including an interface where a browser harness exists. Only the evaluator's own run qualifies it; a test the implementer wrote does not. An auto criterion nobody can drive never gets evidence.\n\nMark a criterion `manual` only when the verdict needs a person's judgment. A manual row a machine could have checked holds the issue open until a person gets to it. If you cannot say how to drive a criterion, it is worded too vaguely; reword it against something observable. A manual criterion carries instructions for a stranger to the code.\n\nA manual criterion with no recorded artifact is the human gate working as designed, and not a finding.\n\n## The bar for filing a bug\n\nFile a bug only when the behavior breaks an approved requirement or a defined acceptance criterion, and name which one first. Everything else, including unclear code and edge cases nobody specified, is feedback or nothing. A queue that mixes defects with taste has to be read in full before any of it can be trusted.\n\n## A decision record is written once\n\nA decision record says what was decided and why, at the time. Do not edit it later; write a superseding record instead. The chain of records shows how the thinking changed, and an edit destroys it. Read existing records before reopening a settled question.\n\n## A secret value never travels in a tool call\n\nNo tool returns a secret value, by design, and none should receive one. A value in a tool call is in the transcript before the server can encrypt it; the only remedy is rotation. Set values with the command-line tool, from a file or standard input, and inject at run time.\n\n## Setting an instruction replaces the whole body\n\nThere is no partial update. An instruction write stores exactly the body you supply and discards everything that was there. Read the live value, compose the complete replacement, write it, then read it again to confirm what landed.\n\nThis guidance and the tool descriptions are a snapshot from the session's start; an edit reaches the next session, never this one. `list_org_instructions` returns the saved rows as they are now; that is the confirming read.\n";
508
+ var DOCTRINE = "# Pipebrain doctrine\n\nHow an agent works inside Pipebrain. These rules ship with the product and are the same in every organization. Each one protects something the product guarantees.\n\n## How to write here\n\nThe person reading what you write is the operator, deciding something. They have about two minutes. Put the point first. Use short sentences and ordinary words. Headings and bullets are fine. Explain why once, not under every bullet.\n\nA criterion is one sentence: a concrete situation, and what must be true afterward. If you need a general rule, write two or three criteria instead.\n\nEvidence on a criterion opens with the verdict, then how you drove it, then what it does not cover. Keep it to one screen; name the test or command, not every file you read.\n\nA closing comment says what landed, what did not and why, what you filed by key, and whether the tests are green.\n\nAnything else you notice goes to a question, when the operator has to choose; to a new issue at triage, when it is real work; or, most often, nowhere. Never add a notes, caveats, or next-steps section. An item filed and repeated in prose gets triaged twice.\n\nYour organization may add its own writing rules below this document. Most have none.\n\n## Search before you create\n\nLook for an existing record before you add one. A duplicate splits one record's history across two keys, and nothing merges them later. Update or link the record you find.\n\n## Link by key, never paste\n\nRefer to a requirement, decision, or issue by its key. Never paste its text into a body. When the source changes the copy stays stale, and the stale copy is what gets built.\n\n## Most choices are yours\n\nInside an approved requirement you choose the design, the libraries, and the order of work. Ask only when a choice changes what a user sees, costs money, or contradicts a requirement; file that one as a question. Asking about everything makes real questions wait behind choices that were yours. A technical choice worth keeping goes in a decision record.\n\n## File decisions as questions\n\nAn open decision goes in a question on the issue, filed with `ask_questions` and carrying the options you propose. It does not belong in a body, under a heading or in a field. Prose asks nobody and records no answer, so a later session cannot tell whether it was ever decided. If your own work settles a question, answer it with `answer_question` \u2014 the answer is worth keeping, and withdrawing throws it away. Withdraw only a question that should never have been asked.\n\n## Cross-cutting requirements arrive on their own\n\nThey bind every epic and come back with a work-item read, at their latest approved revision. Never restate them in a body and never add them as explicit requirement links. A copy drifts, and an explicit link pins one revision where the read delivers the newest.\n\n## Conventions need a person's approval\n\nApproved conventions come back with the same work-item read and bind every project in the organization. Never restate one in a body and never add one as an explicit link; a copy goes stale.\n\nA convention you create or amend over the agent surface is a proposal. It is not in force until a person approves it in a browser. Until then, do not work from your own unapproved wording.\n\nA statement that stops being true when this project ends is a cross-cutting requirement. One still true on the next project is a convention. File one when an evaluator rejects work or a criterion fails, and rarely otherwise: each reaches every read, after a person reviews it.\n\n## Choosing auto or manual\n\nFirst find out what this project can drive end to end, and with which command. A criterion is `auto` when an evaluator can drive the input and read the result on the running system, including an interface where a browser harness exists. Only the evaluator's own run qualifies it; a test the implementer wrote does not. An auto criterion nobody can drive never gets evidence.\n\nMark a criterion `manual` only when the verdict needs a person's judgment. A manual row a machine could have checked holds the issue open until a person gets to it. If you cannot say how to drive a criterion, it is worded too vaguely; reword it against something observable. A manual criterion carries instructions for a stranger to the code.\n\nA manual criterion with no recorded artifact is the human gate working as designed, and not a finding.\n\nThis choice also decides who can finish the work. What gates finishing is the work rather than the caller \u2014 the status tool names what the gate reads, and the server never asks who is calling. A manual row is the one a credential cannot pass, so work carrying one waits for a person; work whose criteria are all machine-checked does not, and the session that has just passed them can set it done.\n\n## The bar for filing a bug\n\nFile a bug only when the behavior breaks an approved requirement or a defined acceptance criterion, and name which one first. Everything else, including unclear code and edge cases nobody specified, is feedback or nothing. A queue that mixes defects with taste has to be read in full before any of it can be trusted.\n\n## A decision record is written once\n\nA decision record says what was decided and why, at the time. A proposed record is still being written: correct it with `update_decision`, and accept it once it says what you decided. An accepted record is settled, so never edit one; write a superseding record instead. The chain of records shows how the thinking changed, and an edit destroys it. Read existing records before reopening a settled question.\n\n## A secret value never travels in a tool call\n\nNo tool returns a secret value, by design, and none should receive one. A value in a tool call is in the transcript before the server can encrypt it; the only remedy is rotation. Set values with the command-line tool, from a file or standard input, and inject at run time.\n\n## Setting an instruction replaces the whole body\n\nThere is no partial update. An instruction write stores exactly the body you supply and discards everything that was there. Read the live value, compose the complete replacement, write it, then read it again to confirm what landed.\n\nThis guidance and the tool descriptions are a snapshot from the session's start; an edit reaches the next session, never this one. `list_org_instructions` returns the saved rows as they are now; that is the confirming read.\n";
509
509
 
510
510
  // ../../libs/agent-shared/dist/org-reference.js
511
511
  var CANONICAL_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -16877,8 +16877,9 @@ var ApiClient = class _ApiClient {
16877
16877
  /**
16878
16878
  * Answers a question by its stable `Q-{n}` number with EXACTLY one of `option`
16879
16879
  * (1-based, into the question's options) or `text` (PIPE-93); answering again
16880
- * revises. Session only — a bearer caller is refused, so this is the web app's
16881
- * call, never the MCP's. Returns the updated detail.
16880
+ * revises. Token OR session (PIPE-450): the bearer refusal PIPE-R22 r2 withdrew is
16881
+ * gone, and the answer records the credential behind it instead. Returns the updated
16882
+ * detail.
16882
16883
  */
16883
16884
  answerQuestion(ref, number4, answer) {
16884
16885
  return this.request("POST", `/api/v1/issues/${enc(ref)}/questions/${number4}/answer`, { body: answer });
@@ -17208,9 +17209,12 @@ var ApiClient = class _ApiClient {
17208
17209
  }
17209
17210
  // --- the waiting queue -----------------------------------------------------
17210
17211
  /**
17211
- * Every gate awaiting a person across ALL projects of the caller's organisation
17212
+ * Everything not moving on its own across ALL projects of the caller's organisation
17212
17213
  * (PIPE-87): revisions awaiting approval, questions awaiting an answer, and work
17213
- * in `verify` with a criterion not yet passed, longest wait first.
17214
+ * in `verify` with a criterion not yet passed, longest wait first. Not all of it
17215
+ * waits on a person — a question clears with `answer_question` and an `auto`
17216
+ * criterion with `set_criterion_state`; the one a credential cannot pass is a
17217
+ * criterion marked for human judgment.
17214
17218
  *
17215
17219
  * Takes NO project and NO org PARAMETER: a queue you point at a project is the
17216
17220
  * per-project surface this replaces, and the organization is a property of the CLIENT,
@@ -17675,10 +17679,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17675
17679
  );
17676
17680
  }
17677
17681
  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.
17682
+ description: "List the organizations this credential may act in, with each name, slug, id, kind and your role. Use it to find the names you may pass. This is the one tool that needs no `org`: every other call names the organization it acts in, and omitting it is refused unless this session has a default. The answer is your memberships right now, so a joined organization appears and a left one does not."
17682
17683
  }, () => (
17683
17684
  // A BARE ARRAY, unlike `list_waiting`'s `{ items }`, and for the reason that tool
17684
17685
  // gives rather than against it: the envelope has already been unwrapped by
@@ -17698,7 +17699,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17698
17699
  run(() => client2().unscoped().listOrgs(), GUIDANCE_POINTER)
17699
17700
  ));
17700
17701
  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).",
17702
+ 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
17703
  // Optional pagination (§9) — bounds mirror list_issues/list_requirements; the
17703
17704
  // server defaults limit to 200 (max 500) and passes limit/offset as query params.
17704
17705
  inputSchema: {
@@ -17707,7 +17708,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17707
17708
  }
17708
17709
  }, (args) => run(() => client2().listProjects(args), GUIDANCE_POINTER));
17709
17710
  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).",
17711
+ 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
17712
  // Inline shape: @pipeline/domain has no standalone create-project schema — the
17712
17713
  // same `{ name, key }` pair lives only inside createOrgInputSchema.project, so
17713
17714
  // this mirrors it exactly (key = 2–5 A–Z via projectKeySchema) and matches
@@ -17718,12 +17719,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17718
17719
  }
17719
17720
  }, (args) => run(() => client2().createProject(args)));
17720
17721
  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.
17722
+ description: "Everything not moving on its own across all projects in the organization named by `org`, longest wait first: a requirement revision awaiting approval, an unanswered question, and work at verify holding a criterion that has not passed. Each entry names its project, the ref to act on, and when the wait began. Read-only. A revision clears with `approve_requirement`, a question with `answer_question`, an `auto` criterion with `set_criterion_state`. A criterion marked for human judgment waits for a person."
17727
17723
  }, () => (
17728
17724
  // `{ items }`, mirroring the HTTP envelope rather than unwrapping to a bare array
17729
17725
  // like the sibling list tools. The reason is the envelope itself: this tool is a
@@ -17746,12 +17742,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17746
17742
  // one description loaded into EVERY session by `anthropic/alwaysLoad`. The clauses
17747
17743
  // that went were the ones arguing for the rule rather than stating it; what a caller
17748
17744
  // 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.
17745
+ 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
17746
  inputSchema: getWorkContextInputSchema,
17756
17747
  // THE TWO ANNOTATIONS PIPE-D30 REQUIRES, and the reason each key is spelled out
17757
17748
  // rather than built: an unknown `_meta` key is IGNORED — no error, no warning — so a
@@ -17792,7 +17783,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17792
17783
  }
17793
17784
  );
17794
17785
  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).",
17786
+ 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
17787
  inputSchema: {
17797
17788
  project: projectKeySchema,
17798
17789
  token: external_exports.string().min(1),
@@ -17802,7 +17793,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17802
17793
  }
17803
17794
  }, ({ project, ...input }) => run(() => client2().createMilestone(project, input)));
17804
17795
  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).",
17796
+ 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
17797
  inputSchema: {
17807
17798
  project: projectKeySchema,
17808
17799
  milestone: external_exports.string().min(1),
@@ -17813,14 +17804,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17813
17804
  }
17814
17805
  }, ({ project, milestone, ...fields }) => run(async () => client2().updateMilestone(project, await resolveMilestoneId(client2(), project, milestone), fields)));
17815
17806
  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).",
17807
+ 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
17808
  inputSchema: {
17818
17809
  project: projectKeySchema,
17819
17810
  milestone: external_exports.string().min(1)
17820
17811
  }
17821
17812
  }, ({ project, milestone }) => run(async () => client2().activateMilestone(project, await resolveMilestoneId(client2(), project, milestone))));
17822
17813
  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).",
17814
+ 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
17815
  inputSchema: {
17825
17816
  project: projectKeySchema,
17826
17817
  milestone: external_exports.string().min(1)
@@ -17831,7 +17822,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17831
17822
  return { deleted: true, project, milestone, id };
17832
17823
  }));
17833
17824
  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.",
17825
+ 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
17826
  inputSchema: {
17836
17827
  project: projectKeySchema,
17837
17828
  slug: platformSlugSchema,
@@ -17841,7 +17832,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17841
17832
  }
17842
17833
  }, ({ project, ...input }) => run(() => client2().createDeliverableTarget(project, input)));
17843
17834
  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.",
17835
+ 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
17836
  inputSchema: {
17846
17837
  project: projectKeySchema,
17847
17838
  target: external_exports.string().min(1),
@@ -17851,7 +17842,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17851
17842
  }
17852
17843
  }, ({ project, target, ...fields }) => run(async () => client2().updateDeliverableTarget(project, await resolveDeliverableTargetId(client2(), project, target), fields)));
17853
17844
  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.",
17845
+ 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
17846
  inputSchema: {
17856
17847
  project: projectKeySchema,
17857
17848
  target: external_exports.string().min(1)
@@ -17864,7 +17855,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17864
17855
  registerStrict(
17865
17856
  "list_issues",
17866
17857
  {
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.`,
17858
+ 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
17859
  inputSchema: listIssuesFilterSchema.shape
17869
17860
  },
17870
17861
  // The default lives in the API now, not here (PIPE-D35, Listing issues
@@ -17880,7 +17871,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17880
17871
  (args) => run(() => client2().listIssues(args), GUIDANCE_POINTER)
17881
17872
  );
17882
17873
  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.`,
17874
+ 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
17875
  inputSchema: getIssueInputSchema.shape,
17885
17876
  // The same result-threshold annotation `get_work_context` declares, and the same
17886
17877
  // key spelled out for the same reason (an unknown `_meta` key is silently
@@ -17908,78 +17899,66 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17908
17899
  })
17909
17900
  ), GUIDANCE_POINTER));
17910
17901
  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.",
17902
+ description: "Create an epic, task, bug or feedback. Status defaults to triage; anything but triage or killed needs a milestone, else refused `milestone_required`. An archived milestone is refused `milestone_archived_write`. 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
17903
  inputSchema: createIssueInputSchema.shape
17913
17904
  }, (args) => run(() => client2().createIssue(args)));
17914
17905
  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.",
17906
+ 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`. Scheduling the new work into an archived milestone is refused `milestone_archived_write`. Closing the source feedback is refused `status_guard` when anything beneath it, at any depth, is not done or killed. A `manual` criterion needs `howTo`, else refused `how_to_required`. See the doctrine `get_work_context` returns, under \u201CThe bar for filing a bug\u201D.",
17916
17907
  inputSchema: {
17917
17908
  issue: external_exports.string().min(1),
17918
17909
  ...promoteFeedbackInputSchema.shape
17919
17910
  }
17920
17911
  }, ({ issue: issue2, ...fields }) => run(() => client2().promoteFeedback(issue2, fields)));
17921
17912
  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.
17913
+ description: "Open an epic\u2019s completion review, read by whoever neither built nor evaluated it: one ready task carrying the checklist. Pass exactly one of `epic`, or `project` plus `milestone`. A first call is refused `review_target_not_epic`, `review_target_terminal` (finished epic), `milestone_required` (no milestone), `milestone_not_found` (unknown token). One review per scope; a second is refused `review_already_open`. Epic-form also holds that milestone scope; milestone-form blocks no epic. File findings under the epic, not the review. See the doctrine `get_work_context` returns, under \u201CHow to write here\u201D.",
17927
17914
  inputSchema: openReviewInputSchema
17928
17915
  }, (args) => run(() => client2().openReview(args)));
17929
17916
  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.",
17917
+ description: "Change an issue\u2019s fields, parent and links. Header fields are refused; `set_criteria` sets criteria, which epics, tasks and bugs carry. An archived milestone is refused `milestone_archived_write`, unless the issue or its parent carries it. A cycle-closing re-parent is refused `parent_cycle`. A finished parent refuses an open issue, `parent_terminal`; a finished one holding open work, `parent_terminal_subtree`. Attaching open work starts a parent that is not blocked, unscheduled or under finished work. See the doctrine `get_work_context` returns, under \u201CMost choices are yours\u201D.",
17931
17918
  inputSchema: updateIssueInputSchema.shape
17932
17919
  }, ({ issue: issue2, ...fields }) => run(() => client2().updateIssue(issue2, fields)));
17933
17920
  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.",
17921
+ 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
17922
  inputSchema: setIssueHeaderInputSchema.shape
17936
17923
  }, ({ issue: issue2, ...fields }) => run(() => client2().setIssueHeader(issue2, fields)));
17937
17924
  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.",
17925
+ description: "Move an issue to another status. An open status under a done or killed parent is refused `parent_terminal_reopen`. verify needs every auto criterion passed; done needs every criterion passed and everything beneath it, at any depth, done or killed. An epic with no criteria is refused at both gates; a task or bug with none moves through. Blocked work cannot reach ready or in_progress until blockers clear. Work with no milestone can only be triage or killed.",
17939
17926
  inputSchema: setStatusInputSchema.shape
17940
17927
  }, (args) => run(() => client2().setStatus(args.issue, args.status)));
17941
17928
  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.",
17929
+ 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
17930
  inputSchema: { issue: issueRefSchema }
17944
17931
  }, (args) => run(() => client2().claimIssue(args.issue)));
17945
17932
  registerStrict("release_issue", {
17946
- description: "Release an issue\u2019s claim, clearing claimed_by (\xA77).",
17933
+ description: "Release your claim on an issue, leaving it unclaimed and its status unchanged.",
17947
17934
  inputSchema: { issue: issueRefSchema }
17948
17935
  }, (args) => run(() => client2().releaseIssue(args.issue)));
17949
17936
  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.",
17937
+ 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
17938
  inputSchema: commentInputSchema.shape
17952
17939
  }, (args) => run(() => client2().addComment(args.issue, args.body)));
17953
17940
  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.",
17941
+ 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
17942
  inputSchema: setBlockedInputSchema.shape
17956
17943
  }, (args) => run(() => client2().setBlocked(args.issue, args.reason)));
17957
17944
  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).",
17945
+ 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
17946
  inputSchema: { issue: issueRefSchema }
17960
17947
  }, (args) => run(() => client2().clearBlocked(args.issue)));
17961
17948
  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.",
17949
+ 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
17950
  inputSchema: linkBlockerInputSchema.shape
17964
17951
  }, (args) => run(() => client2().addBlocker(args.issue, args.blocker)));
17965
17952
  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.",
17953
+ 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
17954
  inputSchema: linkBlockerInputSchema.shape
17968
17955
  }, (args) => run(() => client2().removeBlocker(args.issue, args.blocker)));
17969
17956
  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.",
17957
+ 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
17958
  inputSchema: setCriteriaInputSchema.shape
17972
17959
  }, (args) => run(() => client2().setCriteria(args.issue, args.criteria)));
17973
17960
  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.
17961
+ 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
17962
  inputSchema: {
17984
17963
  issue: issueRefSchema.optional(),
17985
17964
  criterionId: uuidSchema.optional(),
@@ -18002,27 +17981,31 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18002
17981
  });
18003
17982
  }));
18004
17983
  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.",
17984
+ description: "Ask the operator questions on an issue. Send them in one call \u2014 a half-asked set costs a second pass. Each question gets a stable `Q-{n}` number, never reused; `options` are the choices you propose. Answers come back on every later read; read them before asking again. Answer one yourself with `answer_question` when your work settles it; withdraw one with `withdraw_question` if it should never have been asked. See the doctrine `get_work_context` returns, under \u201CFile decisions as questions\u201D.",
18006
17985
  inputSchema: askQuestionsInputSchema.shape
18007
17986
  }, (args) => run(() => client2().askQuestions(args.issue, args.questions)));
18008
17987
  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.",
17988
+ 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 for a question that should never have been asked; one your own work settled deserves `answer_question` instead, which keeps the answer. The question is kept for the audit trail, its number retires, and later reads no longer carry it.",
18010
17989
  inputSchema: withdrawQuestionInputSchema.shape
18011
17990
  }, (args) => run(() => client2().withdrawQuestion(args.issue, args.number)));
17991
+ registerStrict("answer_question", {
17992
+ description: "Answer a question by its `Q-{n}` number, in the organization named by `org`. Pass exactly one of `option` (a 1-based index into the choices offered) or `text` \u2014 both, or neither, is refused here before anything is sent. Answering again revises; every answer records who answered and which credential. An out-of-range index is refused `option_out_of_range`, naming how many choices there are; a withdrawn or unknown number, `question_not_found`. Answer what your own work settled; leave the rest to the operator.",
17993
+ inputSchema: answerQuestionInputSchema
17994
+ }, (args) => run(() => client2().answerQuestion(args.issue, args.number, { option: args.option, text: args.text })));
18012
17995
  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).",
17996
+ 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
17997
  inputSchema: listRequirementsFilterSchema.shape
18015
17998
  }, (args) => run(() => client2().listRequirements(args), GUIDANCE_POINTER));
18016
17999
  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).",
18000
+ 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
18001
  inputSchema: { requirement: requirementRefSchema, revision: positiveIntSchema.optional() }
18019
18002
  }, (args) => run(() => client2().getRequirement(args.requirement, args.revision), GUIDANCE_POINTER));
18020
18003
  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.',
18004
+ 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
18005
  inputSchema: createRequirementInputSchema.shape
18023
18006
  }, (args) => run(() => client2().createRequirement(args)));
18024
18007
  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.",
18008
+ 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
18009
  // Inline shape (updateRequirementInputSchema is a refine, no .shape) — add
18027
18010
  // summary/area here explicitly (§210) and decisionLinks (PIPE-84 §6); the
18028
18011
  // empty-edit refine is enforced by the API, not this shape.
@@ -18038,23 +18021,23 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18038
18021
  }
18039
18022
  }, ({ requirement, ...fields }) => run(() => client2().updateRequirement(requirement, fields)));
18040
18023
  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).",
18024
+ 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
18025
  inputSchema: approveRequirementInputSchema.shape
18043
18026
  }, (args) => run(() => client2().approveRequirement(args.requirement, args.revision)));
18044
18027
  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.",
18028
+ 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
18029
  inputSchema: listConventionsFilterSchema.shape
18047
18030
  }, (args) => run(() => client2().listConventions(args), GUIDANCE_POINTER));
18048
18031
  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.",
18032
+ 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
18033
  inputSchema: { convention: conventionRefSchema }
18051
18034
  }, (args) => run(() => client2().getConvention(args.convention), GUIDANCE_POINTER));
18052
18035
  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.",
18036
+ 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
18037
  inputSchema: createConventionInputSchema.shape
18055
18038
  }, (args) => run(() => client2().createConvention(args)));
18056
18039
  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.",
18040
+ 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
18041
  // Inline shape (updateConventionInputSchema is a refine, so it has no `.shape`);
18059
18042
  // the empty-edit rule is enforced by the API, not by this shape.
18060
18043
  inputSchema: {
@@ -18065,19 +18048,19 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18065
18048
  }
18066
18049
  }, ({ convention, ...fields }) => run(() => client2().updateConvention(convention, fields)));
18067
18050
  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.",
18051
+ 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
18052
  inputSchema: listPromptsFilterSchema.shape
18070
18053
  }, (args) => run(() => client2().listPrompts(args), GUIDANCE_POINTER));
18071
18054
  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.",
18055
+ 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
18056
  inputSchema: { prompt: promptRefSchema }
18074
18057
  }, (args) => run(() => client2().getPrompt(args.prompt), GUIDANCE_POINTER));
18075
18058
  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.",
18059
+ 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
18060
  inputSchema: createPromptInputSchema.shape
18078
18061
  }, (args) => run(() => client2().createPrompt(args)));
18079
18062
  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.",
18063
+ 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
18064
  // Inline shape (updatePromptInputSchema is a refine, so it has no `.shape`),
18082
18065
  // mirroring that schema's fields — it is the wording this copy follows, `.min(1)`
18083
18066
  // bounds included, so a caller learns a blank body is refused from the advertised
@@ -18092,51 +18075,65 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18092
18075
  }
18093
18076
  }, ({ prompt, ...fields }) => run(() => client2().updatePrompt(prompt, fields)));
18094
18077
  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.",
18078
+ 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
18079
  inputSchema: createDecisionInputSchema.shape
18097
18080
  }, (args) => run(() => client2().createDecision(args)));
18098
18081
  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.",
18082
+ 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
18083
  inputSchema: getDecisionInputSchema.shape
18101
18084
  }, (args) => run(() => client2().getDecision(args.decision), GUIDANCE_POINTER));
18102
18085
  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.",
18086
+ 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
18087
  inputSchema: listDecisionsFilterSchema.shape
18105
18088
  }, (args) => run(() => client2().listDecisions(args), GUIDANCE_POINTER));
18089
+ registerStrict("update_decision", {
18090
+ description: "Correct a decision record while it is still `proposed`: `title`, `body`, or both. An edit naming nothing to change is refused here, before anything is sent. Once a record is `accepted` or `superseded` its text is frozen, and the correction is refused `decision_frozen`, which says to write the change as a new record and supersede this one. Use `accept_decision` to accept; asking for `superseded` here is refused, because superseding must name the replacement.",
18091
+ inputSchema: updateDecisionInputSchema
18092
+ }, (args) => run(() => client2().updateDecision(args.decision, {
18093
+ title: args.title,
18094
+ body: args.body,
18095
+ status: args.status
18096
+ })));
18097
+ registerStrict("accept_decision", {
18098
+ description: "Accept a decision record that is still `proposed`: it becomes `accepted`, stamped with who accepted it and when, and its text is frozen from then on \u2014 changing it later means a new record plus `supersede_decision`. The tool names the move, so the record is the only argument. A record that is already `accepted` or `superseded` is refused `decision_transition_not_allowed`, naming what it can still do.",
18099
+ inputSchema: { decision: decisionRefSchema }
18100
+ }, (args) => run(() => client2().updateDecision(args.decision, { status: "accepted" })));
18106
18101
  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.",
18102
+ 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
18103
  inputSchema: supersedeDecisionInputSchema.shape
18109
18104
  }, (args) => run(() => client2().supersedeDecision(args.decision, args.replacedBy)));
18110
18105
  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.",
18106
+ 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
18107
  inputSchema: listContentFilterSchema.shape
18113
18108
  }, (args) => run(() => client2().listContent(args), GUIDANCE_POINTER));
18114
18109
  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.",
18110
+ 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
18111
  inputSchema: { content: contentRefSchema, revision: positiveIntSchema.optional() }
18117
18112
  }, (args) => run(() => client2().getContent(args.content, args.revision), GUIDANCE_POINTER));
18118
18113
  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.',
18114
+ 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
18115
  inputSchema: createContentInputSchema.shape
18121
18116
  }, (args) => run(() => client2().createContent(args)));
18122
18117
  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.",
18118
+ 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
18119
  inputSchema: updateContentInputSchema.shape
18125
18120
  }, ({ content, ...fields }) => run(() => client2().updateContent(content, fields)));
18126
18121
  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.",
18122
+ 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
18123
  inputSchema: createDeliverableInputSchema.shape
18129
18124
  }, ({ content, ...fields }) => run(() => client2().createDeliverable(content, fields)));
18130
18125
  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.",
18126
+ 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
18127
  inputSchema: updateDeliverableInputSchema.shape
18133
18128
  }, ({ deliverable, ...fields }) => run(() => client2().updateDeliverable(deliverable, fields)));
18134
18129
  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).",
18130
+ 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
18131
  inputSchema: recordPostInputSchema.shape
18137
18132
  }, ({ deliverable, ...fields }) => run(() => client2().recordPost(deliverable, fields)));
18138
18133
  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.",
18134
+ // The paste exception is stated because without it an agent holding the doctrine
18135
+ // would refuse this tool's whole purpose.
18136
+ 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 a `milestone`, else refused `milestone_required`. An archived milestone is refused `milestone_archived_write`. 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
18137
  inputSchema: {
18141
18138
  deliverable: external_exports.string().min(1),
18142
18139
  title: external_exports.string().min(1),
@@ -18160,33 +18157,33 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18160
18157
  }
18161
18158
  }, ({ deliverable, ...fields }) => run(() => client2().promoteDeliverable(deliverable, fields)));
18162
18159
  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.",
18160
+ 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
18161
  inputSchema: listAssetsFilterSchema.shape
18165
18162
  }, (args) => run(() => client2().listAssets(args), GUIDANCE_POINTER));
18166
18163
  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`.",
18164
+ 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
18165
  inputSchema: { asset: uuidSchema }
18169
18166
  }, (args) => run(() => client2().getAsset(args.asset), GUIDANCE_POINTER));
18170
18167
  if (surface === "local") {
18171
18168
  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.",
18169
+ 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
18170
  inputSchema: uploadAssetInputSchema.shape
18174
18171
  }, (args) => run(() => uploadAssetFromPath(client2(), apiBaseUrl, args)));
18175
18172
  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).",
18173
+ 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
18174
  inputSchema: downloadAssetInputSchema.shape
18178
18175
  }, (args) => run(() => downloadAssetToPath(client2(), args)));
18179
18176
  }
18180
18177
  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.",
18178
+ 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
18179
  inputSchema: linkAssetInputSchema.shape
18183
18180
  }, (args) => run(() => client2().linkAsset(args)));
18184
18181
  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.",
18182
+ 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
18183
  inputSchema: unlinkAssetInputSchema.shape
18187
18184
  }, (args) => run(() => client2().unlinkAsset(args.linkId)));
18188
18185
  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.",
18186
+ 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
18187
  inputSchema: {
18191
18188
  project: projectKeySchema,
18192
18189
  environment: external_exports.string().min(1),
@@ -18196,14 +18193,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18196
18193
  }
18197
18194
  }, ({ project, environment, name, value, description }) => run(() => client2().setSecret(project, { env: environment, name, value, description })));
18198
18195
  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.",
18196
+ description: "List a project\u2019s secrets: id, name, environment, description, current version, timestamps. No value comes back. Optional `environment` slug narrows it.",
18200
18197
  inputSchema: {
18201
18198
  project: projectKeySchema,
18202
18199
  environment: external_exports.string().min(1).optional()
18203
18200
  }
18204
18201
  }, ({ project, environment }) => run(() => client2().listSecrets(project, { env: environment }), GUIDANCE_POINTER));
18205
18202
  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.",
18203
+ 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
18204
  inputSchema: {
18208
18205
  project: projectKeySchema,
18209
18206
  environment: external_exports.string().min(1),
@@ -18211,7 +18208,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18211
18208
  }
18212
18209
  }, ({ project, environment, name }) => run(() => client2().describeSecret(project, name, { env: environment }), GUIDANCE_POINTER));
18213
18210
  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.",
18211
+ 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
18212
  inputSchema: {
18216
18213
  project: projectKeySchema,
18217
18214
  environment: external_exports.string().min(1),
@@ -18220,7 +18217,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18220
18217
  }
18221
18218
  }, ({ project, environment, name, value }) => run(() => client2().rotateSecret(project, name, { env: environment, value })));
18222
18219
  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.",
18220
+ 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
18221
  inputSchema: {
18225
18222
  project: projectKeySchema,
18226
18223
  environment: external_exports.string().min(1),
@@ -18229,7 +18226,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18229
18226
  }
18230
18227
  }, ({ project, environment, name, description }) => run(() => client2().updateSecretDescription(project, name, { env: environment, description })));
18231
18228
  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.",
18229
+ description: "Delete a secret: soft-delete the entry and crypto-shred every version, so the value is unrecoverable. Returns confirmation metadata only.",
18233
18230
  inputSchema: {
18234
18231
  project: projectKeySchema,
18235
18232
  environment: external_exports.string().min(1),
@@ -18240,14 +18237,14 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18240
18237
  return { deleted: true, project, environment, name };
18241
18238
  }));
18242
18239
  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)."
18240
+ 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
18241
  }, () => run(() => client2().listOrgInstructions(), GUIDANCE_POINTER));
18245
18242
  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.",
18243
+ 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
18244
  inputSchema: setOrgInstructionInputSchema.shape
18248
18245
  }, (args) => run(() => client2().setOrgInstruction(args.scope, args.body)));
18249
18246
  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.",
18247
+ 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
18248
  inputSchema: deleteOrgInstructionInputSchema.shape
18252
18249
  }, (args) => run(() => client2().deleteOrgInstruction(args.scope)));
18253
18250
  return server2;
@@ -18316,7 +18313,7 @@ function loadMcpConfig() {
18316
18313
  }
18317
18314
 
18318
18315
  // src/version.ts
18319
- var VERSION = "0.49.0";
18316
+ var VERSION = "0.51.0";
18320
18317
 
18321
18318
  // src/main.ts
18322
18319
  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.51.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",