pipebrain-mcp 0.37.2 → 0.38.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 +219 -12
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -357,12 +357,35 @@ var require_main = __commonJS({
357
357
  });
358
358
 
359
359
  // ../../libs/agent-shared/dist/agent-config.js
360
+ import { createHash } from "node:crypto";
360
361
  import { existsSync, readFileSync } from "node:fs";
361
362
  import { homedir } from "node:os";
362
363
  import { join } from "node:path";
363
364
 
364
365
  // ../../libs/agent-shared/dist/agent-config-manifest.js
365
- var AGENT_CONFIG_DIGEST = "898da8356dac55bd14531dcb5440aa3bd7bbcaa72e873d0fee4a102e3d718ee1";
366
+ var AGENT_CONFIG_ENTRIES = [
367
+ { target: "agents/blind-spot-pass.md", sha256: "564751a751f7896e89c67e0d20788aff873e599766a3c30360073ba74e4c6099" },
368
+ { target: "agents/code-verifier.md", sha256: "5a09f0f9d6b4c4d75068bfc5e314b3d1250de9328806e709d0c32d1369394124" },
369
+ { target: "agents/heavy-implementer.md", sha256: "7d3af78d4241aaa67c283db245dd4065af6e8f55efce54c2f8623c1f83879830" },
370
+ { target: "agents/spec-reviewer.md", sha256: "a46dc637006d1f810081046c57edcd731406727810ccc5e0b1c7d7b9b98be5ef" },
371
+ { target: "agents/surgical-editor.md", sha256: "c8b7fe2ec8d76c396c0c698f36b0523be430b31e4b73b902409479a52d42ea5c" },
372
+ { target: "pipebrain/hooks/block-bash4-features.js", sha256: "383b38169a1795d9da34aa1c939349b6a1f76bba1359d0af7fd14c11ea5da25f" },
373
+ { target: "pipebrain/hooks/block-browser-dialogs.js", sha256: "b4f16a249881e6bd3c8d7f270c955270047a0636ffa99b89ad409fefd44b2b2f" },
374
+ { target: "pipebrain/hooks/block-credential-echo.js", sha256: "a22ad6746158df0b6cc80039b82ab9a9ba269aab535d5c01b6c917223b10c15c" },
375
+ { target: "pipebrain/hooks/pre-commit-publication-fitness.js", sha256: "f2917cd369c03004a8d02410f695efa3400bcd4f2044d1d389e5ad2c7a9dae5f" },
376
+ { target: "pipebrain/hooks/pre-commit-secret-scan.js", sha256: "1bc634774184e66748b3152016e806ef974449e976920fd710732170fdb4ad6b" },
377
+ { target: "pipebrain/hooks/stop-orphaned-servers.js", sha256: "c3b847832c60605278e54ca1067c216bee073ab0a4bb046439b5f184b728792a" },
378
+ { target: "pipebrain/team.md", sha256: "341c72843253f594632f3c0762e3ee045bdb8703189b80c71d0ab58cbf07eb82" },
379
+ { target: "rules/react-tailwind.md", sha256: "80bb51b2d8fd5782b7c742526544e7841ee38aa575faf7a8c7b4217ba5731989" },
380
+ { target: "rules/shell.md", sha256: "499aac60b6419acb0bb362c0267b9399139dcceafb0eaf67702e40f3118b5c04" },
381
+ { target: "rules/typescript.md", sha256: "c55eab0c243e38cdf903ab50b1e9d6d621516b893e602e1abff9a999a14e72d4" },
382
+ { target: "skills/capture-correction/SKILL.md", sha256: "ebd2ee680a623f54a2a35b2ac75471902f30bd17013c34557fd4ae4a9add3f68" },
383
+ { target: "skills/pipebrain-secrets/SKILL.md", sha256: "1b873f0a12c1384fbb84d071c4f46554cbff22fc502223ad58acd29578912ca6" },
384
+ { target: "skills/pipebrain-work-session/SKILL.md", sha256: "1051dd142f61b8b4a3c4f8be11874a3d2541fd905763350ff3ef05e586c9bc8d" },
385
+ { target: "skills/pipebrain-writing/SKILL.md", sha256: "92422f4ab49a83f470625c62dce070d0cd1caf1212f1bb2668b1bbba5428c5b6" },
386
+ { target: "skills/pipebrain/SKILL.md", sha256: "d30b133f64edb5fbd018eac4f8cd7316cb02fef39c9b5d2f275b5b9c1b20b6a2" },
387
+ { target: "skills/requirements-altitude/SKILL.md", sha256: "05524877946cb1d25b32cac33f30617ba317207cd0862526e4cfbb944b18c181" }
388
+ ];
366
389
 
