@opsee/mcp-server 0.8.2 → 0.8.4

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.
@@ -608,6 +608,13 @@ export declare type DecomposeTaskInput = Message<"api.v1.DecomposeTaskInput"> &
608
608
  * @generated from field: optional uint32 board_column_id = 7;
609
609
  */
610
610
  boardColumnId?: number;
611
+
612
+ /**
613
+ * reconcile only: maps this node to an existing Task (kept). Omit for a new task.
614
+ *
615
+ * @generated from field: optional uint32 existing_task_id = 8;
616
+ */
617
+ existingTaskId?: number;
611
618
  };
612
619
 
613
620
  /**
@@ -689,6 +696,137 @@ export declare type DecomposeInitiativeResponse = Message<"api.v1.DecomposeIniti
689
696
  */
690
697
  export declare const DecomposeInitiativeResponseSchema: GenMessage<DecomposeInitiativeResponse>;
691
698
 
699
+ /**
700
+ * @generated from message api.v1.ReconcileTaskChange
701
+ */
702
+ export declare type ReconcileTaskChange = Message<"api.v1.ReconcileTaskChange"> & {
703
+ /**
704
+ * empty for not-yet-created added tasks on dry_run
705
+ *
706
+ * @generated from field: uint32 task_id = 1;
707
+ */
708
+ taskId: number;
709
+
710
+ /**
711
+ * empty likewise
712
+ *
713
+ * @generated from field: string identifier = 2;
714
+ */
715
+ identifier: string;
716
+
717
+ /**
718
+ * @generated from field: string title = 3;
719
+ */
720
+ title: string;
721
+
722
+ /**
723
+ * set only on flagged: why it carries execution state
724
+ *
725
+ * @generated from field: string reason = 4;
726
+ */
727
+ reason: string;
728
+ };
729
+
730
+ /**
731
+ * Describes the message api.v1.ReconcileTaskChange.
732
+ * Use `create(ReconcileTaskChangeSchema)` to create a new message.
733
+ */
734
+ export declare const ReconcileTaskChangeSchema: GenMessage<ReconcileTaskChange>;
735
+
736
+ /**
737
+ * @generated from message api.v1.ReconcileChangeset
738
+ */
739
+ export declare type ReconcileChangeset = Message<"api.v1.ReconcileChangeset"> & {
740
+ /**
741
+ * @generated from field: repeated api.v1.ReconcileTaskChange added = 1;
742
+ */
743
+ added: ReconcileTaskChange[];
744
+
745
+ /**
746
+ * pristine drops (will be / were deleted)
747
+ *
748
+ * @generated from field: repeated api.v1.ReconcileTaskChange removed = 2;
749
+ */
750
+ removed: ReconcileTaskChange[];
751
+
752
+ /**
753
+ * execution-state drops — left untouched
754
+ *
755
+ * @generated from field: repeated api.v1.ReconcileTaskChange flagged = 3;
756
+ */
757
+ flagged: ReconcileTaskChange[];
758
+
759
+ /**
760
+ * @generated from field: repeated api.v1.InitiativeGraphEdge edges_added = 4;
761
+ */
762
+ edgesAdded: InitiativeGraphEdge[];
763
+
764
+ /**
765
+ * @generated from field: repeated api.v1.InitiativeGraphEdge edges_removed = 5;
766
+ */
767
+ edgesRemoved: InitiativeGraphEdge[];
768
+
769
+ /**
770
+ * resulting (apply) / projected (dry_run)
771
+ *
772
+ * @generated from field: api.v1.InitiativeGraph graph = 6;
773
+ */
774
+ graph?: InitiativeGraph;
775
+ };
776
+
777
+ /**
778
+ * Describes the message api.v1.ReconcileChangeset.
779
+ * Use `create(ReconcileChangesetSchema)` to create a new message.
780
+ */
781
+ export declare const ReconcileChangesetSchema: GenMessage<ReconcileChangeset>;
782
+
783
+ /**
784
+ * @generated from message api.v1.ReconcileInitiativeRequest
785
+ */
786
+ export declare type ReconcileInitiativeRequest = Message<"api.v1.ReconcileInitiativeRequest"> & {
787
+ /**
788
+ * @generated from field: uint32 initiative_id = 1;
789
+ */
790
+ initiativeId: number;
791
+
792
+ /**
793
+ * @generated from field: repeated api.v1.DecomposeTaskInput tasks = 2;
794
+ */
795
+ tasks: DecomposeTaskInput[];
796
+
797
+ /**
798
+ * @generated from field: repeated api.v1.DecomposeEdgeInput edges = 3;
799
+ */
800
+ edges: DecomposeEdgeInput[];
801
+
802
+ /**
803
+ * @generated from field: bool dry_run = 4;
804
+ */
805
+ dryRun: boolean;
806
+ };
807
+
808
+ /**
809
+ * Describes the message api.v1.ReconcileInitiativeRequest.
810
+ * Use `create(ReconcileInitiativeRequestSchema)` to create a new message.
811
+ */
812
+ export declare const ReconcileInitiativeRequestSchema: GenMessage<ReconcileInitiativeRequest>;
813
+
814
+ /**
815
+ * @generated from message api.v1.ReconcileInitiativeResponse
816
+ */
817
+ export declare type ReconcileInitiativeResponse = Message<"api.v1.ReconcileInitiativeResponse"> & {
818
+ /**
819
+ * @generated from field: api.v1.ReconcileChangeset changeset = 1;
820
+ */
821
+ changeset?: ReconcileChangeset;
822
+ };
823
+
824
+ /**
825
+ * Describes the message api.v1.ReconcileInitiativeResponse.
826
+ * Use `create(ReconcileInitiativeResponseSchema)` to create a new message.
827
+ */
828
+ export declare const ReconcileInitiativeResponseSchema: GenMessage<ReconcileInitiativeResponse>;
829
+
692
830
  /**
693
831
  * @generated from service api.v1.InitiativeService
694
832
  */
@@ -781,5 +919,13 @@ export declare const InitiativeService: GenService<{
781
919
  input: typeof DecomposeInitiativeRequestSchema;
782
920
  output: typeof DecomposeInitiativeResponseSchema;
783
921
  },
922
+ /**
923
+ * @generated from rpc api.v1.InitiativeService.ReconcileInitiative
924
+ */
925
+ reconcileInitiative: {
926
+ methodKind: "unary";
927
+ input: typeof ReconcileInitiativeRequestSchema;
928
+ output: typeof ReconcileInitiativeResponseSchema;
929
+ },
784
930
  }>;
785
931
 
@@ -14,7 +14,7 @@ import { file_api_v1_models } from "./models_pb";
14
14
  * Describes the file api/v1/initiative.proto.
15
15
  */
16
16
  export const file_api_v1_initiative = /*@__PURE__*/
