pipebrain-mcp 0.37.3 → 0.38.1

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 +185 -6
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -367,11 +367,12 @@ var AGENT_CONFIG_ENTRIES = [
367
367
  { target: "agents/blind-spot-pass.md", sha256: "564751a751f7896e89c67e0d20788aff873e599766a3c30360073ba74e4c6099" },
368
368
  { target: "agents/code-verifier.md", sha256: "5a09f0f9d6b4c4d75068bfc5e314b3d1250de9328806e709d0c32d1369394124" },
369
369
  { target: "agents/heavy-implementer.md", sha256: "7d3af78d4241aaa67c283db245dd4065af6e8f55efce54c2f8623c1f83879830" },
370
+ { target: "agents/issue-header-writer.md", sha256: "8809b2913c71d021e8fa6c72fe70e2d1db8019b90ed93d85786f5236552c7da6" },
370
371
  { target: "agents/spec-reviewer.md", sha256: "a46dc637006d1f810081046c57edcd731406727810ccc5e0b1c7d7b9b98be5ef" },
371
372
  { target: "agents/surgical-editor.md", sha256: "c8b7fe2ec8d76c396c0c698f36b0523be430b31e4b73b902409479a52d42ea5c" },
372
373
  { target: "pipebrain/hooks/block-bash4-features.js", sha256: "383b38169a1795d9da34aa1c939349b6a1f76bba1359d0af7fd14c11ea5da25f" },
373
374
  { target: "pipebrain/hooks/block-browser-dialogs.js", sha256: "b4f16a249881e6bd3c8d7f270c955270047a0636ffa99b89ad409fefd44b2b2f" },
374
- { target: "pipebrain/hooks/block-credential-echo.js", sha256: "a22ad6746158df0b6cc80039b82ab9a9ba269aab535d5c01b6c917223b10c15c" },
375
+ { target: "pipebrain/hooks/block-credential-echo.js", sha256: "80f95808715744c72347ddcfda7a858b193f5c2b113a3b5aac93435552193437" },
375
376
  { target: "pipebrain/hooks/pre-commit-publication-fitness.js", sha256: "f2917cd369c03004a8d02410f695efa3400bcd4f2044d1d389e5ad2c7a9dae5f" },
376
377
  { target: "pipebrain/hooks/pre-commit-secret-scan.js", sha256: "1bc634774184e66748b3152016e806ef974449e976920fd710732170fdb4ad6b" },
377
378
  { target: "pipebrain/hooks/stop-orphaned-servers.js", sha256: "c3b847832c60605278e54ca1067c216bee073ab0a4bb046439b5f184b728792a" },
@@ -380,9 +381,10 @@ var AGENT_CONFIG_ENTRIES = [
380
381
  { target: "rules/shell.md", sha256: "499aac60b6419acb0bb362c0267b9399139dcceafb0eaf67702e40f3118b5c04" },
381
382
  { target: "rules/typescript.md", sha256: "c55eab0c243e38cdf903ab50b1e9d6d621516b893e602e1abff9a999a14e72d4" },
382
383
  { target: "skills/capture-correction/SKILL.md", sha256: "ebd2ee680a623f54a2a35b2ac75471902f30bd17013c34557fd4ae4a9add3f68" },
384
+ { target: "skills/pipebrain-header-pass/SKILL.md", sha256: "10545e3c1c5215b031e1c76281e65cd2ecf7420b3f07a4221824591a8263c5fc" },
383
385
  { target: "skills/pipebrain-secrets/SKILL.md", sha256: "1b873f0a12c1384fbb84d071c4f46554cbff22fc502223ad58acd29578912ca6" },
384
386
  { target: "skills/pipebrain-work-session/SKILL.md", sha256: "1051dd142f61b8b4a3c4f8be11874a3d2541fd905763350ff3ef05e586c9bc8d" },
385
- { target: "skills/pipebrain-writing/SKILL.md", sha256: "92422f4ab49a83f470625c62dce070d0cd1caf1212f1bb2668b1bbba5428c5b6" },
387
+ { target: "skills/pipebrain-writing/SKILL.md", sha256: "9ab1a188215716907fbb54e03cfdef43bb378800713b000f498bff4ea75462f0" },
386
388
  { target: "skills/pipebrain/SKILL.md", sha256: "d30b133f64edb5fbd018eac4f8cd7316cb02fef39c9b5d2f275b5b9c1b20b6a2" },
387
389
  { target: "skills/requirements-altitude/SKILL.md", sha256: "05524877946cb1d25b32cac33f30617ba317207cd0862526e4cfbb944b18c181" }
388
390
  ];
@@ -15211,6 +15213,24 @@ function parseDeliverableDisplayId(value) {
15211
15213
  }
15212
15214
 
15213
15215
  // ../../libs/domain/dist/project-scope.js
15216
+ var SOURCE_TREE_DIRECTORIES = /* @__PURE__ */ new Set([
15217
+ "apps",
15218
+ "components",
15219
+ "db",
15220
+ "dist",
15221
+ "docs",
15222
+ "e2e",
15223
+ "lib",
15224
+ "libs",
15225
+ "migrations",
15226
+ "modules",
15227
+ "node_modules",
15228
+ "packages",
15229
+ "plugins",
15230
+ "routes",
15231
+ "scripts",
15232
+ "src"
15233
+ ]);
15214
15234
  var CODE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
15215
15235
  "cjs",
15216
15236
  "css",
@@ -15239,6 +15259,83 @@ var SOURCE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
15239
15259
  ...CODE_FILE_EXTENSIONS,
15240
15260
  ...DOCUMENT_FILE_EXTENSIONS
15241
15261
  ]);