367
390
  // ../../libs/agent-shared/dist/agent-config.js
368
391
  function agentConfigDir(env = process.env) {
@@ -388,11 +411,18 @@ function readInstalledStamp(dir) {
388
411
  return null;
389
412
  }
390
413
  }
391
- function agentConfigStatus(dir, shipped = AGENT_CONFIG_DIGEST) {
392
- const stamp = readInstalledStamp(dir);
393
- if (stamp === null)
414
+ function agentConfigStatus(dir, entries = AGENT_CONFIG_ENTRIES) {
415
+ if (readInstalledStamp(dir) === null)
394
416
  return "absent";
395
- return stamp.configDigest === shipped ? "current" : "behind";
417
+ for (const entry of entries) {
418
+ const file2 = join(dir, entry.target);
419
+ if (!existsSync(file2))
420
+ return "behind";
421
+ if (createHash("sha256").update(readFileSync(file2)).digest("hex") !== entry.sha256) {
422
+ return "behind";
423
+ }
424
+ }
425
+ return "current";
396
426
  }
397
427
  var UPDATE_COMMAND = "ppb setup";
398
428
  function agentConfigNotice(status) {
@@ -400,7 +430,7 @@ function agentConfigNotice(status) {
400
430
  }
401
431
 
402
432
  // ../../libs/agent-shared/dist/asset-transfer.js
403
- import { createHash } from "node:crypto";
433
+ import { createHash as createHash2 } from "node:crypto";
404
434
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
405
435
  import { basename, extname } from "node:path";
406
436
  var MIME_BY_EXT = {
@@ -429,7 +459,7 @@ async function uploadAssetFromPath(client2, apiBaseUrl, input) {
429
459
  const bytes = readFileSync2(input.path);
430
460
  const filename = basename(input.path);
431
461
  const contentType = input.contentType ?? guessContentType(filename);
432
- const sha256 = createHash("sha256").update(bytes).digest("hex");
462
+ const sha256 = createHash2("sha256").update(bytes).digest("hex");
433
463
  const { asset, uploadUrl } = await client2.createAsset({
434
464
  project: input.project,
435
465
  filename,
@@ -459,7 +489,7 @@ async function downloadAssetToPath(client2, input) {
459
489
  throw new Error(`asset ${input.asset} is ${asset.status}, not active \u2014 no bytes to download.`);
460
490
  }
461
491
  const buf = Buffer.from(await client2.downloadAsset(input.asset));
462
- const got = createHash("sha256").update(buf).digest("hex");
492
+ const got = createHash2("sha256").update(buf).digest("hex");
463
493
  if (got !== asset.sha256) {
464
494
  throw new Error(`integrity check failed: downloaded sha256 ${got} != recorded ${asset.sha256}.`);
465
495
  }
@@ -15181,6 +15211,24 @@ function parseDeliverableDisplayId(value) {
15181
15211
  }
15182
15212
 
15183
15213
  // ../../libs/domain/dist/project-scope.js
15214
+ var SOURCE_TREE_DIRECTORIES = /* @__PURE__ */ new Set([
15215
+ "apps",
15216
+ "components",
15217
+ "db",
15218
+ "dist",
15219
+ "docs",
15220
+ "e2e",
15221
+ "lib",
15222
+ "libs",
15223
+ "migrations",
15224
+ "modules",
15225
+ "node_modules",
15226
+ "packages",
15227
+ "plugins",
15228
+ "routes",
15229
+ "scripts",
15230
+ "src"
15231
+ ]);
15184
15232
  var CODE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
15185
15233
  "cjs",
15186
15234
  "css",
@@ -15209,6 +15257,83 @@ var SOURCE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
15209
15257
  ...CODE_FILE_EXTENSIONS,
15210
15258
  ...DOCUMENT_FILE_EXTENSIONS
15211
15259
  ]);
15260
+ var PATH_RE = /(?:\.{0,2}[/\\])?[\w.-]+(?:[/\\][\w.-]*[\w-][\w.-]*)+/g;
15261
+ var SEPARATOR_RE = /[/\\]/;
15262
+ var ROOTED_RE = /^\.{0,2}[/\\]/;
15263
+ function trimTrailingPunctuation(value) {
15264
+ return value.replace(/[.,;:!?)\]]+$/, "");
15265
+ }
15266
+ function namesAFile(candidate, extensions) {
15267
+ const segments = candidate.split(SEPARATOR_RE);
15268
+ const last = segments[segments.length - 1];
15269
+ const dot = last.lastIndexOf(".");
15270
+ return dot > 0 && extensions.has(last.slice(dot + 1).toLowerCase());
15271
+ }
15272
+ function namesASourceFile(candidate) {
15273
+ return namesAFile(candidate, SOURCE_FILE_EXTENSIONS);
15274
+ }
15275
+ function isRepositoryPath(candidate) {
15276
+ if (ROOTED_RE.test(candidate))
15277
+ return true;
15278
+ if (candidate.split(SEPARATOR_RE).some((segment) => SOURCE_TREE_DIRECTORIES.has(segment.toLowerCase()))) {
15279
+ return true;
15280
+ }
15281
+ return namesASourceFile(candidate);
15282
+ }
15283
+ function findPath(source, accepts) {
15284
+ for (const raw of source.match(PATH_RE) ?? []) {
15285
+ const candidate = trimTrailingPunctuation(raw);
15286
+ if (accepts(candidate))
15287
+ return candidate;
15288
+ }
15289
+ return null;
15290
+ }
15291
+ function findRepositoryPath(source) {
15292
+ return findPath(source, isRepositoryPath);
15293
+ }
15294
+
15295
+ // ../../libs/domain/dist/code-shape.js
15296
+ var LINK_RE = /\w+:\/\/\S+/g;
15297
+ var BARE_URL_RE = /\b[\w-]+(?:\.[\w-]+)*\.[A-Za-z]{2,}\/\S*/g;
15298
+ var LINE_SUFFIX_RE = /(?:\.{0,2}[/\\])?[\w.-]+(?:[/\\][\w.-]+)*:\d+/g;
15299
+ var LINE_IN_WORDS_RE = /\b(?:at|on|see|near|around|from)\s+lines?\s+\d+\b/i;
15300
+ var BACKTICKED_RE = /`[^`]+`/;
15301
+ var CALL_RE = /\b[A-Za-z_$][\w$]*[A-Z][\w$]*\(\)/;
15302
+ function findLineSuffix(source) {
15303
+ for (const candidate of source.match(LINE_SUFFIX_RE) ?? []) {
15304
+ const token = candidate.slice(0, candidate.lastIndexOf(":"));
15305
+ if (isRepositoryPath(token))
15306
+ return candidate;
15307
+ }
15308
+ return null;
15309
+ }
15310
+ function findCodeShape(text) {
15311
+ const prose = text.replace(LINK_RE, " ").replace(BARE_URL_RE, " ");
15312
+ const path = findRepositoryPath(prose);
15313
+ if (path !== null)
15314
+ return { kind: "repository_path", match: path };
15315
+ const suffixed = findLineSuffix(prose);
15316
+ if (suffixed !== null)
15317
+ return { kind: "line_number", match: suffixed };
15318
+ const spelled = LINE_IN_WORDS_RE.exec(prose);
15319
+ if (spelled !== null)
15320
+ return { kind: "line_number", match: spelled[0] };
15321
+ const backticked = BACKTICKED_RE.exec(prose);
15322
+ if (backticked !== null)
15323
+ return { kind: "symbol_name", match: backticked[0] };
15324
+ const called = CALL_RE.exec(prose);
15325
+ if (called !== null)
15326
+ return { kind: "symbol_name", match: called[0] };
15327
+ return null;
15328
+ }
15329
+ var CODE_SHAPE_NAMES = {
15330
+ repository_path: "repository path",
15331
+ line_number: "line number",
15332
+ symbol_name: "symbol name"
15333
+ };
15334
+ function codeShapeMessage(field, hit) {
15335
+ return `${field} is read by someone without the code open \u2014 take out the ${CODE_SHAPE_NAMES[hit.kind]} \u201C${hit.match}\u201D and say it in plain words`;
15336
+ }
15212
15337
 
15213
15338
  // ../../libs/domain/dist/entities.js
15214
15339
  var organizationSchema = external_exports.object({
@@ -15330,6 +15455,22 @@ var issueSchema = external_exports.object({
15330
15455
  milestone: external_exports.string().nullable(),
15331
15456
  title: external_exports.string().min(1),
15332
15457
  body: markdownSchema,
15458
+ // The header (PIPE-373): what this work is, why it matters, and what it touches —
15459
+ // three fields written by `set_issue_header`, never by the authoring tools, and
15460
+ // carried on every surface that renders a display id.
15461
+ //
15462
+ // OPTIONAL RATHER THAN NULLABLE-REQUIRED, because a header is absent far more often
15463
+ // than it is empty: the columns arrived on a table already full of rows, and the wire
15464
+ // mappers drop an undefined field rather than sending an explicit null.
15465
+ summary: external_exports.string().nullable().optional(),
15466
+ why: external_exports.string().nullable().optional(),
15467
+ affectedSurfaces: external_exports.string().nullable().optional(),
15468
+ // DERIVED ON READ, never stored: true when the header was written from a body that
15469
+ // has since changed, and true as well when there is no header at all — one comparison
15470
+ // covers both, so "absent" and "drifted" cannot answer differently. The provenance
15471
+ // hash it is derived from (`header_body_sha`) is internal and never reaches the wire.
15472
+ headerStale: external_exports.boolean().optional(),
15473
+ headerDerivedAt: external_exports.iso.datetime({ offset: true }).nullable().optional(),
15333
15474
  parentId: uuidSchema.nullable(),
15334
15475
  claimedBy: uuidSchema.nullable(),
15335
15476
  source: sourceSchema,
@@ -15346,6 +15487,7 @@ var criterionFiledBugSchema = external_exports.object({
15346
15487
  projectKey: projectKeySchema,
15347
15488
  displayId: external_exports.string().min(1),
15348
15489
  title: external_exports.string().min(1),
15490
+ summary: external_exports.string().nullable().optional(),
15349
15491
  status: issueStatusSchema
15350
15492
  });
15351
15493
  var criterionSchema = external_exports.object({
@@ -15779,7 +15921,19 @@ var criterionInputSchema = external_exports.strictObject({
15779
15921
  message: "`howTo` applies to manual criteria only \u2014 an auto criterion is checked by a machine, so instructions on one have no reader",
15780
15922
  path: ["howTo"]
15781
15923
  });
15924
+ var refusedIssueHeaderFields = {
15925
+ summary: external_exports.never({
15926
+ error: "summary cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15927
+ }).optional().describe("Not writable here. The header summary is written by `set_issue_header`."),
15928
+ why: external_exports.never({
15929
+ error: "why cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15930
+ }).optional().describe("Not writable here. The header reason is written by `set_issue_header`."),
15931
+ affectedSurfaces: external_exports.never({
15932
+ error: "affected surfaces cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15933
+ }).optional().describe("Not writable here. The affected surfaces are written by `set_issue_header`.")
15934
+ };
15782
15935
  var createIssueInputSchema = external_exports.strictObject({
15936
+ ...refusedIssueHeaderFields,
15783
15937
  project: projectRefSchema,
15784
15938
  type: issueTypeSchema,
15785
15939
  title: external_exports.string().min(1, { message: "issue title must not be empty" }),
@@ -15805,6 +15959,9 @@ var createIssueInputSchema = external_exports.strictObject({
15805
15959
  criterionId: uuidSchema.optional()
15806
15960
  });
15807
15961
  var promoteFeedbackInputSchema = external_exports.strictObject({
15962
+ // Declared here as well as on create/update: this is a standalone strict object that
15963
+ // does NOT spread create's shape, and it funnels into `createIssue` (PIPE-373 AC-1).
15964
+ ...refusedIssueHeaderFields,
15808
15965
  type: external_exports.enum(["epic", "task"]),
15809
15966
  title: external_exports.string().min(1, { message: "issue title must not be empty" }),
15810
15967
  status: issueStatusSchema.optional(),
@@ -15828,6 +15985,7 @@ var decisionLinkChangesSchema = external_exports.strictObject({
15828
15985
  remove: external_exports.array(decisionRefSchema).optional()
15829
15986
  });
15830
15987
  var updateIssueInputSchema = external_exports.strictObject({
15988
+ ...refusedIssueHeaderFields,
15831
15989
  issue: issueRefSchema,
15832
15990
  title: external_exports.string().min(1, { message: "issue title must not be empty" }).optional(),
15833
15991
  body: markdownSchema.optional(),
@@ -15840,6 +15998,23 @@ var updateIssueInputSchema = external_exports.strictObject({
15840
15998
  deliverableLinks: deliverableLinkChangesSchema.optional(),
15841
15999
  decisionLinks: decisionLinkChangesSchema.optional()
15842
16000
  });
16001
+ var ISSUE_SUMMARY_MAX = 350;
16002
+ function inPlainWords(schema, field) {
16003
+ return schema.refine((value) => findCodeShape(value) === null, {
16004
+ error: (iss) => {
16005
+ const hit = findCodeShape(String(iss.input));
16006
+ return hit === null ? `${field} must be written in plain words` : codeShapeMessage(field, hit);
16007
+ }
16008
+ });
16009
+ }
16010
+ var setIssueHeaderInputSchema = external_exports.strictObject({
16011
+ issue: issueRefSchema,
16012
+ summary: inPlainWords(external_exports.string().min(1, { message: "summary must not be empty" }).max(ISSUE_SUMMARY_MAX, {
16013
+ error: (iss) => `summary must be at most ${ISSUE_SUMMARY_MAX} characters \u2014 received ${String(iss.input).length}`
16014
+ }), "summary"),
16015
+ why: inPlainWords(external_exports.string().min(1, { message: "why must not be empty \u2014 say why this matters" }), "why"),
16016
+ affectedSurfaces: inPlainWords(external_exports.string().min(1, { message: "affected surfaces must not be empty" }), "affected surfaces")
16017
+ });
15843
16018
  var setStatusInputSchema = external_exports.strictObject({
15844
16019
  issue: issueRefSchema,
15845
16020
  status: issueStatusSchema
@@ -16034,6 +16209,12 @@ var listIssuesFilterSchema = external_exports.strictObject({
16034
16209
  // query string and lets anything else fail HERE, out loud, because a filter the
16035
16210
  // server quietly dropped returns everything and reads like one that matched.
16036
16211
  blocked: external_exports.boolean().optional(),
16212
+ // Header-less or drifted work (PIPE-373 AC-9): `true` is everything whose header is
16213
+ // missing or no longer describes the body it was written from, which is the list
16214
+ // somebody sits down to write headers from. Omitted means both. A real boolean for
16215
+ // the same reason `blocked` is one — a filter the server quietly dropped returns
16216
+ // everything and reads exactly like one that matched everything.
16217
+ needsHeader: external_exports.boolean().optional(),
16037
16218
  // Pagination — server default 200, max 500 (§9); default applied server-side.
16038
16219
  limit: external_exports.number().int().min(1, { message: "limit must be at least 1" }).max(500, { message: "limit must be at most 500" }).optional(),
16039
16220
  offset: external_exports.number().int().nonnegative().optional()
@@ -16519,6 +16700,22 @@ var ApiClient = class _ApiClient {
16519
16700
  addComment(ref, body) {
16520
16701
  return this.request("POST", `/api/v1/issues/${enc(ref)}/comments`, { body: { body } });
16521
16702
  }
16703
+ /**
16704
+ * Writes the structured header onto an issue that already exists (PIPE-373).
16705
+ *
16706
+ * WHOLE VALUE, all three fields required — a stale header is replaced outright,
16707
+ * so there is nothing to merge and nothing to pin. The server stamps a hash of
16708
+ * the body the three texts were derived from at the same time, which is what
16709
+ * later reads compare against to say whether the header still describes the
16710
+ * issue; a partial write would leave that hash vouching for words nobody
16711
+ * rewrote.
16712
+ *
16713
+ * `issue` is dropped from the body because the ref rides the path, the way it
16714
+ * does on every other whole-value write here.
16715
+ */
16716
+ setIssueHeader(ref, input) {
16717
+ return this.request("PUT", `/api/v1/issues/${enc(ref)}/header`, { body: input });
16718
+ }
16522
16719
  /** Records the issue as blocked with a required reason (PIPE-99). */
16523
16720
  setBlocked(ref, reason) {
16524
16721
  return this.request("PUT", `/api/v1/issues/${enc(ref)}/blocked`, {
@@ -17487,7 +17684,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17487
17684
  server2.registerTool(
17488
17685
  "list_issues",
17489
17686
  {
17490
- description: `List issue summaries (display id, title, type, status, terminal, blocked, priority, milestone, claimer, criteria progress, updated_at) filtered by project, environment, status[], type[], claimedBy (me/unclaimed/uuid), priority[], milestone, text (title+body), parent, and blocked (\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. 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.`,
17687
+ description: `List issue summaries (display id, title, type, status, terminal, blocked, 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. 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.`,
17491
17688
  inputSchema: listIssuesFilterSchema.shape
17492
17689
  },
17493
17690
  // The default lives HERE, not in the API. PIPE-R15 r4 rule 5 makes the default
@@ -17501,7 +17698,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17501
17698
  (args) => run(() => client2.listIssues(args.status === void 0 ? { ...args, status: [...OPEN_STATUSES] } : args), GUIDANCE_POINTER)
17502
17699
  );
17503
17700
  server2.registerTool("get_issue", {
17504
- description: "Full issue detail by display id or uuid: 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. 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 \u2014 such a sentence is true when written and wrong later, and a terminal issue is never revisited to correct it, so the stale reading is what the next reader inherits.",
17701
+ 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. 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 \u2014 such a sentence is true when written and wrong later, and a terminal issue is never revisited to correct it, so the stale reading is what the next reader inherits.",
17505
17702
  inputSchema: { issue: issueRefSchema }
17506
17703
  }, (args) => run(() => client2.getIssue(args.issue), GUIDANCE_POINTER));
17507
17704
  server2.registerTool("create_issue", {
@@ -17527,6 +17724,10 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17527
17724
  description: "Update any subset of an issue\u2019s title, body, type, priority, milestone, environment, parent (self/cycle rejected), 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. Writing guidance for this tool lives in the product doctrine, which `get_work_context` returns \u2014 see \u201CLink by key; never paste another record\u2019s text\u201D and \u201CA decision you need is filed as a question, never written into a body\u201D.",
17528
17725
  inputSchema: updateIssueInputSchema.shape
17529
17726
  }, ({ issue: issue2, ...fields }) => run(() => client2.updateIssue(issue2, fields)));
17727
+ server2.registerTool("set_issue_header", {
17728
+ 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.",
17729
+ inputSchema: setIssueHeaderInputSchema.shape
17730
+ }, ({ issue: issue2, ...fields }) => run(() => client2.setIssueHeader(issue2, fields)));
17530
17731
  server2.registerTool("set_status", {
17531
17732
  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.",
17532
17733
  inputSchema: setStatusInputSchema.shape
@@ -17735,7 +17936,13 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17735
17936
  title: external_exports.string().min(1),
17736
17937
  type: issueTypeSchema,
17737
17938
  status: issueStatusSchema.optional(),
17738
- body: markdownSchema.optional()
17939
+ body: markdownSchema.optional(),
17940
+ // This tool CREATES an issue, so it is a door the header refusal has to cover
17941
+ // (PIPE-373 AC-1). The fields must be DECLARED here rather than left off: the
17942
+ // SDK rebuilds this raw shape as a non-strict object, so an undeclared key is
17943
+ // STRIPPED rather than refused — the call would succeed, the issue would be
17944
+ // created, and the author's three lines would be dropped with nothing said.
17945
+ ...refusedIssueHeaderFields
17739
17946
  }
17740
17947
  }, ({ deliverable, ...fields }) => run(() => client2.promoteDeliverable(deliverable, fields)));
17741
17948
  server2.registerTool("list_assets", {
@@ -17894,7 +18101,7 @@ function loadMcpConfig() {
17894
18101
  }
17895
18102
 
17896
18103
  // src/version.ts
17897
- var VERSION = "0.37.2";
18104
+ var VERSION = "0.38.0";
17898
18105
 
17899
18106
  // src/main.ts
17900
18107
  var config2 = loadMcpConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipebrain-mcp",
3
- "version": "0.37.2",
3
+ "version": "0.38.0",
4
4
  "description": "Pipebrain MCP server for Claude Code and Claude Desktop — issues, requirements, content, assets, and metadata-only secrets over the Pipebrain API.",
5
5
  "type": "module",
6
6
  "license": "MIT",