@opsee/mcp-server 0.8.3 → 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.
- package/gen/api/v1/initiative_pb.d.ts +146 -0
- package/gen/api/v1/initiative_pb.js +29 -1
- package/package.json +1 -1
- package/src/__tests__/initiatives.test.ts +188 -0
- package/src/tools/initiatives.ts +135 -11
- package/src/tools/tasks.ts +1 -1
- package/src/utils/format.ts +64 -1
|
@@ -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+
|
|
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
|
@@ -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
|
+
});
|
package/src/tools/initiatives.ts
CHANGED
|
@@ -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
|
-
{
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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.",
|
package/src/tools/tasks.ts
CHANGED
|
@@ -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"),
|
package/src/utils/format.ts
CHANGED
|
@@ -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;
|