15262
+ var PATH_RE = /(?:\.{0,2}[/\\])?[\w.-]+(?:[/\\][\w.-]*[\w-][\w.-]*)+/g;
15263
+ var SEPARATOR_RE = /[/\\]/;
15264
+ var ROOTED_RE = /^\.{0,2}[/\\]/;
15265
+ function trimTrailingPunctuation(value) {
15266
+ return value.replace(/[.,;:!?)\]]+$/, "");
15267
+ }
15268
+ function namesAFile(candidate, extensions) {
15269
+ const segments = candidate.split(SEPARATOR_RE);
15270
+ const last = segments[segments.length - 1];
15271
+ const dot = last.lastIndexOf(".");
15272
+ return dot > 0 && extensions.has(last.slice(dot + 1).toLowerCase());
15273
+ }
15274
+ function namesASourceFile(candidate) {
15275
+ return namesAFile(candidate, SOURCE_FILE_EXTENSIONS);
15276
+ }
15277
+ function isRepositoryPath(candidate) {
15278
+ if (ROOTED_RE.test(candidate))
15279
+ return true;
15280
+ if (candidate.split(SEPARATOR_RE).some((segment) => SOURCE_TREE_DIRECTORIES.has(segment.toLowerCase()))) {
15281
+ return true;
15282
+ }
15283
+ return namesASourceFile(candidate);
15284
+ }
15285
+ function findPath(source, accepts) {
15286
+ for (const raw of source.match(PATH_RE) ?? []) {
15287
+ const candidate = trimTrailingPunctuation(raw);
15288
+ if (accepts(candidate))
15289
+ return candidate;
15290
+ }
15291
+ return null;
15292
+ }
15293
+ function findRepositoryPath(source) {
15294
+ return findPath(source, isRepositoryPath);
15295
+ }
15296
+
15297
+ // ../../libs/domain/dist/code-shape.js
15298
+ var LINK_RE = /\w+:\/\/\S+/g;
15299
+ var BARE_URL_RE = /\b[\w-]+(?:\.[\w-]+)*\.[A-Za-z]{2,}\/\S*/g;
15300
+ var LINE_SUFFIX_RE = /(?:\.{0,2}[/\\])?[\w.-]+(?:[/\\][\w.-]+)*:\d+/g;
15301
+ var LINE_IN_WORDS_RE = /\b(?:at|on|see|near|around|from)\s+lines?\s+\d+\b/i;
15302
+ var BACKTICKED_RE = /`[^`]+`/;
15303
+ var CALL_RE = /\b[A-Za-z_$][\w$]*[A-Z][\w$]*\(\)/;
15304
+ function findLineSuffix(source) {
15305
+ for (const candidate of source.match(LINE_SUFFIX_RE) ?? []) {
15306
+ const token = candidate.slice(0, candidate.lastIndexOf(":"));
15307
+ if (isRepositoryPath(token))
15308
+ return candidate;
15309
+ }
15310
+ return null;
15311
+ }
15312
+ function findCodeShape(text) {
15313
+ const prose = text.replace(LINK_RE, " ").replace(BARE_URL_RE, " ");
15314
+ const path = findRepositoryPath(prose);
15315
+ if (path !== null)
15316
+ return { kind: "repository_path", match: path };
15317
+ const suffixed = findLineSuffix(prose);
15318
+ if (suffixed !== null)
15319
+ return { kind: "line_number", match: suffixed };
15320
+ const spelled = LINE_IN_WORDS_RE.exec(prose);
15321
+ if (spelled !== null)
15322
+ return { kind: "line_number", match: spelled[0] };
15323
+ const backticked = BACKTICKED_RE.exec(prose);
15324
+ if (backticked !== null)
15325
+ return { kind: "symbol_name", match: backticked[0] };
15326
+ const called = CALL_RE.exec(prose);
15327
+ if (called !== null)
15328
+ return { kind: "symbol_name", match: called[0] };
15329
+ return null;
15330
+ }
15331
+ var CODE_SHAPE_NAMES = {
15332
+ repository_path: "repository path",
15333
+ line_number: "line number",
15334
+ symbol_name: "symbol name"
15335
+ };
15336
+ function codeShapeMessage(field, hit) {
15337
+ 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`;
15338
+ }
15242
15339
 
