agent-planner-mcp 0.9.0 → 1.0.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.
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * BDI intentions — committed actions.
3
3
  *
4
- * v0.9.0 ships queue_decision, resolve_decision, and update_task first.
5
- * Other intention tools (claim_next_task, release_task, add_learning) land in
6
- * subsequent passes.
4
+ * v0.9.0 baseline (execution): queue_decision, resolve_decision, update_task,
5
+ * claim_next_task, release_task, add_learning.
6
+ *
7
+ * v1.0.0 additions (creation, mutation, collaboration):
8
+ * - form_intention, extend_intention, propose_research_chain
9
+ * - link_intentions, unlink_intentions
10
+ * - update_plan, update_node, move_node, delete_plan, delete_node
11
+ * - share_plan, invite_member, update_member_role, remove_member
12
+ *
13
+ * See ../../../docs/MCP_v1.0_FULL_SURFACE.md for design rationale.
7
14
  */
8
15
 
9
16
  const { asOf, formatResponse, errorResponse } = require('./_shared');
@@ -58,13 +65,30 @@ const queueDecisionDefinition = {
58
65
  default: 'normal',
59
66
  },
60
67
  goal_id: { type: 'string', description: 'Optional goal this decision serves' },
68
+ proposed_subtasks: {
69
+ type: 'array',
70
+ description: "Tasks to materialize if the human approves. Agents propose; humans steer structure. On resolve_decision(action='approve'), these are atomically created under the given parent_id and their IDs are returned.",
71
+ items: {
72
+ type: 'object',
73
+ properties: {
74
+ parent_id: { type: 'string', description: 'Where to attach. Must be a node the agent already has access to.' },
75
+ title: { type: 'string' },
76
+ description: { type: 'string' },
77
+ node_type: { type: 'string', enum: ['phase', 'task', 'milestone'], default: 'task' },
78
+ task_mode: { type: 'string', enum: ['research', 'plan', 'implement', 'free'], default: 'free' },
79
+ agent_instructions: { type: 'string' },
80
+ acceptance_criteria: { type: 'string' },
81
+ },
82
+ required: ['parent_id', 'title'],
83
+ },
84
+ },
61
85
  },
62
86
  required: ['title', 'context', 'smallest_input_needed'],
63
87
  },
64
88
  };
65
89
 
66
90
  async function queueDecisionHandler(args, apiClient) {
67
- const { plan_id, node_id, title, context, options, recommendation, smallest_input_needed, urgency, goal_id } = args;
91
+ const { plan_id, node_id, title, context, options, recommendation, smallest_input_needed, urgency, goal_id, proposed_subtasks } = args;
68
92
 
69
93
  let planId = plan_id;
70
94
  if (!planId && node_id) {
@@ -85,7 +109,12 @@ async function queueDecisionHandler(args, apiClient) {
85
109
  options: options || [],
86
110
  recommendation: recommendation || null,
87
111
  urgency: urgency || 'normal',
88
- metadata: { smallest_input_needed, goal_id: goal_id || null, source: 'bdi.queue_decision' },
112
+ metadata: {
113
+ smallest_input_needed,
114
+ goal_id: goal_id || null,
115
+ source: 'bdi.queue_decision',
116
+ proposed_subtasks: Array.isArray(proposed_subtasks) ? proposed_subtasks : undefined,
117
+ },
89
118
  };
90
119
  if (node_id) body.node_id = node_id;
91
120
 
@@ -142,28 +171,70 @@ const resolveDecisionDefinition = {
142
171
 
143
172
  async function resolveDecisionHandler(args, apiClient) {
144
173
  const { decision_id, plan_id, action, message, selected_option } = args;
174
+
175
+ // Fetch the decision first so we can read proposed_subtasks if any.
176
+ let decision = null;
145
177
  try {
146
- const resolved = await apiClient.axiosInstance
178
+ decision = await apiClient.axiosInstance
179
+ .get(`/plans/${plan_id}/decisions/${decision_id}`)
180
+ .then((r) => r.data);
181
+ } catch (err) {
182
+ // Best-effort — if fetch fails, we still try to resolve.
183
+ }
184
+
185
+ let resolved;
186
+ try {
187
+ resolved = await apiClient.axiosInstance
147
188
  .post(`/plans/${plan_id}/decisions/${decision_id}/resolve`, {
148
189
  resolution: action,
149
190
  message: message || null,
150
191
  selected_option: selected_option || null,
151
192
  })
152
193
  .then((r) => r.data);
153
- return formatResponse({
154
- as_of: asOf(),
155
- decision_id,
156
- plan_id,
157
- status: resolved.status || action,
158
- resolved_at: resolved.resolved_at || asOf(),
159
- message: resolved.message || message || null,
160
- });
161
194
  } catch (err) {
162
195
  return errorResponse(
163
196
  'upstream_unavailable',
164
197
  `Failed to resolve decision: ${err.response?.data?.error || err.message}`
165
198
  );
166
199
  }
200
+
201
+ // On approve, materialize any proposed_subtasks atomically (best-effort per task).
202
+ const created = [];
203
+ const createFailures = [];
204
+ if (action === 'approve' && decision?.metadata?.proposed_subtasks?.length) {
205
+ for (const proposal of decision.metadata.proposed_subtasks) {
206
+ try {
207
+ const node = await apiClient.nodes.createNode(plan_id, {
208
+ parent_id: proposal.parent_id,
209
+ node_type: proposal.node_type || 'task',
210
+ title: proposal.title,
211
+ description: proposal.description,
212
+ status: 'not_started',
213
+ task_mode: proposal.task_mode || 'free',
214
+ agent_instructions: proposal.agent_instructions,
215
+ acceptance_criteria: proposal.acceptance_criteria,
216
+ });
217
+ created.push({ id: node.id || node.node?.id, title: proposal.title, parent_id: proposal.parent_id });
218
+ } catch (err) {
219
+ createFailures.push({
220
+ title: proposal.title,
221
+ parent_id: proposal.parent_id,
222
+ error: err.response?.data?.error || err.message,
223
+ });
224
+ }
225
+ }
226
+ }
227
+
228
+ return formatResponse({
229
+ as_of: asOf(),
230
+ decision_id,
231
+ plan_id,
232
+ status: resolved.status || action,
233
+ resolved_at: resolved.resolved_at || asOf(),
234
+ message: resolved.message || message || null,
235
+ created_subtasks: created,
236
+ create_failures: createFailures,
237
+ });
167
238
  }
168
239
 
169
240
  // ─────────────────────────────────────────────────────────────────────────
@@ -320,13 +391,18 @@ const claimNextTaskDefinition = {
320
391
  ttl_minutes: { type: 'integer', default: 30 },
321
392
  fresh: { type: 'boolean', default: false },
322
393
  context_depth: { type: 'integer', enum: [1, 2, 3, 4], default: 2 },
394
+ dry_run: {
395
+ type: 'boolean',
396
+ default: false,
397
+ description: "If true, return the candidate task without claiming. Lets the caller peek before committing. No phantom claim left behind.",
398
+ },
323
399
  },
324
400
  required: ['scope'],
325
401
  },
326
402
  };
327
403
 
328
404
  async function claimNextTaskHandler(args, apiClient) {
329
- const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2 } = args;
405
+ const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2, dry_run = false } = args;
330
406
  const { plan_id, goal_id } = scope;
