pipebrain-mcp 0.53.1 → 0.54.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 (2) hide show
  1. package/dist/main.js +290 -82
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -367,7 +367,7 @@ var AGENT_CONFIG_ENTRIES = [
367
367
  { target: "agents/blind-spot-pass.md", sha256: "564751a751f7896e89c67e0d20788aff873e599766a3c30360073ba74e4c6099" },
368
368
  { target: "agents/code-verifier.md", sha256: "c3f0a3b7397f0c903056971e71f2640a979a52e6efd18b624b3d29716d1e43e0" },
369
369
  { target: "agents/heavy-implementer.md", sha256: "7d3af78d4241aaa67c283db245dd4065af6e8f55efce54c2f8623c1f83879830" },
370
- { target: "agents/issue-header-writer.md", sha256: "8809b2913c71d021e8fa6c72fe70e2d1db8019b90ed93d85786f5236552c7da6" },
370
+ { target: "agents/issue-header-writer.md", sha256: "185d8c656216bfd755f23391761f172d80e23b0d8a10d58acea0110df91d5c84" },
371
371
  { target: "agents/spec-reviewer.md", sha256: "a46dc637006d1f810081046c57edcd731406727810ccc5e0b1c7d7b9b98be5ef" },
372
372
  { target: "agents/surgical-editor.md", sha256: "c8b7fe2ec8d76c396c0c698f36b0523be430b31e4b73b902409479a52d42ea5c" },
373
373
  { target: "pipebrain/hooks/block-bash4-features.js", sha256: "383b38169a1795d9da34aa1c939349b6a1f76bba1359d0af7fd14c11ea5da25f" },
@@ -376,17 +376,17 @@ var AGENT_CONFIG_ENTRIES = [
376
376
  { target: "pipebrain/hooks/pre-commit-publication-fitness.js", sha256: "68a4f5f15dea22057b7217ad54d5cf8c34a8c4aaa5ce139ace79911acece8504" },
377
377
  { target: "pipebrain/hooks/pre-commit-secret-scan.js", sha256: "1bc634774184e66748b3152016e806ef974449e976920fd710732170fdb4ad6b" },
378
378
  { target: "pipebrain/hooks/stop-orphaned-servers.js", sha256: "16ec7e690b3bd5eb7aceacefd462fc3ac4ae31ae0b5ee5280472fb9c34ffc09b" },
379
- { target: "pipebrain/team.md", sha256: "6692dd20f27820cbd22f7272035e1b1e09ef8e1f83ef403874ae6c67d49085c0" },
379
+ { target: "pipebrain/team.md", sha256: "073a1f59681f09153e323b7cf0476c6860aa9a4584cdebf5ba754ad5251da13f" },
380
380
  { target: "rules/react-tailwind.md", sha256: "80bb51b2d8fd5782b7c742526544e7841ee38aa575faf7a8c7b4217ba5731989" },
381
381
  { target: "rules/shell.md", sha256: "499aac60b6419acb0bb362c0267b9399139dcceafb0eaf67702e40f3118b5c04" },
382
382
  { target: "rules/typescript.md", sha256: "c55eab0c243e38cdf903ab50b1e9d6d621516b893e602e1abff9a999a14e72d4" },
383
- { target: "skills/capture-correction/SKILL.md", sha256: "907ee8fc8e7ea15c8296acd23a816e1f9c6454edcf37a93f18af79b1c542fb75" },
384
- { target: "skills/pipebrain-header-pass/SKILL.md", sha256: "10545e3c1c5215b031e1c76281e65cd2ecf7420b3f07a4221824591a8263c5fc" },
385
- { target: "skills/pipebrain-secrets/SKILL.md", sha256: "52c2136a3cca5612981d50b926195eae3e804911e1e9723dd5eba76e66b52d6f" },
386
- { target: "skills/pipebrain-work-session/SKILL.md", sha256: "d65dd271e1fef00bc583eac1cab5007d7c156a43b40ee1698aef1d88e9ae78f9" },
387
- { target: "skills/pipebrain-writing/SKILL.md", sha256: "8e5ed4636afcca0cacc220d8fa5310364d2d6854a9fc68ffa03e4c428989f8b8" },
388
- { target: "skills/pipebrain/SKILL.md", sha256: "9fdad1b78bb506e9cd91ba55f4baee0adc5f4d2c5d9d418d0e8c922554e8df74" },
389
- { target: "skills/requirements-altitude/SKILL.md", sha256: "60d1916da22a52138c046fcf4e5cd4cc420b7a5679a6652eb76573b7a69a1cf4" }
383
+ { target: "skills/capture-correction/SKILL.md", sha256: "43155819d172d2cd32474312df6244cc710f23cc76eeb8c0aa77664bd81dd9ca" },
384
+ { target: "skills/pipebrain-header-pass/SKILL.md", sha256: "ef163f0530def7ba90b1014a47efb98f86e4ba222c6ab4d4c6bfb549dfed10f1" },
385
+ { target: "skills/pipebrain-secrets/SKILL.md", sha256: "f74d3a97a94a026c612698fb3ba17da06891a217c7d44573075dd423189ae084" },
386
+ { target: "skills/pipebrain-work-session/SKILL.md", sha256: "d6803c66123e3c2715376d41f63cedefc08b5ceb12fa0bae1d9ad8c8cd5d200d" },
387
+ { target: "skills/pipebrain-writing/SKILL.md", sha256: "1496937104f6d7469fb8b1eb00b553502e8de0422366b2130a178f68bbcdbe44" },
388
+ { target: "skills/pipebrain/SKILL.md", sha256: "71c287f905af2ed9207d4d2829bbb5b2aa51f5abb486210c78a6a65d4da8f5e2" },
389
+ { target: "skills/requirements-altitude/SKILL.md", sha256: "ffe20eb73fbc73e1fd9bfaec5f9759736a4375dce5fedb015754b135fc12f225" }
390
390
  ];
391
391
 
392
392
  // ../../libs/agent-shared/dist/agent-config.js
@@ -15444,6 +15444,21 @@ var projectSchema = external_exports.object({
15444
15444
  environments: external_exports.array(projectEnvironmentSchema),
15445
15445
  milestones: external_exports.array(projectMilestoneSchema),
15446
15446
  deliverableTargets: external_exports.array(projectDeliverableTargetSchema),
15447
+ /**
15448
+ * The project's current release (PIPE-484), or `null` when none is active.
15449
+ *
15450
+ * DERIVED from the `milestones` array beside it — the row carrying `active: true` —
15451
+ * rather than queried again, so the two spellings of one answer cannot disagree. It
15452
+ * is carried at all because the summary read (see {@link projectSummarySchema}) has
15453
+ * no milestone array to look in, and "which release is current" is the one milestone
15454
+ * question a chooser asks; deriving it in every caller instead would be the same
15455
+ * `find` written in each of them.
15456
+ *
15457
+ * `.nullable()` and REQUIRED, like `defaultEnvironmentId`: the server derives it on
15458
+ * every row, so an absent key would mean a response predating the field while `null`
15459
+ * means this project has no active milestone.
15460
+ */
15461
+ activeMilestone: projectMilestoneSchema.nullable(),
15447
15462
  openIssueCount: external_exports.number().int().nonnegative(),
15448
15463
  /**
15449
15464
  * What an issue's placement falls back to when neither it nor any ancestor carries a
@@ -15467,6 +15482,16 @@ var projectSchema = external_exports.object({
15467
15482
  */
15468
15483
  defaultIntegrationMode: integrationModeSchema
15469
15484
  });
15485
+ var projectSummarySchema = external_exports.object({
15486
+ key: projectKeySchema,
15487
+ name: external_exports.string().min(1),
15488
+ openIssueCount: external_exports.number().int().nonnegative(),
15489
+ activeMilestone: projectMilestoneSchema.nullable(),
15490
+ defaultBranch: external_exports.string().nullable(),
15491
+ defaultWorktree: external_exports.string().nullable(),
15492
+ defaultBase: external_exports.string().nullable(),
15493
+ defaultIntegrationMode: integrationModeSchema
15494
+ });
15470
15495
  var environmentSchema = external_exports.object({
15471
15496
  id: uuidSchema,
15472
15497
  orgId: uuidSchema,
@@ -15992,13 +16017,13 @@ var criterionInputSchema = external_exports.strictObject({
15992
16017
  var refusedIssueHeaderFields = {
15993
16018
  summary: external_exports.never({
15994
16019
  error: "summary cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15995
- }).optional().describe("Not writable here. The header summary is written by `set_issue_header`."),
16020
+ }).optional().describe("Not writable here; `set_issue_header` writes it."),
15996
16021
  why: external_exports.never({
15997
16022
  error: "why cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15998
- }).optional().describe("Not writable here. The header reason is written by `set_issue_header`."),
16023
+ }).optional().describe("Not writable here; `set_issue_header` writes it."),
15999
16024
  affectedSurfaces: external_exports.never({
16000
16025
  error: "affected surfaces cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
16001
- }).optional().describe("Not writable here. The affected surfaces are written by `set_issue_header`.")
16026
+ }).optional().describe("Not writable here; `set_issue_header` writes it.")
16002
16027
  };
