@productbrain/mcp 0.0.1-beta.9574 → 0.0.1-beta.9584

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.
@@ -395,7 +395,7 @@ var TOOL_NAME_MIGRATIONS = /* @__PURE__ */ new Map([
395
395
  ["search", 'context action="search"'],
396
396
  ["relate-entries", 'relations action="create"'],
397
397
  ["batch-relate", 'relations action="batch-create"'],
398
- ["find-related", 'relations action="find"'],
398
+ ["find-related", 'context action="find"'],
399
399
  ["gather-context", 'context action="gather"'],
400
400
  ["get-build-context", 'context action="build"'],
401
401
  ["list-collections", 'collections action="list"'],
@@ -443,7 +443,7 @@ var TOOL_NAME_MIGRATIONS = /* @__PURE__ */ new Map([
443
443
  ["move-entry", 'entries action="move"'],
444
444
  ["session-wrapup", 'session action="wrapup-review|wrapup-commit" (review \u2192 wrapup-review, commit-all \u2192 wrapup-commit)'],
445
445
  ["facilitate", 'session action="resume|commit-constellation" (same action names)'],
446
- ["graph", 'relations action="find" (same action name)'],
446
+ ["graph", 'context action="find" (same action name)'],
447
447
  ["documents", 'context action="last-verified-brief"'],
448
448
  ["labels", 'collections action="label-*" (list/create/update/delete/apply/remove \u2192 label-<same>)'],
449
449
  ["health", 'workspace action="check|whoami|status|audit|self-test" (same action names)'],
@@ -2054,7 +2054,7 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2054
2054
  }
2055
2055
  const next = [];
2056
2056
  if (finalStatus === "committed") {
2057
- next.push({ tool: "relations", description: "See its connections", parameters: { action: "find", entryId: finalEntryId } });
2057
+ next.push({ tool: "context", description: "See its connections", parameters: { action: "find", entryId: finalEntryId } });
2058
2058
  } else {
2059
2059
  next.push({ tool: "context", description: "Review the draft alternative", parameters: { action: "get", entryId: finalEntryId } });
2060
2060
  next.push({ tool: "entries", description: "Accept onto Chain", parameters: { action: "commit", entryId: finalEntryId } });
@@ -2653,7 +2653,7 @@ No DB writes \u2014 call without \`preview:true\` to accept for real.` }],
2653
2653
  } catch {
2654
2654
  }
2655
2655
  const next = [
2656
- { tool: "relations", description: "See its connections", parameters: { action: "find", entryId } },
2656
+ { tool: "context", description: "See its connections", parameters: { action: "find", entryId } },
2657
2657
  { tool: "context", description: "View accepted entry", parameters: { action: "get", entryId } }
2658
2658
  ];
2659
2659
  const summary = `Accepted ${entryId} (${entry.name}) onto the Chain.`;
@@ -3543,7 +3543,7 @@ async function handleGet(entryId) {
3543
3543
  `Found ${entryId}: ${String(e.name)} [${String(e.collectionSlug ?? "")}].`,
3544
3544
  entryData,
3545
3545
  [
3546
- { tool: "relations", description: "See its connections", parameters: { action: "find", entryId } },
3546
+ { tool: "context", description: "See its connections", parameters: { action: "find", entryId } },
3547
3547
  { tool: "context", description: "Gather related context", parameters: { action: "gather", entryId } }
3548
3548
  ]
3549
3549
  )
@@ -3791,194 +3791,64 @@ ${footer}` }],
3791
3791
  }
3792
3792
 
3793
3793
  // src/tools/relations.ts
3794
- import { z as z13 } from "zod/v3";
3795
-
3796
- // src/tools/graph.ts
3797
3794
  import { z as z12 } from "zod/v3";
3798
- var GRAPH_ACTIONS = ["find"];
3799
- var graphSchema = z12.object({
3800
- action: z12.enum(GRAPH_ACTIONS).describe(
3801
- "'find': traverse relations from an entry (graph walk)."
3802
- ),
3803
- entryId: z12.string().max(200).describe("Entry ID, e.g. '<PREFIX>-<n>'"),
3804
- direction: z12.enum(["incoming", "outgoing", "both"]).default("both").optional().describe("For find: 'incoming' = what references this, 'outgoing' = what this references")
3805
- });
3806
- var graphFindOutputSchema = z12.object({
3807
- entryId: z12.string(),
3808
- relations: z12.array(z12.object({
3809
- entryId: z12.string().optional(),
3810
- name: z12.string(),
3811
- type: z12.string(),
3812
- direction: z12.enum(["outgoing", "incoming"])
3813
- })),
3814
- total: z12.number()
3795
+ var RELATIONS_ACTIONS = ["create", "batch-create", "delete"];
3796
+ var relationItemSchema = z12.object({
3797
+ from: z12.string().max(200),
3798
+ to: z12.string().max(200),
3799
+ type: z12.string().max(200)
3815
3800
  });
3816
- async function handleFind(entryId, direction) {
3817
- const relations = await kernelQuery("chain.listEntryRelations", { entryId });
3818
- if (relations.length === 0) {
3819
- return {
3820
- content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relations action=create to create connections.` }],
3821
- structuredContent: success(
3822
- `No relations found for '${entryId}'.`,
3823
- { entryId, relations: [], nodes: [], edges: [], total: 0 },
3824
- [
3825
- { tool: "relations", description: "Create a relation", parameters: { action: "create", from: entryId } }
3826
- ]
3827
- )
3828
- };
3829
- }
3830
- const sourceEntry = await kernelQuery("chain.getEntry", { entryId });
3831
- if (!sourceEntry) {
3832
- return notFoundResult(entryId, `Entry '${entryId}' not found. Try context action=search to find the right ID.`);
3833
- }
3834
- const sourceInternalId = sourceEntry._id;
3835
- const MAX_RELATIONS = 25;
3836
- const truncated = relations.length > MAX_RELATIONS;
3837
- const capped = relations.slice(0, MAX_RELATIONS);
3838
- const otherIds = /* @__PURE__ */ new Set();
3839
- for (const r of capped) {
3840
- const otherId = r.fromId === sourceInternalId ? r.toId : r.fromId;
3841
- otherIds.add(otherId);
3842
- }
3843
- const otherEntries = /* @__PURE__ */ new Map();
3844
- for (const id of otherIds) {
3845
- const entry = await kernelQuery("chain.getEntry", { id });
3846
- if (entry) {
3847
- otherEntries.set(entry._id, { entryId: entry.entryId, name: entry.name, collectionId: entry.collectionId });
3848
- }
3849
- }
3850
- const collections = await kernelQuery("chain.listCollections");
3851
- const collMap = /* @__PURE__ */ new Map();
3852
- for (const c of collections) collMap.set(c._id, c.slug);
3853
- const lines = [`## Find Result`, "", `# Relations for ${entryId}: ${sourceEntry.name}`, ""];
3854
- const enriched = capped.map((r) => {
3855
- const isOutgoing = r.fromId === sourceInternalId;
3856
- const otherId = isOutgoing ? r.toId : r.fromId;
3857
- const other = otherEntries.get(otherId);
3858
- const otherLabel = other?.entryId ? `${other.entryId}: ${other.name}` : other?.name ?? "(deleted)";
3859
- const colSlug = other ? collMap.get(other.collectionId) ?? "unknown" : "unknown";
3860
- return { isOutgoing, type: r.type, otherLabel, colSlug, otherEntryId: other?.entryId, otherName: other?.name ?? "(deleted)" };
3861
- });
3862
- const outgoing = enriched.filter((r) => r.isOutgoing);
3863
- const incoming = enriched.filter((r) => !r.isOutgoing);
3864
- if ((direction === "outgoing" || direction === "both") && outgoing.length > 0) {
3865
- lines.push(`## Outgoing (${outgoing.length})`);
3866
- for (const r of outgoing) {
3867
- lines.push(`- \u2192 **${r.type}** ${r.otherLabel} [${r.colSlug}]`);
3868
- }
3869
- lines.push("");
3870
- }
3871
- if ((direction === "incoming" || direction === "both") && incoming.length > 0) {
3872
- lines.push(`## Incoming (${incoming.length})`);
3873
- for (const r of incoming) {
3874
- lines.push(`- \u2190 **${r.type}** ${r.otherLabel} [${r.colSlug}]`);
3875
- }
3876
- lines.push("");
3877
- }
3878
- const shown = direction === "outgoing" ? outgoing : direction === "incoming" ? incoming : enriched;
3879
- if (shown.length === 0) {
3880
- lines.push(`No ${direction} relations found for \`${entryId}\`.`);
3881
- }
3882
- if (truncated) {
3883
- lines.push(`_Showing first ${MAX_RELATIONS} of ${relations.length} relations. Use context action=get for the full picture._`);
3884
- }
3885
- const structuredRelations = shown.map((r) => ({
3886
- entryId: r.otherEntryId,
3887
- name: r.otherName,
3888
- type: r.type,
3889
- direction: r.isOutgoing ? "outgoing" : "incoming"
3890
- }));
3891
- const nodes = [
3892
- { id: entryId, name: sourceEntry.name, collectionName: "" },
3893
- ...shown.map((r) => ({
3894
- id: r.otherEntryId ?? String(Math.random()),
3895
- name: r.otherName,
3896
- collectionName: r.colSlug
3897
- }))
3898
- ];
3899
- const edges = shown.filter((r) => r.otherEntryId).map((r) => ({
3900
- source: entryId,
3901
- target: r.otherEntryId,
3902
- type: r.type
3903
- }));
3904
- return {
3905
- content: [{ type: "text", text: lines.join("\n") }],
3906
- structuredContent: success(
3907
- `Found ${relations.length} relations for ${entryId}.`,
3908
- { entryId, relations: structuredRelations, nodes, edges, total: relations.length },
3909
- [{ tool: "context", description: "View entry details", parameters: { action: "get", entryId } }]
3910
- )
3911
- };
3912
- }
3913
-
3914
- // src/tools/relations.ts
3915
- var RELATIONS_ACTIONS = ["create", "batch-create", "delete", "find"];
3916
- var relationItemSchema = z13.object({
3917
- from: z13.string().max(200),
3918
- to: z13.string().max(200),
3919
- type: z13.string().max(200)
3920
- });
3921
- var relationsSchema = z13.object({
3922
- action: z13.enum(RELATIONS_ACTIONS).describe(
3923
- "'create': link two entries. 'batch-create': create multiple relations (validates every item before writing any). 'delete': remove a relation. 'find': traverse relations from an entry (graph walk, absorbs graph action=find)."
3801
+ var relationsSchema = z12.object({
3802
+ action: z12.enum(RELATIONS_ACTIONS).describe(
3803
+ "'create': link two entries. 'batch-create': create multiple relations (validates every item before writing any). 'delete': remove a relation."
3924
3804
  ),
3925
- from: z13.string().max(200).optional().describe("For 'create'/'delete': source entry ID."),
3926
- to: z13.string().max(200).optional().describe("For 'create'/'delete': target entry ID."),
3927
- type: z13.string().max(200).optional().describe("For 'create'/'delete': relation type."),
3928
- relations: z13.array(relationItemSchema).min(1).max(RELATION_BATCH_MAX).optional().describe("For 'batch-create': array of {from, to, type}."),
3805
+ from: z12.string().max(200).optional().describe("For 'create'/'delete': source entry ID."),
3806
+ to: z12.string().max(200).optional().describe("For 'create'/'delete': target entry ID."),
3807
+ type: z12.string().max(200).optional().describe("For 'create'/'delete': relation type."),
3808
+ relations: z12.array(relationItemSchema).min(1).max(RELATION_BATCH_MAX).optional().describe("For 'batch-create': array of {from, to, type}."),
3929
3809
  // WP-316 S3: Preview gate — dry-run mode for action=create.
3930
- preview: z13.boolean().optional().describe("For 'create': if true, validates the relation without writing. Returns what would happen. Default false."),
3931
- entryId: z13.string().max(200).optional().describe("For 'find': entry ID, e.g. '<PREFIX>-<n>'."),
3932
- direction: z13.enum(["incoming", "outgoing", "both"]).optional().describe("For 'find': 'incoming' = what references this, 'outgoing' = what this references. Default 'both'.")
3810
+ preview: z12.boolean().optional().describe("For 'create': if true, validates the relation without writing. Returns what would happen. Default false.")
3933
3811
  });
3934
- var relationsCreateVariant = z13.object({
3935
- action: z13.literal("create"),
3936
- from: z13.string().max(200),
3937
- to: z13.string().max(200),
3938
- type: z13.string().max(200),
3939
- preview: z13.boolean().optional()
3812
+ var relationsCreateVariant = z12.object({
3813
+ action: z12.literal("create"),
3814
+ from: z12.string().max(200),
3815
+ to: z12.string().max(200),
3816
+ type: z12.string().max(200),
3817
+ preview: z12.boolean().optional()
3940
3818
  });
3941
- var relationsBatchCreateVariant = z13.object({
3942
- action: z13.literal("batch-create"),
3943
- relations: z13.array(relationItemSchema).min(1).max(RELATION_BATCH_MAX)
3819
+ var relationsBatchCreateVariant = z12.object({
3820
+ action: z12.literal("batch-create"),
3821
+ relations: z12.array(relationItemSchema).min(1).max(RELATION_BATCH_MAX)
3944
3822
  });
3945
- var relationsDeleteVariant = z13.object({
3946
- action: z13.literal("delete"),
3947
- from: z13.string().max(200),
3948
- to: z13.string().max(200),
3949
- type: z13.string().max(200)
3823
+ var relationsDeleteVariant = z12.object({
3824
+ action: z12.literal("delete"),
3825
+ from: z12.string().max(200),
3826
+ to: z12.string().max(200),
3827
+ type: z12.string().max(200)
3950
3828
  });
3951
- var relationsFindVariant = z13.object({
3952
- action: z13.literal("find"),
3953
- entryId: z13.string().max(200),
3954
- direction: z13.enum(["incoming", "outgoing", "both"]).optional().default("both")
3955
- });
3956
- var relationsActionUnion = z13.discriminatedUnion("action", [
3829
+ var relationsActionUnion = z12.discriminatedUnion("action", [
3957
3830
  relationsCreateVariant,
3958
3831
  relationsBatchCreateVariant,
3959
- relationsDeleteVariant,
3960
- relationsFindVariant
3832
+ relationsDeleteVariant
3961
3833
  ]);
3962
3834
  var RELATIONS_ACTION_SPECS = {
3963
3835
  create: { params: ["from", "to", "type", "preview"], description: "from, to, and type are required." },
3964
3836
  "batch-create": { params: ["relations"], description: "relations (1-20 items) is required." },
3965
- delete: { params: ["from", "to", "type"], description: "from, to, and type are required." },
3966
- find: { params: ["entryId", "direction"], description: "entryId is required." }
3837
+ delete: { params: ["from", "to", "type"], description: "from, to, and type are required." }
3967
3838
  };
3968
3839
  function registerRelationsTools(server) {
3969
3840
  const relationsHandlers = {
3970
3841
  create: (data) => handleCreate(data.from, data.to, data.type, data.preview),
3971
3842
  "batch-create": (data) => handleBatchCreate(data.relations),
3972
- delete: (data) => handleDelete(data.from, data.to, data.type),
3973
- find: (data) => handleFind(data.entryId, data.direction ?? "both")
3843
+ delete: (data) => handleDelete(data.from, data.to, data.type)
3974
3844
  };
3975
3845
  server.registerTool(
3976
3846
  "relations",
3977
3847
  {
3978
3848
  title: "Relations",
3979
- description: "Create, manage, and read relations between entries. A relation is written only when you ask for it, with exactly the verb you send.\n\n- **create**: Link two entries with a typed relation.\n- **batch-create**: Create multiple relations in one call (validates every item before writing any).\n- **delete**: Remove a relation by from, to, and type.\n- **find**: Traverse relations from a specific entry \u2014 shows incoming/outgoing connections. NOT full-text search (use context action=search).",
3849
+ description: "Create and delete relations between entries. A relation is written only when you ask for it, with exactly the verb you send. Read an entry's relations with the read-only `context action=find`.\n\n- **create**: Link two entries with a typed relation.\n- **batch-create**: Create multiple relations in one call (validates every item before writing any).\n- **delete**: Remove a relation by from, to, and type.",
3980
3850
  inputSchema: relationsSchema,
3981
- // Mixed read/write noun (§3 R3): find is read-only, create/batch-create/delete write.
3851
+ // DEC-1894 / DEC-74: find lives on the read-only `context` tool; every action left here writes.
3982
3852
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
3983
3853
  },
3984
3854
  thinWrapper(async (args) => {
@@ -4107,7 +3977,7 @@ No DB writes \u2014 call without \`preview:true\` to create for real.${advisoryT
4107
3977
  type,
4108
3978
  ...result2?.advisories && result2.advisories.length > 0 && { advisories: result2.advisories }
4109
3979
  },
4110
- [{ tool: "relations", description: "See connections", parameters: { action: "find", entryId: from } }]
3980
+ [{ tool: "context", description: "See connections", parameters: { action: "find", entryId: from } }]
4111
3981
  )
4112
3982
  };
4113
3983
  }
@@ -4152,7 +4022,7 @@ async function handleBatchCreate(relations) {
4152
4022
  lines.push(`- **${r.from}** \u2192 **${r.to}** (${r.type}): _${r.error}_`);
4153
4023
  }
4154
4024
  }
4155
- const next = failed.length === 0 ? [{ tool: "relations", description: "Check connections", parameters: { action: "find", entryId: relations[0].from } }] : [];
4025
+ const next = failed.length === 0 ? [{ tool: "context", description: "Check connections", parameters: { action: "find", entryId: relations[0].from } }] : [];
4156
4026
  return {
4157
4027
  content: [{ type: "text", text: lines.join("\n") }],
4158
4028
  structuredContent: success(
@@ -4173,7 +4043,7 @@ async function handleDelete(from, to, type) {
4173
4043
  `Removed relation: ${from} \u2014[${type}]\u2192 ${to}.`,
4174
4044
  `Relation ${from} \u2192 ${to} (${type}) removed.`,
4175
4045
  { from, to, type },
4176
- [{ tool: "relations", description: "Check remaining connections", parameters: { action: "find", entryId: from } }]
4046
+ [{ tool: "context", description: "Check remaining connections", parameters: { action: "find", entryId: from } }]
4177
4047
  );
4178
4048
  }
4179
4049
 
@@ -4181,16 +4051,16 @@ async function handleDelete(from, to, type) {
4181
4051
  import { z as z16 } from "zod/v3";
4182
4052
 
4183
4053
  // src/tools/documents.ts
4184
- import { z as z14 } from "zod/v3";
4054
+ import { z as z13 } from "zod/v3";
4185
4055
  var DOCUMENTS_ACTIONS = ["get-last-verified-brief"];
4186
- var documentsSchema = z14.object({
4187
- action: z14.enum(DOCUMENTS_ACTIONS).describe(
4056
+ var documentsSchema = z13.object({
4057
+ action: z13.enum(DOCUMENTS_ACTIONS).describe(
4188
4058
  "'get-last-verified-brief': fetch the most recent verified brief snapshot for a (templateId, scopeKey) pair. Returns the verified summary so agents can build delta narratives ('since you last verified, X workstreams advanced')."
4189
4059
  ),
4190
- templateId: z14.string().max(200).describe(
4060
+ templateId: z13.string().max(200).describe(
4191
4061
  "Brief template identifier \u2014 currently 'steering-brief' is the only registered template."
4192
4062
  ),
4193
- scopeKey: z14.string().max(200).describe(
4063
+ scopeKey: z13.string().max(200).describe(
4194
4064
  "Canonical scope key. Use 'workspace:<workspaceId>' for the full workspace brief, or 'initiative:<INI-ID>' for an initiative-scoped brief. The same formula is applied at write time (chainwork/docKernel/scopeKey.ts), so passing the wrong shape returns exists:false."
4195
4065
  )
4196
4066
  });
@@ -4260,9 +4130,130 @@ This row pre-dates snapshot capture (legacy backfill), so no summary is availabl
4260
4130
 
4261
4131
  // src/tools/contextLookups.ts
4262
4132
  import { z as z15 } from "zod/v3";
4133
+
4134
+ // src/tools/graph.ts
4135
+ import { z as z14 } from "zod/v3";
4136
+ var GRAPH_ACTIONS = ["find"];
4137
+ var graphSchema = z14.object({
4138
+ action: z14.enum(GRAPH_ACTIONS).describe(
4139
+ "'find': traverse relations from an entry (graph walk)."
4140
+ ),
4141
+ entryId: z14.string().max(200).describe("Entry ID, e.g. '<PREFIX>-<n>'"),
4142
+ direction: z14.enum(["incoming", "outgoing", "both"]).default("both").optional().describe("For find: 'incoming' = what references this, 'outgoing' = what this references")
4143
+ });
4144
+ var graphFindOutputSchema = z14.object({
4145
+ entryId: z14.string(),
4146
+ relations: z14.array(z14.object({
4147
+ entryId: z14.string().optional(),
4148
+ name: z14.string(),
4149
+ type: z14.string(),
4150
+ direction: z14.enum(["outgoing", "incoming"])
4151
+ })),
4152
+ total: z14.number()
4153
+ });
4154
+ async function handleFind(entryId, direction) {
4155
+ const relations = await kernelQuery("chain.listEntryRelations", { entryId });
4156
+ if (relations.length === 0) {
4157
+ return {
4158
+ content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relations action=create to create connections.` }],
4159
+ structuredContent: success(
4160
+ `No relations found for '${entryId}'.`,
4161
+ { entryId, relations: [], nodes: [], edges: [], total: 0 },
4162
+ [
4163
+ { tool: "relations", description: "Create a relation", parameters: { action: "create", from: entryId } }
4164
+ ]
4165
+ )
4166
+ };
4167
+ }
4168
+ const sourceEntry = await kernelQuery("chain.getEntry", { entryId });
4169
+ if (!sourceEntry) {
4170
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try context action=search to find the right ID.`);
4171
+ }
4172
+ const sourceInternalId = sourceEntry._id;
4173
+ const MAX_RELATIONS = 25;
4174
+ const truncated = relations.length > MAX_RELATIONS;
4175
+ const capped = relations.slice(0, MAX_RELATIONS);
4176
+ const otherIds = /* @__PURE__ */ new Set();
4177
+ for (const r of capped) {
4178
+ const otherId = r.fromId === sourceInternalId ? r.toId : r.fromId;
4179
+ otherIds.add(otherId);
4180
+ }
4181
+ const otherEntries = /* @__PURE__ */ new Map();
4182
+ for (const id of otherIds) {
4183
+ const entry = await kernelQuery("chain.getEntry", { id });
4184
+ if (entry) {
4185
+ otherEntries.set(entry._id, { entryId: entry.entryId, name: entry.name, collectionId: entry.collectionId });
4186
+ }
4187
+ }
4188
+ const collections = await kernelQuery("chain.listCollections");
4189
+ const collMap = /* @__PURE__ */ new Map();
4190
+ for (const c of collections) collMap.set(c._id, c.slug);
4191
+ const lines = [`## Find Result`, "", `# Relations for ${entryId}: ${sourceEntry.name}`, ""];
4192
+ const enriched = capped.map((r) => {
4193
+ const isOutgoing = r.fromId === sourceInternalId;
4194
+ const otherId = isOutgoing ? r.toId : r.fromId;
4195
+ const other = otherEntries.get(otherId);
4196
+ const otherLabel = other?.entryId ? `${other.entryId}: ${other.name}` : other?.name ?? "(deleted)";
4197
+ const colSlug = other ? collMap.get(other.collectionId) ?? "unknown" : "unknown";
4198
+ return { isOutgoing, type: r.type, otherLabel, colSlug, otherEntryId: other?.entryId, otherName: other?.name ?? "(deleted)" };
4199
+ });
4200
+ const outgoing = enriched.filter((r) => r.isOutgoing);
4201
+ const incoming = enriched.filter((r) => !r.isOutgoing);
4202
+ if ((direction === "outgoing" || direction === "both") && outgoing.length > 0) {
4203
+ lines.push(`## Outgoing (${outgoing.length})`);
4204
+ for (const r of outgoing) {
4205
+ lines.push(`- \u2192 **${r.type}** ${r.otherLabel} [${r.colSlug}]`);
4206
+ }
4207
+ lines.push("");
4208
+ }
4209
+ if ((direction === "incoming" || direction === "both") && incoming.length > 0) {
4210
+ lines.push(`## Incoming (${incoming.length})`);
4211
+ for (const r of incoming) {
4212
+ lines.push(`- \u2190 **${r.type}** ${r.otherLabel} [${r.colSlug}]`);
4213
+ }
4214
+ lines.push("");
4215
+ }
4216
+ const shown = direction === "outgoing" ? outgoing : direction === "incoming" ? incoming : enriched;
4217
+ if (shown.length === 0) {
4218
+ lines.push(`No ${direction} relations found for \`${entryId}\`.`);
4219
+ }
4220
+ if (truncated) {
4221
+ lines.push(`_Showing first ${MAX_RELATIONS} of ${relations.length} relations. Use context action=get for the full picture._`);
4222
+ }
4223
+ const structuredRelations = shown.map((r) => ({
4224
+ entryId: r.otherEntryId,
4225
+ name: r.otherName,
4226
+ type: r.type,
4227
+ direction: r.isOutgoing ? "outgoing" : "incoming"
4228
+ }));
4229
+ const nodes = [
4230
+ { id: entryId, name: sourceEntry.name, collectionName: "" },
4231
+ ...shown.map((r) => ({
4232
+ id: r.otherEntryId ?? String(Math.random()),
4233
+ name: r.otherName,
4234
+ collectionName: r.colSlug
4235
+ }))
4236
+ ];
4237
+ const edges = shown.filter((r) => r.otherEntryId).map((r) => ({
4238
+ source: entryId,
4239
+ target: r.otherEntryId,
4240
+ type: r.type
4241
+ }));
4242
+ return {
4243
+ content: [{ type: "text", text: lines.join("\n") }],
4244
+ structuredContent: success(
4245
+ `Found ${relations.length} relations for ${entryId}.`,
4246
+ { entryId, relations: structuredRelations, nodes, edges, total: relations.length },
4247
+ [{ tool: "context", description: "View entry details", parameters: { action: "get", entryId } }]
4248
+ )
4249
+ };
4250
+ }
4251
+
4252
+ // src/tools/contextLookups.ts
4253
+ var LOOKUP_ACTIONS = ["get", "batch", "history", "list", "search", "find"];
4263
4254
  var statusFilterSchema = z15.union([z15.string().max(200), z15.array(z15.enum(ENTRY_STATUS_READ_WORDS)).max(5)]);
4264
- var LOOKUP_ACTION_DESCRIBE = "'get': one entry in full by ID. 'batch': up to 20 entries by ID. 'history': audit trail for an entry. 'list': browse entries with filters (accepted by default). 'search': full-text search across entries. ";
4265
- var LOOKUP_TOOL_LINES = "- **get**: Fetch a single entry by ID \u2014 full record with data, labels, relations, history.\n- **batch**: Fetch multiple entries (max 20) in one call. Same shape as get per entry.\n- **history**: Audit trail for an entry.\n- **list**: Browse entries with optional filters (collection, status, tag, label). Use collections action=list first to discover slugs.\n- **search**: Full-text search across entries. Scope by collection or filter by status.\n";
4255
+ var LOOKUP_ACTION_DESCRIBE = "'get': one entry in full by ID. 'batch': up to 20 entries by ID. 'history': audit trail for an entry. 'list': browse entries with filters (accepted by default). 'search': full-text search across entries. 'find': an entry's direct relations (direction: incoming|outgoing|both, default both). ";
4256
+ var LOOKUP_TOOL_LINES = "- **get**: Fetch a single entry by ID \u2014 full record with data, labels, relations, history.\n- **batch**: Fetch multiple entries (max 20) in one call. Same shape as get per entry.\n- **history**: Audit trail for an entry.\n- **list**: Browse entries with optional filters (collection, status, tag, label). Use collections action=list first to discover slugs.\n- **search**: Full-text search across entries. Scope by collection or filter by status.\n- **find**: An entry's direct relations, incoming and outgoing. NOT full-text search (use search).\n";
4266
4257
  var lookupParams = {
4267
4258
  entryIds: z15.array(z15.string().max(200)).min(1).max(20).optional().describe("For 'batch': entry IDs, e.g. ['TYPE-strategy', 'STR-jljeg7']"),
4268
4259
  collection: z15.string().max(200).optional().describe("For 'list'/'search': collection slug, e.g. 'glossary', 'tracking-events'."),
@@ -4291,6 +4282,11 @@ var lookupVariants = [
4291
4282
  query: z15.string().min(2).max(500),
4292
4283
  collection: z15.string().max(200).optional(),
4293
4284
  status: statusFilterSchema.optional()
4285
+ }),
4286
+ z15.object({
4287
+ action: z15.literal("find"),
4288
+ entryId: z15.string().max(200),
4289
+ direction: z15.enum(["incoming", "outgoing", "both"]).optional().default("both")
4294
4290
  })
4295
4291
  ];
4296
4292
  var LOOKUP_ACTION_SPECS = {
@@ -4298,7 +4294,8 @@ var LOOKUP_ACTION_SPECS = {
4298
4294
  batch: { params: ["entryIds"], description: "entryIds (1-20) is required." },
4299
4295
  history: { params: ["entryId"], description: "entryId is required." },
4300
4296
  list: { params: ["collection", "status", "tag", "label"], description: "Browse accepted entries by default; status='draft' lists drafts; status is one word. All params optional." },
4301
- search: { params: ["query", "collection", "status"], description: "Full-text search; query is required (min 2 chars)." }
4297
+ search: { params: ["query", "collection", "status"], description: "Full-text search; query is required (min 2 chars)." },
4298
+ find: { params: ["entryId", "direction"], description: "entryId is required." }
4302
4299
  };
4303
4300
  function lookupHandlers(server) {
4304
4301
  return {
@@ -4306,8 +4303,9 @@ function lookupHandlers(server) {
4306
4303
  batch: (data) => handleBatch(data.entryIds),
4307
4304
  history: (data) => handleGetHistory(data),
4308
4305
  list: (data) => handleList(data.collection, data.status, data.tag, data.label),
4309
- search: (data) => handleSearch(server, data.query, data.collection, data.status)
4306
+ search: (data) => handleSearch(server, data.query, data.collection, data.status),
4310
4307
  // logs progress to the client
4308
+ find: (data) => handleFind(data.entryId, data.direction ?? "both")
4311
4309
  };
4312
4310
  }
4313
4311
 
@@ -4322,13 +4320,13 @@ function epistemicCollectionHint(collectionName) {
4322
4320
  if (lc === "assumptions") return " \u2014 _check evidence strength with `context action=get`_";
4323
4321
  return "";
4324
4322
  }
4325
- var CONTEXT_ACTIONS = ["get", "batch", "history", "list", "search", "gather", "build", "neighborhood", "changes", "chain", "cross-cut", "incremental", "brief", "last-verified-brief"];
4323
+ var CONTEXT_ACTIONS = [...LOOKUP_ACTIONS, "gather", "build", "neighborhood", "changes", "chain", "cross-cut", "incremental", "brief", "last-verified-brief"];
4326
4324
  var contextSchema = z16.object({
4327
4325
  // provenance: neighborhood BET-142; changes/chain/cross-cut/incremental/brief BET-239 (E4, E6)
4328
4326
  action: z16.enum(CONTEXT_ACTIONS).describe(
4329
4327
  LOOKUP_ACTION_DESCRIBE + "'gather': assemble knowledge context (entry graph, task auto-load, journey mode, or graph mode). 'build': structured build spec for an entry. 'neighborhood': typed graph neighborhood for an entry \u2014 blocking chain, dependencies, parent context, tensions, staleness. 'changes': entries modified and relations created since a timestamp. Requires 'since' parameter. 'chain': directed traversal along one relation type to depth 4. Requires entryId. Optional: direction, relationType, maxHops (1-4). 'cross-cut': structural aggregation \u2014 all relations of a given type grouped by source collection. Requires 'relationType' parameter. 'incremental': delta since last brief run for a skill. Requires 'skill' parameter. Returns only entries changed since the skill's last brief. 'brief': compound intelligence query. Requires 'briefType' parameter: 'steering' (changes + structure + delta + readiness), 'confidence' (changes + active bets \u2014 use the pb CLI's `tensions list` for tensions), or 'delta' (changes + relations since timestamp). Optional 'since' for delta type. 'last-verified-brief': fetch the most recent verified brief snapshot for a (templateId, scopeKey) pair (absorbs documents action=get-last-verified-brief). Requires templateId and scopeKey."
4330
4328
  ),
4331
- entryId: z16.string().max(200).optional().describe("For 'get'/'history'/'build'/'neighborhood'/'chain': entry ID, e.g. '<PREFIX>-<n>'. For 'gather': optional entry ID for entry-graph mode."),
4329
+ entryId: z16.string().max(200).optional().describe("For 'get'/'history'/'find'/'build'/'neighborhood'/'chain': entry ID, e.g. '<PREFIX>-<n>'. For 'gather': optional (entry-graph mode)."),
4332
4330
  ...lookupParams,
4333
4331
  mapEntryId: z16.string().max(200).optional().describe(
4334
4332
  "For 'gather': journey map entry ID for journey-aware context. Returns context organised by journey stage. Takes precedence over entryId when both are supplied. Example: '<PREFIX>-<n>'."
@@ -4337,7 +4335,7 @@ var contextSchema = z16.object({
4337
4335
  since: z16.string().max(200).optional().describe(
4338
4336
  "For 'changes': ISO 8601 timestamp \u2014 returns entries/relations modified since this time. For 'brief' briefType='delta': optional custom timestamp. Example: '2026-03-24T00:00:00Z'."
4339
4337
  ),
4340
- direction: z16.enum(["outgoing", "incoming"]).default("outgoing").optional().describe("For 'chain' action: traversal direction. 'outgoing' follows relations from source, 'incoming' follows relations to source. Default: outgoing."),
4338
+ direction: z16.enum(["outgoing", "incoming", "both"]).optional().describe("For 'chain': 'outgoing' follows relations from source, 'incoming' to source (default outgoing). For 'find': incoming|outgoing|both (default both)."),
4341
4339
  relationType: z16.string().max(200).optional().describe(
4342
4340
  "Relation type filter. For 'chain': optional filter to traverse only this relation type. For 'cross-cut': required \u2014 scans all relations of this type across the workspace. Examples: 'part_of', 'informs', 'governs', 'depends_on'."
4343
4341
  ),
@@ -9347,7 +9345,7 @@ async function handleGetRun(runId, workflowId) {
9347
9345
  run,
9348
9346
  run.summary ? [
9349
9347
  {
9350
- tool: "relations",
9348
+ tool: "context",
9351
9349
  description: "See the summary entry's connections",
9352
9350
  parameters: { action: "find", entryId: run.summary.entryId }
9353
9351
  }
@@ -9552,7 +9550,7 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
9552
9550
  parameters: { action: "commit", entryId: result2.summary.entryId }
9553
9551
  },
9554
9552
  {
9555
- tool: "relations",
9553
+ tool: "context",
9556
9554
  description: "See its connections",
9557
9555
  parameters: { action: "find", entryId: result2.summary.entryId }
9558
9556
  }
@@ -9728,7 +9726,7 @@ async function handleFacilitatedFinalization(wf, descriptor, roundId, normalized
9728
9726
  },
9729
9727
  [
9730
9728
  {
9731
- tool: "relations",
9729
+ tool: "context",
9732
9730
  description: "See its connections",
9733
9731
  parameters: { action: "find", entryId: summaryEntryId }
9734
9732
  }
@@ -12770,8 +12768,8 @@ var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
12770
12768
  | \`orient\` | Workspace context, governance, active work packages, session start | \`action\`: start, task |
12771
12769
  | \`capture\` | Create entry | \`action\`: capture |
12772
12770
  | \`entries\` | Write entries | \`action\`: update, commit, move, verify, preflight, still-true |
12773
- | \`relations\` | Link entries, delete links, find connections | \`action\`: create, batch-create, delete, find |
12774
- | \`context\` | Reads; only incremental writes | \`action\`: get, batch, history, list, search, gather, build, neighborhood, changes, chain, cross-cut, incremental, brief, last-verified-brief |
12771
+ | \`relations\` | Link entries, delete links | \`action\`: create, batch-create, delete |
12772
+ | \`context\` | Reads; only incremental writes | \`action\`: get, batch, history, list, search, find, gather, build, neighborhood, changes, chain, cross-cut, incremental, brief, last-verified-brief |
12775
12773
  | \`collections\` | Browse, describe, audit and export collections; manage labels | \`action\`: list, update, describe, audit, export, label-* |
12776
12774
  | \`quality\` | Score an entry, verify the Chain, spine check | \`action\`: check, re-evaluate, verify-chain, spine-check |
12777
12775
  | \`session\` | Start / close, wrapup, resume, constellation accept | \`action\`: start, close, status, wrapup-review, wrapup-commit, resume, commit-constellation |
@@ -12849,7 +12847,7 @@ Tags for filtering (e.g. \`severity:high\`), relations via \`entryRelations\`, v
12849
12847
  '## Rules\nThe rules that bind you are in the briefing above, each with its why. A rule on no list binds nobody.\nOpen one: `context action=get entryId="<ID>"`. Find one: `context action=search query="<noun>"`.\nDraft a new rule: use the `draft-rule-from-context` prompt.'
12850
12848
  );
12851
12849
  sections.push(
12852
- "## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept (typed successor: `anchored_to`)\n- `informs` \u2014 a decision or insight informs a feature\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `context action=gather` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `relations action=create` \u2014 create a typed link between two entries\n- `relations action=find` \u2014 list direct relations for an entry\n\n**Convention:** A relation is written only when you ask for it, with exactly the verb you send \u2014 link the entries you know are related with `relations action=create`."
12850
+ "## Knowledge Graph\nEntries are connected via typed relations (`entryRelations` table). Relations are bidirectional and collection-agnostic \u2014 any entry can link to any other entry.\n\n**Recommended relation types** (extensible \u2014 any string accepted):\n- `governs` \u2014 a rule constrains behavior of a feature\n- `defines_term_for` \u2014 a glossary term is canonical vocabulary for a feature/area\n- `belongs_to` \u2014 a feature belongs to a product area or parent concept (typed successor: `anchored_to`)\n- `informs` \u2014 a decision or insight informs a feature\n- `surfaces_tension_in` \u2014 a tension exists within a feature area\n- `related_to`, `depends_on`, `replaces`, `conflicts_with`, `references`, `confused_with`\n\nEach relation type is defined as a glossary entry (prefix `GT-REL-*`) to prevent terminology drift.\n\n**Tools:**\n- `context action=gather` \u2014 get the full context around any entry (multi-hop graph traversal)\n- `relations action=create` \u2014 create a typed link between two entries\n- `context action=find` \u2014 list direct relations for an entry\n\n**Convention:** A relation is written only when you ask for it, with exactly the verb you send \u2014 link the entries you know are related with `relations action=create`."
12853
12851
  );
12854
12852
  sections.push(
12855
12853
  `## Creating Knowledge
@@ -13552,4 +13550,4 @@ export {
13552
13550
  createProductBrainServer,
13553
13551
  initFeatureFlags
13554
13552
  };
13555
- //# sourceMappingURL=chunk-O6HEYDXJ.js.map
13553
+ //# sourceMappingURL=chunk-DT756ONZ.js.map