17
- fileDesc("ChdhcGkvdjEvaW5pdGlhdGl2ZS5wcm90bxIGYXBpLnYxItoCChRBZGRJbml0aWF0aXZlUmVxdWVzdBIZCgV0aXRsZRgBIAEoCUIK+kIHcgUQARjIARIUCgdzdW1tYXJ5GAIgASgJSACIAQESQgoGc3RhdHVzGAMgASgJQjL6Qi9yLVIFZHJhZnRSBmFjdGl2ZVIGcGF1c2VkUgljb21wbGV0ZWRSCWFiYW5kb25lZBIbCgpwcm9qZWN0X2lkGAQgASgNQgf6QgQqAigBEjgKC2FuY2hvcl90eXBlGAUgASgJQiP6QiByHlIEbm9uZVIEdGFza1IJbWlsZXN0b25lUgVjeWNsZRIWCglhbmNob3JfaWQYBiABKA1IAYgBARIWCgljb3JlX2lkZWEYByABKAlIAogBARIeChZjb3JlX2lkZWFfY29udGVudF90eXBlGAggASgJQgoKCF9zdW1tYXJ5QgwKCl9hbmNob3JfaWRCDAoKX2NvcmVfaWRlYSLzAgoVRWRpdEluaXRpYXRpdmVSZXF1ZXN0EhMKAmlkGAEgASgNQgf6QgQqAigBEhkKBXRpdGxlGAIgASgJQgr6QgdyBRABGMgBEhQKB3N1bW1hcnkYAyABKAlIAIgBARJCCgZzdGF0dXMYBCABKAlCMvpCL3ItUgVkcmFmdFIGYWN0aXZlUgZwYXVzZWRSCWNvbXBsZXRlZFIJYWJhbmRvbmVkEjgKC2FuY2hvcl90eXBlGAUgASgJQiP6QiByHlIEbm9uZVIEdGFza1IJbWlsZXN0b25lUgVjeWNsZRIWCglhbmNob3JfaWQYBiABKA1IAYgBARIWCgljb3JlX2lkZWEYByABKAlIAogBARIjChZjb3JlX2lkZWFfY29udGVudF90eXBlGAggASgJSAOIAQFCCgoIX3N1bW1hcnlCDAoKX2FuY2hvcl9pZEIMCgpfY29yZV9pZGVhQhkKF19jb3JlX2lkZWFfY29udGVudF90eXBlIiIKFEdldEluaXRpYXRpdmVSZXF1ZXN0EgoKAmlkGAEgASgNIjQKGkdldEluaXRpYXRpdmVCeVVVSURSZXF1ZXN0EhYKBHV1aWQYASABKAlCCPpCBXIDsAEBIpUBChVHZXRJbml0aWF0aXZlc1JlcXVlc3QSGwoKcHJvamVjdF9pZBgBIAEoDUIH+kIEKgIoARIwCgpwYWdpbmF0aW9uGAIgASgLMhIuYXBpLnYxLlBhZ2luYXRpb25CCPpCBYoBAhABEi0KDmZpbHRlcl9vcHRpb25zGAMgASgLMhUuYXBpLnYxLkZpbHRlck9wdGlvbnMiJQoXRGVsZXRlSW5pdGlhdGl2ZVJlcXVlc3QSCgoCaWQYASABKA0iPwoVQWRkSW5pdGlhdGl2ZVJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZSJAChZFZGl0SW5pdGlhdGl2ZVJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZSI/ChVHZXRJbml0aWF0aXZlUmVzcG9uc2USJgoKaW5pdGlhdGl2ZRgBIAEoCzISLmFwaS52MS5Jbml0aWF0aXZlImkKFkdldEluaXRpYXRpdmVzUmVzcG9uc2USJwoLaW5pdGlhdGl2ZXMYASADKAsyEi5hcGkudjEuSW5pdGlhdGl2ZRImCgpwYWdpbmF0aW9uGAIgASgLMhIuYXBpLnYxLlBhZ2luYXRpb24iTQoTSW5pdGlhdGl2ZUdyYXBoRWRnZRIUCgxmcm9tX3Rhc2tfaWQYASABKA0SEgoKdG9fdGFza19pZBgCIAEoDRIMCgR0eXBlGAMgASgJIjIKD0luaXRpYXRpdmVTbGljZRINCgVsYXllchgBIAEoBRIQCgh0YXNrX2lkcxgCIAMoDSKDAQoPSW5pdGlhdGl2ZUdyYXBoEhsKBXRhc2tzGAEgAygLMgwuYXBpLnYxLlRhc2sSKgoFZWRnZXMYAiADKAsyGy5hcGkudjEuSW5pdGlhdGl2ZUdyYXBoRWRnZRInCgZzbGljZXMYAyADKAsyFy5hcGkudjEuSW5pdGlhdGl2ZVNsaWNlIjsKGUdldEluaXRpYXRpdmVHcmFwaFJlcXVlc3QSHgoNaW5pdGlhdGl2ZV9pZBgBIAEoDUIH+kIEKgIoASJEChpHZXRJbml0aWF0aXZlR3JhcGhSZXNwb25zZRImCgVncmFwaBgBIAEoCzIXLmFwaS52MS5Jbml0aWF0aXZlR3JhcGgiUwobR2V0SW5pdGlhdGl2ZUNvbnRleHRSZXF1ZXN0Eh4KDWluaXRpYXRpdmVfaWQYASABKA1CB/pCBCoCKAESFAoMbWVtb3J5X2xpbWl0GAIgASgNIvgBChxHZXRJbml0aWF0aXZlQ29udGV4dFJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZRImCgVncmFwaBgCIAEoCzIXLmFwaS52MS5Jbml0aWF0aXZlR3JhcGgSNQoObWVtb3J5X2VudHJpZXMYAyADKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5Ei4KDXB1bGxfcmVxdWVzdHMYBCADKAsyFy5hcGkudjEuVGFza1B1bGxSZXF1ZXN0EiEKCGNvbW1lbnRzGAUgAygLMg8uYXBpLnYxLkNvbW1lbnQi2AIKH0FkZEluaXRpYXRpdmVNZW1vcnlFbnRyeVJlcXVlc3QSHgoNaW5pdGlhdGl2ZV9pZBgBIAEoDUIH+kIEKgIoARJCCgRraW5kGAIgASgJQjT6QjFyL1IIZGVjaXNpb25SB291dGNvbWVSCGxlYXJuaW5nUgdibG9ja2VyUgdjb250ZXh0EhUKBGJvZHkYAyABKAlCB/pCBHICEAESJwoMY29udGVudF90eXBlGAQgASgJQhH6Qg5yDFIEdGV4dFIEanNvbhJDCgtzb3VyY2VfdHlwZRgFIAEoCUIu+kIrcilSBG5vbmVSBHRhc2tSDHB1bGxfcmVxdWVzdFIDZG9jUgh3b3JrZmxvdxIWCglzb3VyY2VfaWQYBiABKA1IAIgBARIXCgpzb3VyY2VfdXJsGAcgASgJSAGIAQFCDAoKX3NvdXJjZV9pZEINCgtfc291cmNlX3VybCJXCiBBZGRJbml0aWF0aXZlTWVtb3J5RW50cnlSZXNwb25zZRIzCgxtZW1vcnlfZW50cnkYASABKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5IrgBChpHZXRJbml0aWF0aXZlTWVtb3J5UmVxdWVzdBIeCg1pbml0aWF0aXZlX2lkGAEgASgNQgf6QgQqAigBEjAKCnBhZ2luYXRpb24YAiABKAsyEi5hcGkudjEuUGFnaW5hdGlvbkII+kIFigECEAESDQoFa2luZHMYAyADKAkSEwoLc291cmNlX3R5cGUYBCABKAkSFgoJc291cmNlX2lkGAUgASgNSACIAQFCDAoKX3NvdXJjZV9pZCJ8ChtHZXRJbml0aWF0aXZlTWVtb3J5UmVzcG9uc2USNQoObWVtb3J5X2VudHJpZXMYASADKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5EiYKCnBhZ2luYXRpb24YAiABKAsyEi5hcGkudjEuUGFnaW5hdGlvbiKpAgoSRGVjb21wb3NlVGFza0lucHV0EhQKA3JlZhgBIAEoCUIH+kIEcgIQARIZCgV0aXRsZRgCIAEoCUIK+kIHcgUQARj/ARIYCgtkZXNjcmlwdGlvbhgDIAEoCUgAiAEBEhcKCnBhcmVudF9yZWYYBCABKAlIAYgBARIZCgx0YXNrX3R5cGVfaWQYBSABKA1IAogBARIdChB0YXNrX3ByaW9yaXR5X2lkGAYgASgNSAOIAQESHAoPYm9hcmRfY29sdW1uX2lkGAcgASgNSASIAQFCDgoMX2Rlc2NyaXB0aW9uQg0KC19wYXJlbnRfcmVmQg8KDV90YXNrX3R5cGVfaWRCEwoRX3Rhc2tfcHJpb3JpdHlfaWRCEgoQX2JvYXJkX2NvbHVtbl9pZCKJAQoSRGVjb21wb3NlRWRnZUlucHV0EhkKCGZyb21fcmVmGAEgASgJQgf6QgRyAhABEhcKBnRvX3JlZhgCIAEoCUIH+kIEcgIQARI/CgR0eXBlGAMgASgJQjH6Qi5yLFIGYmxvY2tzUgpibG9ja2VkX2J5UgpkdXBsaWNhdGVzUgpyZWxhdGVzX3RvIpwBChpEZWNvbXBvc2VJbml0aWF0aXZlUmVxdWVzdBIeCg1pbml0aWF0aXZlX2lkGAEgASgNQgf6QgQqAigBEjMKBXRhc2tzGAIgAygLMhouYXBpLnYxLkRlY29tcG9zZVRhc2tJbnB1dEII+kIFkgECCAESKQoFZWRnZXMYAyADKAsyGi5hcGkudjEuRGVjb21wb3NlRWRnZUlucHV0ImoKG0RlY29tcG9zZUluaXRpYXRpdmVSZXNwb25zZRIjCg1jcmVhdGVkX3Rhc2tzGAEgAygLMgwuYXBpLnYxLlRhc2sSJgoFZ3JhcGgYAiABKAsyFy5hcGkudjEuSW5pdGlhdGl2ZUdyYXBoMskLChFJbml0aWF0aXZlU2VydmljZRJsCg1BZGRJbml0aWF0aXZlEhwuYXBpLnYxLkFkZEluaXRpYXRpdmVSZXF1ZXN0Gh0uYXBpLnYxLkFkZEluaXRpYXRpdmVSZXNwb25zZSIegtPkkwIYOgEqIhMvYXBpL3YxL2luaXRpYXRpdmVzEnQKDkVkaXRJbml0aWF0aXZlEh0uYXBpLnYxLkVkaXRJbml0aWF0aXZlUmVxdWVzdBoeLmFwaS52MS5FZGl0SW5pdGlhdGl2ZVJlc3BvbnNlIiOC0+STAh06ASoaGC9hcGkvdjEvaW5pdGlhdGl2ZXMve2lkfRJuCg1HZXRJbml0aWF0aXZlEhwuYXBpLnYxLkdldEluaXRpYXRpdmVSZXF1ZXN0Gh0uYXBpLnYxLkdldEluaXRpYXRpdmVSZXNwb25zZSIggtPkkwIaEhgvYXBpL3YxL2luaXRpYXRpdmVzL3tpZH0SgQEKE0dldEluaXRpYXRpdmVCeVVVSUQSIi5hcGkudjEuR2V0SW5pdGlhdGl2ZUJ5VVVJRFJlcXVlc3QaHS5hcGkudjEuR2V0SW5pdGlhdGl2ZVJlc3BvbnNlIieC0+STAiESHy9hcGkvdjEvaW5pdGlhdGl2ZXMvdXVpZC97dXVpZH0SbAoOR2V0SW5pdGlhdGl2ZXMSHS5hcGkudjEuR2V0SW5pdGlhdGl2ZXNSZXF1ZXN0Gh4uYXBpLnYxLkdldEluaXRpYXRpdmVzUmVzcG9uc2UiG4LT5JMCFRITL2FwaS92MS9pbml0aWF0aXZlcxJtChBEZWxldGVJbml0aWF0aXZlEh8uYXBpLnYxLkRlbGV0ZUluaXRpYXRpdmVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IiCC0+STAhoqGC9hcGkvdjEvaW5pdGlhdGl2ZXMve2lkfRKOAQoSR2V0SW5pdGlhdGl2ZUdyYXBoEiEuYXBpLnYxLkdldEluaXRpYXRpdmVHcmFwaFJlcXVlc3QaIi5hcGkudjEuR2V0SW5pdGlhdGl2ZUdyYXBoUmVzcG9uc2UiMYLT5JMCKxIpL2FwaS92MS9pbml0aWF0aXZlcy97aW5pdGlhdGl2ZV9pZH0vZ3JhcGgSlgEKFEdldEluaXRpYXRpdmVDb250ZXh0EiMuYXBpLnYxLkdldEluaXRpYXRpdmVDb250ZXh0UmVxdWVzdBokLmFwaS52MS5HZXRJbml0aWF0aXZlQ29udGV4dFJlc3BvbnNlIjOC0+STAi0SKy9hcGkvdjEvaW5pdGlhdGl2ZXMve2luaXRpYXRpdmVfaWR9L2NvbnRleHQSpAEKGEFkZEluaXRpYXRpdmVNZW1vcnlFbnRyeRInLmFwaS52MS5BZGRJbml0aWF0aXZlTWVtb3J5RW50cnlSZXF1ZXN0GiguYXBpLnYxLkFkZEluaXRpYXRpdmVNZW1vcnlFbnRyeVJlc3BvbnNlIjWC0+STAi86ASoiKi9hcGkvdjEvaW5pdGlhdGl2ZXMve2luaXRpYXRpdmVfaWR9L21lbW9yeRKSAQoTR2V0SW5pdGlhdGl2ZU1lbW9yeRIiLmFwaS52MS5HZXRJbml0aWF0aXZlTWVtb3J5UmVxdWVzdBojLmFwaS52MS5HZXRJbml0aWF0aXZlTWVtb3J5UmVzcG9uc2UiMoLT5JMCLBIqL2FwaS92MS9pbml0aWF0aXZlcy97aW5pdGlhdGl2ZV9pZH0vbWVtb3J5EpgBChNEZWNvbXBvc2VJbml0aWF0aXZlEiIuYXBpLnYxLkRlY29tcG9zZUluaXRpYXRpdmVSZXF1ZXN0GiMuYXBpLnYxLkRlY29tcG9zZUluaXRpYXRpdmVSZXNwb25zZSI4gtPkkwIyOgEqIi0vYXBpL3YxL2luaXRpYXRpdmVzL3tpbml0aWF0aXZlX2lkfS9kZWNvbXBvc2VCdAoKY29tLmFwaS52MUIPSW5pdGlhdGl2ZVByb3RvUAFaHG9wc2VlL2JhY2tlbmQvZ2VuL2FwaS92MTtnZW6iAgNBWFiqAgZBcGkuVjHKAgZBcGlcVjHiAhJBcGlcVjFcR1BCTWV0YWRhdGHqAgdBcGk6OlYxYgZwcm90bzM", [file_google_protobuf_empty, file_google_api_annotations, file_validate_validate, file_api_v1_pagination, file_api_v1_filter, file_api_v1_models]);
17
+ fileDesc("ChdhcGkvdjEvaW5pdGlhdGl2ZS5wcm90bxIGYXBpLnYxItoCChRBZGRJbml0aWF0aXZlUmVxdWVzdBIZCgV0aXRsZRgBIAEoCUIK+kIHcgUQARjIARIUCgdzdW1tYXJ5GAIgASgJSACIAQESQgoGc3RhdHVzGAMgASgJQjL6Qi9yLVIFZHJhZnRSBmFjdGl2ZVIGcGF1c2VkUgljb21wbGV0ZWRSCWFiYW5kb25lZBIbCgpwcm9qZWN0X2lkGAQgASgNQgf6QgQqAigBEjgKC2FuY2hvcl90eXBlGAUgASgJQiP6QiByHlIEbm9uZVIEdGFza1IJbWlsZXN0b25lUgVjeWNsZRIWCglhbmNob3JfaWQYBiABKA1IAYgBARIWCgljb3JlX2lkZWEYByABKAlIAogBARIeChZjb3JlX2lkZWFfY29udGVudF90eXBlGAggASgJQgoKCF9zdW1tYXJ5QgwKCl9hbmNob3JfaWRCDAoKX2NvcmVfaWRlYSLzAgoVRWRpdEluaXRpYXRpdmVSZXF1ZXN0EhMKAmlkGAEgASgNQgf6QgQqAigBEhkKBXRpdGxlGAIgASgJQgr6QgdyBRABGMgBEhQKB3N1bW1hcnkYAyABKAlIAIgBARJCCgZzdGF0dXMYBCABKAlCMvpCL3ItUgVkcmFmdFIGYWN0aXZlUgZwYXVzZWRSCWNvbXBsZXRlZFIJYWJhbmRvbmVkEjgKC2FuY2hvcl90eXBlGAUgASgJQiP6QiByHlIEbm9uZVIEdGFza1IJbWlsZXN0b25lUgVjeWNsZRIWCglhbmNob3JfaWQYBiABKA1IAYgBARIWCgljb3JlX2lkZWEYByABKAlIAogBARIjChZjb3JlX2lkZWFfY29udGVudF90eXBlGAggASgJSAOIAQFCCgoIX3N1bW1hcnlCDAoKX2FuY2hvcl9pZEIMCgpfY29yZV9pZGVhQhkKF19jb3JlX2lkZWFfY29udGVudF90eXBlIiIKFEdldEluaXRpYXRpdmVSZXF1ZXN0EgoKAmlkGAEgASgNIjQKGkdldEluaXRpYXRpdmVCeVVVSURSZXF1ZXN0EhYKBHV1aWQYASABKAlCCPpCBXIDsAEBIpUBChVHZXRJbml0aWF0aXZlc1JlcXVlc3QSGwoKcHJvamVjdF9pZBgBIAEoDUIH+kIEKgIoARIwCgpwYWdpbmF0aW9uGAIgASgLMhIuYXBpLnYxLlBhZ2luYXRpb25CCPpCBYoBAhABEi0KDmZpbHRlcl9vcHRpb25zGAMgASgLMhUuYXBpLnYxLkZpbHRlck9wdGlvbnMiJQoXRGVsZXRlSW5pdGlhdGl2ZVJlcXVlc3QSCgoCaWQYASABKA0iPwoVQWRkSW5pdGlhdGl2ZVJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZSJAChZFZGl0SW5pdGlhdGl2ZVJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZSI/ChVHZXRJbml0aWF0aXZlUmVzcG9uc2USJgoKaW5pdGlhdGl2ZRgBIAEoCzISLmFwaS52MS5Jbml0aWF0aXZlImkKFkdldEluaXRpYXRpdmVzUmVzcG9uc2USJwoLaW5pdGlhdGl2ZXMYASADKAsyEi5hcGkudjEuSW5pdGlhdGl2ZRImCgpwYWdpbmF0aW9uGAIgASgLMhIuYXBpLnYxLlBhZ2luYXRpb24iTQoTSW5pdGlhdGl2ZUdyYXBoRWRnZRIUCgxmcm9tX3Rhc2tfaWQYASABKA0SEgoKdG9fdGFza19pZBgCIAEoDRIMCgR0eXBlGAMgASgJIjIKD0luaXRpYXRpdmVTbGljZRINCgVsYXllchgBIAEoBRIQCgh0YXNrX2lkcxgCIAMoDSKDAQoPSW5pdGlhdGl2ZUdyYXBoEhsKBXRhc2tzGAEgAygLMgwuYXBpLnYxLlRhc2sSKgoFZWRnZXMYAiADKAsyGy5hcGkudjEuSW5pdGlhdGl2ZUdyYXBoRWRnZRInCgZzbGljZXMYAyADKAsyFy5hcGkudjEuSW5pdGlhdGl2ZVNsaWNlIjsKGUdldEluaXRpYXRpdmVHcmFwaFJlcXVlc3QSHgoNaW5pdGlhdGl2ZV9pZBgBIAEoDUIH+kIEKgIoASJEChpHZXRJbml0aWF0aXZlR3JhcGhSZXNwb25zZRImCgVncmFwaBgBIAEoCzIXLmFwaS52MS5Jbml0aWF0aXZlR3JhcGgiUwobR2V0SW5pdGlhdGl2ZUNvbnRleHRSZXF1ZXN0Eh4KDWluaXRpYXRpdmVfaWQYASABKA1CB/pCBCoCKAESFAoMbWVtb3J5X2xpbWl0GAIgASgNIvgBChxHZXRJbml0aWF0aXZlQ29udGV4dFJlc3BvbnNlEiYKCmluaXRpYXRpdmUYASABKAsyEi5hcGkudjEuSW5pdGlhdGl2ZRImCgVncmFwaBgCIAEoCzIXLmFwaS52MS5Jbml0aWF0aXZlR3JhcGgSNQoObWVtb3J5X2VudHJpZXMYAyADKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5Ei4KDXB1bGxfcmVxdWVzdHMYBCADKAsyFy5hcGkudjEuVGFza1B1bGxSZXF1ZXN0EiEKCGNvbW1lbnRzGAUgAygLMg8uYXBpLnYxLkNvbW1lbnQi2AIKH0FkZEluaXRpYXRpdmVNZW1vcnlFbnRyeVJlcXVlc3QSHgoNaW5pdGlhdGl2ZV9pZBgBIAEoDUIH+kIEKgIoARJCCgRraW5kGAIgASgJQjT6QjFyL1IIZGVjaXNpb25SB291dGNvbWVSCGxlYXJuaW5nUgdibG9ja2VyUgdjb250ZXh0EhUKBGJvZHkYAyABKAlCB/pCBHICEAESJwoMY29udGVudF90eXBlGAQgASgJQhH6Qg5yDFIEdGV4dFIEanNvbhJDCgtzb3VyY2VfdHlwZRgFIAEoCUIu+kIrcilSBG5vbmVSBHRhc2tSDHB1bGxfcmVxdWVzdFIDZG9jUgh3b3JrZmxvdxIWCglzb3VyY2VfaWQYBiABKA1IAIgBARIXCgpzb3VyY2VfdXJsGAcgASgJSAGIAQFCDAoKX3NvdXJjZV9pZEINCgtfc291cmNlX3VybCJXCiBBZGRJbml0aWF0aXZlTWVtb3J5RW50cnlSZXNwb25zZRIzCgxtZW1vcnlfZW50cnkYASABKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5IrgBChpHZXRJbml0aWF0aXZlTWVtb3J5UmVxdWVzdBIeCg1pbml0aWF0aXZlX2lkGAEgASgNQgf6QgQqAigBEjAKCnBhZ2luYXRpb24YAiABKAsyEi5hcGkudjEuUGFnaW5hdGlvbkII+kIFigECEAESDQoFa2luZHMYAyADKAkSEwoLc291cmNlX3R5cGUYBCABKAkSFgoJc291cmNlX2lkGAUgASgNSACIAQFCDAoKX3NvdXJjZV9pZCJ8ChtHZXRJbml0aWF0aXZlTWVtb3J5UmVzcG9uc2USNQoObWVtb3J5X2VudHJpZXMYASADKAsyHS5hcGkudjEuSW5pdGlhdGl2ZU1lbW9yeUVudHJ5EiYKCnBhZ2luYXRpb24YAiABKAsyEi5hcGkudjEuUGFnaW5hdGlvbiLdAgoSRGVjb21wb3NlVGFza0lucHV0EhQKA3JlZhgBIAEoCUIH+kIEcgIQARIZCgV0aXRsZRgCIAEoCUIK+kIHcgUQARj/ARIYCgtkZXNjcmlwdGlvbhgDIAEoCUgAiAEBEhcKCnBhcmVudF9yZWYYBCABKAlIAYgBARIZCgx0YXNrX3R5cGVfaWQYBSABKA1IAogBARIdChB0YXNrX3ByaW9yaXR5X2lkGAYgASgNSAOIAQESHAoPYm9hcmRfY29sdW1uX2lkGAcgASgNSASIAQESHQoQZXhpc3RpbmdfdGFza19pZBgIIAEoDUgFiAEBQg4KDF9kZXNjcmlwdGlvbkINCgtfcGFyZW50X3JlZkIPCg1fdGFza190eXBlX2lkQhMKEV90YXNrX3ByaW9yaXR5X2lkQhIKEF9ib2FyZF9jb2x1bW5faWRCEwoRX2V4aXN0aW5nX3Rhc2tfaWQiiQEKEkRlY29tcG9zZUVkZ2VJbnB1dBIZCghmcm9tX3JlZhgBIAEoCUIH+kIEcgIQARIXCgZ0b19yZWYYAiABKAlCB/pCBHICEAESPwoEdHlwZRgDIAEoCUIx+kIucixSBmJsb2Nrc1IKYmxvY2tlZF9ieVIKZHVwbGljYXRlc1IKcmVsYXRlc190byKcAQoaRGVjb21wb3NlSW5pdGlhdGl2ZVJlcXVlc3QSHgoNaW5pdGlhdGl2ZV9pZBgBIAEoDUIH+kIEKgIoARIzCgV0YXNrcxgCIAMoCzIaLmFwaS52MS5EZWNvbXBvc2VUYXNrSW5wdXRCCPpCBZIBAggBEikKBWVkZ2VzGAMgAygLMhouYXBpLnYxLkRlY29tcG9zZUVkZ2VJbnB1dCJqChtEZWNvbXBvc2VJbml0aWF0aXZlUmVzcG9uc2USIwoNY3JlYXRlZF90YXNrcxgBIAMoCzIMLmFwaS52MS5UYXNrEiYKBWdyYXBoGAIgASgLMhcuYXBpLnYxLkluaXRpYXRpdmVHcmFwaCJZChNSZWNvbmNpbGVUYXNrQ2hhbmdlEg8KB3Rhc2tfaWQYASABKA0SEgoKaWRlbnRpZmllchgCIAEoCRINCgV0aXRsZRgDIAEoCRIOCgZyZWFzb24YBCABKAkiqgIKElJlY29uY2lsZUNoYW5nZXNldBIqCgVhZGRlZBgBIAMoCzIbLmFwaS52MS5SZWNvbmNpbGVUYXNrQ2hhbmdlEiwKB3JlbW92ZWQYAiADKAsyGy5hcGkudjEuUmVjb25jaWxlVGFza0NoYW5nZRIsCgdmbGFnZ2VkGAMgAygLMhsuYXBpLnYxLlJlY29uY2lsZVRhc2tDaGFuZ2USMAoLZWRnZXNfYWRkZWQYBCADKAsyGy5hcGkudjEuSW5pdGlhdGl2ZUdyYXBoRWRnZRIyCg1lZGdlc19yZW1vdmVkGAUgAygLMhsuYXBpLnYxLkluaXRpYXRpdmVHcmFwaEVkZ2USJgoFZ3JhcGgYBiABKAsyFy5hcGkudjEuSW5pdGlhdGl2ZUdyYXBoIq0BChpSZWNvbmNpbGVJbml0aWF0aXZlUmVxdWVzdBIeCg1pbml0aWF0aXZlX2lkGAEgASgNQgf6QgQqAigBEjMKBXRhc2tzGAIgAygLMhouYXBpLnYxLkRlY29tcG9zZVRhc2tJbnB1dEII+kIFkgECCAESKQoFZWRnZXMYAyADKAsyGi5hcGkudjEuRGVjb21wb3NlRWRnZUlucHV0Eg8KB2RyeV9ydW4YBCABKAgiTAobUmVjb25jaWxlSW5pdGlhdGl2ZVJlc3BvbnNlEi0KCWNoYW5nZXNldBgBIAEoCzIaLmFwaS52MS5SZWNvbmNpbGVDaGFuZ2VzZXQy5AwKEUluaXRpYXRpdmVTZXJ2aWNlEmwKDUFkZEluaXRpYXRpdmUSHC5hcGkudjEuQWRkSW5pdGlhdGl2ZVJlcXVlc3QaHS5hcGkudjEuQWRkSW5pdGlhdGl2ZVJlc3BvbnNlIh6C0+STAhg6ASoiEy9hcGkvdjEvaW5pdGlhdGl2ZXMSdAoORWRpdEluaXRpYXRpdmUSHS5hcGkudjEuRWRpdEluaXRpYXRpdmVSZXF1ZXN0Gh4uYXBpLnYxLkVkaXRJbml0aWF0aXZlUmVzcG9uc2UiI4LT5JMCHToBKhoYL2FwaS92MS9pbml0aWF0aXZlcy97aWR9Em4KDUdldEluaXRpYXRpdmUSHC5hcGkudjEuR2V0SW5pdGlhdGl2ZVJlcXVlc3QaHS5hcGkudjEuR2V0SW5pdGlhdGl2ZVJlc3BvbnNlIiCC0+STAhoSGC9hcGkvdjEvaW5pdGlhdGl2ZXMve2lkfRKBAQoTR2V0SW5pdGlhdGl2ZUJ5VVVJRBIiLmFwaS52MS5HZXRJbml0aWF0aXZlQnlVVUlEUmVxdWVzdBodLmFwaS52MS5HZXRJbml0aWF0aXZlUmVzcG9uc2UiJ4LT5JMCIRIfL2FwaS92MS9pbml0aWF0aXZlcy91dWlkL3t1dWlkfRJsCg5HZXRJbml0aWF0aXZlcxIdLmFwaS52MS5HZXRJbml0aWF0aXZlc1JlcXVlc3QaHi5hcGkudjEuR2V0SW5pdGlhdGl2ZXNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL2luaXRpYXRpdmVzEm0KEERlbGV0ZUluaXRpYXRpdmUSHy5hcGkudjEuRGVsZXRlSW5pdGlhdGl2ZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiIILT5JMCGioYL2FwaS92MS9pbml0aWF0aXZlcy97aWR9Eo4BChJHZXRJbml0aWF0aXZlR3JhcGgSIS5hcGkudjEuR2V0SW5pdGlhdGl2ZUdyYXBoUmVxdWVzdBoiLmFwaS52MS5HZXRJbml0aWF0aXZlR3JhcGhSZXNwb25zZSIxgtPkkwIrEikvYXBpL3YxL2luaXRpYXRpdmVzL3tpbml0aWF0aXZlX2lkfS9ncmFwaBKWAQoUR2V0SW5pdGlhdGl2ZUNvbnRleHQSIy5hcGkudjEuR2V0SW5pdGlhdGl2ZUNvbnRleHRSZXF1ZXN0GiQuYXBpLnYxLkdldEluaXRpYXRpdmVDb250ZXh0UmVzcG9uc2UiM4LT5JMCLRIrL2FwaS92MS9pbml0aWF0aXZlcy97aW5pdGlhdGl2ZV9pZH0vY29udGV4dBKkAQoYQWRkSW5pdGlhdGl2ZU1lbW9yeUVudHJ5EicuYXBpLnYxLkFkZEluaXRpYXRpdmVNZW1vcnlFbnRyeVJlcXVlc3QaKC5hcGkudjEuQWRkSW5pdGlhdGl2ZU1lbW9yeUVudHJ5UmVzcG9uc2UiNYLT5JMCLzoBKiIqL2FwaS92MS9pbml0aWF0aXZlcy97aW5pdGlhdGl2ZV9pZH0vbWVtb3J5EpIBChNHZXRJbml0aWF0aXZlTWVtb3J5EiIuYXBpLnYxLkdldEluaXRpYXRpdmVNZW1vcnlSZXF1ZXN0GiMuYXBpLnYxLkdldEluaXRpYXRpdmVNZW1vcnlSZXNwb25zZSIygtPkkwIsEiovYXBpL3YxL2luaXRpYXRpdmVzL3tpbml0aWF0aXZlX2lkfS9tZW1vcnkSmAEKE0RlY29tcG9zZUluaXRpYXRpdmUSIi5hcGkudjEuRGVjb21wb3NlSW5pdGlhdGl2ZVJlcXVlc3QaIy5hcGkudjEuRGVjb21wb3NlSW5pdGlhdGl2ZVJlc3BvbnNlIjiC0+STAjI6ASoiLS9hcGkvdjEvaW5pdGlhdGl2ZXMve2luaXRpYXRpdmVfaWR9L2RlY29tcG9zZRKYAQoTUmVjb25jaWxlSW5pdGlhdGl2ZRIiLmFwaS52MS5SZWNvbmNpbGVJbml0aWF0aXZlUmVxdWVzdBojLmFwaS52MS5SZWNvbmNpbGVJbml0aWF0aXZlUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS9pbml0aWF0aXZlcy97aW5pdGlhdGl2ZV9pZH0vcmVjb25jaWxlQnQKCmNvbS5hcGkudjFCD0luaXRpYXRpdmVQcm90b1ABWhxvcHNlZS9iYWNrZW5kL2dlbi9hcGkvdjE7Z2VuogIDQVhYqgIGQXBpLlYxygIGQXBpXFYx4gISQXBpXFYxXEdQQk1ldGFkYXRh6gIHQXBpOjpWMWIGcHJvdG8z", [file_google_protobuf_empty, file_google_api_annotations, file_validate_validate, file_api_v1_pagination, file_api_v1_filter, file_api_v1_models]);
18
18
 
