pipebrain-mcp 0.29.0 → 0.31.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 +128 -151
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -429,6 +429,9 @@ function apiBindingLabel(apiBaseUrl) {
429
429
  return /\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/.test(apiBaseUrl) ? "LOCAL dev" : "production/remote";
430
430
  }
431
431
 
432
+ // ../../libs/agent-shared/dist/doctrine.js
433
+ var DOCTRINE = "# Pipebrain doctrine\n\nHow an agent works inside Pipebrain. These rules ship with the product and read the same in every organization, because each exists where disagreeing breaks something the product guarantees rather than merely changing house style.\n\nHow your organization *writes* is its own text, edited in Settings, and follows this document under a heading naming each scope.\n\n## Decisions are yours; escalate on consequence\n\nInside an approved requirement you choose the design, the libraries, the structure, and the sequencing. Do not ask, report, or seek ratification for the choice. Escalate on consequence, not uncertainty: a choice that changes what a user sees, costs money, or contradicts an approved requirement is filed as a question with the options you propose; everything else you decide and carry on. A technical choice worth keeping is a decision record, never a paragraph.\n\n## Every loose end has one destination, and none of them is prose\n\nAnything you notice that is not the work in front of you goes to exactly one of three places: a question, when the operator has to choose; a new issue at triage, when it is real work outside this item's scope; or nowhere. Nowhere is the default, and the right outcome whenever knowing the thing would change nothing anyone does. Never open a section called notes, caveats, observations, things to know, or next steps in a body or a comment. Filing is the report: an item recorded and then repeated in prose asks the operator to triage it twice.\n\n## A decision you need is filed as a question, never written into a body\n\nAn open decision belongs in a question on the issue, filed with `ask_questions`, carrying the options you propose. It does not belong in a body \u2014 not under a heading, and not in a field. A decision written into prose prompts nobody and records no answer, so the operator has no place to make it and a later session cannot tell whether it ever was. Retract one your own work has since settled, so nothing asks the operator to decide something nobody waits on.\n\n## Search before you create\n\nLook for the existing record before adding one. A duplicate splits one thing's history across two identifiers, and the split is permanent \u2014 nothing merges them afterward.\n\n## Link by key; never paste another record's text\n\nReference a requirement, a decision, or an issue by its key, with its title on first mention. Never paste its text into a body. The source changes and the copy does not, and the stale copy is what gets built.\n\n## Cross-cutting requirements arrive on their own\n\nThey bind every epic automatically and come back with a work-item read, at their latest approved revision. Never restate them in a body and never add them as explicit requirement links. An explicit link pins a document at an approved revision, which is a different act.\n\n## Verification modes follow what can be driven\n\nBefore assigning a mode, establish what this project can drive end to end: a harness, and the command that runs it. A criterion is `auto` when an evaluator can drive the input and read the result against the running system, including behavior shown in an interface wherever such a harness exists. What qualifies it is the evaluator's own run, never the existence of a test the implementer wrote.\n\nMark `manual` only when the verdict needs a person's judgment. A criterion you cannot describe as drivable is usually worded too vaguely: reword it against the observable rather than fall back to a person. A `manual` criterion carries instructions for whoever checks it by hand, written for someone who has not seen the code or the conversation that produced it.\n\nA manual criterion carrying no recorded artifact is the human gate working as designed rather than a coverage gap, so it is neither a finding nor weaker than one carrying evidence.\n\n## The bar for filing a bug\n\nFile a bug only when the observed behavior breaks an approved requirement or a defined acceptance criterion, and name which one before you file. Failing that, it is not a bug: code you find unclear, an edge case nobody specified, a performance number nobody set, and anything you would phrase as \"this could be better\" are opinions about the product, so file each as feedback or not at all. The bar exists because a queue holding both real defects and unshared taste has to be read entirely before any of it can be trusted.\n\n## A decision record is written once\n\nIt states what was decided and why, at the time it was decided, and is not edited as understanding moves. A superseding record replaces it, and the chain of the two is the account of how the thinking changed \u2014 which an edited record destroys. Read the existing records before re-opening a settled question.\n\n## A secret value never travels in a tool call\n\nNo tool here returns one, and that is deliberate rather than an omission to be fixed. Nor should you put one in: a value typed into a tool call is in the transcript before the server can encrypt it, so it is already disclosed and the only remedy left is rotation. Set values with the command-line tool, from a file or on standard input, and inject them at run time rather than reading them into context.\n\n## Setting an instruction replaces the whole body\n\nThere is no partial update. Writing an instruction stores exactly the body you supply and discards everything that was there, so a partial write destroys the rest of the text. The procedure is: read the live value, compose the complete replacement, write it, then read the live value again to confirm what landed.\n\nWhat you are reading now is a snapshot taken when this session started, and so are the tool descriptions. An edit reaches the next session, never this one, so reading the text back after a write shows the pre-edit value. An agent that reports a diff of what it just wrote has read the snapshot instead of the record.\n";
434
+
432
435
  // ../../libs/agent-shared/dist/resolve-agent-env.js