15243
15340
  // ../../libs/domain/dist/entities.js
15244
15341
  var organizationSchema = external_exports.object({
@@ -15360,6 +15457,22 @@ var issueSchema = external_exports.object({
15360
15457
  milestone: external_exports.string().nullable(),
15361
15458
  title: external_exports.string().min(1),
15362
15459
  body: markdownSchema,
15460
+ // The header (PIPE-373): what this work is, why it matters, and what it touches —
15461
+ // three fields written by `set_issue_header`, never by the authoring tools, and
15462
+ // carried on every surface that renders a display id.
15463
+ //
15464
+ // OPTIONAL RATHER THAN NULLABLE-REQUIRED, because a header is absent far more often
15465
+ // than it is empty: the columns arrived on a table already full of rows, and the wire
15466
+ // mappers drop an undefined field rather than sending an explicit null.
15467
+ summary: external_exports.string().nullable().optional(),
15468
+ why: external_exports.string().nullable().optional(),
15469
+ affectedSurfaces: external_exports.string().nullable().optional(),
15470
+ // DERIVED ON READ, never stored: true when the header was written from a body that
15471
+ // has since changed, and true as well when there is no header at all — one comparison
15472
+ // covers both, so "absent" and "drifted" cannot answer differently. The provenance
15473
+ // hash it is derived from (`header_body_sha`) is internal and never reaches the wire.
15474
+ headerStale: external_exports.boolean().optional(),
15475
+ headerDerivedAt: external_exports.iso.datetime({ offset: true }).nullable().optional(),
15363
15476
  parentId: uuidSchema.nullable(),
15364
15477
  claimedBy: uuidSchema.nullable(),
15365
15478
  source: sourceSchema,
@@ -15376,6 +15489,7 @@ var criterionFiledBugSchema = external_exports.object({
15376
15489
  projectKey: projectKeySchema,
15377
15490
  displayId: external_exports.string().min(1),
15378
15491
  title: external_exports.string().min(1),
15492
+ summary: external_exports.string().nullable().optional(),
15379
15493
  status: issueStatusSchema
15380
15494
  });
15381
15495
  var criterionSchema = external_exports.object({
@@ -15809,7 +15923,19 @@ var criterionInputSchema = external_exports.strictObject({
15809
15923
  message: "`howTo` applies to manual criteria only \u2014 an auto criterion is checked by a machine, so instructions on one have no reader",
15810
15924
  path: ["howTo"]
15811
15925
  });
15926
+ var refusedIssueHeaderFields = {
15927
+ summary: external_exports.never({
15928
+ error: "summary cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15929
+ }).optional().describe("Not writable here. The header summary is written by `set_issue_header`."),
15930
+ why: external_exports.never({
15931
+ error: "why cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15932
+ }).optional().describe("Not writable here. The header reason is written by `set_issue_header`."),
15933
+ affectedSurfaces: external_exports.never({
15934
+ error: "affected surfaces cannot be set here \u2014 create or update the issue first, then write its header with set_issue_header"
15935
+ }).optional().describe("Not writable here. The affected surfaces are written by `set_issue_header`.")
15936
+ };
15812
15937
  var createIssueInputSchema = external_exports.strictObject({
15938
+ ...refusedIssueHeaderFields,
15813
15939
  project: projectRefSchema,
15814
15940
  type: issueTypeSchema,
15815
15941
  title: external_exports.string().min(1, { message: "issue title must not be empty" }),
@@ -15835,6 +15961,9 @@ var createIssueInputSchema = external_exports.strictObject({
15835
15961
  criterionId: uuidSchema.optional()
15836
15962
  });
15837
15963
  var promoteFeedbackInputSchema = external_exports.strictObject({
15964
+ // Declared here as well as on create/update: this is a standalone strict object that
15965
+ // does NOT spread create's shape, and it funnels into `createIssue` (PIPE-373 AC-1).
15966
+ ...refusedIssueHeaderFields,
15838
15967
  type: external_exports.enum(["epic", "task"]),
15839
15968
  title: external_exports.string().min(1, { message: "issue title must not be empty" }),
15840
15969
  status: issueStatusSchema.optional(),
@@ -15858,6 +15987,7 @@ var decisionLinkChangesSchema = external_exports.strictObject({
15858
15987
  remove: external_exports.array(decisionRefSchema).optional()
15859
15988
  });
15860
15989
  var updateIssueInputSchema = external_exports.strictObject({
15990
+ ...refusedIssueHeaderFields,
15861
15991
  issue: issueRefSchema,
15862
15992
  title: external_exports.string().min(1, { message: "issue title must not be empty" }).optional(),
15863
15993
  body: markdownSchema.optional(),
@@ -15870,6 +16000,23 @@ var updateIssueInputSchema = external_exports.strictObject({
15870
16000
  deliverableLinks: deliverableLinkChangesSchema.optional(),
15871
16001
  decisionLinks: decisionLinkChangesSchema.optional()
15872
16002
  });
16003
+ var ISSUE_SUMMARY_MAX = 350;
16004
+ function inPlainWords(schema, field) {
16005
+ return schema.refine((value) => findCodeShape(value) === null, {
16006
+ error: (iss) => {
16007
+ const hit = findCodeShape(String(iss.input));
16008
+ return hit === null ? `${field} must be written in plain words` : codeShapeMessage(field, hit);
16009
+ }
16010
+ });
16011
+ }
16012
+ var setIssueHeaderInputSchema = external_exports.strictObject({
16013
+ issue: issueRefSchema,
16014
+ summary: inPlainWords(external_exports.string().min(1, { message: "summary must not be empty" }).max(ISSUE_SUMMARY_MAX, {
16015
+ error: (iss) => `summary must be at most ${ISSUE_SUMMARY_MAX} characters \u2014 received ${String(iss.input).length}`
16016
+ }), "summary"),
16017
+ why: inPlainWords(external_exports.string().min(1, { message: "why must not be empty \u2014 say why this matters" }), "why"),
16018
+ affectedSurfaces: inPlainWords(external_exports.string().min(1, { message: "affected surfaces must not be empty" }), "affected surfaces")
16019
+ });
15873
16020
  var setStatusInputSchema = external_exports.strictObject({
15874
16021
  issue: issueRefSchema,
15875
16022
  status: issueStatusSchema
@@ -16064,6 +16211,12 @@ var listIssuesFilterSchema = external_exports.strictObject({
16064
16211
  // query string and lets anything else fail HERE, out loud, because a filter the
16065
16212
  // server quietly dropped returns everything and reads like one that matched.
16066
16213
  blocked: external_exports.boolean().optional(),
16214
+ // Header-less or drifted work (PIPE-373 AC-9): `true` is everything whose header is
16215
+ // missing or no longer describes the body it was written from, which is the list
16216
+ // somebody sits down to write headers from. Omitted means both. A real boolean for
16217
+ // the same reason `blocked` is one — a filter the server quietly dropped returns
16218
+ // everything and reads exactly like one that matched everything.
16219
+ needsHeader: external_exports.boolean().optional(),
16067
16220
  // Pagination — server default 200, max 500 (§9); default applied server-side.
16068
16221
  limit: external_exports.number().int().min(1, { message: "limit must be at least 1" }).max(500, { message: "limit must be at most 500" }).optional(),
16069
16222
  offset: external_exports.number().int().nonnegative().optional()
@@ -16549,6 +16702,22 @@ var ApiClient = class _ApiClient {
16549
16702
  addComment(ref, body) {
16550
16703
  return this.request("POST", `/api/v1/issues/${enc(ref)}/comments`, { body: { body } });
16551
16704
  }
16705
+ /**
16706
+ * Writes the structured header onto an issue that already exists (PIPE-373).
16707
+ *
16708
+ * WHOLE VALUE, all three fields required — a stale header is replaced outright,
16709
+ * so there is nothing to merge and nothing to pin. The server stamps a hash of
16710
+ * the body the three texts were derived from at the same time, which is what
16711
+ * later reads compare against to say whether the header still describes the
16712
+ * issue; a partial write would leave that hash vouching for words nobody
16713
+ * rewrote.
16714
+ *
16715
+ * `issue` is dropped from the body because the ref rides the path, the way it
16716
+ * does on every other whole-value write here.
16717
+ */
16718
+ setIssueHeader(ref, input) {
16719
+ return this.request("PUT", `/api/v1/issues/${enc(ref)}/header`, { body: input });
16720
+ }
16552
16721
  /** Records the issue as blocked with a required reason (PIPE-99). */
16553
16722
  setBlocked(ref, reason) {
16554
16723
  return this.request("PUT", `/api/v1/issues/${enc(ref)}/blocked`, {
@@ -17517,7 +17686,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17517
17686
  server2.registerTool(
17518
17687
  "list_issues",
17519
17688
  {
17520
- 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.`,
17689
+ 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.`,
17521
17690
  inputSchema: listIssuesFilterSchema.shape
17522
17691
  },
17523
17692
  // The default lives HERE, not in the API. PIPE-R15 r4 rule 5 makes the default
@@ -17531,7 +17700,7 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17531
17700
  (args) => run(() => client2.listIssues(args.status === void 0 ? { ...args, status: [...OPEN_STATUSES] } : args), GUIDANCE_POINTER)
17532
17701
  );
17533
17702
  server2.registerTool("get_issue", {
17534
- 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.",
17703
+ 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.",
17535
17704
  inputSchema: { issue: issueRefSchema }
17536
17705
  }, (args) => run(() => client2.getIssue(args.issue), GUIDANCE_POINTER));
17537
17706
  server2.registerTool("create_issue", {
@@ -17557,6 +17726,10 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17557
17726
  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.",
17558
17727
  inputSchema: updateIssueInputSchema.shape
17559
17728
  }, ({ issue: issue2, ...fields }) => run(() => client2.updateIssue(issue2, fields)));
17729
+ server2.registerTool("set_issue_header", {
17730
+ 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.",
17731
+ inputSchema: setIssueHeaderInputSchema.shape
17732
+ }, ({ issue: issue2, ...fields }) => run(() => client2.setIssueHeader(issue2, fields)));
17560
17733
  server2.registerTool("set_status", {
17561
17734
  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.",
17562
17735
  inputSchema: setStatusInputSchema.shape
@@ -17765,7 +17938,13 @@ function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */
17765
17938
  title: external_exports.string().min(1),
17766
17939
  type: issueTypeSchema,
17767
17940
  status: issueStatusSchema.optional(),
17768
- body: markdownSchema.optional()
17941
+ body: markdownSchema.optional(),
17942
+ // This tool CREATES an issue, so it is a door the header refusal has to cover
17943
+ // (PIPE-373 AC-1). The fields must be DECLARED here rather than left off: the
17944
+ // SDK rebuilds this raw shape as a non-strict object, so an undeclared key is
17945
+ // STRIPPED rather than refused — the call would succeed, the issue would be
17946
+ // created, and the author's three lines would be dropped with nothing said.
17947
+ ...refusedIssueHeaderFields
17769
17948
  }
17770
17949
  }, ({ deliverable, ...fields }) => run(() => client2.promoteDeliverable(deliverable, fields)));
17771
17950
  server2.registerTool("list_assets", {
@@ -17924,7 +18103,7 @@ function loadMcpConfig() {
17924
18103
  }
17925
18104
 
17926
18105
  // src/version.ts
17927
- var VERSION = "0.37.3";
18106
+ var VERSION = "0.38.1";
17928
18107
 
17929
18108
  // src/main.ts
17930
18109
  var config2 = loadMcpConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipebrain-mcp",
3
- "version": "0.37.3",
3
+ "version": "0.38.1",
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",