19
19
  /**
20
20
  * Describes the message api.v1.AddInitiativeRequest.
@@ -191,6 +191,34 @@ export const DecomposeInitiativeRequestSchema = /*@__PURE__*/
191
191
  export const DecomposeInitiativeResponseSchema = /*@__PURE__*/
192
192
  messageDesc(file_api_v1_initiative, 24);
193
193
 
194
+ /**
195
+ * Describes the message api.v1.ReconcileTaskChange.
196
+ * Use `create(ReconcileTaskChangeSchema)` to create a new message.
197
+ */
198
+ export const ReconcileTaskChangeSchema = /*@__PURE__*/
199
+ messageDesc(file_api_v1_initiative, 25);
200
+
201
+ /**
202
+ * Describes the message api.v1.ReconcileChangeset.
203
+ * Use `create(ReconcileChangesetSchema)` to create a new message.
204
+ */
205
+ export const ReconcileChangesetSchema = /*@__PURE__*/
206
+ messageDesc(file_api_v1_initiative, 26);
207
+
208
+ /**
209
+ * Describes the message api.v1.ReconcileInitiativeRequest.
210
+ * Use `create(ReconcileInitiativeRequestSchema)` to create a new message.
211
+ */
212
+ export const ReconcileInitiativeRequestSchema = /*@__PURE__*/
213
+ messageDesc(file_api_v1_initiative, 27);
214
+
215
+ /**
216
+ * Describes the message api.v1.ReconcileInitiativeResponse.
217
+ * Use `create(ReconcileInitiativeResponseSchema)` to create a new message.
218
+ */
219
+ export const ReconcileInitiativeResponseSchema = /*@__PURE__*/
220
+ messageDesc(file_api_v1_initiative, 28);
221
+
194
222
  /**
195
223
  * @generated from service api.v1.InitiativeService
196
224
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsee/mcp-server",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Opsee MCP server — manage projects, tasks, docs, and cycles from AI coding environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,188 @@
1
+ import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { createServer } from "../server.js";
5
+ import type { ApiClients } from "../client/api.js";
6
+
7
+ const mockInitiative = {
8
+ id: 2,
9
+ uuid: "u-2",
10
+ title: "Async brain",
11
+ status: "active",
12
+ anchorType: "none",
13
+ coreIdea: "",
14
+ projectId: 1,
15
+ createdByUserId: 7,
16
+ };
17
+
18
+ // Records the last addInitiativeMemoryEntry request so checkpoint tests can assert the mapping.
19
+ export let lastMemoryReq: any = null;
20
+
21
+ // Records the last getInitiatives request so list tests can assert the mapping.
22
+ export let lastListReq: any = null;
23
+
24
+ // Records the last reconcileInitiative request so reconcile tests can assert pass-through.
25
+ export let lastReconcileReq: any = null;
26
+
27
+ const mockListInitiative = {
28
+ id: 2, uuid: "u-2", title: "Auth refactor", status: "active",
29
+ anchorType: "none", coreIdea: "", projectId: 1, createdByUserId: 7,
30
+ };
31
+
32
+ function createMockClients(): ApiClients {
33
+ lastMemoryReq = null;
34
+ lastListReq = null;
35
+ lastReconcileReq = null;
36
+ return {
37
+ initiatives: {
38
+ getInitiatives: async (req: any) => {
39
+ lastListReq = req;
40
+ return { initiatives: [mockListInitiative], pagination: {} };
41
+ },
42
+ getInitiativeContext: async () => ({
43
+ initiative: mockInitiative,
44
+ graph: undefined,
45
+ memoryEntries: [],
46
+ pullRequests: [],
47
+ comments: [],
48
+ }),
49
+ reconcileInitiative: async (req: any) => {
50
+ lastReconcileReq = req;
51
+ return {
52
+ changeset: {
53
+ added: [{ taskId: 0, identifier: "", title: "New", reason: "" }],
54
+ removed: [],
55
+ flagged: [{ taskId: 3, identifier: "T-3", title: "InFlight", reason: "has linked PR" }],
56
+ edgesAdded: [],
57
+ edgesRemoved: [],
58
+ },
59
+ };
60
+ },
61
+ addInitiativeMemoryEntry: async (req: any) => {
62
+ lastMemoryReq = req;
63
+ return {
64
+ memoryEntry: {
65
+ id: 99,
66
+ initiativeId: req.initiativeId,
67
+ kind: req.kind,
68
+ body: req.body,
69
+ contentType: req.contentType,
70
+ sourceType: req.sourceType,
71
+ sourceId: req.sourceId,
72
+ sourceUrl: req.sourceUrl,
73
+ },
74
+ };
75
+ },
76
+ },
77
+ } as unknown as ApiClients;
78
+ }
79
+
80
+ describe("Initiative write-back discipline", () => {
81
+ let client: Client;
82
+ let cleanup: () => Promise<void>;
83
+
84
+ beforeAll(async () => {
85
+ const [ct, st] = InMemoryTransport.createLinkedPair();
86
+ const server = createServer(() => createMockClients());
87
+ await server.connect(st);
88
+ client = new Client({ name: "init-test", version: "1.0.0" });
89
+ await client.connect(ct);
90
+ cleanup = async () => { await client.close(); await server.close(); };
91
+ });
92
+
93
+ afterAll(async () => { await cleanup(); });
94
+
95
+ beforeEach(() => { lastMemoryReq = null; lastListReq = null; lastReconcileReq = null; });
96
+
97
+ test("get_initiative_context appends the working agreement", async () => {
98
+ const result = await client.callTool({
99
+ name: "opsee_get_initiative_context",
100
+ arguments: { initiativeId: 2 },
101
+ });
102
+ const text = (result.content as Array<{ type: string; text: string }>)[0].text;
103
+ expect(text).toContain("Working agreement");
104
+ expect(text).toContain("opsee_checkpoint");
105
+ expect(text).toContain("opsee_add_initiative_memory");
106
+ });
107
+
108
+ test("checkpoint writes an outcome entry by default with no source", async () => {
109
+ const result = await client.callTool({
110
+ name: "opsee_checkpoint",
111
+ arguments: { initiativeId: 2, summary: "Wired the parser" },
112
+ });
113
+ const text = (result.content as Array<{ type: string; text: string }>)[0].text;
114
+ expect(text).toContain("Checkpoint recorded");
115
+ expect(lastMemoryReq.kind).toBe("outcome");
116
+ expect(lastMemoryReq.body).toBe("Wired the parser");
117
+ expect(lastMemoryReq.sourceType).toBe("none");
118
+ // next-obligations nudge
119
+ expect(text).toContain("Link the PR");
120
+ });
121
+
122
+ test("checkpoint maps taskId to a task source", async () => {
123
+ await client.callTool({
124
+ name: "opsee_checkpoint",
125
+ arguments: { initiativeId: 2, summary: "done", taskId: 918 },
126
+ });
127
+ expect(lastMemoryReq.sourceType).toBe("task");
128
+ expect(lastMemoryReq.sourceId).toBe(918);
129
+ expect(lastMemoryReq.sourceUrl).toBeUndefined();
130
+ });
131
+
132
+ test("checkpoint maps prUrl to a pull_request source", async () => {
133
+ await client.callTool({
134
+ name: "opsee_checkpoint",
135
+ arguments: { initiativeId: 2, summary: "shipped", prUrl: "https://example.com/mr/94" },
136
+ });
137
+ expect(lastMemoryReq.sourceType).toBe("pull_request");
138
+ expect(lastMemoryReq.sourceUrl).toBe("https://example.com/mr/94");
139
+ expect(lastMemoryReq.sourceId).toBeUndefined();
140
+ });
141
+
142
+ test("checkpoint with both taskId and prUrl prefers task source and keeps the url", async () => {
143
+ await client.callTool({
144
+ name: "opsee_checkpoint",
145
+ arguments: { initiativeId: 2, summary: "both", taskId: 918, prUrl: "https://example.com/mr/94" },
146
+ });
147
+ expect(lastMemoryReq.sourceType).toBe("task");
148
+ expect(lastMemoryReq.sourceId).toBe(918);
149
+ expect(lastMemoryReq.sourceUrl).toBe("https://example.com/mr/94");
150
+ });
151
+
152
+ test("checkpoint honors an explicit kind", async () => {
153
+ await client.callTool({
154
+ name: "opsee_checkpoint",
155
+ arguments: { initiativeId: 2, summary: "learned a thing", kind: "learning" },
156
+ });
157
+ expect(lastMemoryReq.kind).toBe("learning");
158
+ });
159
+
160
+ test("list_initiatives passes search through filterOptions", async () => {
161
+ await client.callTool({
162
+ name: "opsee_list_initiatives",
163
+ arguments: { projectId: 1, search: "auth" },
164
+ });
165
+ expect(lastListReq.filterOptions?.search).toBe("auth");
166
+ });
167
+
168
+ test("list_initiatives omits filterOptions when no search", async () => {
169
+ await client.callTool({
170
+ name: "opsee_list_initiatives",
171
+ arguments: { projectId: 1 },
172
+ });
173
+ expect(lastListReq.filterOptions).toBeUndefined();
174
+ });
175
+
176
+ test("reconcile passes dryRun + existingTaskId and renders flagged drops", async () => {
177
+ const result = await client.callTool({
178
+ name: "opsee_reconcile_initiative",
179
+ arguments: { initiativeId: 2, dryRun: true, tasks: [{ ref: "a", title: "Keep", existingTaskId: 1 }], edges: [] },
180
+ });
181
+ expect(lastReconcileReq.dryRun).toBe(true);
182
+ expect(lastReconcileReq.tasks[0].existingTaskId).toBe(1);
183
+ const text = (result.content as Array<{ type: string; text: string }>)[0].text;
184
+ expect(text).toContain("[DRY RUN");
185
+ expect(text).toContain("flagged");
186
+ expect(text).toContain("has linked PR");
187
+ });
188
+ });
@@ -9,6 +9,7 @@ import {
9
9
  formatInitiativeMemoryList,
10
10
  formatInitiativeGraph,
11
11
  formatInitiativeContext,
12
+ formatReconcileChangeset,
12
13
  formatError,
13
14
  } from "../utils/format.js";
14
15
 
@@ -24,15 +25,19 @@ export function registerInitiativeTools(
24
25
  ): void {
25
26
  server.tool(
26
27
  "opsee_list_initiatives",
27
- "List initiatives in an Opsee project.",
28
- { projectId: z.number().describe("The project ID") },
28
+ "List initiatives in an Opsee project. Pass `search` to find a specific initiative by keyword — it matches the title and summary, tokenized, and ranks the best match first (most-recently-updated breaks ties). Use this to resume work by name, e.g. for \"resume the auth-refactor initiative\" call with search: \"auth\". Without `search`, returns all initiatives, most recent first.",
29
+ {
30
+ projectId: z.number().describe("The project ID"),
31
+ search: z.string().optional().describe("Keyword to match against initiative title + summary (ranked); omit to list all by recency"),
32
+ },
29
33
  { readOnlyHint: true, destructiveHint: false },
30
- async ({ projectId }) => {
34
+ async ({ projectId, search }) => {
31
35
  try {
32
36
  const clients = getClients();
33
37
  const res = await clients.initiatives.getInitiatives({
34
38
  projectId,
35
39
  pagination: overridePagination(),
40
+ filterOptions: search ? { search } : undefined,
36
41
  });
37
42
  return { content: [{ type: "text", text: formatInitiativeList(res.initiatives) }] };
38
43
  } catch (error) {
@@ -43,7 +48,7 @@ export function registerInitiativeTools(
43
48
 
44
49
  server.tool(
45
50
  "opsee_get_initiative",
46
- "Get details of a specific initiative by ID.",
51
+ "Get details of a specific initiative by ID (title, status, summary, core idea). To pick up or resume WORK on it — the task tree, memory log, and PRs together — call opsee_get_initiative_context instead.",
47
52
  { initiativeId: z.number().describe("The initiative ID") },
48
53
  { readOnlyHint: true, destructiveHint: false },
49
54
  async ({ initiativeId }) => {
@@ -60,7 +65,7 @@ export function registerInitiativeTools(
60
65
 
61
66
  server.tool(
62
67
  "opsee_create_initiative",
63
- "Create a new initiative in an Opsee project.",
68
+ "Create a new initiative in an Opsee project. After creating, expand the plan in the core idea (the coreIdea field); when the plan is ready, propose a decomposition with opsee_decompose_initiative rather than creating tasks by hand.",
64
69
  {
65
70
  projectId: z.number().describe("The project ID"),
66
71
  title: z.string().describe("Initiative title"),
@@ -98,7 +103,7 @@ export function registerInitiativeTools(
98
103
 
99
104
  server.tool(
100
105
  "opsee_update_initiative",
101
- "Update an initiative's fields. Fetches the current initiative first, then applies only the provided changes.",
106
+ "Update an initiative's fields. Fetches the current initiative first, then applies only the provided changes. Status lifecycle: draft → active (opsee_decompose_initiative flips this automatically) → completed when the work is done. For dropped work, set status to 'abandoned' WITH a reason (in the summary or a memory entry) rather than deleting — that preserves the shared brain.",
102
107
  {
103
108
  initiativeId: z.number().describe("The initiative ID to update"),
104
109
  title: z.string().optional().describe("New title"),
@@ -140,7 +145,7 @@ export function registerInitiativeTools(
140
145
 
141
146
  server.tool(
142
147
  "opsee_delete_initiative",
143
- "Delete an initiative permanently.",
148
+ "Delete an initiative permanently. Prefer setting status to 'abandoned' (via opsee_update_initiative) with a reason — that preserves the initiative's memory/shared brain for later reference. Only hard-delete an initiative created in error.",
144
149
  { initiativeId: z.number().describe("The initiative ID to delete") },
145
150
  { readOnlyHint: false, destructiveHint: true },
146
151
  async ({ initiativeId }) => {
@@ -156,7 +161,7 @@ export function registerInitiativeTools(
156
161
 
157
162
  server.tool(
158
163
  "opsee_get_initiative_context",
159
- "Load an entire Initiative work session in one call: the core-idea doc body, the execution task tree with parallel slices (what can run concurrently vs sequentially), the append-only memory log, and rolled-up PRs. Call this first when picking up or resuming work on an Initiative.",
164
+ "Load an entire Initiative work session in one call: the core-idea doc body, the execution task tree with parallel slices (what can run concurrently vs sequentially), the append-only memory log, and rolled-up PRs. Call this FIRST when picking up or resuming work on an Initiative. The response ends with a working agreement: treat this context as the source of truth, log decisions/blockers/learnings as you go with opsee_add_initiative_memory, and call opsee_checkpoint when you finish a unit of work.",
160
165
  {
161
166
  initiativeId: z.number().describe("The initiative ID"),
162
167
  memoryLimit: z.number().optional().describe("Cap on memory entries returned (most recent first); 0 or omitted = service default"),
@@ -186,7 +191,7 @@ export function registerInitiativeTools(
186
191
 
187
192
  server.tool(
188
193
  "opsee_get_initiative_graph",
189
- "Get the task dependency graph for an initiative, including parallel execution batches (slices).",
194
+ "Get the task dependency graph for an initiative, including parallel execution batches (slices). Use the slices to choose the next batch of tasks that can be worked concurrently. (opsee_get_initiative_context already includes this graph — call this only when you need the graph on its own.)",
190
195
  { initiativeId: z.number().describe("The initiative ID") },
191
196
  { readOnlyHint: true, destructiveHint: false },
192
197
  async ({ initiativeId }) => {
@@ -203,7 +208,7 @@ export function registerInitiativeTools(
203
208
 
204
209
  server.tool(
205
210
  "opsee_decompose_initiative",
206
- "Materialize a proposed breakdown as real Tasks under the Initiative in one atomic call. Wire the tree with temporary `ref` ids: set `parentRef` to nest a subtask, and add `edges` (from_ref blocks to_ref) to express sequencing — tasks with no blocking edge between them become parallel slices. All-or-nothing: a bad ref rolls the whole thing back.",
211
+ "Materialize a proposed breakdown as real Tasks under the Initiative. PROPOSE FIRST: present the proposed task tree (titles, structure, dependencies) to the human and get their approval BEFORE calling this — that approval is the \"confirm\" step. Nothing is created until you call it, so to let the human decline, simply don't call it. Calling it materializes the tasks onto the board in one atomic operation and moves a draft Initiative to active (the conclude→execute transition). Wire the tree with temporary `ref` ids: set `parentRef` to nest a subtask, and add `edges` (from_ref blocks to_ref) to express sequencing — tasks with no blocking edge between them become parallel slices. All-or-nothing: a bad ref rolls the whole thing back.",
207
212
  {
208
213
  initiativeId: z.number().describe("The initiative ID"),
209
214
  tasks: z.array(z.object({
@@ -256,9 +261,81 @@ export function registerInitiativeTools(
256
261
  },
257
262
  );
258
263
 
264
+ server.tool(
265
+ "opsee_reconcile_initiative",
266
+ [
267
+ "Reconcile a proposed task tree against the current Initiative — diff, flag, and optionally apply.",
268
+ "",
269
+ "Use this when the Initiative already has tasks (from opsee_decompose_initiative) and the plan has changed.",
270
+ "For first-time task creation on a fresh Initiative, use opsee_decompose_initiative instead.",
271
+ "",
272
+ "Workflow (follow this every time):",
273
+ "1. Call opsee_get_initiative_context to load the current task tree. Note each task's ID.",
274
+ "2. Build your proposed task list. For tasks that already exist and should be kept, set existingTaskId",
275
+ " to the task's real ID (from the context). Omit existingTaskId for brand-new tasks.",
276
+ "3. Call opsee_reconcile_initiative with dryRun:true to preview the changeset.",
277
+ "4. Present the changeset to the human — ESPECIALLY the flagged items. Flagged tasks carry real",
278
+ " execution state (linked PRs, worklogs, in-progress status) and will NOT be deleted. They stay",
279
+ " on the board unchanged; the human must decide what to do with them.",
280
+ "5. Get explicit human approval before applying.",
281
+ "6. Call opsee_reconcile_initiative with dryRun:false to apply the approved plan.",
282
+ ].join("\n"),
283
+ {
284
+ initiativeId: z.number().describe("The initiative ID"),
285
+ tasks: z.array(z.object({
286
+ ref: z.string().describe("Temporary reference ID used to wire edges and parent relationships"),
287
+ title: z.string().describe("Task title"),
288
+ description: z.string().optional().describe("Task description"),
289
+ parentRef: z.string().optional().describe("Ref of the parent task (to nest this as a subtask)"),
290
+ taskTypeId: z.number().optional().describe("Task type ID"),
291
+ taskPriorityId: z.number().optional().describe("Task priority ID"),
292
+ boardColumnId: z.number().optional().describe("Board column ID (status)"),
293
+ existingTaskId: z.number().optional().describe("Real task ID from the current Initiative — tag kept tasks with this value (obtained from opsee_get_initiative_context). Omit for new tasks."),
294
+ })).describe("Proposed task tree — include kept tasks with existingTaskId, new tasks without"),
295
+ edges: z.array(z.object({
296
+ fromRef: z.string().describe("Ref of the source task"),
297
+ toRef: z.string().describe("Ref of the target task"),
298
+ type: z.enum(EDGE_TYPES).describe("Dependency type: blocks | blocked_by | duplicates | relates_to"),
299
+ })).describe("Dependency edges between tasks (by ref)"),
300
+ dryRun: z.boolean().optional().describe("When true, compute and return the changeset without writing anything. Always call with dryRun:true first to preview."),
301
+ },
302
+ { readOnlyHint: false, destructiveHint: true },
303
+ async ({ initiativeId, tasks, edges, dryRun }) => {
304
+ try {
305
+ const clients = getClients();
306
+ const res = await clients.initiatives.reconcileInitiative({
307
+ initiativeId,
308
+ tasks: tasks.map((t) => ({
309
+ ref: t.ref,
310
+ title: t.title,
311
+ description: t.description,
312
+ parentRef: t.parentRef,
313
+ taskTypeId: t.taskTypeId,
314
+ taskPriorityId: t.taskPriorityId,
315
+ boardColumnId: t.boardColumnId,
316
+ existingTaskId: t.existingTaskId,
317
+ })),
318
+ edges: edges.map((e) => ({
319
+ fromRef: e.fromRef,
320
+ toRef: e.toRef,
321
+ type: e.type,
322
+ })),
323
+ dryRun: dryRun ?? false,
324
+ });
325
+ if (!res.changeset) {
326
+ return { content: [{ type: "text", text: "Reconcile returned no changeset. Retry; if it persists, surface the call to the Opsee team." }], isError: true };
327
+ }
328
+ const preview = dryRun ?? false ? "[DRY RUN — no changes applied]\n\n" : "";
329
+ return { content: [{ type: "text", text: preview + formatReconcileChangeset(res.changeset) }] };
330
+ } catch (error) {
331
+ return { content: [{ type: "text", text: formatError(error) }], isError: true };
332
+ }
333
+ },
334
+ );
335
+
259
336
  server.tool(
260
337
  "opsee_add_initiative_memory",
261
- "Record a decision, outcome, learning, blocker, or context entry to the Initiative's append-only memory log. Agents should record here as they work, citing the source task or PR where relevant.",
338
+ "Log a SINGLE decision, outcome, learning, blocker, or context entry to the Initiative's append-only memory the moment it happens the as-you-go granular logger. Cite the source task or PR where relevant. For end-of-unit wrap-ups, use opsee_checkpoint instead.",
262
339
  {
263
340
  initiativeId: z.number().describe("The initiative ID"),
264
341
  kind: z.enum(MEMORY_KINDS).describe("Entry kind: decision | outcome | learning | blocker | context"),
@@ -292,6 +369,53 @@ export function registerInitiativeTools(
292
369
  },
293
370
  );
294
371
 
372
+ server.tool(
373
+ "opsee_checkpoint",
374
+ "End-of-unit wrap-up for an Initiative: records what a unit of work accomplished to the Initiative's memory and links what it touched. Call this when you finish a coherent chunk of work (a task, a PR, an investigation) — distinct from opsee_add_initiative_memory, which logs a single decision/blocker the moment it happens. Defaults the entry kind to 'outcome'.",
375
+ {
376
+ initiativeId: z.number().describe("The initiative ID"),
377
+ summary: z.string().describe("What this unit of work accomplished and what's next"),
378
+ kind: z.enum(MEMORY_KINDS).optional().describe("Memory entry kind. Defaults to 'outcome'"),
379
+ taskId: z.number().optional().describe("The task this work advanced (links the entry to it)"),
380
+ prUrl: z.string().optional().describe("The PR/MR this work produced (recorded as the entry's source URL)"),
381
+ },
382
+ { readOnlyHint: false, destructiveHint: false },
383
+ async ({ initiativeId, summary, kind, taskId, prUrl }) => {
384
+ try {
385
+ const clients = getClients();
386
+ let sourceType = "none";
387
+ let sourceId: number | undefined = undefined;
388
+ let sourceUrl: string | undefined = undefined;
389
+ if (taskId !== undefined) {
390
+ sourceType = "task";
391
+ sourceId = taskId;
392
+ if (prUrl !== undefined) sourceUrl = prUrl;
393
+ } else if (prUrl !== undefined) {
394
+ sourceType = "pull_request";
395
+ sourceUrl = prUrl;
396
+ }
397
+ const res = await clients.initiatives.addInitiativeMemoryEntry({
398
+ initiativeId,
399
+ kind: kind ?? "outcome",
400
+ body: summary,
401
+ contentType: "text",
402
+ sourceType,
403
+ sourceId,
404
+ sourceUrl,
405
+ });
406
+ if (!res.memoryEntry)
407
+ return { content: [{ type: "text", text: "Failed to record checkpoint. The backend returned an empty response (no error). Retry; if it persists, surface the call to the Opsee team." }], isError: true };
408
+ const nudge =
409
+ "\n\nNext: Link the PR to its task and advance the task's board column if you haven't — so this work rolls up on the Initiative.";
410
+ return {
411
+ content: [{ type: "text", text: `Checkpoint recorded:\n${formatInitiativeMemoryEntry(res.memoryEntry)}${nudge}` }],
412
+ };
413
+ } catch (error) {
414
+ return { content: [{ type: "text", text: formatError(error) }], isError: true };
415
+ }
416
+ },
417
+ );
418
+
295
419
  server.tool(
296
420
  "opsee_list_initiative_memory",
297
421
  "List memory entries for an initiative (newest first). Optionally filter by kind, source type, or source ID.",
@@ -574,7 +574,7 @@ export function registerTaskTools(
574
574
 
575
575
  server.tool(
576
576
  "opsee_link_task_to_initiative",
577
- "Link an existing task to an Initiative. Fetches the task first so no other fields are blanked.",
577
+ "Link an existing task to an Initiative — use this to bring an already-existing task into an initiative's scope. To create NEW tasks under an initiative, use opsee_decompose_initiative (first time) or opsee_reconcile_initiative (re-planning) instead. Fetches the task first so no other fields are blanked.",
578
578
  {
579
579
  taskId: z.number().describe("The task ID to link"),
580
580
  initiativeId: z.number().int().min(1).describe("The initiative ID to link the task to"),
@@ -1,6 +1,6 @@
1
1
  import type { Task, Project, Cycle, DocPage, DocSpace, Milestone, MilestoneTask, User, Label, TaskLabel, Comment, TaskDependency, Initiative, InitiativeMemoryEntry, TaskPullRequest } from "../../gen/api/v1/models_pb.js";
2
2
  import { TaskDependencyType } from "../../gen/api/v1/models_pb.js";
3
- import type { InitiativeGraph, InitiativeSlice } from "../../gen/api/v1/initiative_pb.js";
3
+ import type { InitiativeGraph, InitiativeSlice, ReconcileChangeset } from "../../gen/api/v1/initiative_pb.js";
4
4
 
5
5
  export function formatProject(p: Project): string {
6
6
  const lines = [
@@ -472,6 +472,15 @@ export function formatInitiativeGraph(graph: InitiativeGraph, taskMap?: Map<numb
472
472
  return lines.join("\n");
473
473
  }
474
474
 
475
+ export const WORKING_AGREEMENT = [
476
+ "— Working agreement —",
477
+ "This Initiative is a shared brain that outlives this session. To keep it useful:",
478
+ "1. This context (core idea · task tree · memory · PRs) is the source of truth — read it before acting.",
479
+ "2. Log each decision / blocker / learning as it happens with opsee_add_initiative_memory (cite the task or PR).",
480
+ "3. When you finish a unit of work, call opsee_checkpoint to record what changed and what's next.",
481
+ "4. Link PRs to their tasks so they roll up here.",
482
+ ].join("\n");
483
+
475
484
  export function formatInitiativeContext(
476
485
  initiative: Initiative,
477
486
  graph: InitiativeGraph | undefined,
@@ -524,9 +533,63 @@ export function formatInitiativeContext(
524
533
  });
525
534
  }
526
535
 
536
+ sections.push("\n" + WORKING_AGREEMENT);
527
537
  return sections.join("\n");
528
538
  }
529
539
 
540
+ export function formatReconcileChangeset(changeset: ReconcileChangeset): string {
541
+ const lines: string[] = [];
542
+
543
+ if (changeset.added.length > 0) {
544
+ lines.push(`Added (${changeset.added.length}):`);
545
+ changeset.added.forEach((t) => {
546
+ const id = t.identifier || (t.taskId ? `#${t.taskId}` : "(new)");
547
+ lines.push(` + [${id}] ${t.title}`);
548
+ });
549
+ }
550
+
551
+ if (changeset.removed.length > 0) {
552
+ lines.push(`Removed — pristine, no work detected (${changeset.removed.length}):`);
553
+ changeset.removed.forEach((t) => {
554
+ const id = t.identifier || `#${t.taskId}`;
555
+ lines.push(` − [${id}] ${t.title}`);
556
+ });
557
+ }
558
+
559
+ if (changeset.flagged.length > 0) {
560
+ lines.push(`Flagged — NOT removed, carries execution state (${changeset.flagged.length}):`);
561
+ lines.push(" These tasks have real work in progress and were left untouched.");
562
+ changeset.flagged.forEach((t) => {
563
+ const id = t.identifier || `#${t.taskId}`;
564
+ lines.push(` ! [${id}] ${t.title} — flagged: ${t.reason}`);
565
+ });
566
+ }
567
+
568
+ if (changeset.edgesAdded.length > 0) {
569
+ lines.push(`Edges + (${changeset.edgesAdded.length}):`);
570
+ changeset.edgesAdded.forEach((e) => {
571
+ lines.push(` + #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
572
+ });
573
+ }
574
+
575
+ if (changeset.edgesRemoved.length > 0) {
576
+ lines.push(`Edges − (${changeset.edgesRemoved.length}):`);
577
+ changeset.edgesRemoved.forEach((e) => {
578
+ lines.push(` − #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
579
+ });
580
+ }
581
+
582
+ if (lines.length === 0) {
583
+ lines.push("No changes — the proposed plan matches the existing task tree.");
584
+ }
585
+
586
+ if (changeset.graph) {
587
+ lines.push("\n" + formatInitiativeGraph(changeset.graph));
588
+ }
589
+
590
+ return lines.join("\n");
591
+ }
592
+
530
593
  export function formatError(error: unknown): string {
531
594
  if (error instanceof Error) {
532
595
  const msg = error.message;