331
407
 
332
408
  let chosen = null;
@@ -377,6 +453,24 @@ async function claimNextTaskHandler(args, apiClient) {
377
453
  const taskPlanId = chosen.plan_id || plan_id;
378
454
  const taskId = chosen.id;
379
455
 
456
+ // Dry run: return candidate without claiming.
457
+ if (dry_run) {
458
+ return formatResponse({
459
+ as_of: asOf(),
460
+ candidate: {
461
+ id: taskId,
462
+ title: chosen.title,
463
+ status: chosen.status,
464
+ plan_id: taskPlanId,
465
+ task_mode: chosen.task_mode,
466
+ },
467
+ source,
468
+ claim: null,
469
+ dry_run: true,
470
+ next_action_hint: 'Call again with dry_run=false to claim, or pick a different task.',
471
+ });
472
+ }
473
+
380
474
  // Claim
381
475
  let claim = null;
382
476
  try {
@@ -512,6 +606,976 @@ async function addLearningHandler(args, apiClient) {
512
606
  }
513
607
  }
514
608
 
609
+ // ─────────────────────────────────────────────────────────────────────────
610
+ // form_intention — create plan + initial tree atomically (v1.0).
611
+ // ─────────────────────────────────────────────────────────────────────────
612
+
613
+ const VALID_NODE_TYPES = ['phase', 'task', 'milestone'];
614
+ const VALID_TASK_MODES = ['free', 'research', 'plan', 'implement'];
615
+
616
+ function validateTreeShape(tree, depth = 0) {
617
+ if (!Array.isArray(tree)) {
618
+ return 'tree must be an array';
619
+ }
620
+ for (const node of tree) {
621
+ if (!node || typeof node !== 'object') return 'tree node must be an object';
622
+ if (!node.title) return 'tree node missing title';
623
+ if (node.node_type && !VALID_NODE_TYPES.includes(node.node_type)) {
624
+ return `invalid node_type "${node.node_type}" — must be one of ${VALID_NODE_TYPES.join(', ')}`;
625
+ }
626
+ if (node.task_mode && !VALID_TASK_MODES.includes(node.task_mode)) {
627
+ return `invalid task_mode "${node.task_mode}"`;
628
+ }
629
+ if (node.children) {
630
+ const err = validateTreeShape(node.children, depth + 1);
631
+ if (err) return err;
632
+ }
633
+ }
634
+ return null;
635
+ }
636
+
637
+ const formIntentionDefinition = {
638
+ name: 'form_intention',
639
+ description:
640
+ "Create a plan that achieves a goal, including an initial phase/task " +
641
+ "tree, in one call. Defaults to status='active' for human-directed " +
642
+ "creation; pass status='draft' for autonomous loops so a human can " +
643
+ "review before promotion. Drafts surface in the dashboard pending " +
644
+ "queue and auto-promote to active when work begins on any node.",
645
+ inputSchema: {
646
+ type: 'object',
647
+ properties: {
648
+ goal_id: { type: 'string', description: "Goal this plan serves." },
649
+ title: { type: 'string' },
650
+ description: { type: 'string' },
651
+ rationale: { type: 'string', description: "Why this plan. Surfaces in human review when status=draft." },
652
+ status: {
653
+ type: 'string',
654
+ enum: ['draft', 'active'],
655
+ default: 'active',
656
+ },
657
+ visibility: {
658
+ type: 'string',
659
+ enum: ['private', 'unlisted', 'public'],
660
+ default: 'private',
661
+ },
662
+ tree: {
663
+ type: 'array',
664
+ description: "Recursive tree of nodes (phases, tasks, milestones). Children nest under parents via the 'children' array.",
665
+ items: {
666
+ type: 'object',
667
+ properties: {
668
+ node_type: { type: 'string', enum: VALID_NODE_TYPES, default: 'task' },
669
+ title: { type: 'string' },
670
+ description: { type: 'string' },
671
+ task_mode: { type: 'string', enum: VALID_TASK_MODES, default: 'free' },
672
+ agent_instructions: { type: 'string' },
673
+ children: { type: 'array' },
674
+ },
675
+ required: ['title'],
676
+ },
677
+ },
678
+ },
679
+ required: ['goal_id', 'title', 'rationale'],
680
+ },
681
+ };
682
+
683
+ async function createSubtree(apiClient, planId, parentId, children, results) {
684
+ for (const child of children || []) {
685
+ let createdNode;
686
+ try {
687
+ const payload = {
688
+ node_type: child.node_type || 'task',
689
+ title: child.title,
690
+ description: child.description || '',
691
+ task_mode: child.task_mode || 'free',
692
+ };
693
+ if (parentId) payload.parent_id = parentId;
694
+ if (child.agent_instructions) payload.agent_instructions = child.agent_instructions;
695
+
696
+ const resp = await apiClient.nodes.createNode(planId, payload);
697
+ // createNode returns { result, created } — unwrap.
698
+ createdNode = resp.result || resp;
699
+ results.push({ id: createdNode.id, title: createdNode.title, node_type: createdNode.node_type });
700
+ } catch (err) {
701
+ results.push({ title: child.title, error: err.response?.data?.error || err.message });
702
+ continue;
703
+ }
704
+
705
+ if (child.children?.length && createdNode?.id) {
706
+ await createSubtree(apiClient, planId, createdNode.id, child.children, results);
707
+ }
708
+ }
709
+ }
710
+
711
+ async function formIntentionHandler(args, apiClient) {
712
+ const { goal_id, title, description, rationale, status = 'active', visibility = 'private', tree = [] } = args;
713
+
714
+ // Validate goal exists.
715
+ let goal;
716
+ try {
717
+ goal = await apiClient.goals.get(goal_id);
718
+ } catch (err) {
719
+ return errorResponse('not_found', `Goal ${goal_id} not found or not accessible: ${err.message}`);
720
+ }
721
+
722
+ // Validate tree shape upfront (atomic-ish — fail before partial creation).
723
+ const treeError = validateTreeShape(tree);
724
+ if (treeError) {
725
+ return errorResponse('tree_shape_invalid', treeError);
726
+ }
727
+
728
+ // Compose plan description (rationale + optional description).
729
+ const composedDescription = description ? `${rationale}\n\n${description}` : rationale;
730
+
731
+ // 1. Create plan. Visibility goes through a separate endpoint —
732
+ // POST /plans schema is .strict() and only accepts title, description, status, metadata.
733
+ let plan;
734
+ try {
735
+ plan = await apiClient.plans.createPlan({
736
+ title,
737
+ description: composedDescription,
738
+ status,
739
+ });
740
+ } catch (err) {
741
+ return errorResponse('create_failed', `Failed to create plan: ${err.response?.data?.error || err.message}`);
742
+ }
743
+
744
+ // 2. Apply non-default visibility (separate endpoint).
745
+ if (visibility && visibility !== 'private') {
746
+ try {
747
+ await apiClient.plans.updateVisibility(plan.id, { visibility });
748
+ } catch (err) {
749
+ // Non-fatal — plan exists with default visibility, caller can retry.
750
+ }
751
+ }
752
+
753
+ // 3. Link plan to goal (best-effort).
754
+ try {
755
+ await apiClient.goals.linkPlan(goal_id, plan.id);
756
+ } catch (err) {
757
+ // Non-fatal — plan exists, link can be retried by user.
758
+ }
759
+
760
+ // 3. Create tree (top-level children parent to root via omitted parent_id).
761
+ const nodeResults = [];
762
+ await createSubtree(apiClient, plan.id, null, tree, nodeResults);
763
+
764
+ return formatResponse({
765
+ as_of: asOf(),
766
+ plan_id: plan.id,
767
+ goal_id,
768
+ status: plan.status,
769
+ is_draft: plan.status === 'draft',
770
+ nodes_created: nodeResults.filter((n) => n.id).length,
771
+ node_failures: nodeResults.filter((n) => n.error),
772
+ nodes: nodeResults,
773
+ next_step: plan.status === 'draft'
774
+ ? "Plan created as draft. Will surface in dashboard pending for human review. Auto-promotes to active when first task moves to in_progress."
775
+ : "Plan active. Claim a task with claim_next_task({plan_id}) to begin work.",
776
+ });
777
+ }
778
+
779
+ // ─────────────────────────────────────────────────────────────────────────
780
+ // extend_intention — add children under an existing parent (v1.0).
781
+ // Lightweight — does not go through the decision queue.
782
+ // ─────────────────────────────────────────────────────────────────────────
783
+
784
+ const extendIntentionDefinition = {
785
+ name: 'extend_intention',
786
+ description:
787
+ "Add children under an existing phase or task. Use when an agent has " +
788
+ "implicit authority to decompose work (e.g., a parent task they have " +
789
+ "claimed). For high-stakes structural proposals, use queue_decision " +
790
+ "with proposed_subtasks instead. Defaults to status='active'.",
791
+ inputSchema: {
792
+ type: 'object',
793
+ properties: {
794
+ parent_id: { type: 'string', description: "Phase or task to add children under." },
795
+ plan_id: { type: 'string', description: "Plan that owns the parent (auto-resolved if omitted)." },
796
+ rationale: { type: 'string', description: "Why these children. Stored in metadata for audit." },
797
+ children: {
798
+ type: 'array',
799
+ items: {
800
+ type: 'object',
801
+ properties: {
802
+ node_type: { type: 'string', enum: VALID_NODE_TYPES, default: 'task' },
803
+ title: { type: 'string' },
804
+ description: { type: 'string' },
805
+ task_mode: { type: 'string', enum: VALID_TASK_MODES, default: 'free' },
806
+ agent_instructions: { type: 'string' },
807
+ children: { type: 'array' },
808
+ },
809
+ required: ['title'],
810
+ },
811
+ },
812
+ },
813
+ required: ['parent_id', 'rationale', 'children'],
814
+ },
815
+ };
816
+
817
+ async function extendIntentionHandler(args, apiClient) {
818
+ const { parent_id, rationale, children = [] } = args;
819
+ let { plan_id } = args;
820
+
821
+ // Resolve plan_id from parent if not provided.
822
+ if (!plan_id) {
823
+ try {
824
+ const parent = await apiClient.axiosInstance.get(`/nodes/${parent_id}`).then((r) => r.data);
825
+ plan_id = parent.plan_id || parent.planId;
826
+ } catch (err) {
827
+ return errorResponse('not_found', `Could not resolve plan_id from parent ${parent_id}: ${err.message}`);
828
+ }
829
+ }
830
+
831
+ const treeError = validateTreeShape(children);
832
+ if (treeError) {
833
+ return errorResponse('tree_shape_invalid', treeError);
834
+ }
835
+
836
+ const nodeResults = [];
837
+ await createSubtree(apiClient, plan_id, parent_id, children, nodeResults);
838
+
839
+ return formatResponse({
840
+ as_of: asOf(),
841
+ plan_id,
842
+ parent_id,
843
+ rationale,
844
+ nodes_created: nodeResults.filter((n) => n.id).length,
845
+ node_failures: nodeResults.filter((n) => n.error),
846
+ nodes: nodeResults,
847
+ });
848
+ }
849
+
850
+ // ─────────────────────────────────────────────────────────────────────────
851
+ // propose_research_chain — RPI shortcut (v1.0).
852
+ // Creates Research → Plan → Implement under parent with two blocking edges.
853
+ // ─────────────────────────────────────────────────────────────────────────
854
+
855
+ const proposeResearchChainDefinition = {
856
+ name: 'propose_research_chain',
857
+ description:
858
+ "Create a Research → Plan → Implement triple under an existing parent " +
859
+ "task or phase. The Research task feeds Plan; Plan feeds Implement " +
860
+ "(via 'blocks' dependency edges). Use when tackling work with " +
861
+ "significant unknowns. Defaults to status='active'.",
862
+ inputSchema: {
863
+ type: 'object',
864
+ properties: {
865
+ parent_id: { type: 'string', description: "Parent task or phase the chain attaches to." },
866
+ plan_id: { type: 'string', description: "Plan that owns the parent (auto-resolved if omitted)." },
867
+ research_question: { type: 'string', description: "What the Research task investigates." },
868
+ implementation_target: { type: 'string', description: "What the Implement task ultimately produces." },
869
+ rationale: { type: 'string', description: "Why an RPI chain is appropriate here." },
870
+ },
871
+ required: ['parent_id', 'research_question', 'implementation_target', 'rationale'],
872
+ },
873
+ };
874
+
875
+ async function proposeResearchChainHandler(args, apiClient) {
876
+ const { parent_id, research_question, implementation_target, rationale } = args;
877
+ let { plan_id } = args;
878
+
879
+ if (!plan_id) {
880
+ try {
881
+ const parent = await apiClient.axiosInstance.get(`/nodes/${parent_id}`).then((r) => r.data);
882
+ plan_id = parent.plan_id || parent.planId;
883
+ } catch (err) {
884
+ return errorResponse('not_found', `Could not resolve plan_id from parent ${parent_id}: ${err.message}`);
885
+ }
886
+ }
887
+
888
+ const created = {};
889
+ const failures = [];
890
+
891
+ // 1. Create the three tasks.
892
+ for (const [key, spec] of [
893
+ ['research', { title: `Research: ${research_question}`, description: research_question, task_mode: 'research' }],
894
+ ['plan', { title: `Plan: ${implementation_target}`, description: `Plan implementation based on research findings`, task_mode: 'plan' }],
895
+ ['implement', { title: `Implement: ${implementation_target}`, description: implementation_target, task_mode: 'implement' }],
896
+ ]) {
897
+ try {
898
+ const resp = await apiClient.nodes.createNode(plan_id, {
899
+ node_type: 'task',
900
+ title: spec.title,
901
+ description: spec.description,
902
+ task_mode: spec.task_mode,
903
+ parent_id,
904
+ });
905
+ created[key] = resp.result || resp;
906
+ } catch (err) {
907
+ failures.push({ step: `create_${key}`, error: err.response?.data?.error || err.message });
908
+ }
909
+ }
910
+
911
+ if (failures.length) {
912
+ return formatResponse({
913
+ as_of: asOf(),
914
+ plan_id,
915
+ parent_id,
916
+ partial: true,
917
+ created: Object.fromEntries(Object.entries(created).map(([k, v]) => [k, { id: v.id, title: v.title }])),
918
+ failures,
919
+ });
920
+ }
921
+
922
+ // 2. Create the two blocking edges: research blocks plan, plan blocks implement.
923
+ const edges = [];
924
+ for (const [from, to] of [
925
+ [created.research.id, created.plan.id],
926
+ [created.plan.id, created.implement.id],
927
+ ]) {
928
+ try {
929
+ await apiClient.axiosInstance.post('/dependencies', {
930
+ source_node_id: from,
931
+ target_node_id: to,
932
+ dependency_type: 'blocks',
933
+ });
934
+ edges.push({ from, to, relation: 'blocks' });
935
+ } catch (err) {
936
+ failures.push({ step: 'create_edge', error: err.response?.data?.error || err.message, from, to });
937
+ }
938
+ }
939
+
940
+ return formatResponse({
941
+ as_of: asOf(),
942
+ plan_id,
943
+ parent_id,
944
+ rationale,
945
+ research: { id: created.research.id, title: created.research.title },
946
+ plan: { id: created.plan.id, title: created.plan.title },
947
+ implement: { id: created.implement.id, title: created.implement.title },
948
+ edges,
949
+ failures,
950
+ next_step: "Claim the Research task with claim_next_task({plan_id}) to begin investigation.",
951
+ });
952
+ }
953
+
954
+ // ─────────────────────────────────────────────────────────────────────────
955
+ // link_intentions — create dependency edge between two existing tasks (v1.0).
956
+ // Cycle detection happens server-side; we surface the 409 cleanly.
957
+ // ─────────────────────────────────────────────────────────────────────────
958
+
959
+ const VALID_RELATIONS = ['blocks', 'requires', 'relates_to'];
960
+
961
+ const linkIntentionsDefinition = {
962
+ name: 'link_intentions',
963
+ description:
964
+ "Create a dependency edge between two existing tasks. Use to express " +
965
+ "discovered ordering constraints (e.g., agent realizes task B requires " +
966
+ "task A's output). Server rejects cycles. Both tasks must be in the " +
967
+ "same plan.",
968
+ inputSchema: {
969
+ type: 'object',
970
+ properties: {
971
+ from_task_id: { type: 'string' },
972
+ to_task_id: { type: 'string' },
973
+ relation: { type: 'string', enum: VALID_RELATIONS, default: 'blocks' },
974
+ rationale: { type: 'string', description: "Why this link. Stored in dependency metadata." },
975
+ },
976
+ required: ['from_task_id', 'to_task_id', 'rationale'],
977
+ },
978
+ };
979
+
980
+ async function linkIntentionsHandler(args, apiClient) {
981
+ const { from_task_id, to_task_id, relation = 'blocks', rationale } = args;
982
+
983
+ if (from_task_id === to_task_id) {
984
+ return errorResponse('invalid_argument', 'from_task_id and to_task_id must differ');
985
+ }
986
+
987
+ // Resolve plan_id from the source task; validate target is in the same plan.
988
+ let planId, fromPlan, toPlan;
989
+ try {
990
+ const fromNode = await apiClient.axiosInstance.get(`/nodes/${from_task_id}`).then((r) => r.data);
991
+ fromPlan = fromNode.plan_id || fromNode.planId;
992
+ planId = fromPlan;
993
+ } catch (err) {
994
+ return errorResponse('not_found', `from_task ${from_task_id} not found: ${err.message}`);
995
+ }
996
+ try {
997
+ const toNode = await apiClient.axiosInstance.get(`/nodes/${to_task_id}`).then((r) => r.data);
998
+ toPlan = toNode.plan_id || toNode.planId;
999
+ } catch (err) {
1000
+ return errorResponse('not_found', `to_task ${to_task_id} not found: ${err.message}`);
1001
+ }
1002
+
1003
+ if (fromPlan !== toPlan) {
1004
+ return errorResponse('cross_plan_unsupported', `Both tasks must be in the same plan (from: ${fromPlan}, to: ${toPlan}). Cross-plan links require a separate API.`);
1005
+ }
1006
+
1007
+ try {
1008
+ const dep = await apiClient.axiosInstance.post(`/plans/${planId}/dependencies`, {
1009
+ source_node_id: from_task_id,
1010
+ target_node_id: to_task_id,
1011
+ dependency_type: relation,
1012
+ metadata: { rationale },
1013
+ }).then((r) => r.data);
1014
+
1015
+ return formatResponse({
1016
+ as_of: asOf(),
1017
+ dependency_id: dep.id,
1018
+ plan_id: planId,
1019
+ from_task_id,
1020
+ to_task_id,
1021
+ relation,
1022
+ });
1023
+ } catch (err) {
1024
+ const status = err.response?.status;
1025
+ const upstream = err.response?.data?.error || err.message;
1026
+ if (status === 409) {
1027
+ return errorResponse('cycle_detected', `Edge rejected — would create a cycle: ${upstream}`);
1028
+ }
1029
+ return errorResponse('create_failed', `Failed to create dependency: ${upstream}`);
1030
+ }
1031
+ }
1032
+
1033
+ // ─────────────────────────────────────────────────────────────────────────
1034
+ // unlink_intentions — remove a dependency edge (v1.0).
1035
+ // ─────────────────────────────────────────────────────────────────────────
1036
+
1037
+ const unlinkIntentionsDefinition = {
1038
+ name: 'unlink_intentions',
1039
+ description: "Remove a dependency edge by id.",
1040
+ inputSchema: {
1041
+ type: 'object',
1042
+ properties: {
1043
+ dependency_id: { type: 'string' },
1044
+ plan_id: { type: 'string', description: "Plan that owns the dependency. Required for the route." },
1045
+ reason: { type: 'string', description: "Why removed. Logged for audit." },
1046
+ },
1047
+ required: ['dependency_id', 'plan_id'],
1048
+ },
1049
+ };
1050
+
1051
+ async function unlinkIntentionsHandler(args, apiClient) {
1052
+ const { dependency_id, plan_id, reason } = args;
1053
+
1054
+ try {
1055
+ await apiClient.axiosInstance.delete(`/plans/${plan_id}/dependencies/${dependency_id}`);
1056
+ return formatResponse({
1057
+ as_of: asOf(),
1058
+ dependency_id,
1059
+ plan_id,
1060
+ reason: reason || null,
1061
+ removed: true,
1062
+ });
1063
+ } catch (err) {
1064
+ const upstream = err.response?.data?.error || err.message;
1065
+ if (err.response?.status === 404) {
1066
+ return errorResponse('not_found', `Dependency ${dependency_id} not found in plan ${plan_id}`);
1067
+ }
1068
+ return errorResponse('delete_failed', `Failed to remove dependency: ${upstream}`);
1069
+ }
1070
+ }
1071
+
1072
+ // ─────────────────────────────────────────────────────────────────────────
1073
+ // update_plan — edit any plan property atomically (v1.0).
1074
+ // Status changes route here; visibility, github linkage, metadata, etc.
1075
+ // ─────────────────────────────────────────────────────────────────────────
1076
+
1077
+ const updatePlanDefinition = {
1078
+ name: 'update_plan',
1079
+ description:
1080
+ "Edit any plan property atomically: title, description, status, " +
1081
+ "visibility, GitHub linkage, metadata. Use status='archived' to " +
1082
+ "soft-delete (recoverable via status='active' + restore=true). " +
1083
+ "Hard delete stays REST-only with admin auth.",
1084
+ inputSchema: {
1085
+ type: 'object',
1086
+ properties: {
1087
+ plan_id: { type: 'string' },
1088
+ title: { type: 'string' },
1089
+ description: { type: 'string' },
1090
+ status: {
1091
+ type: 'string',
1092
+ enum: ['draft', 'active', 'completed', 'archived'],
1093
+ },
1094
+ visibility: { type: 'string', enum: ['private', 'unlisted', 'public'] },
1095
+ metadata: { type: 'object', description: "Shallow-merged into existing metadata." },
1096
+ restore: {
1097
+ type: 'boolean',
1098
+ description: "Required when un-archiving (status: 'archived' → 'active'). Guards against accidental restoration.",
1099
+ default: false,
1100
+ },
1101
+ },
1102
+ required: ['plan_id'],
1103
+ },
1104
+ };
1105
+
1106
+ async function updatePlanHandler(args, apiClient) {
1107
+ const { plan_id, title, description, status, visibility, metadata, restore } = args;
1108
+
1109
+ // Guard: un-archiving requires explicit restore=true.
1110
+ if (status && status !== 'archived') {
1111
+ try {
1112
+ const current = await apiClient.plans.getPlan(plan_id);
1113
+ if (current.status === 'archived' && !restore) {
1114
+ return errorResponse(
1115
+ 'restore_required',
1116
+ `Plan ${plan_id} is archived. Pass restore=true to un-archive.`
1117
+ );
1118
+ }
1119
+ } catch (err) {
1120
+ // If we can't fetch, fall through — the update will fail loudly anyway.
1121
+ }
1122
+ }
1123
+
1124
+ const payload = {};
1125
+ if (title !== undefined) payload.title = title;
1126
+ if (description !== undefined) payload.description = description;
1127
+ if (status !== undefined) payload.status = status;
1128
+ if (metadata !== undefined) payload.metadata = metadata;
1129
+
1130
+ const applied = [];
1131
+ const failures = [];
1132
+
1133
+ if (Object.keys(payload).length) {
1134
+ try {
1135
+ await apiClient.plans.updatePlan(plan_id, payload);
1136
+ applied.push(...Object.keys(payload));
1137
+ } catch (err) {
1138
+ failures.push({ step: 'update_plan', error: err.response?.data?.error || err.message });
1139
+ }
1140
+ }
1141
+
1142
+ if (visibility !== undefined) {
1143
+ try {
1144
+ await apiClient.plans.updateVisibility(plan_id, { visibility });
1145
+ applied.push('visibility');
1146
+ } catch (err) {
1147
+ failures.push({ step: 'update_visibility', error: err.response?.data?.error || err.message });
1148
+ }
1149
+ }
1150
+
1151
+ let plan = null;
1152
+ try { plan = await apiClient.plans.getPlan(plan_id); } catch {}
1153
+
1154
+ return formatResponse({
1155
+ as_of: asOf(),
1156
+ plan_id,
1157
+ applied_changes: applied,
1158
+ failures,
1159
+ plan: plan ? { id: plan.id, title: plan.title, status: plan.status, visibility: plan.visibility } : null,
1160
+ });
1161
+ }
1162
+
1163
+ // ─────────────────────────────────────────────────────────────────────────
1164
+ // update_node — edit any node property except status (v1.0).
1165
+ // Status transitions route through update_task (existing tool) since they
1166
+ // trigger claim/log side effects.
1167
+ // ─────────────────────────────────────────────────────────────────────────
1168
+
1169
+ const updateNodeDefinition = {
1170
+ name: 'update_node',
1171
+ description:
1172
+ "Edit any node property atomically: title, description, node_type, " +
1173
+ "task_mode, agent_instructions, metadata. Status transitions belong " +
1174
+ "on update_task (which handles claim/log side effects). Rejects " +
1175
+ "node_type changes when the node has children.",
1176
+ inputSchema: {
1177
+ type: 'object',
1178
+ properties: {
1179
+ node_id: { type: 'string' },
1180
+ plan_id: { type: 'string', description: "Auto-resolved if omitted." },
1181
+ title: { type: 'string' },
1182
+ description: { type: 'string' },
1183
+ node_type: { type: 'string', enum: ['phase', 'task', 'milestone'] },
1184
+ task_mode: { type: 'string', enum: VALID_TASK_MODES },
1185
+ agent_instructions: { type: 'string' },
1186
+ metadata: { type: 'object' },
1187
+ },
1188
+ required: ['node_id'],
1189
+ },
1190
+ };
1191
+
1192
+ async function updateNodeHandler(args, apiClient) {
1193
+ const { node_id, title, description, node_type, task_mode, agent_instructions, metadata } = args;
1194
+ let { plan_id } = args;
1195
+
1196
+ if (!plan_id) {
1197
+ try {
1198
+ const node = await apiClient.axiosInstance.get(`/nodes/${node_id}`).then((r) => r.data);
1199
+ plan_id = node.plan_id || node.planId;
1200
+ } catch (err) {
1201
+ return errorResponse('not_found', `Could not resolve plan_id from node ${node_id}: ${err.message}`);
1202
+ }
1203
+ }
1204
+
1205
+ const payload = {};
1206
+ if (title !== undefined) payload.title = title;
1207
+ if (description !== undefined) payload.description = description;
1208
+ if (node_type !== undefined) payload.node_type = node_type;
1209
+ if (task_mode !== undefined) payload.task_mode = task_mode;
1210
+ if (agent_instructions !== undefined) payload.agent_instructions = agent_instructions;
1211
+ if (metadata !== undefined) payload.metadata = metadata;
1212
+
1213
+ if (!Object.keys(payload).length) {
1214
+ return errorResponse('no_changes', 'At least one field to update must be provided.');
1215
+ }
1216
+
1217
+ try {
1218
+ const updated = await apiClient.nodes.updateNode(plan_id, node_id, payload);
1219
+ return formatResponse({
1220
+ as_of: asOf(),
1221
+ plan_id,
1222
+ node_id,
1223
+ applied_changes: Object.keys(payload),
1224
+ node: updated.result || updated,
1225
+ });
1226
+ } catch (err) {
1227
+ return errorResponse('update_failed', `Failed to update node: ${err.response?.data?.error || err.message}`);
1228
+ }
1229
+ }
1230
+
1231
+ // ─────────────────────────────────────────────────────────────────────────
1232
+ // move_node — reparent a node within the same plan (v1.0).
1233
+ // ─────────────────────────────────────────────────────────────────────────
1234
+
1235
+ const moveNodeDefinition = {
1236
+ name: 'move_node',
1237
+ description:
1238
+ "Reparent a node within the same plan. Cycle-safe (server rejects " +
1239
+ "moves that would create a tree cycle). Optional position sets the " +
1240
+ "order_index among siblings.",
1241
+ inputSchema: {
1242
+ type: 'object',
1243
+ properties: {
1244
+ node_id: { type: 'string' },
1245
+ new_parent_id: { type: 'string' },
1246
+ plan_id: { type: 'string', description: "Auto-resolved if omitted." },
1247
+ position: { type: 'integer', description: "Optional order_index among siblings." },
1248
+ },
1249
+ required: ['node_id', 'new_parent_id'],
1250
+ },
1251
+ };
1252
+
1253
+ async function moveNodeHandler(args, apiClient) {
1254
+ const { node_id, new_parent_id, position } = args;
1255
+ let { plan_id } = args;
1256
+
1257
+ if (!plan_id) {
1258
+ try {
1259
+ const node = await apiClient.axiosInstance.get(`/nodes/${node_id}`).then((r) => r.data);
1260
+ plan_id = node.plan_id || node.planId;
1261
+ } catch (err) {
1262
+ return errorResponse('not_found', `Could not resolve plan_id from node ${node_id}: ${err.message}`);
1263
+ }
1264
+ }
1265
+
1266
+ const payload = { parent_id: new_parent_id };
1267
+ if (typeof position === 'number') payload.order_index = position;
1268
+
1269
+ try {
1270
+ const result = await apiClient.axiosInstance.post(
1271
+ `/plans/${plan_id}/nodes/${node_id}/move`,
1272
+ payload
1273
+ ).then((r) => r.data);
1274
+
1275
+ return formatResponse({
1276
+ as_of: asOf(),
1277
+ plan_id,
1278
+ node_id,
1279
+ new_parent_id,
1280
+ position: position ?? null,
1281
+ node: result,
1282
+ });
1283
+ } catch (err) {
1284
+ return errorResponse('move_failed', `Failed to move node: ${err.response?.data?.error || err.message}`);
1285
+ }
1286
+ }
1287
+
1288
+ // ─────────────────────────────────────────────────────────────────────────
1289
+ // delete_plan / delete_node — soft delete via status='archived' (v1.0).
1290
+ // Hard delete stays REST-only with admin auth.
1291
+ // ─────────────────────────────────────────────────────────────────────────
1292
+
1293
+ const deletePlanDefinition = {
1294
+ name: 'delete_plan',
1295
+ description:
1296
+ "Soft-delete a plan by setting status='archived'. Recoverable via " +
1297
+ "update_plan({status: 'active', restore: true}). Hard delete is not " +
1298
+ "agent-callable — use REST + admin token if absolutely needed.",
1299
+ inputSchema: {
1300
+ type: 'object',
1301
+ properties: {
1302
+ plan_id: { type: 'string' },
1303
+ reason: { type: 'string', description: "Logged for audit." },
1304
+ },
1305
+ required: ['plan_id'],
1306
+ },
1307
+ };
1308
+
1309
+ async function deletePlanHandler(args, apiClient) {
1310
+ const { plan_id, reason } = args;
1311
+
1312
+ try {
1313
+ await apiClient.plans.updatePlan(plan_id, { status: 'archived' });
1314
+ return formatResponse({
1315
+ as_of: asOf(),
1316
+ plan_id,
1317
+ archived: true,
1318
+ reason: reason || null,
1319
+ next_step: "Plan archived. To restore: update_plan({plan_id, status: 'active', restore: true})",
1320
+ });
1321
+ } catch (err) {
1322
+ return errorResponse('archive_failed', `Failed to archive plan: ${err.response?.data?.error || err.message}`);
1323
+ }
1324
+ }
1325
+
1326
+ const deleteNodeDefinition = {
1327
+ name: 'delete_node',
1328
+ description:
1329
+ "Soft-delete a node by setting status='archived'. Cascades to children " +
1330
+ "by default. Recoverable via update_task({status: 'not_started'}).",
1331
+ inputSchema: {
1332
+ type: 'object',
1333
+ properties: {
1334
+ node_id: { type: 'string' },
1335
+ plan_id: { type: 'string', description: "Auto-resolved if omitted." },
1336
+ reason: { type: 'string' },
1337
+ cascade_children: { type: 'boolean', default: true },
1338
+ },
1339
+ required: ['node_id'],
1340
+ },
1341
+ };
1342
+
1343
+ async function deleteNodeHandler(args, apiClient) {
1344
+ const { node_id, reason } = args;
1345
+ let { plan_id } = args;
1346
+ // cascade_children is intentionally ignored at the MCP layer for now —
1347
+ // backend cascade behavior on archived status is implicit (children remain
1348
+ // accessible until explicitly archived themselves). When backend gains
1349
+ // explicit cascade-on-archive, surface it here.
1350
+
1351
+ if (!plan_id) {
1352
+ try {
1353
+ const node = await apiClient.axiosInstance.get(`/nodes/${node_id}`).then((r) => r.data);
1354
+ plan_id = node.plan_id || node.planId;
1355
+ } catch (err) {
1356
+ return errorResponse('not_found', `Could not resolve plan_id from node ${node_id}: ${err.message}`);
1357
+ }
1358
+ }
1359
+
1360
+ try {
1361
+ await apiClient.nodes.updateNode(plan_id, node_id, { status: 'archived' });
1362
+ return formatResponse({
1363
+ as_of: asOf(),
1364
+ plan_id,
1365
+ node_id,
1366
+ archived: true,
1367
+ reason: reason || null,
1368
+ });
1369
+ } catch (err) {
1370
+ return errorResponse('archive_failed', `Failed to archive node: ${err.response?.data?.error || err.message}`);
1371
+ }
1372
+ }
1373
+
1374
+ // ─────────────────────────────────────────────────────────────────────────
1375
+ // share_plan — atomic visibility + collaborator changes (v1.0).
1376
+ // Collaborators specified by user_id (email resolution stays UI-side for now).
1377
+ // ─────────────────────────────────────────────────────────────────────────
1378
+
1379
+ const VALID_COLLAB_ROLES = ['viewer', 'editor', 'admin'];
1380
+
1381
+ const sharePlanDefinition = {
1382
+ name: 'share_plan',
1383
+ description:
1384
+ "Atomically change a plan's visibility and add/remove collaborators in " +
1385
+ "one call. Collaborators are specified by user_id (email-based invites " +
1386
+ "stay UI-only for now). Caller must be plan owner or admin.",
1387
+ inputSchema: {
1388
+ type: 'object',
1389
+ properties: {
1390
+ plan_id: { type: 'string' },
1391
+ visibility: { type: 'string', enum: ['private', 'unlisted', 'public'] },
1392
+ add_collaborators: {
1393
+ type: 'array',
1394
+ items: {
1395
+ type: 'object',
1396
+ properties: {
1397
+ user_id: { type: 'string' },
1398
+ role: { type: 'string', enum: VALID_COLLAB_ROLES, default: 'viewer' },
1399
+ },
1400
+ required: ['user_id'],
1401
+ },
1402
+ },
1403
+ remove_collaborators: {
1404
+ type: 'array',
1405
+ description: "Array of user_ids to remove from the plan.",
1406
+ items: { type: 'string' },
1407
+ },
1408
+ },
1409
+ required: ['plan_id'],
1410
+ },
1411
+ };
1412
+
1413
+ async function sharePlanHandler(args, apiClient) {
1414
+ const { plan_id, visibility, add_collaborators = [], remove_collaborators = [] } = args;
1415
+ const applied = [];
1416
+ const failures = [];
1417
+
1418
+ if (visibility) {
1419
+ try {
1420
+ await apiClient.plans.updateVisibility(plan_id, { visibility });
1421
+ applied.push(`visibility:${visibility}`);
1422
+ } catch (err) {
1423
+ failures.push({ step: 'visibility', error: err.response?.data?.error || err.message });
1424
+ }
1425
+ }
1426
+
1427
+ for (const collab of add_collaborators) {
1428
+ try {
1429
+ await apiClient.axiosInstance.post(`/plans/${plan_id}/collaborators`, {
1430
+ user_id: collab.user_id,
1431
+ role: collab.role || 'viewer',
1432
+ });
1433
+ applied.push(`add:${collab.user_id}:${collab.role || 'viewer'}`);
1434
+ } catch (err) {
1435
+ failures.push({ step: `add:${collab.user_id}`, error: err.response?.data?.error || err.message });
1436
+ }
1437
+ }
1438
+
1439
+ for (const userId of remove_collaborators) {
1440
+ try {
1441
+ await apiClient.axiosInstance.delete(`/plans/${plan_id}/collaborators/${userId}`);
1442
+ applied.push(`remove:${userId}`);
1443
+ } catch (err) {
1444
+ failures.push({ step: `remove:${userId}`, error: err.response?.data?.error || err.message });
1445
+ }
1446
+ }
1447
+
1448
+ return formatResponse({
1449
+ as_of: asOf(),
1450
+ plan_id,
1451
+ applied_changes: applied,
1452
+ failures,
1453
+ });
1454
+ }
1455
+
1456
+ // ─────────────────────────────────────────────────────────────────────────
1457
+ // invite_member / update_member_role / remove_member — org membership (v1.0).
1458
+ // ─────────────────────────────────────────────────────────────────────────
1459
+
1460
+ const inviteMemberDefinition = {
1461
+ name: 'invite_member',
1462
+ description:
1463
+ "Add a user to an organization by user_id or email. Caller must be " +
1464
+ "org owner or admin. If email is provided and the user doesn't exist, " +
1465
+ "the API returns 404 (email-invite flow stays UI-only).",
1466
+ inputSchema: {
1467
+ type: 'object',
1468
+ properties: {
1469
+ organization_id: { type: 'string' },
1470
+ user_id: { type: 'string' },
1471
+ email: { type: 'string' },
1472
+ role: { type: 'string', enum: ['admin', 'member'], default: 'member' },
1473
+ },
1474
+ required: ['organization_id'],
1475
+ },
1476
+ };
1477
+
1478
+ async function inviteMemberHandler(args, apiClient) {
1479
+ const { organization_id, user_id, email, role = 'member' } = args;
1480
+
1481
+ if (!user_id && !email) {
1482
+ return errorResponse('invalid_argument', 'Either user_id or email must be provided.');
1483
+ }
1484
+
1485
+ const payload = { role };
1486
+ if (user_id) payload.user_id = user_id;
1487
+ if (email) payload.email = email;
1488
+
1489
+ try {
1490
+ const member = await apiClient.organizations.addMember(organization_id, payload);
1491
+ return formatResponse({
1492
+ as_of: asOf(),
1493
+ organization_id,
1494
+ member: {
1495
+ membership_id: member.id,
1496
+ user_id: member.user?.id,
1497
+ email: member.user?.email,
1498
+ role: member.role,
1499
+ },
1500
+ });
1501
+ } catch (err) {
1502
+ const upstream = err.response?.data?.error || err.message;
1503
+ const code = err.response?.status === 404 ? 'user_not_found' : 'invite_failed';
1504
+ return errorResponse(code, `Failed to invite member: ${upstream}`);
1505
+ }
1506
+ }
1507
+
1508
+ const updateMemberRoleDefinition = {
1509
+ name: 'update_member_role',
1510
+ description:
1511
+ "Change a member's role within an organization. Caller must be org " +
1512
+ "owner. Server rejects demoting the last admin.",
1513
+ inputSchema: {
1514
+ type: 'object',
1515
+ properties: {
1516
+ organization_id: { type: 'string' },
1517
+ membership_id: { type: 'string', description: "Membership row id (from listMembers)." },
1518
+ new_role: { type: 'string', enum: ['admin', 'member'] },
1519
+ },
1520
+ required: ['organization_id', 'membership_id', 'new_role'],
1521
+ },
1522
+ };
1523
+
1524
+ async function updateMemberRoleHandler(args, apiClient) {
1525
+ const { organization_id, membership_id, new_role } = args;
1526
+
1527
+ try {
1528
+ const result = await apiClient.axiosInstance.put(
1529
+ `/organizations/${organization_id}/members/${membership_id}/role`,
1530
+ { role: new_role }
1531
+ ).then((r) => r.data);
1532
+
1533
+ return formatResponse({
1534
+ as_of: asOf(),
1535
+ organization_id,
1536
+ membership_id,
1537
+ new_role,
1538
+ member: result,
1539
+ });
1540
+ } catch (err) {
1541
+ return errorResponse('update_role_failed', `Failed to update member role: ${err.response?.data?.error || err.message}`);
1542
+ }
1543
+ }
1544
+
1545
+ const removeMemberDefinition = {
1546
+ name: 'remove_member',
1547
+ description:
1548
+ "Remove a member from an organization. Caller must be org owner or " +
1549
+ "admin (admins cannot remove other admins). Server rejects removing " +
1550
+ "the org owner.",
1551
+ inputSchema: {
1552
+ type: 'object',
1553
+ properties: {
1554
+ organization_id: { type: 'string' },
1555
+ membership_id: { type: 'string' },
1556
+ reason: { type: 'string' },
1557
+ },
1558
+ required: ['organization_id', 'membership_id'],
1559
+ },
1560
+ };
1561
+
1562
+ async function removeMemberHandler(args, apiClient) {
1563
+ const { organization_id, membership_id, reason } = args;
1564
+
1565
+ try {
1566
+ await apiClient.organizations.removeMember(organization_id, membership_id);
1567
+ return formatResponse({
1568
+ as_of: asOf(),
1569
+ organization_id,
1570
+ membership_id,
1571
+ removed: true,
1572
+ reason: reason || null,
1573
+ });
1574
+ } catch (err) {
1575
+ return errorResponse('remove_failed', `Failed to remove member: ${err.response?.data?.error || err.message}`);
1576
+ }
1577
+ }
1578
+
515
1579
  module.exports = {
516
1580
  definitions: [
517
1581
  queueDecisionDefinition,
@@ -520,6 +1584,20 @@ module.exports = {
520
1584
  claimNextTaskDefinition,
521
1585
  releaseTaskDefinition,
522
1586
  addLearningDefinition,
1587
+ formIntentionDefinition,
1588
+ extendIntentionDefinition,
1589
+ proposeResearchChainDefinition,
1590
+ linkIntentionsDefinition,
1591
+ unlinkIntentionsDefinition,
1592
+ updatePlanDefinition,
1593
+ updateNodeDefinition,
1594
+ moveNodeDefinition,
1595
+ deletePlanDefinition,
1596
+ deleteNodeDefinition,
1597
+ sharePlanDefinition,
1598
+ inviteMemberDefinition,
1599
+ updateMemberRoleDefinition,
1600
+ removeMemberDefinition,
523
1601
  ],
524
1602
  handlers: {
525
1603
  queue_decision: queueDecisionHandler,
@@ -528,5 +1606,19 @@ module.exports = {
528
1606
  claim_next_task: claimNextTaskHandler,
529
1607
  release_task: releaseTaskHandler,
530
1608
  add_learning: addLearningHandler,
1609
+ form_intention: formIntentionHandler,
1610
+ extend_intention: extendIntentionHandler,
1611
+ propose_research_chain: proposeResearchChainHandler,
1612
+ link_intentions: linkIntentionsHandler,
1613
+ unlink_intentions: unlinkIntentionsHandler,
1614
+ update_plan: updatePlanHandler,
1615
+ update_node: updateNodeHandler,
1616
+ move_node: moveNodeHandler,
1617
+ delete_plan: deletePlanHandler,
1618
+ delete_node: deleteNodeHandler,
1619
+ share_plan: sharePlanHandler,
1620
+ invite_member: inviteMemberHandler,
1621
+ update_member_role: updateMemberRoleHandler,
1622
+ remove_member: removeMemberHandler,
531
1623
  },
532
1624
  };