433
436
  function pick(env, primary, legacy) {
434
437
  const fromPrimary = env[primary];
@@ -15098,6 +15101,13 @@ var DisplayIdError = class extends Error {
15098
15101
  };
15099
15102
  var PROJECT_KEY = /^[A-Z]{2,5}$/;
15100
15103
  var DELIVERABLE_RE = /^([A-Z]{2,5})-C([1-9][0-9]*)\/([a-z0-9]+)$/;
15104
+ var INT4_MAX = 2147483647;
15105
+ function matchDisplayId(re, value, numberGroup) {
15106
+ const match = re.exec(value);
15107
+ if (match === null)
15108
+ return null;
15109
+ return Number(match[numberGroup]) > INT4_MAX ? null : match;
15110
+ }
15101
15111
  function assertKey(key) {
15102
15112
  if (!PROJECT_KEY.test(key)) {
15103
15113
  throw new DisplayIdError("project key must be 2\u20135 uppercase letters (A\u2013Z)", key);
@@ -15114,7 +15124,7 @@ function formatContentDisplayId(key, number4) {
15114
15124
  return `${key}-C${number4}`;
15115
15125
  }
15116
15126
  function parseDeliverableDisplayId(value) {
15117
- const match = DELIVERABLE_RE.exec(value);
15127
+ const match = matchDisplayId(DELIVERABLE_RE, value, 2);
15118
15128
  if (match === null) {
15119
15129
  throw new DisplayIdError("not a valid deliverable display id (expected `KEY-C{N}/{platform}`)", value);
15120
15130
  }
@@ -15585,6 +15595,9 @@ var orgInstructionSchema = external_exports.object({
15585
15595
  });
15586
15596
 
15587
15597
  // ../../libs/domain/dist/inputs.js
15598
+ var int4Schema = positiveIntSchema.max(2147483647, {
15599
+ message: "must be no greater than 2147483647"
15600
+ });
15588
15601
  var criterionInputSchema = external_exports.strictObject({
15589
15602
  text: markdownSchema.min(1, { message: "criterion text must not be empty" }),
15590
15603
  verification: verificationSchema.optional(),
@@ -15699,7 +15712,7 @@ var setCriteriaInputSchema = external_exports.strictObject({
15699
15712
  var setCriterionStateInputSchema = external_exports.strictObject({
15700
15713
  criterionId: uuidSchema.optional(),
15701
15714
  issue: issueRefSchema.optional(),
15702
- criterionNumber: positiveIntSchema.optional(),
15715
+ criterionNumber: int4Schema.optional(),
15703
15716
  state: criterionStateSchema,
15704
15717
  evidence: markdownSchema.optional()
15705
15718
  }).refine((v) => {
@@ -15718,9 +15731,7 @@ var askQuestionsInputSchema = external_exports.strictObject({
15718
15731
  issue: issueRefSchema,
15719
15732
  questions: external_exports.array(questionInputSchema).min(1, { message: "ask at least one question" })
15720
15733
  });
15721
- var questionNumberSchema = positiveIntSchema.max(2147483647, {
15722
- message: "question number is out of range"
15723
- });
15734
+ var questionNumberSchema = int4Schema;
15724
15735
  var withdrawQuestionInputSchema = external_exports.strictObject({
15725
15736
  issue: issueRefSchema,
15726
15737
  number: questionNumberSchema
@@ -15755,7 +15766,8 @@ var updateRequirementInputSchema = external_exports.strictObject({
15755
15766
  });
15756
15767
  var approveRequirementInputSchema = external_exports.strictObject({
15757
15768
  requirement: requirementRefSchema,
15758
- revision: positiveIntSchema.optional()
15769
+ // → requirement_revisions.revision_number, an `integer` column (PIPE-149).
15770
+ revision: int4Schema.optional()
15759
15771
  });
15760
15772
  var createDecisionInputSchema = external_exports.strictObject({
15761
15773
  project: projectRefSchema,
@@ -15879,7 +15891,8 @@ var updateSecretDescriptionInputSchema = external_exports.strictObject({
15879
15891
  });
15880
15892
  var rollbackSecretInputSchema = external_exports.strictObject({
15881
15893
  env: external_exports.string().min(1, { message: "environment must not be empty" }),
15882
- version: positiveIntSchema
15894
+ // → secret_versions.version, an `integer` column (PIPE-149).
15895
+ version: int4Schema
15883
15896
  });
15884
15897
  var secretRefSchema = external_exports.strictObject({
15885
15898
  project: projectKeySchema,
@@ -15999,7 +16012,8 @@ var listAssetsFilterSchema = external_exports.strictObject({
15999
16012
  var createChatMessageReferenceInputSchema = external_exports.strictObject({
16000
16013
  entityType: chatEntityTypeSchema,
16001
16014
  entityId: uuidSchema,
16002
- revision: positiveIntSchema.optional()
16015
+ // → requirement_revisions.revision_number, an `integer` column (PIPE-149).
16016
+ revision: int4Schema.optional()
16003
16017
  });
16004
16018
  var createChatMessageInputSchema = external_exports.strictObject({
16005
16019
  body: external_exports.string().min(1, { message: "message body must not be empty" }).max(8e3, { message: "message body must be at most 8000 characters" }),
@@ -16029,32 +16043,6 @@ var deleteOrgInstructionInputSchema = external_exports.strictObject({
16029
16043
  scope: instructionScopeSchema
16030
16044
  });
16031
16045
 
16032
- // ../../libs/domain/dist/instructions.js
16033
- var ISSUE_WRITE_TOOLS = [
16034
- "create_issue",
16035
- "update_issue",
16036
- "promote_feedback",
16037
- "open_review"
16038
- ];
16039
- var INSTRUCTION_SCOPE_TARGETS = {
16040
- general: { kind: "server" },
16041
- requirements: { kind: "tools", tools: ["create_requirement", "update_requirement"] },
16042
- content: {
16043
- kind: "tools",
16044
- tools: ["create_content", "update_content", "create_deliverable", "update_deliverable"]
16045
- },
16046
- "work.epic": { kind: "issue_tools", issueType: "epic", tools: ISSUE_WRITE_TOOLS },
16047
- "work.task": { kind: "issue_tools", issueType: "task", tools: ISSUE_WRITE_TOOLS },
16048
- "work.bug": { kind: "issue_tools", issueType: "bug", tools: ISSUE_WRITE_TOOLS },
16049
- "work.feedback": { kind: "issue_tools", issueType: "feedback", tools: ISSUE_WRITE_TOOLS }
16050
- };
16051
- function scopesForTool(tool) {
16052
- return instructionScopeSchema.options.filter((scope) => {
16053
- const target = INSTRUCTION_SCOPE_TARGETS[scope];
16054
- return target.kind !== "server" && target.tools.includes(tool);
16055
- });
16056
- }
16057
-
16058
16046
  // ../../libs/agent-shared/dist/token-shape.js
16059
16047
  function isAcceptedTokenShape(value) {
16060
16048
  return ACCEPTED_TOKEN_PREFIXES.some((prefix) => value.startsWith(prefix));
@@ -16889,7 +16877,7 @@ async function fetchOrgInstructionsSafe(client2) {
16889
16877
  return map2;
16890
16878
  } catch (err) {
16891
16879
  const message = err instanceof Error ? err.message : String(err);
16892
- process.stderr.write(`pipebrain mcp: org instructions unavailable (${message}) \u2014 starting with base tool descriptions
16880
+ process.stderr.write(`pipebrain mcp: org instructions unavailable (${message}) \u2014 starting with the doctrine alone
16893
16881
  `);
16894
16882
  return /* @__PURE__ */ new Map();
16895
16883
  } finally {
@@ -16897,34 +16885,17 @@ async function fetchOrgInstructionsSafe(client2) {
16897
16885
  clearTimeout(timer);
16898
16886
  }
16899
16887
  }
16900
- function decorateDescription(toolName, base, instructions) {
16901
- let out = base;
16902
- const kindLines = [];
16903
- for (const scope of scopesForTool(toolName)) {
16888
+ function composeGuidance(base, instructions) {
16889
+ const blocks = [base, DOCTRINE.trim()];
16890
+ for (const scope of instructionScopeSchema.options) {
16904
16891
  const body = instructions.get(scope);
16905
16892
  if (body === void 0)
16906
16893
  continue;
16907
- const target = INSTRUCTION_SCOPE_TARGETS[scope];
16908
- if (target.kind === "issue_tools") {
16909
- kindLines.push(`[${target.issueType}] ${body}`);
16910
- } else {
16911
- out += `
16894
+ blocks.push(`## Organization instructions \u2014 ${scope}
16912
16895
 
16913
- Org instructions (${scope}):
16914
- ${body}`;
16915
- }
16916
- }
16917
- if (kindLines.length > 0) {
16918
- out += "\n\nOrg instructions by issue `type` \u2014 apply ONLY the block matching the `type` you pass:\n\n" + kindLines.join("\n\n");
16896
+ ${body}`);
16919
16897
  }
16920
- return out;
16921
- }
16922
- function withGeneralInstructions(base, instructions) {
16923
- const general = instructions.get("general");
16924
- return general === void 0 ? base : `${base}
16925
-
16926
- Org instructions:
16927
- ${general}`;
16898
+ return blocks.join("\n\n");
16928
16899
  }
16929
16900
 
16930
16901
  // ../../libs/mcp-server/dist/server.js
@@ -16986,14 +16957,15 @@ async function resolveDeliverableTargetId(client2, projectKey, ref) {
16986
16957
  function createMcpServer(client2, apiBaseUrl, orgInstructions2 = /* @__PURE__ */ new Map(), opts = {}) {
16987
16958
  const surface = opts.surface ?? "local";
16988
16959
  const server2 = new McpServer({ name: "pipebrain", version: opts.version ?? DEFAULT_VERSION }, {
16989
- instructions: withGeneralInstructions(successorNotice() + `Pipebrain MCP server, bound to the ${apiBindingLabel(apiBaseUrl)} API at ${apiBaseUrl}. Every tool reads and writes THAT environment \u2014 confirm it is the one you intend (a local dev API vs production) before mutating. Tools are namespaced by this server's registration name in .mcp.json (e.g. pipebrain-local:* vs pipebrain:*).
16990
-
16991
- Technical decisions are yours. Inside an approved requirement you choose the design, the libraries, the structure, and the sequencing. Do not ask, do not report the choice, and do not seek ratification for it. Escalate on consequence, not on uncertainty: a choice that changes what a user sees, costs money, or contradicts an approved requirement is filed as a question with the options you propose, and everything else you decide and carry on. A technical choice worth keeping is recorded as a decision record, never as a paragraph.
16992
-
16993
- Every loose end has one destination, and none of them is prose. Anything you notice that is not the work in front of you goes to exactly one of three places: a question, when the operator has to choose; a new issue at triage, when it is real work outside this item\u2019s scope; or nowhere. Drop it unless it clears the bar for one of the first two \u2014 nowhere is the default, and it is the right outcome whenever knowing the thing would change nothing anyone does. Never open a section called notes, caveats, observations, things to know, or next steps in a body or a comment. Filing is the report: an item recorded and then repeated in prose asks the operator to triage it twice.`, orgInstructions2)
16960
+ // The BASE is this server's own two facts — which package is speaking (only
16961
+ // when the legacy wrapper arms the notice) and which environment it is bound
16962
+ // to. Everything else a session is handed comes from `docs/DOCTRINE.md` and the
16963
+ // org's saved texts, composed below. The two doctrine paragraphs that used to
16964
+ // be written out here (PIPE-257, carrying decision record PIPE-D25) are now
16965
+ // sections 1 and 2 of that file, so they are stated once and published.
16966
+ instructions: composeGuidance(successorNotice() + `Pipebrain MCP server, bound to the ${apiBindingLabel(apiBaseUrl)} API at ${apiBaseUrl}. Every tool reads and writes THAT environment \u2014 confirm it is the one you intend (a local dev API vs production) before mutating. Tools are namespaced by this server's registration name in .mcp.json (e.g. pipebrain-local:* vs pipebrain:*).`, orgInstructions2)
16994
16967
  });
16995
- const registerTool = (name, config3, cb) => server2.registerTool(name, { ...config3, description: decorateDescription(name, config3.description ?? "", orgInstructions2) }, cb);
16996
- registerTool("list_projects", {
16968
+ server2.registerTool("list_projects", {
16997
16969
  description: "List all projects with their key, environments, milestones (token, label, active), and open-issue counts \u2014 the project switcher and workflow entry point (\xA77).",
16998
16970
  // Optional pagination (§9) — bounds mirror list_issues/list_requirements; the
16999
16971
  // server defaults limit to 200 (max 500) and passes limit/offset as query params.
@@ -17002,7 +16974,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17002
16974
  offset: external_exports.number().int().nonnegative().optional()
17003
16975
  }
17004
16976
  }, (args) => run(() => client2.listProjects(args)));
17005
- registerTool("create_project", {
16977
+ server2.registerTool("create_project", {
17006
16978
  description: "Create a new project \u2014 the workflow bootstrap entry point, since issues and requirements are all project-scoped and nothing can be filed until a project exists. Both `name` (its display label) and `key` are required; the key is 2\u20135 uppercase letters (A\u2013Z, e.g. `DWB`), unique per org, and becomes the prefix of every display id in the project. Returns the created project (\xA77).",
17007
16979
  // Inline shape: @pipeline/domain has no standalone create-project schema — the
17008
16980
  // same `{ name, key }` pair lives only inside createOrgInputSchema.project, so
@@ -17013,7 +16985,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17013
16985
  key: projectKeySchema
17014
16986
  }
17015
16987
  }, (args) => run(() => client2.createProject(args)));
17016
- registerTool("list_waiting", {
16988
+ server2.registerTool("list_waiting", {
17017
16989
  description: "Every gate awaiting a PERSON across ALL projects in the token\u2019s org, longest wait first: a requirement revision awaiting approval, a question awaiting an answer, and work in `verify` holding at least one criterion not yet passed. Each entry names its project, the ref the action is taken on (`KEY-R{n}` for a revision, the ISSUE\u2019s `KEY-{n}` for a question or for work at verify), and `waitingSince` \u2014 the instant the wait began, not a duration. Nothing else is a gate: work in `triage` never appears, and neither does work in verify whose criteria have ALL passed. READ-ONLY, and there is no tool on this surface that clears any of the three \u2014 approving a revision, answering a question and marking a manual criterion are all decisions a person takes in the web app. Use it to see what your work is blocked on before asking, rather than to act on it."
17018
16990
  // NO arguments — not even an org. Org scope arrives from the token, exactly as
17019
16991
  // it does for `list_projects`, and an org parameter would open a cross-org read
@@ -17026,7 +16998,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17026
16998
  // makes that comparable at a glance.
17027
16999
  run(async () => ({ items: await client2.listWaiting() }))
17028
17000
  ));
17029
- registerTool("get_work_context", {
17001
+ server2.registerTool("get_work_context", {
17030
17002
  description: "Everything a session needs before it starts work, in ONE read \u2014 assembled by the API and returned verbatim. Addressed by `issue` OR by `project`, never both and never neither: an issue names its own project, so the pair is refused rather than resolved for you (\xA77). Given an ISSUE: that issue\u2019s full detail \u2014 the same payload `get_issue` returns \u2014 PLUS the BODY TEXT of every requirement it links, AT THE REVISION THE LINK IS PINNED AT. That text is the round trip this removes: `get_issue` names which documents bind the work and at which revision, never what they SAY, so a session that has read the issue still owes one fetch per link \u2014 and the one it skips leaves it building against rules it never read. Comments come CAPPED at the newest `commentLimit`, with `commentCount` \u2014 the issue\u2019s TRUE total \u2014 beside them, so a shortened list reads AS shortened rather than as the whole history; read the issue directly for the rest. Given a PROJECT: `available`, the work a session could START right now. It EXCLUDES finished and abandoned work (`done`, `killed`), work another person has claimed, and anything blocked \u2014 the status guard would refuse to start that, and offering work that cannot be started is offering a refusal. Your OWN claims stay, and so does unscheduled work carrying no milestone. The item form carries no `available`; the project form carries no item. BOTH forms also carry the project half: the release currently being worked (the active milestone) and the project\u2019s cross-cutting requirements WITH their text \u2014 they bind every epic automatically, and each is otherwise a separate call a session can simply forget to make. READ-ONLY \u2014 this claims nothing, moves nothing and writes nothing. CALL IT when you begin a piece of work, or when you pick work up in a project \u2014 the whole pack, once, before you start. Read narrowly the rest of the time; that is the ordinary case and needs no justification.",
17031
17003
  // The WHOLE domain schema rather than its `.shape`: the exactly-one-of rule is a
17032
17004
  // refinement on the OBJECT, and a raw shape drops it — the SDK validates against
@@ -17035,7 +17007,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17035
17007
  // naming the two arguments it refused rather than a reworded stand-in.
17036
17008
  inputSchema: getWorkContextInputSchema
17037
17009
  }, (args) => run(() => client2.getWorkContext(args)));
17038
- registerTool("create_milestone", {
17010
+ server2.registerTool("create_milestone", {
17039
17011
  description: "Create a milestone on a project. `token` is the short ref issues point at via their `milestone` field (e.g. `M1`) and is unique per project (a duplicate is rejected); `label` is its display name (e.g. `Feature Verification`). Optional `position` orders it (0-based, defaults to last); optional `active` marks it the current milestone \u2014 AT MOST ONE milestone per project is active, so `active: true` transactionally DEMOTES the previously active one rather than failing. Returns the created milestone (\xA77).",
17040
17012
  inputSchema: {
17041
17013
  project: projectKeySchema,
@@ -17045,7 +17017,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17045
17017
  active: external_exports.boolean().optional()
17046
17018
  }
17047
17019
  }, ({ project, ...input }) => run(() => client2.createMilestone(project, input)));
17048
- registerTool("update_milestone", {
17020
+ server2.registerTool("update_milestone", {
17049
17021
  description: "Rename or reorder a milestone, addressed by its token (e.g. `M1`) or uuid. Supply at least one of `token`, `label`, or `position` \u2014 an empty edit is rejected. Renaming the `token` does NOT rewrite issues pointing at the old one (`issues.milestone` is a loose ref), so prefer changing `label`. Use set_active_milestone to change `active` (\xA77).",
17050
17022
  inputSchema: {
17051
17023
  project: projectKeySchema,
@@ -17055,14 +17027,14 @@ Every loose end has one destination, and none of them is prose. Anything you not
17055
17027
  position: external_exports.number().int().nonnegative().optional()
17056
17028
  }
17057
17029
  }, ({ project, milestone, ...fields }) => run(async () => client2.updateMilestone(project, await resolveMilestoneId(client2, project, milestone), fields)));
17058
- registerTool("set_active_milestone", {
17030
+ server2.registerTool("set_active_milestone", {
17059
17031
  description: "Make a milestone the project\u2019s active one, addressed by its token (e.g. `M2`) or uuid. At most one milestone per project is active; this clears the previous one in the same transaction. Returns the activated milestone (\xA77).",
17060
17032
  inputSchema: {
17061
17033
  project: projectKeySchema,
17062
17034
  milestone: external_exports.string().min(1)
17063
17035
  }
17064
17036
  }, ({ project, milestone }) => run(async () => client2.activateMilestone(project, await resolveMilestoneId(client2, project, milestone))));
17065
- registerTool("delete_milestone", {
17037
+ server2.registerTool("delete_milestone", {
17066
17038
  description: "Soft-delete a milestone, addressed by its token (e.g. `M3`) or uuid; its token frees up for reuse. Issues still naming it are NOT rewritten and keep the now-dangling token (`issues.milestone` is a loose ref) \u2014 re-point them first. Returns confirmation metadata only (\xA77).",
17067
17039
  inputSchema: {
17068
17040
  project: projectKeySchema,
@@ -17073,7 +17045,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17073
17045
  await client2.deleteMilestone(project, id);
17074
17046
  return { deleted: true, project, milestone, id };
17075
17047
  }));
17076
- registerTool("create_deliverable_target", {
17048
+ server2.registerTool("create_deliverable_target", {
17077
17049
  description: "Add a deliverable target (platform) to a project. `slug` is the immutable lowercase-alphanumeric machine id a deliverable stores as its platform (e.g. `bsky`, `threads`) and is unique per project; `label` is its display name (e.g. `Bluesky`). Optional `position` orders it (0-based, defaults to last); optional `active` (default true) gates whether new deliverables can target it. Returns the created target.",
17078
17050
  inputSchema: {
17079
17051
  project: projectKeySchema,
@@ -17083,7 +17055,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17083
17055
  active: external_exports.boolean().optional()
17084
17056
  }
17085
17057
  }, ({ project, ...input }) => run(() => client2.createDeliverableTarget(project, input)));
17086
- registerTool("update_deliverable_target", {
17058
+ server2.registerTool("update_deliverable_target", {
17087
17059
  description: "Update a deliverable target, addressed by its slug (e.g. `bsky`) or uuid. Supply at least one of `label`, `position`, or `active` \u2014 an empty edit is rejected. The `slug` is immutable (it is the value stored on existing deliverables). Set `active: false` to remove it from the add-deliverable menu while keeping existing deliverables working.",
17088
17060
  inputSchema: {
17089
17061
  project: projectKeySchema,
@@ -17093,7 +17065,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17093
17065
  active: external_exports.boolean().optional()
17094
17066
  }
17095
17067
  }, ({ project, target, ...fields }) => run(async () => client2.updateDeliverableTarget(project, await resolveDeliverableTargetId(client2, project, target), fields)));
17096
- registerTool("delete_deliverable_target", {
17068
+ server2.registerTool("delete_deliverable_target", {
17097
17069
  description: "Delete a deliverable target, addressed by its slug (e.g. `bsky`) or uuid. Only a target with NO deliverables can be deleted; one already in use is rejected (deactivate it with update_deliverable_target instead). Returns confirmation metadata only.",
17098
17070
  inputSchema: {
17099
17071
  project: projectKeySchema,
@@ -17104,7 +17076,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17104
17076
  await client2.deleteDeliverableTarget(project, id);
17105
17077
  return { deleted: true, project, target, id };
17106
17078
  }));
17107
- registerTool(
17079
+ server2.registerTool(
17108
17080
  "list_issues",
17109
17081
  {
17110
17082
  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.`,
@@ -17120,78 +17092,83 @@ Every loose end has one destination, and none of them is prose. Anything you not
17120
17092
  // all, passes through untouched.
17121
17093
  (args) => run(() => client2.listIssues(args.status === void 0 ? { ...args, status: [...OPEN_STATUSES] } : args))
17122
17094
  );
17123
- registerTool("get_issue", {
17124
- 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, 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. Cross-cutting requirements bind every epic automatically and arrive with this payload at their latest approved revision \u2014 never restate them in a body; explicit requirement links pin the approved revision, so link by key rather than pasting a document\u2019s text. 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. Absent evidence there is the human gate working rather than a gap: do not report it as a finding, do not call the criterion unverified or unevidenced, and do not weigh it below an `auto` criterion that carries evidence. 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.",
17095
+ server2.registerTool("get_issue", {
17096
+ 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, 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.",
17125
17097
  inputSchema: { issue: issueRefSchema }
17126
17098
  }, (args) => run(() => client2.getIssue(args.issue)));
17127
- registerTool("create_issue", {
17128
- description: 'Create an issue (epic/task/bug/feedback). Omitted status defaults to triage (still subject to \xA76 guards); omitted environment resolves explicit \u2192 parent\u2019s environment \u2192 project default (prod). Optional parent (self/cycle rejected), priority, milestone, requirement_links (pins each at its approved revision; a target with no approved revision is rejected), deliverable_links (content deliverables like DWB-C3/bsky \u2014 pins each at its current revision; the promotion path), and ordered {text, verification, howTo} criteria (\xA77). A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9): work past triage belongs to a release, and the create is refused (`milestone_required`) without one. Search before you create: `list_issues` takes a `text` filter \u2014 run it on the distinctive nouns first. Sessions do not see each other, so a duplicate is invisible until someone finds both; on a near match, comment or amend the existing record rather than filing again. The bar for filing a bug: file `type: bug` only when the observed behavior breaks an approved requirement or a defined acceptance criterion, and name which one before you file. If you cannot name one, this is not a bug \u2014 code you find unclear, an edge case nobody specified, a performance number nobody set, and anything you would phrase as "this could be better" are opinions about the product, so file each as `type: feedback`, or not at all. The bar exists because a queue holding both real defects and unshared taste has to be read entirely before any of it can be trusted. When the criterion you named is an acceptance criterion, pass its uuid as `criterionId` and the bug records which criterion it was filed against \u2014 the criterion then names what has already been reported (it arrives as `filedBugs` on every criteria read) beside the action that files again, so a duplicate is prevented by information rather than by removal. Filing a second bug against one criterion is a supported situation, not a mistake: one row can fail for two different reasons at once. Bugs only: any other type naming a criterion is refused (`criterion_requires_bug`). Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built. A decision you need from the operator is filed as a question via `ask_questions`, never written as an "Open questions" heading inside a body \u2014 a heading prompts nobody and records no answer, so a later session cannot tell whether the decision was made. Before assigning any verification mode, establish what this project can drive end to end \u2014 an end-to-end test harness and the command that runs it. A criterion is `auto` when an evaluator can drive the input and read the result against the running system, which includes behavior shown in the interface wherever such a harness exists; the mode follows from what can be driven, not from where the behavior appears. What qualifies a criterion as `auto` is the evaluator\u2019s own run, never the existence of a test the implementer wrote. Mark `manual` only when the verdict needs a person\u2019s judgment; a criterion you cannot describe as drivable is usually worded too vaguely, so reword it against the observable rather than fall back to a person. A `manual` criterion must also carry `howTo` \u2014 one or two sentences, not an essay, saying where to look, what to do, and what counts as passing for whoever checks it by hand. Writing or changing one without it is refused (`how_to_required`); so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug \u2014 because what they answer is how anyone knows the thing is done, and that question does not get easier as the work gets smaller; feedback is the one exception, refused with `criteria_not_supported`, because it is a report rather than agreed work and `promote_feedback` retypes it into work before anything is built. Cross-cutting requirements bind every epic automatically and arrive with `get_issue` \u2014 never restate them in a body and never add them to `requirement_links`; an explicit link pins the linked document\u2019s approved revision.',
17099
+ server2.registerTool("create_issue", {
17100
+ description: "Create an issue (epic/task/bug/feedback). Omitted status defaults to triage (still subject to \xA76 guards); omitted environment resolves explicit \u2192 parent\u2019s environment \u2192 project default (prod). Optional parent (self/cycle rejected), priority, milestone, requirement_links (pins each at its approved revision; a target with no approved revision is rejected), deliverable_links (content deliverables like DWB-C3/bsky \u2014 pins each at its current revision; the promotion path), and ordered {text, verification, howTo} criteria (\xA77). A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9): work past triage belongs to a release, and the create is refused (`milestone_required`) without one. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. When a bug is filed against a defined acceptance criterion, pass that criterion\u2019s uuid as `criterionId` and the bug records which criterion it was filed against \u2014 the criterion then names what has already been reported (it arrives as `filedBugs` on every criteria read) beside the action that files again, so a duplicate is prevented by information rather than by removal. Filing a second bug against one criterion is a supported situation, not a mistake: one row can fail for two different reasons at once. Bugs only: any other type naming a criterion is refused (`criterion_requires_bug`). Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
17129
17101
  inputSchema: createIssueInputSchema.shape
17130
17102
  }, (args) => run(() => client2.createIssue(args)));
17131
- registerTool("promote_feedback", {
17132
- description: 'Promote a FEEDBACK issue into a NEW epic or task (PIPE-143): creates the issue in the feedback\u2019s project (`title` + `type` required; omitted environment inherits the FEEDBACK\u2019s environment, not the project default; optional status/body/priority/milestone/requirement_links/criteria as in create_issue), records the feedback as its permanent source (`promotedFrom`/`promotedTo` on both details), closes the feedback as done, and comments the new key on it. The feedback\u2019s own title/body are never touched \u2014 write the new issue\u2019s body as real work, do not paste the feedback in. Rejected (409) for non-feedback, terminal, or already-promoted sources. Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue; the SOURCE feedback needs none to be closed by the promotion. Search before you promote: `list_issues` takes a `text` filter \u2014 run it on the feedback\u2019s distinctive nouns first; an issue covering it may already exist, and sessions do not see each other. On a near match, comment on or amend the existing issue rather than promoting a duplicate. The bar for promoting to a bug: choose `type: bug` only when the reported behavior breaks an approved requirement or a defined acceptance criterion, and name which one. If you cannot name one, promote it as `task` \u2014 or leave it as feedback. A queue holding both real defects and unshared taste has to be read entirely before any of it can be trusted. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built. A decision you need from the operator is filed as a question via `ask_questions`, never written as an "Open questions" heading inside a body \u2014 a heading prompts nobody and records no answer, so a later session cannot tell whether the decision was made. Before assigning any verification mode, establish what this project can drive end to end \u2014 an end-to-end test harness and the command that runs it. A criterion is `auto` when an evaluator can drive the input and read the result against the running system, which includes behavior shown in the interface wherever such a harness exists; the mode follows from what can be driven, not from where the behavior appears. What qualifies a criterion as `auto` is the evaluator\u2019s own run, never the existence of a test the implementer wrote. Mark `manual` only when the verdict needs a person\u2019s judgment; a criterion you cannot describe as drivable is usually worded too vaguely, so reword it against the observable rather than fall back to a person. A `manual` criterion must also carry `howTo` \u2014 one or two sentences, not an essay, saying where to look, what to do, and what counts as passing for whoever checks it by hand. Writing or changing one without it is refused (`how_to_required`); so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug \u2014 because what they answer is how anyone knows the thing is done, and that question does not get easier as the work gets smaller; feedback is the one exception, refused with `criteria_not_supported`, because it is a report rather than agreed work and `promote_feedback` retypes it into work before anything is built. Cross-cutting requirements bind every epic automatically and arrive with `get_issue` \u2014 never restate them in a body and never add them to `requirement_links`; an explicit link pins the linked document\u2019s approved revision.',
17103
+ server2.registerTool("promote_feedback", {
17104
+ description: "Promote a FEEDBACK issue into a NEW epic or task (PIPE-143): creates the issue in the feedback\u2019s project (`title` + `type` required; omitted environment inherits the FEEDBACK\u2019s environment, not the project default; optional status/body/priority/milestone/requirement_links/criteria as in create_issue), records the feedback as its permanent source (`promotedFrom`/`promotedTo` on both details), closes the feedback as done, and comments the new key on it. The feedback\u2019s own title/body are never touched \u2014 write the new issue\u2019s body as real work. Rejected (409) for non-feedback, terminal, or already-promoted sources. Returns the created issue. A `status` other than `triage` or `killed` REQUIRES `milestone` (PIPE-R10 rule 9), exactly as in create_issue; the SOURCE feedback needs none to be closed by the promotion. A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; this tool is what retypes it into work before anything is built. Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CSearch before you create\u201D and \u201CThe bar for filing a bug\u201D.",
17133
17105
  inputSchema: {
17134
17106
  issue: external_exports.string().min(1),
17135
17107
  ...promoteFeedbackInputSchema.shape
17136
17108
  }
17137
17109
  }, ({ issue: issue2, ...fields }) => run(() => client2.promoteFeedback(issue2, fields)));
17138
- registerTool("open_review", {
17139
- description: "Open the COMPLETION REVIEW \u2014 the reading of an epic\u2019s WHOLE accumulated change as one body of work, by a third fresh session that neither built it nor evaluated it. It runs at one moment: once the epic\u2019s contract is green and nothing is left to check, and before anyone sets it `done`. It creates one ORDINARY `task` at `ready` carrying the review checklist as its body \u2014 no new record type. The epic form (`epic`) titles it `Completion review \u2014 <KEY-n> <epic title>`, files it as a CHILD of the epic and inherits the epic\u2019s milestone; the milestone form (`project` + `milestone`) titles it `Completion review \u2014 milestone <token>`, carries that token and has no parent \u2014 nothing here models a milestone as complete. Pass EXACTLY ONE of `epic` or `project` + `milestone`: both, or neither, is refused. One review at a time per scope \u2014 a second is refused (`review_already_open`) NAMING the standing one, so read, finish or kill that task instead \u2014 and a review opens only on an epic (`review_target_not_epic`). NO NEW GATE was added to `done`, and the whole mechanism depends on that: file each finding as ordinary work \u2014 a CHILD OF THE EPIC, a sibling of this review \u2014 and the children clause already on `done` holds the epic open until every finding is done or killed. A finding filed UNDER the review gates nothing, because that gate reads one level deep. Killing the review, or any finding, is the recorded waiver: deliberately ungated, and written to the status history with who did it and through which token. An epic that never opened a review still closes exactly as it did before.",
17110
+ server2.registerTool("open_review", {
17111
+ description: "Open the COMPLETION REVIEW \u2014 the reading of an epic\u2019s WHOLE accumulated change as one body of work, by a third fresh session that neither built it nor evaluated it. It runs at one moment: once the epic\u2019s contract is green and nothing is left to check, and before anyone sets it `done`. It creates one ORDINARY `task` at `ready` carrying the review checklist as its body \u2014 no new record type. The epic form (`epic`) titles it `Completion review \u2014 <KEY-n> <epic title>`, files it as a CHILD of the epic and inherits the epic\u2019s milestone; the milestone form (`project` + `milestone`) titles it `Completion review \u2014 milestone <token>`, carries that token and has no parent \u2014 nothing here models a milestone as complete. Pass EXACTLY ONE of `epic` or `project` + `milestone`: both, or neither, is refused. One review at a time per scope \u2014 a second is refused (`review_already_open`) NAMING the standing one, so read, finish or kill that task instead \u2014 and a review opens only on an epic (`review_target_not_epic`). NO NEW GATE was added to `done`, and the whole mechanism depends on that: file each finding as ordinary work \u2014 a CHILD OF THE EPIC, a sibling of this review \u2014 and the children clause already on `done` holds the epic open until every finding is done or killed. A finding filed UNDER the review gates nothing, because that gate reads one level deep. Killing the review, or any finding, is the recorded waiver: deliberately ungated, and written to the status history with who did it and through which token. An epic that never opened a review still closes exactly as it did before. Writing guidance for the checklist and for each finding lives in the product doctrine delivered at the start of this session.",
17140
17112
  // The WHOLE domain schema rather than its `.shape`, for `get_work_context`'s
17141
17113
  // reason: the exactly-one-of rule is a refinement on the OBJECT, and a raw
17142
17114
  // shape drops it — the refusal naming the two forms would never reach the
17143
17115
  // caller, and both arguments (or neither) would be accepted here.
17144
17116
  inputSchema: openReviewInputSchema
17145
17117
  }, (args) => run(() => client2.openReview(args)));
17146
- registerTool("update_issue", {
17147
- 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. Cite the decision record a piece of work implements rather than restating what was decided in its body: the record is where the reasoning stays correct, and a body repeating it goes stale the moment the decision is superseded. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built. A decision you need from the operator is filed as a question via `ask_questions`, never written as an "Open questions" heading inside a body \u2014 a heading prompts nobody and records no answer, so a later session cannot tell whether the decision was made. Cross-cutting requirements bind every epic automatically and arrive with `get_issue` \u2014 never restate them in a body and never add them to `requirement_links`; an explicit link pins the linked document\u2019s approved revision.',
17118
+ server2.registerTool("update_issue", {
17119
+ 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 delivered at the start of this session \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.",
17148
17120
  inputSchema: updateIssueInputSchema.shape
17149
17121
  }, ({ issue: issue2, ...fields }) => run(() => client2.updateIssue(issue2, fields)));
17150
- registerTool("set_status", {
17122
+ server2.registerTool("set_status", {
17151
17123
  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.",
17152
17124
  inputSchema: setStatusInputSchema.shape
17153
17125
  }, (args) => run(() => client2.setStatus(args.issue, args.status)));
17154
- registerTool("claim_issue", {
17126
+ server2.registerTool("claim_issue", {
17155
17127
  description: "Atomically claim an issue (\xA76 rule 1): succeeds only if unclaimed; under a race exactly one wins and the loser\u2019s error names the current claimer. Claim is orthogonal to status (\xA77).",
17156
17128
  inputSchema: { issue: issueRefSchema }
17157
17129
  }, (args) => run(() => client2.claimIssue(args.issue)));
17158
- registerTool("release_issue", {
17130
+ server2.registerTool("release_issue", {
17159
17131
  description: "Release an issue\u2019s claim, clearing claimed_by (\xA77).",
17160
17132
  inputSchema: { issue: issueRefSchema }
17161
17133
  }, (args) => run(() => client2.releaseIssue(args.issue)));
17162
- registerTool("add_comment", {
17163
- description: "Add a markdown comment to an issue, attributed to the operator (and token label over MCP). A comment EXPLAINS a blockage \u2014 the full account of what you hit and why you stopped \u2014 but never records one: `set_blocked` (with a reason) and `link_blocker` (when you are waiting on another issue) are what put an issue in the blocked state (\xA77/WORKFLOW).",
17134
+ server2.registerTool("add_comment", {
17135
+ description: "Add a markdown comment to an issue, attributed to the operator (and token label over MCP). A comment EXPLAINS a blockage \u2014 the full account of what you hit and why you stopped \u2014 but never records one: `set_blocked` (with a reason) and `link_blocker` (when you are waiting on another issue) are what put an issue in the blocked state (\xA77/WORKFLOW). Writing guidance for a comment lives in the product doctrine delivered at the start of this session \u2014 see \u201CEvery loose end has one destination, and none of them is prose\u201D.",
17164
17136
  inputSchema: commentInputSchema.shape
17165
17137
  }, (args) => run(() => client2.addComment(args.issue, args.body)));
17166
- registerTool("set_blocked", {
17167
- description: "Record an issue as BLOCKED with a required human-readable `reason` \u2014 state on the issue, not a comment: it shows on the issue and counts on the project\u2019s Work page, and only clearing it (`clear_blocked`) ends it. Setting it again on an already-blocked issue replaces the standing reason and keeps the old one as history, attributed to whoever set it. The reason is one or two lines saying what the work is waiting on; the full account of what you tried still belongs in a comment. Returns the updated issue detail, whose `blocked` carries the reason and who set it.",
17138
+ server2.registerTool("set_blocked", {
17139
+ description: "Record an issue as BLOCKED with a required human-readable `reason` \u2014 state on the issue, not a comment: it shows on the issue and counts on the project\u2019s Work page, and only clearing it (`clear_blocked`) ends it. Setting it again on an already-blocked issue replaces the standing reason and keeps the old one as history, attributed to whoever set it. The reason is one or two lines saying what the work is waiting on; the full account of what you tried still belongs in a comment. Returns the updated issue detail, whose `blocked` carries the reason and who set it. Writing guidance for the reason lives in the product doctrine delivered at the start of this session.",
17168
17140
  inputSchema: setBlockedInputSchema.shape
17169
17141
  }, (args) => run(() => client2.setBlocked(args.issue, args.reason)));
17170
- registerTool("clear_blocked", {
17142
+ server2.registerTool("clear_blocked", {
17171
17143
  description: "Clear an issue\u2019s blocked mark \u2014 the blockage is over. Idempotent: clearing an issue that is not blocked is a no-op, not an error. The cleared mark is kept as history with who ended it, so a past blockage stays readable. Returns the updated issue detail (`blocked` is then null).",
17172
17144
  inputSchema: { issue: issueRefSchema }
17173
17145
  }, (args) => run(() => client2.clearBlocked(args.issue)));
17174
- registerTool("link_blocker", {
17146
+ server2.registerTool("link_blocker", {
17175
17147
  description: "Record that `issue` is WAITING ON `blocker` \u2014 the dependency between two issues, written from the waiting issue\u2019s side. One call records both directions: `get_issue` on the waiting issue lists it under `blockedBy` (with the blocker\u2019s display id, title, and whether it is already `done`/`killed`), and `get_issue` on the blocker lists it under `blocks`, so finishing work shows what it frees. Both issues must be in the SAME project. An issue cannot block itself, and an edge that would close a cycle (directly or through other issues) is refused with nothing recorded. Adding the same blocker twice records no second edge. Returns the waiting issue\u2019s updated detail.",
17176
17148
  inputSchema: linkBlockerInputSchema.shape
17177
17149
  }, (args) => run(() => client2.addBlocker(args.issue, args.blocker)));
17178
- registerTool("unlink_blocker", {
17150
+ server2.registerTool("unlink_blocker", {
17179
17151
  description: "Remove the blocked-by edge between `issue` and `blocker` \u2014 the dependency is gone, and one call clears it from BOTH issues\u2019 views. Idempotent: unlinking a pair that is not linked is a no-op, not an error. Returns the waiting issue\u2019s updated detail.",
17180
17152
  inputSchema: linkBlockerInputSchema.shape
17181
17153
  }, (args) => run(() => client2.removeBlocker(args.issue, args.blocker)));
17182
- registerTool("set_criteria", {
17183
- description: "Define or replace the ordered acceptance criteria list ({text, verification, howTo}) on a piece of work. Matched rows keep their stable AC-{n} number; new items get fresh never-reused numbers; removed items are soft-deleted. A matched row keeps its state only when BOTH its text and verification mode are unchanged \u2014 a mode flip resets it to pending (\xA77). Before assigning any verification mode, establish what this project can drive end to end \u2014 an end-to-end test harness and the command that runs it. A criterion is `auto` when an evaluator can drive the input and read the result against the running system, which includes behavior shown in the interface wherever such a harness exists; the mode follows from what can be driven, not from where the behavior appears. What qualifies a criterion as `auto` is the evaluator\u2019s own run, never the existence of a test the implementer wrote. Mark `manual` only when the verdict needs a person\u2019s judgment; a criterion you cannot describe as drivable is usually worded too vaguely, so reword it against the observable rather than fall back to a person. A `manual` criterion must also carry `howTo` \u2014 one or two sentences, not an essay, saying where to look, what to do, and what counts as passing for whoever checks it by hand. Writing or changing one without it is refused (`how_to_required`); so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug \u2014 because what they answer is how anyone knows the thing is done, and that question does not get easier as the work gets smaller; feedback is the one exception, refused with `criteria_not_supported`, because it is a report rather than agreed work and `promote_feedback` retypes it into work before anything is built. An evaluator RAISES a criterion from `manual` to `auto` only on a drive that actually succeeded \u2014 never on one it expects to succeed \u2014 and never lowers one. The flip both resets the state and clears the stored `howTo`, so a criterion moved to `auto` has lost its checking instructions \u2014 moving it back to `manual` does not restore them, and is refused (`how_to_required`) until you write new ones.",
17154
+ server2.registerTool("set_criteria", {
17155
+ description: "Define or replace the ordered acceptance criteria list ({text, verification, howTo}) on a piece of work. Matched rows keep their stable AC-{n} number; new items get fresh never-reused numbers; removed items are soft-deleted. A matched row keeps its state only when BOTH its text and verification mode are unchanged \u2014 a mode flip resets it to pending (\xA77). A `manual` criterion must carry `howTo` \u2014 one or two sentences saying where to look, what to do, and what counts as passing; writing or changing one without it is refused (`how_to_required`), and so is supplying it on an `auto` criterion, which a machine checks. Work of any kind carries criteria \u2014 an epic, a task, a bug alike \u2014 with feedback the one exception, refused with `criteria_not_supported` because it is a report rather than agreed work; `promote_feedback` retypes it into work before anything is built. Raising a criterion from `manual` to `auto` both resets its state and clears the stored `howTo`, so a criterion moved to `auto` has lost its checking instructions \u2014 moving it back to `manual` does not restore them, and is refused (`how_to_required`) until you write new ones. Writing guidance for a criterion, and for choosing its mode, lives in the product doctrine delivered at the start of this session \u2014 see \u201CVerification modes follow what can be driven\u201D.",
17184
17156
  inputSchema: setCriteriaInputSchema.shape
17185
17157
  }, (args) => run(() => client2.setCriteria(args.issue, args.criteria)));
17186
- registerTool("set_criterion_state", {
17187
- description: "Set a criterion\u2019s state (pending/passed/failed) on whatever work carries it \u2014 an epic, a task and a bug alike \u2014 addressed by criterionId ALONE, or by issue + criterionNumber (the stable AC-{n}); exactly one form. Feedback never carries criteria, so neither address resolves to one. A token-authenticated call is REJECTED on a manual criterion (manual state is web-session-only, \xA74); evidence is required when a token marks passed/failed (\xA77).",
17158
+ server2.registerTool("set_criterion_state", {
17159
+ description: "Set a criterion\u2019s state (pending/passed/failed) on whatever work carries it \u2014 an epic, a task and a bug alike \u2014 addressed by criterionId ALONE, or by issue + criterionNumber (the stable AC-{n}); exactly one form. Feedback never carries criteria, so neither address resolves to one. A token-authenticated call is REJECTED on a manual criterion (manual state is web-session-only, \xA74); evidence is required when a token marks passed/failed (\xA77). Writing guidance for that evidence lives in the product doctrine delivered at the start of this session \u2014 see \u201CVerification modes follow what can be driven\u201D.",
17188
17160
  // `issue` is OPTIONAL: the criterionId-only form needs no issue ref. The
17189
17161
  // exactly-one-of rule can't be expressed in a raw shape, so the handler
17190
17162
  // enforces it via the shared `@pipeline/domain` refine (mirror of §7).
17163
+ //
17164
+ // `criterionNumber` takes `int4Schema` so the ADVERTISED JSON Schema carries
17165
+ // the real ceiling (PIPE-149) — hand-rolling it from `positiveIntSchema` told
17166
+ // clients the maximum was 2^53-1 while the column and the API both stop at
17167
+ // 2,147,483,647, so a client refusal that should happen never did.
17191
17168
  inputSchema: {
17192
17169
  issue: issueRefSchema.optional(),
17193
17170
  criterionId: uuidSchema.optional(),
17194
- criterionNumber: positiveIntSchema.optional(),
17171
+ criterionNumber: int4Schema.optional(),
17195
17172
  state: criterionStateSchema,
17196
17173
  evidence: markdownSchema.optional()
17197
17174
  }
@@ -17209,28 +17186,28 @@ Every loose end has one destination, and none of them is prose. Anything you not
17209
17186
  evidence: input.evidence
17210
17187
  });
17211
17188
  }));
17212
- registerTool("ask_questions", {
17213
- description: 'Ask the operator a BATCH of questions on an issue \u2014 `questions` takes 1 or more {text, options?}, and a session with one open decision usually has two or three, so ask them in ONE call rather than one at a time (a half-asked set costs the operator a second pass). `options` are the choices you propose, in the order you offer them; omit it to ask in open words. Each question gets a stable per-issue `Q-{n}` number that is NEVER reused, so an answer stays addressable in a later session. Returns the updated issue detail, and every issue read from then on carries `questions` with their answers \u2014 so read the answer in the `get_issue` you were going to make anyway, and READ the existing questions and answers BEFORE asking again rather than re-asking a decision already taken. Answering is reserved for a person in the web UI: there is deliberately no tool for it on this surface (PIPE-R22 rule 4). A decision you need from the operator is filed HERE, never written as an "Open questions" heading inside an issue body \u2014 a heading prompts nobody and records no answer, so a later session cannot tell whether the decision was made. Withdraw any question your own work has since settled, with `withdraw_question` \u2014 an open question the work already answered still asks the operator to decide something nobody is waiting on.',
17189
+ server2.registerTool("ask_questions", {
17190
+ description: "Ask the operator a BATCH of questions on an issue \u2014 `questions` takes 1 or more {text, options?}, and a session with one open decision usually has two or three, so ask them in ONE call rather than one at a time (a half-asked set costs the operator a second pass). `options` are the choices you propose, in the order you offer them; omit it to ask in open words. Each question gets a stable per-issue `Q-{n}` number that is NEVER reused. Returns the updated issue detail, and every issue read from then on carries `questions` with their answers \u2014 so read the answer in the `get_issue` you were going to make anyway, and READ the existing questions and answers BEFORE asking again rather than re-asking a decision already taken. Answering is reserved for a person in the web UI: there is deliberately no tool for it on this surface (PIPE-R22 rule 4). A question is withdrawn with `withdraw_question`. Writing guidance for a question lives in the product doctrine delivered at the start of this session \u2014 see \u201CA decision you need is filed as a question, never written into a body\u201D.",
17214
17191
  inputSchema: askQuestionsInputSchema.shape
17215
17192
  }, (args) => run(() => client2.askQuestions(args.issue, args.questions)));
17216
- registerTool("withdraw_question", {
17193
+ server2.registerTool("withdraw_question", {
17217
17194
  description: "Withdraw a question by its stable `Q-{n}` number \u2014 asker only: the asker is the token\u2019s OWNING user, so another agent\u2019s question is not yours to clear. Deliberately NOT human-gated, because the point is to clear a question you no longer need \u2014 the work answered it, or it was wrong to ask \u2014 instead of leaving the operator to answer something nobody is waiting on. It is recorded, never an answer: the question is soft-deleted (kept for the audit trail), its number retires and is not reissued, and the next issue read no longer carries it in `questions`. Returns the updated issue detail.",
17218
17195
  inputSchema: withdrawQuestionInputSchema.shape
17219
17196
  }, (args) => run(() => client2.withdrawQuestion(args.issue, args.number)));
17220
- registerTool("list_requirements", {
17197
+ server2.registerTool("list_requirements", {
17221
17198
  description: "List requirements filtered by project and text: display id, title, summary (the latest revision\u2019s one/two-line outline description), area (product-area label), cross_cutting flag, lifecycle (draft/approved/amended), latest + approved revision numbers, and linked-epic count (\xA77).",
17222
17199
  inputSchema: listRequirementsFilterSchema.shape
17223
17200
  }, (args) => run(() => client2.listRequirements(args)));
17224
- registerTool("get_requirement", {
17201
+ server2.registerTool("get_requirement", {
17225
17202
  description: "Get a requirement by display id or uuid. Defaults to the approved revision (latest if never approved) unless `revision` is given; includes lifecycle, revision list, approvals, and linked issues (\xA77).",
17226
17203
  inputSchema: { requirement: requirementRefSchema, revision: positiveIntSchema.optional() }
17227
17204
  }, (args) => run(() => client2.getRequirement(args.requirement, args.revision)));
17228
- registerTool("create_requirement", {
17229
- description: 'Create a project-scoped requirement document with revision 1 (title, body). Optional summary is a short one/two-line outline description (versioned on the revision, like title/body); optional area is a free-text product-area label ("Backup", "Billing") used to group/filter the outline (metadata, like cross_cutting). Optional cross_cutting flags it as binding every epic in the project automatically \u2014 never link cross-cutting documents (\xA75 rule 6, \xA77). Search before you create: `list_requirements` takes a `text` filter \u2014 run it on the distinctive nouns first. Sessions do not see each other, so a duplicate is invisible until someone finds both; on a near match, amend the existing document rather than filing again. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built.',
17205
+ server2.registerTool("create_requirement", {
17206
+ description: 'Create a project-scoped requirement document with revision 1 (title, body). Optional summary is a short one/two-line outline description (versioned on the revision, like title/body); optional area is a free-text product-area label ("Backup", "Billing") used to group/filter the outline (metadata, like cross_cutting). Optional cross_cutting flags it as binding every epic in the project automatically \u2014 never link cross-cutting documents (\xA75 rule 6, \xA77). Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CSearch before you create\u201D and \u201CLink by key; never paste another record\u2019s text\u201D.',
17230
17207
  inputSchema: createRequirementInputSchema.shape
17231
17208
  }, (args) => run(() => client2.createRequirement(args)));
17232
- registerTool("update_requirement", {
17233
- description: "Update a requirement: title, body, and/or summary APPENDS a new revision (nothing is edited in place; summary versions like title/body); optional note records why. Optional area (product-area label), cross_cutting, and decision_links (add/remove decision records this rule sits under \u2014 `KEY-D{n}`, from `list_decisions`) are metadata changes (no revision) \u2014 toggling cross_cutting ON is rejected while explicit links exist; unlink first. Supplying none of title/body/summary/area/cross_cutting/decision_links is an empty edit (400) (\xA77). Cite the decision record a rule rests on rather than restating what was decided in its body: the record is where the reasoning stays correct, and a body repeating it goes stale the moment the decision is superseded. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built.",
17209
+ server2.registerTool("update_requirement", {
17210
+ description: "Update a requirement: title, body, and/or summary APPENDS a new revision (nothing is edited in place; summary versions like title/body); optional note records why. Optional area (product-area label), cross_cutting, and decision_links (add/remove decision records this rule sits under \u2014 `KEY-D{n}`, from `list_decisions`) are metadata changes (no revision) \u2014 toggling cross_cutting ON is rejected while explicit links exist; unlink first. Supplying none of title/body/summary/area/cross_cutting/decision_links is an empty edit (400) (\xA77). Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CLink by key; never paste another record\u2019s text\u201D.",
17234
17211
  // Inline shape (updateRequirementInputSchema is a refine, no .shape) — add
17235
17212
  // summary/area here explicitly (§210) and decisionLinks (PIPE-84 §6); the
17236
17213
  // empty-edit refine is enforced by the API, not this shape.
@@ -17245,56 +17222,56 @@ Every loose end has one destination, and none of them is prose. Anything you not
17245
17222
  decisionLinks: decisionLinkChangesSchema.optional()
17246
17223
  }
17247
17224
  }, ({ requirement, ...fields }) => run(() => client2.updateRequirement(requirement, fields)));
17248
- registerTool("approve_requirement", {
17225
+ server2.registerTool("approve_requirement", {
17249
17226
  description: "Approve (pin) a requirement\u2019s latest revision, or a specified `revision`, recording it in approvals history and advancing the approved revision that downstream links and reads build against (\xA75, \xA77).",
17250
17227
  inputSchema: approveRequirementInputSchema.shape
17251
17228
  }, (args) => run(() => client2.approveRequirement(args.requirement, args.revision)));
17252
- registerTool("create_decision", {
17253
- description: "Record ONE decision as a project-scoped decision record (`KEY-D{n}`): `title` names the decision and `body` says what was decided and why. A new record starts `proposed`; moving it to `accepted` IS the acceptance \u2014 there is no separate approval step. Returns the created record. A decision record is written ONCE. Title and body can be corrected only while it is `proposed`; from `accepted` onward the record is frozen, and the way to change a decision is to write a NEW record and `supersede_decision` the old one \u2014 which leaves the original readable and says where the current decision went. Nothing on any surface deletes one. Search before you create: `list_decisions` takes a `text` filter matching TITLE AND BODY \u2014 run it on the distinctive nouns first. Sessions do not see each other, so re-arguing a settled question leaves two answers on the ledger with nothing saying which one holds. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built.",
17229
+ server2.registerTool("create_decision", {
17230
+ description: "Record ONE decision as a project-scoped decision record (`KEY-D{n}`): `title` names the decision and `body` says what was decided and why. A new record starts `proposed`; moving it to `accepted` IS the acceptance \u2014 there is no separate approval step. Returns the created record. Title and body can be corrected only while the record is `proposed`; from `accepted` onward it is frozen, and the way to change a decision is to write a NEW record and `supersede_decision` the old one \u2014 which leaves the original readable and says where the current decision went. Nothing on any surface deletes one. Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CA decision record is written once\u201D and \u201CSearch before you create\u201D.",
17254
17231
  inputSchema: createDecisionInputSchema.shape
17255
17232
  }, (args) => run(() => client2.createDecision(args)));
17256
- registerTool("get_decision", {
17233
+ server2.registerTool("get_decision", {
17257
17234
  description: "Get a decision record by display id (`KEY-D{n}`) or uuid: title, body, lifecycle state (`proposed`/`accepted`/`superseded`), who accepted it and when, the record that replaced it, where the CURRENT decision lives when this one has been superseded, and everything that references it \u2014 every requirement and every work item, gathered from both directions.",
17258
17235
  inputSchema: getDecisionInputSchema.shape
17259
17236
  }, (args) => run(() => client2.getDecision(args.decision)));
17260
- registerTool("list_decisions", {
17261
- description: "List decision records filtered by `project` and `text`: display id, title, lifecycle state, the replacement when superseded, and how many requirements and work items cite each one. `text` matches over TITLE AND BODY, so search on what a decision was about rather than on what someone chose to call it. Read the decisions before re-opening a settled question: an `accepted` record is the project\u2019s answer, and arguing it again inside an issue or requirement body produces a second answer with nothing saying which one holds. If the answer genuinely has changed, record a NEW decision and `supersede_decision` the old one rather than editing anything.",
17237
+ server2.registerTool("list_decisions", {
17238
+ description: "List decision records filtered by `project` and `text`: display id, title, lifecycle state, the replacement when superseded, and how many requirements and work items cite each one. `text` matches over TITLE AND BODY, so search on what a decision was about rather than on what someone chose to call it.",
17262
17239
  inputSchema: listDecisionsFilterSchema.shape
17263
17240
  }, (args) => run(() => client2.listDecisions(args)));
17264
- registerTool("supersede_decision", {
17241
+ server2.registerTool("supersede_decision", {
17265
17242
  description: "Replace an ACCEPTED decision record with another one: `decision` moves to `superseded` and names `replacedBy` as where the current decision now lives, so whoever opens the old record is sent to the standing one. This is the only way a decision changes \u2014 the original is never edited and never removed, and the whole chain stays readable. Refused when the record is still `proposed` (nothing has been decided yet, so correct it instead), when a record is named as its own replacement, when the replacement belongs to another project, or when the replacement\u2019s own chain already leads back to this record. Returns the superseded record.",
17266
17243
  inputSchema: supersedeDecisionInputSchema.shape
17267
17244
  }, (args) => run(() => client2.supersedeDecision(args.decision, args.replacedBy)));
17268
- registerTool("list_content", {
17245
+ server2.registerTool("list_content", {
17269
17246
  description: "List content drafts filtered by project, text, area, and author: display id (KEY-C{n}), title, summary (latest revision), area, author, latest revision number, deliverable count, and linked-issue count. Content is a project-scoped sibling of requirements \u2014 raw material that gets promoted into work, not an issue type.",
17270
17247
  inputSchema: listContentFilterSchema.shape
17271
17248
  }, (args) => run(() => client2.listContent(args)));
17272
- registerTool("get_content", {
17249
+ server2.registerTool("get_content", {
17273
17250
  description: "Get a content draft by display id (KEY-C{n}) or uuid: its revision history (default latest, or `revision` to pin) plus every per-platform deliverable with its own revisions, posted metadata (url/postedAt), linked issues (with staleness), and linked assets.",
17274
17251
  inputSchema: { content: contentRefSchema, revision: positiveIntSchema.optional() }
17275
17252
  }, (args) => run(() => client2.getContent(args.content, args.revision)));
17276
- registerTool("create_content", {
17277
- description: 'Create a project-scoped content draft with revision 1 (title, body). Optional summary is a short outline description (versioned on the revision, like title/body); optional area is a free-text product-area label (metadata). Optional author is the editorial byline the draft is credited to (free text, e.g. "jason") \u2014 distinct from the acting operator, and the field the list filters by. Optional metadata is an open bag for capture provenance (tags, score, sensitivity, source refs) \u2014 stored passively, never acted on. This is the capture-sink target: a captured topic becomes a draft here. Search before you create: `list_content` takes a `text` filter \u2014 run it on the distinctive nouns first. Sessions do not see each other, so a duplicate is invisible until someone finds both; on a near match, amend the existing draft rather than filing again.',
17253
+ server2.registerTool("create_content", {
17254
+ description: 'Create a project-scoped content draft with revision 1 (title, body). Optional summary is a short outline description (versioned on the revision, like title/body); optional area is a free-text product-area label (metadata). Optional author is the editorial byline the draft is credited to (free text, e.g. "jason") \u2014 distinct from the acting operator, and the field the list filters by. Optional metadata is an open bag for capture provenance (tags, score, sensitivity, source refs) \u2014 stored passively, never acted on. This is the capture-sink target: a captured topic becomes a draft here. Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CSearch before you create\u201D.',
17278
17255
  inputSchema: createContentInputSchema.shape
17279
17256
  }, (args) => run(() => client2.createContent(args)));
17280
- registerTool("update_content", {
17281
- description: "Update a content draft: title, body, and/or summary APPENDS a new revision (nothing is edited in place); optional note records why. Optional area, author, and metadata are metadata changes (no revision). Supplying none of title/body/summary/area/author/metadata is an empty edit (400).",
17257
+ server2.registerTool("update_content", {
17258
+ description: "Update a content draft: title, body, and/or summary APPENDS a new revision (nothing is edited in place); optional note records why. Optional area, author, and metadata are metadata changes (no revision). Supplying none of title/body/summary/area/author/metadata is an empty edit (400). Writing guidance for this tool lives in the product doctrine delivered at the start of this session.",
17282
17259
  inputSchema: updateContentInputSchema.shape
17283
17260
  }, ({ content, ...fields }) => run(() => client2.updateContent(content, fields)));
17284
- registerTool("create_deliverable", {
17285
- description: "Add a per-platform deliverable to a draft with revision 1 (the post text). `platform` is a slug configured for the project \u2014 one of its ACTIVE deliverable targets (see list_projects \u2192 deliverableTargets; defaults are bsky/x/reddit/hn/blog, but projects can add/remove them); an unconfigured slug is rejected. Optional body seeds revision 1. There is one deliverable per platform per draft \u2014 a variant is a revision (update_deliverable), not a second deliverable. Rendered id is DRAFT-KEY/platform (e.g. DWB-C3/bsky).",
17261
+ server2.registerTool("create_deliverable", {
17262
+ description: "Add a per-platform deliverable to a draft with revision 1 (the post text). `platform` is a slug configured for the project \u2014 one of its ACTIVE deliverable targets (see list_projects \u2192 deliverableTargets; defaults are bsky/x/reddit/hn/blog, but projects can add/remove them); an unconfigured slug is rejected. Optional body seeds revision 1. There is one deliverable per platform per draft \u2014 a variant is a revision (update_deliverable), not a second deliverable. Rendered id is DRAFT-KEY/platform (e.g. DWB-C3/bsky). Writing guidance for the post text lives in the product doctrine delivered at the start of this session.",
17286
17263
  inputSchema: createDeliverableInputSchema.shape
17287
17264
  }, ({ content, ...fields }) => run(() => client2.createDeliverable(content, fields)));
17288
- registerTool("update_deliverable", {
17289
- description: "Append a new revision (post text) to a deliverable, addressed as DRAFT-KEY/platform (e.g. DWB-C3/bsky); optional note records why. Platform identity is immutable \u2014 this only versions the post text.",
17265
+ server2.registerTool("update_deliverable", {
17266
+ description: "Append a new revision (post text) to a deliverable, addressed as DRAFT-KEY/platform (e.g. DWB-C3/bsky); optional note records why. Platform identity is immutable \u2014 this only versions the post text. Writing guidance for the post text lives in the product doctrine delivered at the start of this session.",
17290
17267
  inputSchema: updateDeliverableInputSchema.shape
17291
17268
  }, ({ deliverable, ...fields }) => run(() => client2.updateDeliverable(deliverable, fields)));
17292
- registerTool("record_post", {
17269
+ server2.registerTool("record_post", {
17293
17270
  description: "Record that a deliverable (DRAFT-KEY/platform) was posted: its public url and when (postedAt defaults to now), pinning the deliverable\u2019s current revision as the posted one. Pipebrain never posts \u2014 a human posts, then records the url here. Re-calling overwrites (a correction).",
17294
17271
  inputSchema: recordPostInputSchema.shape
17295
17272
  }, ({ deliverable, ...fields }) => run(() => client2.recordPost(deliverable, fields)));
17296
- registerTool("promote_deliverable", {
17297
- description: 'Promote a deliverable (DRAFT-KEY/platform, e.g. DWB-C3/bsky) to a NEW issue: creates the issue (`title` + `type` required; optional `status` defaults to triage; optional `body` seeds it \u2014 typically the current post text) AND a PROTECTED link pinning the deliverable\u2019s current revision. That link CANNOT be unlinked \u2014 the issue exists to ship this deliverable. (To link an EXISTING issue instead, use update_issue.deliverable_links, which stays removable.) Returns the created issue. Search before you promote: `list_issues` takes a `text` filter \u2014 run it on the deliverable\u2019s distinctive nouns first; an issue for this deliverable may already exist, and sessions do not see each other. Link another record by key plus title on first mention; never paste its text into a body \u2014 the source changes, the copy does not, and a stale copy gets built. The promoted deliverable itself is the exception: seeding the body with its text is the point of this tool, and the protected link pins the revision that text came from. A decision you need from the operator is filed as a question via `ask_questions`, never written as an "Open questions" heading inside a body \u2014 a heading prompts nobody and records no answer, so a later session cannot tell whether the decision was made.',
17273
+ server2.registerTool("promote_deliverable", {
17274
+ description: "Promote a deliverable (DRAFT-KEY/platform, e.g. DWB-C3/bsky) to a NEW issue: creates the issue (`title` + `type` required; optional `status` defaults to triage; optional `body` seeds it \u2014 typically the current post text) AND a PROTECTED link pinning the deliverable\u2019s current revision. That link CANNOT be unlinked \u2014 the issue exists to ship this deliverable. (To link an EXISTING issue instead, use update_issue.deliverable_links, which stays removable.) Returns the created issue. Seeding the body with the deliverable\u2019s own text is the exception to linking rather than pasting \u2014 it is the point of this tool, and the protected link pins the revision that text came from. Writing guidance for this tool lives in the product doctrine delivered at the start of this session \u2014 see \u201CSearch before you create\u201D and \u201CLink by key; never paste another record\u2019s text\u201D.",
17298
17275
  inputSchema: {
17299
17276
  deliverable: external_exports.string().min(1),
17300
17277
  title: external_exports.string().min(1),
@@ -17303,34 +17280,34 @@ Every loose end has one destination, and none of them is prose. Anything you not
17303
17280
  body: markdownSchema.optional()
17304
17281
  }
17305
17282
  }, ({ deliverable, ...fields }) => run(() => client2.promoteDeliverable(deliverable, fields)));
17306
- registerTool("list_assets", {
17283
+ server2.registerTool("list_assets", {
17307
17284
  description: "List project assets (S3-backed files of any type) filtered by project, status (pending/active/rejected), category (image/video/document), and filename text, sorted by date or size (asc/desc): id, filename, contentType, size, sha256, status, hasThumbnail, and linkCount (live references). Metadata only \u2014 the bytes move by path, never through this result.",
17308
17285
  inputSchema: listAssetsFilterSchema.shape
17309
17286
  }, (args) => run(() => client2.listAssets(args)));
17310
- registerTool("get_asset", {
17287
+ server2.registerTool("get_asset", {
17311
17288
  description: "Get an asset\u2019s metadata by uuid: filename, contentType, size, sha256 (client-computed, server-verified by read-back), status, hasThumbnail, linkCount (live references), and timestamps. Metadata only \u2014 no value/URL is returned over MCP. To get the bytes, use download_asset (a LOCAL server only \u2014 it writes to a path on the machine running the server) or `ppb assets download`.",
17312
17289
  inputSchema: { asset: uuidSchema }
17313
17290
  }, (args) => run(() => client2.getAsset(args.asset)));
17314
17291
  if (surface === "local") {
17315
- registerTool("upload_asset", {
17292
+ server2.registerTool("upload_asset", {
17316
17293
  description: "Upload a LOCAL FILE (by path) as a project asset and activate it: reads and SHA-256-hashes the file locally, registers the asset, PUTs the bytes directly to storage, and completes (server verifies the digest by read-back). Returns the asset metadata (status active on success). Only the file PATH crosses the wire \u2014 the bytes never do; do NOT pass base64 or file contents. contentType is guessed from the extension when omitted. Then use link_asset to attach the returned asset to a deliverable revision or a content draft.",
17317
17294
  inputSchema: uploadAssetInputSchema.shape
17318
17295
  }, (args) => run(() => uploadAssetFromPath(client2, apiBaseUrl, args)));
17319
- registerTool("download_asset", {
17296
+ server2.registerTool("download_asset", {
17320
17297
  description: "Download an ACTIVE asset\u2019s bytes to a LOCAL FILE at `path` (created/overwritten), so you can read or view an uploaded image, mockup, or document. The bytes are written to disk and re-verified against the recorded sha256; the RESULT is metadata + the written path \u2014 never the bytes or a URL. Addressed by asset uuid (from list_assets, get_asset, or get_content).",
17321
17298
  inputSchema: downloadAssetInputSchema.shape
17322
17299
  }, (args) => run(() => downloadAssetToPath(client2, args)));
17323
17300
  }
17324
- registerTool("link_asset", {
17301
+ server2.registerTool("link_asset", {
17325
17302
  description: "Reference an active asset FROM a target. Two targets are wired: `deliverable_revision` (targetId is a content_deliverable_revision uuid, e.g. from get_content) and `content_draft` (targetId is a content_draft uuid \u2014 the asset attaches to the topic doc and is snapshot-copied onto deliverables when they are created). Rejects a non-active asset or a cross-project link.",
17326
17303
  inputSchema: linkAssetInputSchema.shape
17327
17304
  }, (args) => run(() => client2.linkAsset(args)));
17328
- registerTool("unlink_asset", {
17305
+ server2.registerTool("unlink_asset", {
17329
17306
  description: "Remove an asset reference by its link id (soft-delete). The asset itself is untouched \u2014 this only drops the reference from its target.",
17330
17307
  inputSchema: unlinkAssetInputSchema.shape
17331
17308
  }, (args) => run(() => client2.unlinkAsset(args.linkId)));
17332
- registerTool("set_secret", {
17333
- description: "Create a secret or add a new version under a project + environment (metadata only \u2014 the response NEVER echoes the value). For sensitive values use the `ppb` CLI instead (`ppb secrets set --from-file <f>` or pipe via stdin): a value typed here is placed in the chat transcript before the server can encrypt it. `name` is UPPER_SNAKE_CASE (e.g. API_KEY); `environment` is the environment slug (dev/prod); optional `description`.",
17309
+ server2.registerTool("set_secret", {
17310
+ description: "Create a secret or add a new version under a project + environment (metadata only \u2014 the response NEVER echoes the value). `name` is UPPER_SNAKE_CASE (e.g. API_KEY); `environment` is the environment slug (dev/prod); optional `description`.",
17334
17311
  inputSchema: {
17335
17312
  project: projectKeySchema,
17336
17313
  environment: external_exports.string().min(1),
@@ -17339,14 +17316,14 @@ Every loose end has one destination, and none of them is prose. Anything you not
17339
17316
  description: external_exports.string().optional()
17340
17317
  }
17341
17318
  }, ({ project, environment, name, value, description }) => run(() => client2.setSecret(project, { env: environment, name, value, description })));
17342
- registerTool("list_secrets", {
17319
+ server2.registerTool("list_secrets", {
17343
17320
  description: "List a project's secrets as METADATA ONLY (id, name, environment, description, current version, timestamps) \u2014 never any value. Optional `environment` slug filter.",
17344
17321
  inputSchema: {
17345
17322
  project: projectKeySchema,
17346
17323
  environment: external_exports.string().min(1).optional()
17347
17324
  }
17348
17325
  }, ({ project, environment }) => run(() => client2.listSecrets(project, { env: environment })));
17349
- registerTool("describe_secret", {
17326
+ server2.registerTool("describe_secret", {
17350
17327
  description: "Describe one secret: its metadata plus its version history (version, created_at, created_by, and whether each version has been crypto-shredded). METADATA ONLY \u2014 no value is ever returned.",
17351
17328
  inputSchema: {
17352
17329
  project: projectKeySchema,
@@ -17354,8 +17331,8 @@ Every loose end has one destination, and none of them is prose. Anything you not
17354
17331
  name: secretNameSchema
17355
17332
  }
17356
17333
  }, ({ project, environment, name }) => run(() => client2.describeSecret(project, name, { env: environment })));
17357
- registerTool("rotate_secret", {
17358
- description: "Rotate an EXISTING secret: store a NEW version and advance the pointer (metadata only \u2014 the response NEVER echoes the value). For sensitive values use the `ppb` CLI instead (`ppb secrets set --from-file <f>` or pipe via stdin): a value typed here is placed in the chat transcript before the server can encrypt it.",
17334
+ server2.registerTool("rotate_secret", {
17335
+ description: "Rotate an EXISTING secret: store a NEW version and advance the pointer (metadata only \u2014 the response NEVER echoes the value).",
17359
17336
  inputSchema: {
17360
17337
  project: projectKeySchema,
17361
17338
  environment: external_exports.string().min(1),
@@ -17363,8 +17340,8 @@ Every loose end has one destination, and none of them is prose. Anything you not
17363
17340
  value: external_exports.string().min(1)
17364
17341
  }
17365
17342
  }, ({ project, environment, name, value }) => run(() => client2.rotateSecret(project, name, { env: environment, value })));
17366
- registerTool("update_secret_description", {
17367
- description: "Update a secret's description (metadata-only edit \u2014 does not change the value or add a version).",
17343
+ server2.registerTool("update_secret_description", {
17344
+ description: "Update a secret's description (metadata-only edit \u2014 does not change the value or add a version). Writing guidance for that description lives in the product doctrine delivered at the start of this session.",
17368
17345
  inputSchema: {
17369
17346
  project: projectKeySchema,
17370
17347
  environment: external_exports.string().min(1),
@@ -17372,7 +17349,7 @@ Every loose end has one destination, and none of them is prose. Anything you not
17372
17349
  description: external_exports.string()
17373
17350
  }
17374
17351
  }, ({ project, environment, name, description }) => run(() => client2.updateSecretDescription(project, name, { env: environment, description })));
17375
- registerTool("delete_secret", {
17352
+ server2.registerTool("delete_secret", {
17376
17353
  description: "Delete a secret: soft-delete the entry and crypto-shred all its versions (the value becomes unrecoverable). Returns confirmation metadata only.",
17377
17354
  inputSchema: {
17378
17355
  project: projectKeySchema,
@@ -17383,14 +17360,14 @@ Every loose end has one destination, and none of them is prose. Anything you not
17383
17360
  await client2.deleteSecret(project, name, { env: environment });
17384
17361
  return { deleted: true, project, environment, name };
17385
17362
  }));
17386
- registerTool("list_org_instructions", {
17387
- description: "List the org\u2019s agent instructions \u2014 the per-scope texts appended to this server\u2019s tool descriptions (and, for `general`, its server instructions) at session start. Edits apply to NEW sessions only; use this to see the CURRENT saved values mid-session (at most one row per scope)."
17363
+ server2.registerTool("list_org_instructions", {
17364
+ description: "List the org\u2019s agent instructions \u2014 the per-scope texts this server delivers to a session at start, in its server instructions, under a heading naming each scope. Edits apply to NEW sessions only; use this to see the CURRENT saved values mid-session (at most one row per scope)."
17388
17365
  }, () => run(() => client2.listOrgInstructions()));
17389
- registerTool("set_org_instruction", {
17390
- description: "Set (create or replace) one scope\u2019s org-level agent instruction text, max 4000 characters \u2014 over-cap is rejected, never truncated. Scopes: `general` (server-wide), `requirements`, `content`, and `work.<type>` (one per issue type, e.g. `work.epic`). Requires the ADMIN role in the token\u2019s org. Takes effect at the NEXT session start \u2014 running sessions keep the text they launched with. Returns the saved instruction. Organization instructions say how this organization writes, not how Pipebrain works: if a sentence would read identically for an organization that has never met yours, it is product behavior and belongs in the server\u2019s shipped tool descriptions, not here.",
17366
+ server2.registerTool("set_org_instruction", {
17367
+ description: "Set (create or replace) one scope\u2019s org-level agent instruction text, max 4000 characters \u2014 over-cap is rejected, never truncated. Scopes: `general` (server-wide), `requirements`, `content`, and `work.<type>` (one per issue type, e.g. `work.epic`). Requires the ADMIN role in the token\u2019s org. Takes effect at the NEXT session start \u2014 running sessions keep the text they launched with. Returns the saved instruction. Writing guidance for an instruction body lives in the product doctrine delivered at the start of this session \u2014 see \u201CSetting an instruction replaces the whole body\u201D.",
17391
17368
  inputSchema: setOrgInstructionInputSchema.shape
17392
17369
  }, (args) => run(() => client2.setOrgInstruction(args.scope, args.body)));
17393
- registerTool("delete_org_instruction", {
17370
+ server2.registerTool("delete_org_instruction", {
17394
17371
  description: "Clear one scope\u2019s org-level agent instruction (soft delete; an unset scope is an error naming it). Requires the ADMIN role in the token\u2019s org. Takes effect at the NEXT session start. Returns the cleared instruction.",
17395
17372
  inputSchema: deleteOrgInstructionInputSchema.shape
17396
17373
  }, (args) => run(() => client2.deleteOrgInstruction(args.scope)));
@@ -17459,7 +17436,7 @@ function loadMcpConfig() {
17459
17436
  }
17460
17437
 
17461
17438
  // src/version.ts
17462
- var VERSION = "0.29.0";
17439
+ var VERSION = "0.31.0";
17463
17440
 
17464
17441
  // src/main.ts
17465
17442
  var config2 = loadMcpConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipebrain-mcp",
3
- "version": "0.29.0",
3
+ "version": "0.31.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",