16003
16028
  var createIssueInputSchema = external_exports.strictObject({
16004
16029
  ...refusedIssueHeaderFields,
@@ -16075,9 +16100,9 @@ var updateIssueInputSchema = external_exports.strictObject({
16075
16100
  * asks and a schema that could not tell them apart would make an issue's own value
16076
16101
  * impossible to remove.
16077
16102
  */
16078
- branch: external_exports.string().min(1, { message: "branch must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("The branch this issue\u2019s work happens on. Pass null to clear it, so the issue inherits from the nearest ancestor carrying one and then from the project default."),
16079
- worktree: external_exports.string().min(1, { message: "worktree must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("A label for the working tree this issue\u2019s work happens in \u2014 never a path, which is true only on the machine that wrote it. Pass null to clear it."),
16080
- base: external_exports.string().min(1, { message: "base must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("What this issue\u2019s work integrates into. Pass null to clear it, so the issue inherits from its chain and then from the project default.")
16103
+ branch: external_exports.string().min(1, { message: "branch must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("The branch this issue\u2019s work happens on. Null clears it, so the issue inherits from the nearest ancestor carrying one, then from the project default."),
16104
+ worktree: external_exports.string().min(1, { message: "worktree must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("A label for the working tree this issue\u2019s work happens in, never a path. Null clears it."),
16105
+ base: external_exports.string().min(1, { message: "base must not be empty \u2014 pass null to clear it" }).nullable().optional().describe("What this issue\u2019s work integrates into. Null clears it, so the issue inherits from its chain, then from the project default.")
16081
16106
  });
16082
16107
  var ISSUE_SUMMARY_MAX = 350;
16083
16108
  function inPlainWords(schema, field) {
@@ -16103,16 +16128,35 @@ var setStatusInputSchema = external_exports.strictObject({
16103
16128
  var WORK_CONTEXT_COMMENT_LIMIT = 20;
16104
16129
  var ISSUE_BODY_CHAR_LIMIT = 1e5;
16105
16130
  var ISSUE_COMMENT_LIMIT = 50;
16131
+ var crossCuttingModeSchema = external_exports.enum(["full", "keys"]);
16132
+ var ISSUE_LIST_LIMIT = 50;
16133
+ var issueListDetailSchema = external_exports.enum(["summary", "header"]);
16106
16134
  var issueReadWindowSchema = external_exports.strictObject({
16107
16135
  bodyOffset: nonNegativeIntSchema.optional(),
16108
16136
  bodyLimit: positiveIntSchema.optional(),
16109
16137
  commentOffset: nonNegativeIntSchema.optional(),
16110
- commentLimit: positiveIntSchema.optional()
16138
+ commentLimit: positiveIntSchema.optional(),
16139
+ /**
16140
+ * How much of the cross-cutting requirements to carry (PIPE-482). It rides HERE
16141
+ * rather than on a schema of its own because it is the same kind of thing as the
16142
+ * four above — a caller saying how much of the record it needs — and both surfaces
16143
+ * that parse this object are the two that must accept it. Omitted means `full`,
16144
+ * resolved server-side, so a caller that says nothing reads what it always read.
16145
+ *
16146
+ * IT CARRIES A `.describe()`, unlike the four sibling window fields, for the reason the
16147
+ * refused header fields above carry theirs: this one is an ENUM whose two members read
16148
+ * as interchangeable from the schema alone, and the JSON Schema is what an agent has in
16149
+ * front of it when it decides which to pass. `bodyOffset` and its siblings name
16150
+ * themselves; `keys` does not say what it drops.
16151
+ */
16152
+ crossCutting: crossCuttingModeSchema.describe("`full` (the default) carries each cross-cutting requirement body; `keys` omits them.").optional()
16111
16153
  });
16112
16154
  var getIssueInputSchema = external_exports.strictObject({
16113
16155
  issue: issueRefSchema,
16114
16156
  ...issueReadWindowSchema.shape
16115
16157
  });
16158
+ var WORK_CONTEXT_AVAILABLE_LIMIT = 25;
16159
+ var WORK_CONTEXT_AVAILABLE_LIMIT_MAX = 200;
16116
16160
  var getWorkContextInputSchema = external_exports.strictObject({
16117
16161
  issue: issueRefSchema.optional(),
16118
16162
  project: projectRefSchema.optional(),
@@ -16126,7 +16170,34 @@ var getWorkContextInputSchema = external_exports.strictObject({
16126
16170
  * exception and buy nothing, since naming a session cannot change what an item read
16127
16171
  * returns.
16128
16172
  */
16129
- session: sessionIdSchema.optional()
16173
+ session: sessionIdSchema.optional(),
16174
+ /**
16175
+ * How much of the cross-cutting requirements to carry (PIPE-482), accepted on BOTH
16176
+ * forms for the reason `session` is accepted on both: a session that holds them
16177
+ * passes this on every read it makes, and making one call the exception buys
16178
+ * nothing. Omitted means `full`.
16179
+ */
16180
+ crossCutting: crossCuttingModeSchema.optional(),
16181
+ /**
16182
+ * How many rows of `available` the PROJECT form returns (PIPE-483), defaulting to
16183
+ * {@link WORK_CONTEXT_AVAILABLE_LIMIT} and bounded at {@link
16184
+ * WORK_CONTEXT_AVAILABLE_LIMIT_MAX}. The tool description has named this field as
16185
+ * the applied cap since PIPE-282 while the schema refused it as an unknown key, so a
16186
+ * caller that read the description and passed it was told the key does not exist.
16187
+ *
16188
+ * Accepted on both forms, like `session` above and for the same reason: naming a cap
16189
+ * cannot change what an ITEM read returns, and refusing it there would make one call
16190
+ * the exception.
16191
+ *
16192
+ * IT CARRIES A `.describe()`, like `crossCutting` on {@link issueReadWindowSchema} and
16193
+ * for the same reason: the JSON Schema is what an agent has in front of it when it
16194
+ * decides what to pass, and a bare integer says neither what arrives when it passes
16195
+ * nothing nor how much it may ask for. Both numbers are INTERPOLATED from the
16196
+ * constants they document, so a bound that moves cannot leave the advertised text
16197
+ * claiming the old one. The text also names `availableCount`, because a cap is only
16198
+ * usable beside the total that says whether it was reached.
16199
+ */
16200
+ availableLimit: positiveIntSchema.max(WORK_CONTEXT_AVAILABLE_LIMIT_MAX).describe(`How many rows of \`available\` the project form returns: ${WORK_CONTEXT_AVAILABLE_LIMIT} when omitted, ${WORK_CONTEXT_AVAILABLE_LIMIT_MAX} at most. \`availableCount\` beside the list is the true total.`).optional()
16130
16201
  }).refine((v) => v.issue === void 0 !== (v.project === void 0), {
16131
16202
  message: "pass exactly one of `issue` or `project` \u2014 an issue names its own project, and passing neither names nothing to read"
16132
16203
  });
@@ -16336,6 +16407,13 @@ var listIssuesFilterSchema = external_exports.strictObject({
16336
16407
  * asks — the filter narrows to sessions that said who they were.
16337
16408
  */
16338
16409
  session: sessionIdSchema.optional(),
16410
+ /**
16411
+ * How much of the header each row carries (PIPE-484) — see {@link
16412
+ * issueListDetailSchema}. Omitted means `summary` on every surface: the route
16413
+ * applies that default, so the agent tool and the raw read cannot disagree about
16414
+ * what a bare list returns.
16415
+ */
16416
+ detail: issueListDetailSchema.optional(),
16339
16417
  // Pagination — server default 200, max 500 (§9); default applied server-side.
16340
16418
  limit: external_exports.number().int().min(1, { message: "limit must be at least 1" }).max(500, { message: "limit must be at most 500" }).optional(),
16341
16419
  offset: external_exports.number().int().nonnegative().optional()
@@ -16827,12 +16905,30 @@ var ApiClient = class _ApiClient {
16827
16905
  return this.requestList("GET", "/api/v1/issues", { query: filter });
16828
16906
  }
16829
16907
  /**
16830
- * Reads the full detail, optionally windowed (PIPE-386): `window`'s four fields ride
16908
+ * The same read as {@link listIssues}, returning the whole envelope (PIPE-484):
16909
+ * `items` plus the true `total` the filter matched and the `next` offset, or null at
16910
+ * the end of the list.
16911
+ *
16912
+ * A SECOND method rather than a widened return on the first, because the two callers
16913
+ * want different things and neither should carry the other's shape. The web renders
16914
+ * rows and pages from `offset: 0`, so a total beside them is nothing it reads; the
16915
+ * agent surface is bounded by a page it did not choose, and there `total` and `next`
16916
+ * are the difference between a first page and a complete answer.
16917
+ */
16918
+ listIssuesPage(filter = {}) {
16919
+ return this.request("GET", "/api/v1/issues", { query: filter });
16920
+ }
16921
+ /**
16922
+ * Reads the full detail, optionally windowed (PIPE-386): `window`'s paging fields ride
16831
16923
  * as the route's query params of the same names, and a response that left anything
16832
16924
  * out carries `omitted` saying so. No window means the WHOLE record — this client
16833
16925
  * injects no default, so the split between the bounded agent surface and the
16834
16926
  * unbounded raw read is decided by the caller that owns it (the MCP handler), not
16835
16927
  * silently here for everyone.
16928
+ *
16929
+ * `window.crossCutting` rides the same way (PIPE-482): `keys` asks for the project's
16930
+ * cross-cutting requirements without their bodies, for a caller that already holds
16931
+ * them and needs only to check the approved revision it has against the current one.
16836
16932
  */
16837
16933
  getIssue(ref, window) {
16838
16934
  return this.request("GET", `/api/v1/issues/${enc(ref)}`, { query: window });
@@ -16845,6 +16941,10 @@ var ApiClient = class _ApiClient {
16845
16941
  * `placement` omits keeps whatever the issue already recorded; the call never clears
16846
16942
  * one. Refused 409 `placement_conflict` when another live claim in the project holds
16847
16943
  * the branch or the worktree.
16944
+ *
16945
+ * Answers with an {@link IssueReceipt}, like the five state-changing writes below it
16946
+ * (PIPE-481): the display id and the fields the call changed, with the whole issue one
16947
+ * {@link ApiClient.getIssue} away.
16848
16948
  */
16849
16949
  claimIssue(ref, placement) {
16850
16950
  return this.request("POST", `/api/v1/issues/${enc(ref)}/claim`, {
@@ -16874,9 +16974,14 @@ var ApiClient = class _ApiClient {
16874
16974
  *
16875
16975
  * `issue` is dropped from the body because the ref rides the path, the way it
16876
16976
  * does on every other whole-value write here.
16977
+ *
16978
+ * Answers with an {@link IssueHeaderReceipt} (PIPE-481) — the ordinary receipt plus
16979
+ * all three texts this call wrote, so the write can be verified from its own answer.
16877
16980
  */
16878
16981
  setIssueHeader(ref, input) {
16879
- return this.request("PUT", `/api/v1/issues/${enc(ref)}/header`, { body: input });
16982
+ return this.request("PUT", `/api/v1/issues/${enc(ref)}/header`, {
16983
+ body: input
16984
+ });
16880
16985
  }
16881
16986
  /** Records the issue as blocked with a required reason (PIPE-99). */
16882
16987
  setBlocked(ref, reason) {
@@ -17216,6 +17321,17 @@ var ApiClient = class _ApiClient {
17216
17321
  createProject(input) {
17217
17322
  return this.request("POST", "/api/v1/projects", { body: input });
17218
17323
  }
17324
+ /**
17325
+ * Every project this token can see (§7). `detail` chooses the row shape (PIPE-484):
17326
+ * `full` — the default the route applies — is the row with its environments,
17327
+ * milestones and deliverable targets, and `summary` is the chooser's row.
17328
+ *
17329
+ * The return type stays `Project[]`, the full row, because that is what every caller
17330
+ * in this repository that does not pass `detail` receives. A caller asking for
17331
+ * `summary` narrows it itself: `listProjects({ detail: 'summary' }) as ProjectSummary[]`
17332
+ * is the honest cast, and the overload that would express it in the type system would
17333
+ * have to be repeated on every wrapper this client is reached through.
17334
+ */
17219
17335
  listProjects(page) {
17220
17336
  return this.requestList("GET", "/api/v1/projects", { query: page });
17221
17337
  }
@@ -17316,8 +17432,15 @@ var ApiClient = class _ApiClient {
17316
17432
  * answer.
17317
17433
  */
17318
17434
  getWorkContext(input) {
17319
- return input.issue !== void 0 ? this.request("GET", `/api/v1/issues/${enc(input.issue)}/work-context`) : this.request("GET", `/api/v1/projects/${enc(input.project)}/work-context`, {
17320
- query: input.session === void 0 ? {} : { session: input.session }
17435
+ const crossCutting = input.crossCutting === void 0 ? {} : { crossCutting: input.crossCutting };
17436
+ return input.issue !== void 0 ? this.request("GET", `/api/v1/issues/${enc(input.issue)}/work-context`, {
17437
+ query: crossCutting
17438
+ }) : this.request("GET", `/api/v1/projects/${enc(input.project)}/work-context`, {
17439
+ query: {
17440
+ ...crossCutting,
17441
+ ...input.session === void 0 ? {} : { session: input.session },
17442
+ ...input.availableLimit === void 0 ? {} : { availableLimit: input.availableLimit }
17443
+ }
17321
17444
  });
17322
17445
  }
17323
17446
  // --- secrets ---------------------------------------------------------------
@@ -17568,6 +17691,8 @@ function safeJsonParse(text) {
17568
17691
  // ../../libs/mcp-server/dist/server.js
17569
17692
  import { AsyncLocalStorage } from "node:async_hooks";
17570
17693
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17694
+ import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
17695
+ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
17571
17696
 
17572
17697
  // ../../libs/mcp-server/dist/instructions.js
17573
17698
  var STARTUP_FETCH_TIMEOUT_MS = 5e3;
@@ -17666,7 +17791,7 @@ async function run(body, appended) {
17666
17791
  }
17667
17792
  }
17668
17793
  async function resolveMilestoneId(client2, projectKey, ref) {
17669
- const projects = await client2.listProjects();
17794
+ const projects = await client2.listProjects({ detail: "full" });
17670
17795
  const project = projects.find((p) => p.key === projectKey);
17671
17796
  if (project === void 0) {
17672
17797
  throw new Error(`No project with key \`${projectKey}\` is visible to this token.`);
@@ -17679,7 +17804,7 @@ async function resolveMilestoneId(client2, projectKey, ref) {
17679
17804
  return match.id;
17680
17805
  }
17681
17806
  async function resolveDeliverableTargetId(client2, projectKey, ref) {
17682
- const projects = await client2.listProjects();
17807
+ const projects = await client2.listProjects({ detail: "full" });
17683
17808
  const project = projects.find((p) => p.key === projectKey);
17684
17809
  if (project === void 0) {
17685
17810
  throw new Error(`No project with key \`${projectKey}\` is visible to this token.`);
@@ -17691,10 +17816,14 @@ async function resolveDeliverableTargetId(client2, projectKey, ref) {
17691
17816
  }
17692
17817
  return match.id;
17693
17818
  }
17694
- var ORG_ARGUMENT_DESCRIPTION = "The organization this call acts in \u2014 its slug or its uuid. Omitting it is refused unless this session has a default.";
17819
+ var ORG_ARGUMENT_DESCRIPTION = "Org slug or uuid; omit only with a session default.";
17695
17820
  var orgArgumentSchema = external_exports.string().min(1).describe(ORG_ARGUMENT_DESCRIPTION).optional();
17696
17821
  function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__ */ new Map(), opts = {}) {
17822
+ return createMcpServerWithRegistry(baseClient, apiBaseUrl, orgInstructions2, opts).server;
17823
+ }
17824
+ function createMcpServerWithRegistry(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__ */ new Map(), opts = {}) {
17697
17825
  const surface = opts.surface ?? "local";
17826
+ const registry2 = [];
17698
17827
  const scope = new AsyncLocalStorage();
17699
17828
  function client2() {
17700
17829
  return scope.getStore()?.client ?? baseClient;
@@ -17726,6 +17855,12 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17726
17855
  const declared = config3.inputSchema;
17727
17856
  const strict = declared === void 0 ? external_exports.strictObject({}) : "_zod" in declared || "_def" in declared ? declared : external_exports.strictObject(declared);
17728
17857
  const inputSchema = strict.extend({ org: orgArgumentSchema });
17858
+ registry2.push({
17859
+ name,
17860
+ description: config3.description,
17861
+ inputSchema,
17862
+ ...config3._meta === void 0 ? {} : { _meta: config3._meta }
17863
+ });
17729
17864
  server2.registerTool(
17730
17865
  name,
17731
17866
  { ...config3, inputSchema },
@@ -17752,7 +17887,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17752
17887
  );
17753
17888
  }
17754
17889
  registerStrict("list_orgs", {
17755
- 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."
17890
+ description: "List the organizations this credential may act in: name, slug, id, kind and your role. This is the one tool that needs no `org`: every other call names the organization it acts in, and omitting it is refused unless this session has a default. The answer is your memberships right now."
17756
17891
  }, () => (
17757
17892
  // A BARE ARRAY, unlike `list_waiting`'s `{ items }`, and for the reason that tool
17758
17893
  // gives rather than against it: the envelope has already been unwrapped by
@@ -17771,17 +17906,31 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17771
17906
  // makes the tool's own description true.
17772
17907
  run(() => client2().unscoped().listOrgs(), GUIDANCE_POINTER)
17773
17908
  ));
17774
- registerStrict("list_projects", {
17775
- 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`.",
17776
- // Optional pagination (§9) — bounds mirror list_issues/list_requirements; the
17777
- // server defaults limit to 200 (max 500) and passes limit/offset as query params.
17778
- inputSchema: {
17779
- limit: external_exports.number().int().min(1).max(500).optional(),
17780
- offset: external_exports.number().int().nonnegative().optional()
17781
- }
17782
- }, (args) => run(() => client2().listProjects(args), GUIDANCE_POINTER));
17909
+ registerStrict(
17910
+ "list_projects",
17911
+ {
17912
+ description: "List every project: key, name, open-issue count, the active milestone and the placement defaults work falls back to. Summary rows carry no `id`, so chain off `key`; `detail: full` returns the whole project row \u2014 environments, milestones (an archived one stays valid on issues naming it), deliverable targets, and the widget and default fields. Pages with `limit` (default 200, max 500) and `offset`.",
17913
+ // Optional pagination (§9) — bounds mirror list_issues/list_requirements; the
17914
+ // server defaults limit to 200 (max 500) and passes limit/offset as query params.
17915
+ // `detail` is declared INLINE like the rest of this shape: there is no
17916
+ // list-projects schema in @pipeline/domain, so this is where the option lives.
17917
+ inputSchema: {
17918
+ limit: external_exports.number().int().min(1).max(500).optional(),
17919
+ offset: external_exports.number().int().nonnegative().optional(),
17920
+ detail: external_exports.enum(["summary", "full"]).optional()
17921
+ }
17922
+ },
17923
+ // THE AGENT-SURFACE DEFAULT (PIPE-484), injected here — the same deliberate
17924
+ // exception to this side's inject-no-default rule (PIPE-D26) that `get_issue`'s
17925
+ // caps are, and for the same reason: the two surfaces differ by design. A browser
17926
+ // reading this route is rendering a settings page and needs every milestone and
17927
+ // every deliverable target, so the route keeps `full`; an agent reading it is
17928
+ // choosing a project, and was being handed every milestone of every project to do
17929
+ // it. A caller that wants the arrays here says `detail: full` and gets them.
17930
+ (args) => run(() => client2().listProjects({ ...args, detail: args.detail ?? "summary" }), GUIDANCE_POINTER)
17931
+ );
17783
17932
  registerStrict("create_project", {
17784
- 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.",
17933
+ description: "Create a project. Nothing can be filed until one exists. `name` is the display label; `key` is 2\u20135 uppercase letters, unique per organization, and prefixes every display id.",
17785
17934
  // Inline shape: @pipeline/domain has no standalone create-project schema — the
17786
17935
  // same `{ name, key }` pair lives only inside createOrgInputSchema.project, so
17787
17936
  // this mirrors it exactly (key = 2–5 A–Z via projectKeySchema) and matches
@@ -17792,7 +17941,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17792
17941
  }
17793
17942
  }, (args) => run(() => client2().createProject(args)));
17794
17943
  registerStrict("list_waiting", {
17795
- 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."
17944
+ 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`; one left to human judgment waits for a person."
17796
17945
  }, () => (
17797
17946
  // `{ items }`, mirroring the HTTP envelope rather than unwrapping to a bare array
17798
17947
  // like the sibling list tools. The reason is the envelope itself: this tool is a
@@ -17815,7 +17964,36 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17815
17964
  // one description loaded into EVERY session by `anthropic/alwaysLoad`. The clauses
17816
17965
  // that went were the ones arguing for the rule rather than stating it; what a caller
17817
17966
  // needs to predict a refusal all stayed.
17818
- description: "Everything a session needs before starting work. Call it first: it alone returns the doctrine and your organization\u2019s writing rules in full. Pass `issue` or `project`, never both. An issue returns its detail plus each linked requirement\u2019s text, 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, others\u2019 claims and, with `session`, your other sessions\u2019 \u2014 capped at `availableLimit` beside the true `availableCount`. Read-only.",
17967
+ //
17968
+ // IT SITS AT THE 80-WORD CAP `plain-descriptions.test.ts` enforces, so a new clause
17969
+ // has to displace an old one. Four suites also demand named tokens in here:
17970
+ // `availableCount` inside a clause that states a cap, and `availableLimit`, from
17971
+ // `work-context-bounds.test.ts`; `done`, `killed` and an exclusion verb from the
17972
+ // e2e AC-9 spec.
17973
+ //
17974
+ // TWO CLAUSES THE SHAPE ITSELF PAID FOR. `item.issue` is no longer the whole issue
17975
+ // read: PIPE-480 moved the cross-cutting requirements onto `project` and the
17976
+ // conventions to the pack's top level, so a description that promised "its detail"
17977
+ // was sending a caller to read two lists off a key that no longer carries them —
17978
+ // `undefined.map`, not an empty list. And `available` arrives in SELECTION ORDER
17979
+ // (PIPE-483): a bounded page is only the right page if the ordering is the server's,
17980
+ // so a caller that re-sorts what it was handed re-sorts one page of the queue.
17981
+ //
17982
+ // THE COMMENT CAP IS A NUMBER AGAIN, AND THAT IS THE POINT. It read "capped at
17983
+ // `commentLimit`", which names a field of the RESPONSE as though it were an argument:
17984
+ // this schema is strict and accepts `issue`, `project`, `session`, `crossCutting` and
17985
+ // `availableLimit`, so a caller that tried to raise the cap was refused for an unknown
17986
+ // key. Interpolating {@link WORK_CONTEXT_COMMENT_LIMIT} says the size of the tail
17987
+ // without offering a knob that does not exist.
17988
+ //
17989
+ // IT AND THE CONVENTIONS CLAUSE WERE PAID FOR BY "active milestone first", which the
17990
+ // cap forced out. The conventions now sit "at the top level" rather than "beside it" —
17991
+ // "it" being the `project` block the clause had just named, so the old words said only
17992
+ // that the conventions were not quite where the sentence had been looking. What went
17993
+ // is the head of the selection order, which is the one clause here a caller cannot act
17994
+ // on differently: "in selection order" already tells it not to re-sort, and which
17995
+ // release leads the order changes nothing it does with the rows.
17996
+ description: "Call it first: it alone returns the doctrine and your organization\u2019s writing rules whole. Pass `issue` or `project`, never both. An issue returns its detail, linked requirements\u2019 text and the newest " + String(WORK_CONTEXT_COMMENT_LIMIT) + " comments (capped; `commentCount` the total); cross-cutting requirements ride on `project`, conventions at the top level. A project returns `available` in selection order, excluding done, killed, blocked, person-reserved, others\u2019 claims and, with `session`, your other sessions\u2019, capped at `availableLimit` beside the true `availableCount`. `crossCutting: keys` drops requirement bodies. Read-only.",
17819
17997
  inputSchema: getWorkContextInputSchema,
17820
17998
  // THE TWO ANNOTATIONS PIPE-D30 REQUIRES, and the reason each key is spelled out
17821
17999
  // rather than built: an unknown `_meta` key is IGNORED — no error, no warning — so a
@@ -17915,7 +18093,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17915
18093
  }
17916
18094
  }, ({ project, target, ...fields }) => run(async () => client2().updateDeliverableTarget(project, await resolveDeliverableTargetId(client2(), project, target), fields)));
17917
18095
  registerStrict("delete_deliverable_target", {
17918
- 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.",
18096
+ description: "Delete a deliverable target by slug or uuid. Only one with no deliverables can be deleted; one in use is rejected \u2014 deactivate it with `update_deliverable_target` instead.",
17919
18097
  inputSchema: {
17920
18098
  project: projectKeySchema,
17921
18099
  target: external_exports.string().min(1)
@@ -17928,8 +18106,15 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17928
18106
  registerStrict(
17929
18107
  "list_issues",
17930
18108
  {
17931
- 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, needsHeader and session. With no status filter the default is buildable work: " + OPEN_STATUSES.join(", ") + '. Terminal work is excluded until you name it: pass status ["done","killed"]. An explicit filter is used verbatim, never merged. needsHeader true returns issues whose header is missing or stale; session drops work another session holds.',
17932
- inputSchema: listIssuesFilterSchema.shape
18109
+ description: "List issue summaries: status, priority, milestone, claimer, criteria progress and the header summary. `detail: header` adds the reason and affected surfaces. " + String(ISSUE_LIST_LIMIT) + " `items` come back by default, beside the true `total` and the `next` offset. With no status filter the default is buildable work: " + OPEN_STATUSES.join(", ") + "; name `done` or `killed` for terminal work. An explicit filter is used verbatim. `session` drops work another session holds; `needsHeader: true` returns issues whose header is missing or stale.",
18110
+ inputSchema: listIssuesFilterSchema.shape,
18111
+ // The same result-threshold annotation `get_work_context` and `get_issue`
18112
+ // declare, spelled out the same way and for the same reason (an unknown `_meta`
18113
+ // key is silently ignored). A long list is where its absence BIT: 157 open issues
18114
+ // measured 161,128 bytes, past the client's own default output cap, so what
18115
+ // reached the session was a truncated list with nothing on it saying rows were
18116
+ // missing. No `alwaysLoad`: that stays the session-start call's alone.
18117
+ _meta: { "anthropic/maxResultSizeChars": WORK_CONTEXT_RESULT_CHARS }
17933
18118
  },
17934
18119
  // The default lives in the API now, not here (PIPE-D35, Listing issues
17935
18120
  // without a status returns only non-terminal work, and the default lives in
@@ -17941,10 +18126,17 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17941
18126
  // and an explicit filter, terminal values and all, is forwarded verbatim as
17942
18127
  // it always was. (The work page kept its terminal rows by naming every
17943
18128
  // status on its reads, so nothing over there leant on the defaultless API.)
17944
- (args) => run(() => client2().listIssues(args), GUIDANCE_POINTER)
18129
+ //
18130
+ // THE PAGE SIZE IS THE EXCEPTION (PIPE-484), injected here as an explicit `limit`
18131
+ // exactly as `get_issue`'s caps are and for the same reason: the raw HTTP route
18132
+ // keeps its 200 because the web reads there and passes its own limit, while every
18133
+ // response on THIS surface has to land whole. The envelope read — `items`, `total`,
18134
+ // `next` — is what makes a bounded page honest: an agent handed 50 of 157 rows can
18135
+ // see that it was handed 50 of 157.
18136
+ (args) => run(() => client2().listIssuesPage({ ...args, limit: args.limit ?? ISSUE_LIST_LIMIT }), GUIDANCE_POINTER)
17945
18137
  );
17946
18138
  registerStrict("get_issue", {
17947
- 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.",
18139
+ description: "Full issue detail by key or uuid: header, body, criteria, requirement links, cross-cutting requirements, approved conventions, children and comments. `terminal` is true for done or killed work; `projectArchived` for a retired project, which this read still answers under while lists and writes do not. `headerStale` is true when no header was written or the body moved. `crossCutting: keys` returns the cross-cutting requirements without bodies. Long records page with `bodyOffset` and `commentOffset`; `omitted` and `next` say what was left out.",
17948
18140
  inputSchema: getIssueInputSchema.shape,
17949
18141
  // The same result-threshold annotation `get_work_context` declares, and the same
17950
18142
  // key spelled out for the same reason (an unknown `_meta` key is silently
@@ -17954,7 +18146,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17954
18146
  // reintroduced by a default this server cannot see. No `alwaysLoad`: that stays
17955
18147
  // the session-start call's alone.
17956
18148
  _meta: { "anthropic/maxResultSizeChars": WORK_CONTEXT_RESULT_CHARS }
17957
- }, ({ issue: issue2, bodyOffset, bodyLimit, commentOffset, commentLimit }) => run(() => (
18149
+ }, ({ issue: issue2, bodyOffset, bodyLimit, commentOffset, commentLimit, crossCutting }) => run(() => (
17958
18150
  // THE AGENT-SURFACE DEFAULTS (PIPE-386), injected here as explicit wire
17959
18151
  // params — the ONE deliberate exception to this side's inject-no-default
17960
18152
  // rule (PIPE-D26), because the two surfaces genuinely differ by design: the
@@ -17964,34 +18156,42 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17964
18156
  // comment tail needs no offset — an omitted `commentOffset` beside a present
17965
18157
  // `commentLimit` IS the newest tail, resolved server-side, so no extra round
17966
18158
  // trip asks how many comments exist (PIPE-R15 rule 4).
18159
+ //
18160
+ // `crossCutting` (PIPE-482) is FORWARDED AND NOT DEFAULTED — the exception
18161
+ // above stays exactly as wide as it was. It exists because a body can grow
18162
+ // past the response ceiling and has to be bounded by somebody; the number of
18163
+ // documents a project flags cross-cutting cannot, so there is nothing here to
18164
+ // protect a caller from, and a default injected on this side would make the
18165
+ // agent surface answer a different question from the route it mirrors.
17967
18166
  client2().getIssue(issue2, {
17968
18167
  bodyOffset,
17969
18168
  bodyLimit: bodyLimit ?? ISSUE_BODY_CHAR_LIMIT,
17970
18169
  commentOffset,
17971
- commentLimit: commentLimit ?? ISSUE_COMMENT_LIMIT
18170
+ commentLimit: commentLimit ?? ISSUE_COMMENT_LIMIT,
18171
+ crossCutting
17972
18172
  })
17973
18173
  ), GUIDANCE_POINTER));
17974
18174
  registerStrict("create_issue", {
17975
- 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.",
18175
+ 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 \u201CSearch before you create\u201D in the doctrine `get_work_context` returns.",
17976
18176
  inputSchema: createIssueInputSchema.shape
17977
18177
  }, (args) => run(() => client2().createIssue(args)));
17978
18178
  registerStrict("promote_feedback", {
17979
- 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.",
18179
+ description: "Retype a feedback report as work that can be built: an epic or a task. Epics, tasks and bugs carry criteria; 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 \u201CThe bar for filing a bug\u201D in the doctrine `get_work_context` returns.",
17980
18180
  inputSchema: {
17981
18181
  issue: external_exports.string().min(1),
17982
18182
  ...promoteFeedbackInputSchema.shape
17983
18183
  }
17984
18184
  }, ({ issue: issue2, ...fields }) => run(() => client2().promoteFeedback(issue2, fields)));
17985
18185
  registerStrict("open_review", {
17986
- 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.",
18186
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
17987
18187
  inputSchema: openReviewInputSchema
17988
18188
  }, (args) => run(() => client2().openReview(args)));
17989
18189
  registerStrict("update_issue", {
17990
- 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.",
18190
+ 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 \u201CMost choices are yours\u201D in the doctrine `get_work_context` returns.",
17991
18191
  inputSchema: updateIssueInputSchema.shape
17992
18192
  }, ({ issue: issue2, ...fields }) => run(() => client2().updateIssue(issue2, fields)));
17993
18193
  registerStrict("set_issue_header", {
17994
- 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.",
18194
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
17995
18195
  inputSchema: setIssueHeaderInputSchema.shape
17996
18196
  }, ({ issue: issue2, ...fields }) => run(() => client2().setIssueHeader(issue2, fields)));
17997
18197
  registerStrict("set_status", {
@@ -17999,7 +18199,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
17999
18199
  inputSchema: setStatusInputSchema.shape
18000
18200
  }, (args) => run(() => client2().setStatus(args.issue, args.status)));
18001
18201
  registerStrict("claim_issue", {
18002
- description: "Claim an issue for this session and reserve where the work happens. It succeeds only if unclaimed; under a race one caller wins and the loser is told who holds it. `branch` and `worktree` are refused `placement_conflict` when another live claim in the project holds either, naming that issue. A field you omit keeps what the issue already recorded. Claiming is separate from status, and is refused `reserved_for_person` when an operator has kept the work for a person.",
18202
+ description: "Claim an issue for this session and reserve where the work happens. It succeeds only if unclaimed, and the loser of a race is told who holds it. `branch` and `worktree` are refused `placement_conflict` when another live claim in the project holds either, naming that issue. A field you omit keeps what the issue already recorded. Claiming is separate from status, and is refused `reserved_for_person` when an operator has kept the work for a person.",
18003
18203
  inputSchema: claimInputSchema.shape
18004
18204
  }, ({ issue: issue2, ...placement }) => run(() => client2().claimIssue(issue2, placement)));
18005
18205
  registerStrict("release_issue", {
@@ -18007,15 +18207,15 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18007
18207
  inputSchema: { issue: issueRefSchema }
18008
18208
  }, (args) => run(() => client2().releaseIssue(args.issue)));
18009
18209
  registerStrict("add_comment", {
18010
- 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.",
18210
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18011
18211
  inputSchema: commentInputSchema.shape
18012
18212
  }, (args) => run(() => client2().addComment(args.issue, args.body)));
18013
18213
  registerStrict("set_blocked", {
18014
- 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.",
18214
+ 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 what the work waits on; what you tried goes in a comment. See \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18015
18215
  inputSchema: setBlockedInputSchema.shape
18016
18216
  }, (args) => run(() => client2().setBlocked(args.issue, args.reason)));
18017
18217
  registerStrict("clear_blocked", {
18018
- 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.",
18218
+ 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 the receipt carries `blocked` as null.",
18019
18219
  inputSchema: { issue: issueRefSchema }
18020
18220
  }, (args) => run(() => client2().clearBlocked(args.issue)));
18021
18221
  registerStrict("link_blocker", {
@@ -18023,15 +18223,15 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18023
18223
  inputSchema: linkBlockerInputSchema.shape
18024
18224
  }, (args) => run(() => client2().addBlocker(args.issue, args.blocker)));
18025
18225
  registerStrict("unlink_blocker", {
18026
- 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.",
18226
+ description: "Remove the blocked-by edge between `issue` and `blocker`; one call clears both sides. Unlinking a pair that is not linked is a no-op, not an error.",
18027
18227
  inputSchema: linkBlockerInputSchema.shape
18028
18228
  }, (args) => run(() => client2().removeBlocker(args.issue, args.blocker)));
18029
18229
  registerStrict("set_criteria", {
18030
- 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.",
18230
+ description: "Define or replace an issue\u2019s acceptance criteria. 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. `verification` defaults to `manual`; a `manual` criterion needs `howTo`, else refused `how_to_required`; `howTo` on an `auto` criterion is refused by the schema. See \u201CChoosing auto or manual\u201D in the doctrine `get_work_context` returns.",
18031
18231
  inputSchema: setCriteriaInputSchema.shape
18032
18232
  }, (args) => run(() => client2().setCriteria(args.issue, args.criteria)));
18033
18233
  registerStrict("set_criterion_state", {
18034
- 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.",
18234
+ 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 \u201CChoosing auto or manual\u201D in the doctrine `get_work_context` returns.",
18035
18235
  inputSchema: {
18036
18236
  issue: issueRefSchema.optional(),
18037
18237
  criterionId: uuidSchema.optional(),
@@ -18054,19 +18254,19 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18054
18254
  });
18055
18255
  }));
18056
18256
  registerStrict("ask_questions", {
18057
- 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.",
18257
+ description: "Ask the operator questions on an issue. Send them in one call. 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 \u201CFile decisions as questions\u201D in the doctrine `get_work_context` returns.",
18058
18258
  inputSchema: askQuestionsInputSchema.shape
18059
18259
  }, (args) => run(() => client2().askQuestions(args.issue, args.questions)));
18060
18260
  registerStrict("withdraw_question", {
18061
- 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.",
18261
+ 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. Use it for a question that should never have been asked; one your own work settled deserves `answer_question`, which keeps the answer. The question is kept for the audit trail, its number retires, and later reads no longer carry it.",
18062
18262
  inputSchema: withdrawQuestionInputSchema.shape
18063
18263
  }, (args) => run(() => client2().withdrawQuestion(args.issue, args.number)));
18064
18264
  registerStrict("answer_question", {
18065
- 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.",
18265
+ description: "Answer a question by its `Q-{n}` number. Pass exactly one of `option` (a 1-based index into the choices offered) or `text`; both, or neither, is refused before anything is sent. Answering again revises, and 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`.",
18066
18266
  inputSchema: answerQuestionInputSchema
18067
18267
  }, (args) => run(() => client2().answerQuestion(args.issue, args.number, { option: args.option, text: args.text })));
18068
18268
  registerStrict("list_requirements", {
18069
- 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.",
18269
+ 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. Up to 200 by default; page with `limit`/`offset`.",
18070
18270
  inputSchema: listRequirementsFilterSchema.shape
18071
18271
  }, (args) => run(() => client2().listRequirements(args), GUIDANCE_POINTER));
18072
18272
  registerStrict("get_requirement", {
@@ -18074,11 +18274,11 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18074
18274
  inputSchema: { requirement: requirementRefSchema, revision: positiveIntSchema.optional() }
18075
18275
  }, (args) => run(() => client2().getRequirement(args.requirement, args.revision), GUIDANCE_POINTER));
18076
18276
  registerStrict("create_requirement", {
18077
- 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.",
18277
+ 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 \u201CSearch before you create\u201D in the doctrine `get_work_context` returns.",
18078
18278
  inputSchema: createRequirementInputSchema.shape
18079
18279
  }, (args) => run(() => client2().createRequirement(args)));
18080
18280
  registerStrict("update_requirement", {
18081
- 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.",
18281
+ 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 \u201CLink by key, never paste\u201D in the doctrine `get_work_context` returns.",
18082
18282
  // Inline shape (updateRequirementInputSchema is a refine, no .shape) — add
18083
18283
  // summary/area here explicitly (§210) and decisionLinks (PIPE-84 §6); the
18084
18284
  // empty-edit refine is enforced by the API, not this shape.
@@ -18098,7 +18298,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18098
18298
  inputSchema: approveRequirementInputSchema.shape
18099
18299
  }, (args) => run(() => client2().approveRequirement(args.requirement, args.revision)));
18100
18300
  registerStrict("list_conventions", {
18101
- 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.",
18301
+ description: "List this organization\u2019s conventions \u2014 standing guidance that binds every project here: 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.",
18102
18302
  inputSchema: listConventionsFilterSchema.shape
18103
18303
  }, (args) => run(() => client2().listConventions(args), GUIDANCE_POINTER));
18104
18304
  registerStrict("get_convention", {
@@ -18110,7 +18310,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18110
18310
  inputSchema: createConventionInputSchema.shape
18111
18311
  }, (args) => run(() => client2().createConvention(args)));
18112
18312
  registerStrict("update_convention", {
18113
- 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.",
18313
+ 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 \u201CConventions need a person\u2019s approval\u201D in the doctrine `get_work_context` returns.",
18114
18314
  // Inline shape (updateConventionInputSchema is a refine, so it has no `.shape`);
18115
18315
  // the empty-edit rule is enforced by the API, not by this shape.
18116
18316
  inputSchema: {
@@ -18125,7 +18325,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18125
18325
  inputSchema: listPromptsFilterSchema.shape
18126
18326
  }, (args) => run(() => client2().listPrompts(args), GUIDANCE_POINTER));
18127
18327
  registerStrict("get_prompt", {
18128
- 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.",
18328
+ description: "Get one prompt by display id (`PROMPT-{n}`) or uuid: its current text, which is the latest revision, plus every earlier one with who saved it, when and why.",
18129
18329
  inputSchema: { prompt: promptRefSchema }
18130
18330
  }, (args) => run(() => client2().getPrompt(args.prompt), GUIDANCE_POINTER));
18131
18331
  registerStrict("create_prompt", {
@@ -18133,7 +18333,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18133
18333
  inputSchema: createPromptInputSchema.shape
18134
18334
  }, (args) => run(() => client2().createPrompt(args)));
18135
18335
  registerStrict("update_prompt", {
18136
- 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.",
18336
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18137
18337
  // Inline shape (updatePromptInputSchema is a refine, so it has no `.shape`),
18138
18338
  // mirroring that schema's fields — it is the wording this copy follows, `.min(1)`
18139
18339
  // bounds included, so a caller learns a blank body is refused from the advertised
@@ -18148,7 +18348,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18148
18348
  }
18149
18349
  }, ({ prompt, ...fields }) => run(() => client2().updatePrompt(prompt, fields)));
18150
18350
  registerStrict("create_decision", {
18151
- 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.",
18351
+ 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 \u201CA decision record is written once\u201D in the doctrine `get_work_context` returns.",
18152
18352
  inputSchema: createDecisionInputSchema.shape
18153
18353
  }, (args) => run(() => client2().createDecision(args)));
18154
18354
  registerStrict("get_decision", {
@@ -18156,11 +18356,11 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18156
18356
  inputSchema: getDecisionInputSchema.shape
18157
18357
  }, (args) => run(() => client2().getDecision(args.decision), GUIDANCE_POINTER));
18158
18358
  registerStrict("list_decisions", {
18159
- 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.",
18359
+ 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. Up to 200 by default; page with `limit`/`offset`.",
18160
18360
  inputSchema: listDecisionsFilterSchema.shape
18161
18361
  }, (args) => run(() => client2().listDecisions(args), GUIDANCE_POINTER));
18162
18362
  registerStrict("update_decision", {
18163
- 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.",
18363
+ description: "Correct a decision record while it is still `proposed`: `title`, `body`, or both. An edit naming nothing to change is refused 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.",
18164
18364
  inputSchema: updateDecisionInputSchema
18165
18365
  }, (args) => run(() => client2().updateDecision(args.decision, {
18166
18366
  title: args.title,
@@ -18168,7 +18368,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18168
18368
  status: args.status
18169
18369
  })));
18170
18370
  registerStrict("accept_decision", {
18171
- 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.",
18371
+ 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`. A record that is already `accepted` or `superseded` is refused `decision_transition_not_allowed`, naming what it can still do.",
18172
18372
  inputSchema: { decision: decisionRefSchema }
18173
18373
  }, (args) => run(() => client2().updateDecision(args.decision, { status: "accepted" })));
18174
18374
  registerStrict("supersede_decision", {
@@ -18184,19 +18384,19 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18184
18384
  inputSchema: { content: contentRefSchema, revision: positiveIntSchema.optional() }
18185
18385
  }, (args) => run(() => client2().getContent(args.content, args.revision), GUIDANCE_POINTER));
18186
18386
  registerStrict("create_content", {
18187
- 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.",
18387
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18188
18388
  inputSchema: createContentInputSchema.shape
18189
18389
  }, (args) => run(() => client2().createContent(args)));
18190
18390
  registerStrict("update_content", {
18191
- 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.",
18391
+ 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 \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18192
18392
  inputSchema: updateContentInputSchema.shape
18193
18393
  }, ({ content, ...fields }) => run(() => client2().updateContent(content, fields)));
18194
18394
  registerStrict("create_deliverable", {
18195
- 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.",
18395
+ 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` with `detail: full`); any other slug is rejected. One deliverable per platform per draft; another wording is a revision (`update_deliverable`). See \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18196
18396
  inputSchema: createDeliverableInputSchema.shape
18197
18397
  }, ({ content, ...fields }) => run(() => client2().createDeliverable(content, fields)));
18198
18398
  registerStrict("update_deliverable", {
18199
- 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.",
18399
+ description: "Append a revision of the post text to a deliverable, addressed `DRAFT-KEY/platform`; `note` says why. The platform itself is immutable. See \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18200
18400
  inputSchema: updateDeliverableInputSchema.shape
18201
18401
  }, ({ deliverable, ...fields }) => run(() => client2().updateDeliverable(deliverable, fields)));
18202
18402
  registerStrict("record_post", {
@@ -18206,7 +18406,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18206
18406
  registerStrict("promote_deliverable", {
18207
18407
  // The paste exception is stated because without it an agent holding the doctrine
18208
18408
  // would refuse this tool's whole purpose.
18209
- 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.",
18409
+ description: "Create a NEW issue from a deliverable (`DRAFT-KEY/platform`) with a protected link, which cannot be removed, pinning its current revision. 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 \u201CLink by key, never paste\u201D in the doctrine `get_work_context` returns.",
18210
18410
  inputSchema: {
18211
18411
  deliverable: external_exports.string().min(1),
18212
18412
  title: external_exports.string().min(1),
@@ -18243,7 +18443,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18243
18443
  inputSchema: uploadAssetInputSchema.shape
18244
18444
  }, (args) => run(() => uploadAssetFromPath(client2(), apiBaseUrl, args)));
18245
18445
  registerStrict("download_asset", {
18246
- 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.",
18446
+ description: "Write an ACTIVE asset\u2019s bytes to a local file at `path`, created or overwritten. Addressed by asset uuid. The bytes are checked against the recorded sha256; the result is metadata and the path.",
18247
18447
  inputSchema: downloadAssetInputSchema.shape
18248
18448
  }, (args) => run(() => downloadAssetToPath(client2(), args)));
18249
18449
  }
@@ -18256,7 +18456,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18256
18456
  inputSchema: unlinkAssetInputSchema.shape
18257
18457
  }, (args) => run(() => client2().unlinkAsset(args.linkId)));
18258
18458
  registerStrict("set_secret", {
18259
- 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.",
18459
+ 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 \u201CA secret value never travels in a tool call\u201D in the doctrine `get_work_context` returns.",
18260
18460
  inputSchema: {
18261
18461
  project: projectKeySchema,
18262
18462
  environment: external_exports.string().min(1),
@@ -18290,7 +18490,7 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18290
18490
  }
18291
18491
  }, ({ project, environment, name, value }) => run(() => client2().rotateSecret(project, name, { env: environment, value })));
18292
18492
  registerStrict("update_secret_description", {
18293
- 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.",
18493
+ description: "Change a secret\u2019s description \u2014 metadata only: no new version, and nothing here returns a value. See \u201CHow to write here\u201D in the doctrine `get_work_context` returns.",
18294
18494
  inputSchema: {
18295
18495
  project: projectKeySchema,
18296
18496
  environment: external_exports.string().min(1),
@@ -18313,14 +18513,22 @@ function createMcpServer(baseClient, apiBaseUrl, orgInstructions2 = /* @__PURE__
18313
18513
  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."
18314
18514
  }, () => run(() => client2().listOrgInstructions(), GUIDANCE_POINTER));
18315
18515
  registerStrict("set_org_instruction", {
18316
- 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.",
18516
+ description: "Set one scope\u2019s organization-level agent instruction, replacing that scope\u2019s body whole; over 4000 characters 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. New sessions pick it up; running ones keep what they started with. See \u201CSetting an instruction replaces the whole body\u201D in the doctrine `get_work_context` returns.",
18317
18517
  inputSchema: setOrgInstructionInputSchema.shape
18318
18518
  }, (args) => run(() => client2().setOrgInstruction(args.scope, args.body)));
18319
18519
  registerStrict("delete_org_instruction", {
18320
- 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.",
18520
+ 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. New sessions pick up the change; running ones keep what they started with.",
18321
18521
  inputSchema: deleteOrgInstructionInputSchema.shape
18322
18522
  }, (args) => run(() => client2().deleteOrgInstruction(args.scope)));
18323
- return server2;
18523
+ const advertised = registry2.map(({ name, description, inputSchema, _meta }) => {
18524
+ const { $schema: _draft07, ...jsonSchema } = toJsonSchemaCompat(inputSchema, {
18525
+ strictUnions: true,
18526
+ pipeStrategy: "input"
18527
+ });
18528
+ return _meta === void 0 ? { name, description, inputSchema: jsonSchema } : { name, description, inputSchema: jsonSchema, _meta };
18529
+ });
18530
+ server2.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: advertised }));
18531
+ return { server: server2, registry: registry2 };
18324
18532
  }
18325
18533
 
18326
18534
  // src/main.ts
@@ -18386,7 +18594,7 @@ function loadMcpConfig() {
18386
18594
  }
18387
18595
 
18388
18596
  // src/version.ts
18389
- var VERSION = "0.53.1";
18597
+ var VERSION = "0.54.0";
18390
18598
 
18391
18599
  // src/main.ts
18392
18600
  async function scopedClient(base, org) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipebrain-mcp",
3
- "version": "0.53.1",
3
+ "version": "0.54.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",