fix: remove redundant code in processing logic
Some checks failed
CI / build-and-test (push) Failing after -31m35s
Some checks failed
CI / build-and-test (push) Failing after -31m35s
This commit is contained in:
@@ -205,6 +205,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
Projects: tools.NewProjectsTool(db, activeProjects),
|
Projects: tools.NewProjectsTool(db, activeProjects),
|
||||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||||
|
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
||||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||||
|
|||||||
1
internal/app/ui/dist/placeholder.txt
vendored
1
internal/app/ui/dist/placeholder.txt
vendored
@@ -1 +0,0 @@
|
|||||||
placeholder file to keep ui/dist present for go:embed in test environments
|
|
||||||
@@ -15,8 +15,9 @@ type ModelPublicAgentGuardrails struct {
|
|||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
||||||
Tags resolvespec_common.SqlString `bun:"tags,type:text,nullzero," json:"tags"`
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
|
RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||||
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ type ModelPublicAgentSkills struct {
|
|||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Tags resolvespec_common.SqlString `bun:"tags,type:text,nullzero," json:"tags"`
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||||
|
RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||||
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ModelPublicChatHistories struct {
|
|||||||
AgentID resolvespec_common.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
AgentID resolvespec_common.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
||||||
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'[',notnull," json:"messages"`
|
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'',notnull," json:"messages"`
|
||||||
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
||||||
ProjectID resolvespec_common.SqlUUID `bun:"project_id,type:uuid,nullzero," json:"project_id"`
|
ProjectID resolvespec_common.SqlUUID `bun:"project_id,type:uuid,nullzero," json:"project_id"`
|
||||||
SessionID resolvespec_common.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
SessionID resolvespec_common.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type ModelPublicEmbeddings struct {
|
|||||||
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
|
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||||
Dim resolvespec_common.SqlInt32 `bun:"dim,type:int,notnull," json:"dim"`
|
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
|
||||||
Embedding resolvespec_common.SqlString `bun:"embedding,type:vector,notnull," json:"embedding"`
|
Embedding resolvespec_common.SqlString `bun:"embedding,type:vector,notnull," json:"embedding"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Model resolvespec_common.SqlString `bun:"model,type:text,notnull,unique:uidx_embeddings_thought_id_model," json:"model"`
|
Model resolvespec_common.SqlString `bun:"model,type:text,notnull,unique:uidx_embeddings_thought_id_model," json:"model"`
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type ModelPublicImportantDates struct {
|
|||||||
FamilyMemberID resolvespec_common.SqlUUID `bun:"family_member_id,type:uuid,nullzero," json:"family_member_id"`
|
FamilyMemberID resolvespec_common.SqlUUID `bun:"family_member_id,type:uuid,nullzero," json:"family_member_id"`
|
||||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
||||||
RecurringYearly bool `bun:"recurring_yearly,type:boolean,default:false,notnull," json:"recurring_yearly"`
|
RecurringYearly bool `bun:"recurring_yearly,type:boolean,default:false,notnull," json:"recurring_yearly"`
|
||||||
ReminderDaysBefore resolvespec_common.SqlInt32 `bun:"reminder_days_before,type:int,default:7,notnull," json:"reminder_days_before"`
|
ReminderDaysBefore int32 `bun:"reminder_days_before,type:int,default:7,notnull," json:"reminder_days_before"`
|
||||||
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
|
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||||
RelFamilyMemberID *ModelPublicFamilyMembers `bun:"rel:has-one,join:family_member_id=id" json:"relfamilymemberid,omitempty"` // Has one ModelPublicFamilyMembers
|
RelFamilyMemberID *ModelPublicFamilyMembers `bun:"rel:has-one,join:family_member_id=id" json:"relfamilymemberid,omitempty"` // Has one ModelPublicFamilyMembers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type ModelPublicLearnings struct {
|
|||||||
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||||
SupersedesLearningID resolvespec_common.SqlUUID `bun:"supersedes_learning_id,type:uuid,nullzero," json:"supersedes_learning_id"`
|
SupersedesLearningID resolvespec_common.SqlUUID `bun:"supersedes_learning_id,type:uuid,nullzero," json:"supersedes_learning_id"`
|
||||||
Tags resolvespec_common.SqlString `bun:"tags,type:text,nullzero," json:"tags"`
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=guid" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=guid" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
|
|||||||
63
internal/generatedmodels/sql_public_plan_dependencies.go
Normal file
63
internal/generatedmodels/sql_public_plan_dependencies.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicPlanDependencies struct {
|
||||||
|
bun.BaseModel `bun:"table:public.plan_dependencies,alias:plan_dependencies"`
|
||||||
|
ID resolvespec_common.SqlInt32 `bun:"id,type:serial,pk,autoincrement," json:"id"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
DependsOnPlanID resolvespec_common.SqlUUID `bun:"depends_on_plan_id,type:uuid,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"depends_on_plan_id"`
|
||||||
|
PlanID resolvespec_common.SqlUUID `bun:"plan_id,type:uuid,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"plan_id"`
|
||||||
|
RelDependsOnPlanID *ModelPublicPlans `bun:"rel:has-one,join:depends_on_plan_id=id" json:"reldependsonplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicPlanDependencies
|
||||||
|
func (m ModelPublicPlanDependencies) TableName() string {
|
||||||
|
return "public.plan_dependencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicPlanDependencies
|
||||||
|
func (m ModelPublicPlanDependencies) TableNameOnly() string {
|
||||||
|
return "plan_dependencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicPlanDependencies
|
||||||
|
func (m ModelPublicPlanDependencies) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicPlanDependencies) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicPlanDependencies) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicPlanDependencies) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicPlanDependencies) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicPlanDependencies) GetPrefix() string {
|
||||||
|
return "PDL"
|
||||||
|
}
|
||||||
63
internal/generatedmodels/sql_public_plan_guardrails.go
Normal file
63
internal/generatedmodels/sql_public_plan_guardrails.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicPlanGuardrails struct {
|
||||||
|
bun.BaseModel `bun:"table:public.plan_guardrails,alias:plan_guardrails"`
|
||||||
|
ID resolvespec_common.SqlInt32 `bun:"id,type:serial,pk,autoincrement," json:"id"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
GuardrailID resolvespec_common.SqlUUID `bun:"guardrail_id,type:uuid,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"guardrail_id"`
|
||||||
|
PlanID resolvespec_common.SqlUUID `bun:"plan_id,type:uuid,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"plan_id"`
|
||||||
|
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||||
|
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicPlanGuardrails
|
||||||
|
func (m ModelPublicPlanGuardrails) TableName() string {
|
||||||
|
return "public.plan_guardrails"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicPlanGuardrails
|
||||||
|
func (m ModelPublicPlanGuardrails) TableNameOnly() string {
|
||||||
|
return "plan_guardrails"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicPlanGuardrails
|
||||||
|
func (m ModelPublicPlanGuardrails) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicPlanGuardrails) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicPlanGuardrails) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicPlanGuardrails) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicPlanGuardrails) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicPlanGuardrails) GetPrefix() string {
|
||||||
|
return "PGL"
|
||||||
|
}
|
||||||
63
internal/generatedmodels/sql_public_plan_related_plans.go
Normal file
63
internal/generatedmodels/sql_public_plan_related_plans.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicPlanRelatedPlans struct {
|
||||||
|
bun.BaseModel `bun:"table:public.plan_related_plans,alias:plan_related_plans"`
|
||||||
|
ID resolvespec_common.SqlInt32 `bun:"id,type:serial,pk,autoincrement," json:"id"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
PlanAID resolvespec_common.SqlUUID `bun:"plan_a_id,type:uuid,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_a_id"`
|
||||||
|
PlanBID resolvespec_common.SqlUUID `bun:"plan_b_id,type:uuid,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_b_id"`
|
||||||
|
RelPlanAID *ModelPublicPlans `bun:"rel:has-one,join:plan_a_id=id" json:"relplanaid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
RelPlanBID *ModelPublicPlans `bun:"rel:has-one,join:plan_b_id=id" json:"relplanbid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicPlanRelatedPlans
|
||||||
|
func (m ModelPublicPlanRelatedPlans) TableName() string {
|
||||||
|
return "public.plan_related_plans"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicPlanRelatedPlans
|
||||||
|
func (m ModelPublicPlanRelatedPlans) TableNameOnly() string {
|
||||||
|
return "plan_related_plans"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicPlanRelatedPlans
|
||||||
|
func (m ModelPublicPlanRelatedPlans) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicPlanRelatedPlans) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicPlanRelatedPlans) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicPlanRelatedPlans) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicPlanRelatedPlans) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicPlanRelatedPlans) GetPrefix() string {
|
||||||
|
return "PRP"
|
||||||
|
}
|
||||||
63
internal/generatedmodels/sql_public_plan_skills.go
Normal file
63
internal/generatedmodels/sql_public_plan_skills.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicPlanSkills struct {
|
||||||
|
bun.BaseModel `bun:"table:public.plan_skills,alias:plan_skills"`
|
||||||
|
ID resolvespec_common.SqlInt32 `bun:"id,type:serial,pk,autoincrement," json:"id"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
PlanID resolvespec_common.SqlUUID `bun:"plan_id,type:uuid,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"plan_id"`
|
||||||
|
SkillID resolvespec_common.SqlUUID `bun:"skill_id,type:uuid,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"skill_id"`
|
||||||
|
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicPlanSkills
|
||||||
|
func (m ModelPublicPlanSkills) TableName() string {
|
||||||
|
return "public.plan_skills"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicPlanSkills
|
||||||
|
func (m ModelPublicPlanSkills) TableNameOnly() string {
|
||||||
|
return "plan_skills"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicPlanSkills
|
||||||
|
func (m ModelPublicPlanSkills) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicPlanSkills) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicPlanSkills) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicPlanSkills) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicPlanSkills) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicPlanSkills) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicPlanSkills) GetPrefix() string {
|
||||||
|
return "PSL"
|
||||||
|
}
|
||||||
80
internal/generatedmodels/sql_public_plans.go
Normal file
80
internal/generatedmodels/sql_public_plans.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicPlans struct {
|
||||||
|
bun.BaseModel `bun:"table:public.plans,alias:plans"`
|
||||||
|
ID resolvespec_common.SqlUUID `bun:"id,type:uuid,pk,default:gen_random_uuid()," json:"id"`
|
||||||
|
CompletedAt resolvespec_common.SqlTimeStamp `bun:"completed_at,type:timestamptz,nullzero," json:"completed_at"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
|
DueDate resolvespec_common.SqlTimeStamp `bun:"due_date,type:timestamptz,nullzero," json:"due_date"`
|
||||||
|
LastReviewedAt resolvespec_common.SqlTimeStamp `bun:"last_reviewed_at,type:timestamptz,nullzero," json:"last_reviewed_at"`
|
||||||
|
Owner resolvespec_common.SqlString `bun:"owner,type:text,nullzero," json:"owner"`
|
||||||
|
Priority resolvespec_common.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"` // low, medium, high, critical
|
||||||
|
ProjectID resolvespec_common.SqlUUID `bun:"project_id,type:uuid,nullzero," json:"project_id"`
|
||||||
|
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||||
|
Status resolvespec_common.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
|
||||||
|
SupersedesPlanID resolvespec_common.SqlUUID `bun:"supersedes_plan_id,type:uuid,nullzero," json:"supersedes_plan_id"`
|
||||||
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
|
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||||
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=guid" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
|
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
|
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||||
|
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||||
|
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||||
|
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||||
|
RelPlanIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||||
|
RelPlanIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicPlans
|
||||||
|
func (m ModelPublicPlans) TableName() string {
|
||||||
|
return "public.plans"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicPlans
|
||||||
|
func (m ModelPublicPlans) TableNameOnly() string {
|
||||||
|
return "plans"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicPlans
|
||||||
|
func (m ModelPublicPlans) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicPlans) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicPlans) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%v", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicPlans) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicPlans) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicPlans) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicPlans) GetPrefix() string {
|
||||||
|
return "PLA"
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ type ModelPublicProfessionalContacts struct {
|
|||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
||||||
Phone resolvespec_common.SqlString `bun:"phone,type:text,nullzero," json:"phone"`
|
Phone resolvespec_common.SqlString `bun:"phone,type:text,nullzero," json:"phone"`
|
||||||
Tags resolvespec_common.SqlString `bun:"tags,type:text,nullzero," json:"tags"`
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
Title resolvespec_common.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
Title resolvespec_common.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelContactIDPublicContactInteractions []*ModelPublicContactInteractions `bun:"rel:has-many,join:id=contact_id" json:"relcontactidpubliccontactinteractions,omitempty"` // Has many ModelPublicContactInteractions
|
RelContactIDPublicContactInteractions []*ModelPublicContactInteractions `bun:"rel:has-many,join:id=contact_id" json:"relcontactidpubliccontactinteractions,omitempty"` // Has many ModelPublicContactInteractions
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type ModelPublicProjects struct {
|
|||||||
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||||
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
||||||
RelProjectIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
RelProjectIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||||
|
RelProjectIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicplans,omitempty"` // Has many ModelPublicPlans
|
||||||
RelProjectIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
RelProjectIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
||||||
RelProjectIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
RelProjectIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:guid=project_id" json:"relprojectidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ type ModelPublicRecipes struct {
|
|||||||
CookTimeMinutes resolvespec_common.SqlInt32 `bun:"cook_time_minutes,type:int,nullzero," json:"cook_time_minutes"`
|
CookTimeMinutes resolvespec_common.SqlInt32 `bun:"cook_time_minutes,type:int,nullzero," json:"cook_time_minutes"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Cuisine resolvespec_common.SqlString `bun:"cuisine,type:text,nullzero," json:"cuisine"`
|
Cuisine resolvespec_common.SqlString `bun:"cuisine,type:text,nullzero," json:"cuisine"`
|
||||||
Ingredients resolvespec_common.SqlJSONB `bun:"ingredients,type:jsonb,default:'[',notnull," json:"ingredients"`
|
Ingredients resolvespec_common.SqlJSONB `bun:"ingredients,type:jsonb,default:'',notnull," json:"ingredients"`
|
||||||
Instructions resolvespec_common.SqlJSONB `bun:"instructions,type:jsonb,default:'[',notnull," json:"instructions"`
|
Instructions resolvespec_common.SqlJSONB `bun:"instructions,type:jsonb,default:'',notnull," json:"instructions"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
||||||
PrepTimeMinutes resolvespec_common.SqlInt32 `bun:"prep_time_minutes,type:int,nullzero," json:"prep_time_minutes"`
|
PrepTimeMinutes resolvespec_common.SqlInt32 `bun:"prep_time_minutes,type:int,nullzero," json:"prep_time_minutes"`
|
||||||
Rating resolvespec_common.SqlInt32 `bun:"rating,type:int,nullzero," json:"rating"`
|
Rating resolvespec_common.SqlInt32 `bun:"rating,type:int,nullzero," json:"rating"`
|
||||||
Servings resolvespec_common.SqlInt32 `bun:"servings,type:int,nullzero," json:"servings"`
|
Servings resolvespec_common.SqlInt32 `bun:"servings,type:int,nullzero," json:"servings"`
|
||||||
Tags resolvespec_common.SqlString `bun:"tags,type:text,nullzero," json:"tags"`
|
Tags resolvespec_common.SqlString `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelRecipeIDPublicMealPlans []*ModelPublicMealPlans `bun:"rel:has-many,join:id=recipe_id" json:"relrecipeidpublicmealplans,omitempty"` // Has many ModelPublicMealPlans
|
RelRecipeIDPublicMealPlans []*ModelPublicMealPlans `bun:"rel:has-many,join:id=recipe_id" json:"relrecipeidpublicmealplans,omitempty"` // Has many ModelPublicMealPlans
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type ModelPublicShoppingLists struct {
|
|||||||
bun.BaseModel `bun:"table:public.shopping_lists,alias:shopping_lists"`
|
bun.BaseModel `bun:"table:public.shopping_lists,alias:shopping_lists"`
|
||||||
ID resolvespec_common.SqlUUID `bun:"id,type:uuid,pk,default:gen_random_uuid()," json:"id"`
|
ID resolvespec_common.SqlUUID `bun:"id,type:uuid,pk,default:gen_random_uuid()," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Items resolvespec_common.SqlJSONB `bun:"items,type:jsonb,default:'[',notnull," json:"items"`
|
Items resolvespec_common.SqlJSONB `bun:"items,type:jsonb,default:'',notnull," json:"items"`
|
||||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
Notes resolvespec_common.SqlString `bun:"notes,type:text,nullzero," json:"notes"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
WeekStart resolvespec_common.SqlDate `bun:"week_start,type:date,notnull," json:"week_start"`
|
WeekStart resolvespec_common.SqlDate `bun:"week_start,type:date,notnull," json:"week_start"`
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type ToolSet struct {
|
|||||||
ChatHistory *tools.ChatHistoryTool
|
ChatHistory *tools.ChatHistoryTool
|
||||||
Describe *tools.DescribeTool
|
Describe *tools.DescribeTool
|
||||||
Learnings *tools.LearningsTool
|
Learnings *tools.LearningsTool
|
||||||
|
Plans *tools.PlansTool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
||||||
@@ -85,6 +86,7 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
|||||||
registerThoughtTools,
|
registerThoughtTools,
|
||||||
registerProjectTools,
|
registerProjectTools,
|
||||||
registerLearningTools,
|
registerLearningTools,
|
||||||
|
registerPlanTools,
|
||||||
registerFileTools,
|
registerFileTools,
|
||||||
registerMaintenanceTools,
|
registerMaintenanceTools,
|
||||||
registerSkillTools,
|
registerSkillTools,
|
||||||
@@ -273,6 +275,100 @@ func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet Tool
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "create_plan",
|
||||||
|
Description: "Create a structured plan linked to a project.",
|
||||||
|
}, toolSet.Plans.Create); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "get_plan",
|
||||||
|
Description: "Retrieve a plan with its dependencies, related plans, skills, and guardrails.",
|
||||||
|
}, toolSet.Plans.Get); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "update_plan",
|
||||||
|
Description: "Update plan fields; only provided fields are changed.",
|
||||||
|
}, toolSet.Plans.Update); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "delete_plan",
|
||||||
|
Description: "Hard-delete a plan by id.",
|
||||||
|
}, toolSet.Plans.Delete); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "list_plans",
|
||||||
|
Description: "List plans with optional project, status, priority, owner, tag, and text filters.",
|
||||||
|
}, toolSet.Plans.List); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "add_plan_dependency",
|
||||||
|
Description: "Mark plan_id as depending on depends_on_plan_id (must complete first).",
|
||||||
|
}, toolSet.Plans.AddDependency); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "remove_plan_dependency",
|
||||||
|
Description: "Remove a dependency between two plans.",
|
||||||
|
}, toolSet.Plans.RemoveDependency); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "add_related_plan",
|
||||||
|
Description: "Link two plans as thematically related (bidirectional).",
|
||||||
|
}, toolSet.Plans.AddRelated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "remove_related_plan",
|
||||||
|
Description: "Unlink two related plans.",
|
||||||
|
}, toolSet.Plans.RemoveRelated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "add_plan_skill",
|
||||||
|
Description: "Link an agent skill to a plan.",
|
||||||
|
}, toolSet.Plans.AddSkill); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "remove_plan_skill",
|
||||||
|
Description: "Unlink an agent skill from a plan.",
|
||||||
|
}, toolSet.Plans.RemoveSkill); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "list_plan_skills",
|
||||||
|
Description: "List skills linked to a plan.",
|
||||||
|
}, toolSet.Plans.ListSkills); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "add_plan_guardrail",
|
||||||
|
Description: "Link an agent guardrail to a plan.",
|
||||||
|
}, toolSet.Plans.AddGuardrail); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "remove_plan_guardrail",
|
||||||
|
Description: "Unlink an agent guardrail from a plan.",
|
||||||
|
}, toolSet.Plans.RemoveGuardrail); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "list_plan_guardrails",
|
||||||
|
Description: "List guardrails linked to a plan.",
|
||||||
|
}, toolSet.Plans.ListGuardrails); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
server.AddResourceTemplate(&mcp.ResourceTemplate{
|
server.AddResourceTemplate(&mcp.ResourceTemplate{
|
||||||
Name: "stored_file",
|
Name: "stored_file",
|
||||||
@@ -460,7 +556,7 @@ func registerChatHistoryTools(server *mcp.Server, logger *slog.Logger, toolSet T
|
|||||||
func registerDescribeTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
func registerDescribeTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
if err := addTool(server, logger, &mcp.Tool{
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
Name: "describe_tools",
|
Name: "describe_tools",
|
||||||
Description: "Call first each session. All tools with categories and usage notes. Categories: system, thoughts, projects, files, admin, maintenance, skills, chat, meta.",
|
Description: "Call first each session. All tools with categories and usage notes. Categories: system, thoughts, projects, files, admin, maintenance, skills, plans, chat, meta.",
|
||||||
}, toolSet.Describe.Describe); err != nil {
|
}, toolSet.Describe.Describe); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -506,6 +602,23 @@ func BuildToolCatalog() []tools.ToolEntry {
|
|||||||
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
||||||
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
||||||
|
|
||||||
|
// plans
|
||||||
|
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
|
||||||
|
{Name: "get_plan", Description: "Retrieve a full plan including dependencies (depends_on/blocks), related plans, linked skills, and guardrails.", Category: "plans"},
|
||||||
|
{Name: "update_plan", Description: "Partially update a plan; only provided fields are changed. Use mark_reviewed to stamp last_reviewed_at.", Category: "plans"},
|
||||||
|
{Name: "delete_plan", Description: "Hard-delete a plan by id.", Category: "plans"},
|
||||||
|
{Name: "list_plans", Description: "List plans with optional filters: project, status, priority, owner, tag, and full-text query.", Category: "plans"},
|
||||||
|
{Name: "add_plan_dependency", Description: "Declare that plan_id cannot proceed until depends_on_plan_id is complete.", Category: "plans"},
|
||||||
|
{Name: "remove_plan_dependency", Description: "Remove a directional dependency between two plans.", Category: "plans"},
|
||||||
|
{Name: "add_related_plan", Description: "Link two plans as thematically related (bidirectional, order-independent).", Category: "plans"},
|
||||||
|
{Name: "remove_related_plan", Description: "Unlink two related plans.", Category: "plans"},
|
||||||
|
{Name: "add_plan_skill", Description: "Link an agent skill to a plan so it is loaded with the plan's context.", Category: "plans"},
|
||||||
|
{Name: "remove_plan_skill", Description: "Unlink an agent skill from a plan.", Category: "plans"},
|
||||||
|
{Name: "list_plan_skills", Description: "List all skills linked to a plan.", Category: "plans"},
|
||||||
|
{Name: "add_plan_guardrail", Description: "Link an agent guardrail to a plan so it applies during plan execution.", Category: "plans"},
|
||||||
|
{Name: "remove_plan_guardrail", Description: "Unlink an agent guardrail from a plan.", Category: "plans"},
|
||||||
|
{Name: "list_plan_guardrails", Description: "List all guardrails linked to a plan.", Category: "plans"},
|
||||||
|
|
||||||
// files
|
// files
|
||||||
{Name: "upload_file", Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.", Category: "files"},
|
{Name: "upload_file", Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.", Category: "files"},
|
||||||
{Name: "save_file", Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.", Category: "files"},
|
{Name: "save_file", Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.", Category: "files"},
|
||||||
@@ -544,7 +657,7 @@ func BuildToolCatalog() []tools.ToolEntry {
|
|||||||
{Name: "delete_chat_history", Description: "Permanently delete a saved chat history by id.", Category: "chat"},
|
{Name: "delete_chat_history", Description: "Permanently delete a saved chat history by id.", Category: "chat"},
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
{Name: "describe_tools", Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, chat, meta.", Category: "meta"},
|
{Name: "describe_tools", Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, plans, chat, meta.", Category: "meta"},
|
||||||
{Name: "annotate_tool", Description: "Persist usage notes, gotchas, or workflow patterns for a specific tool. Notes survive across sessions and are returned by describe_tools. Call this whenever you discover something non-obvious about a tool's behaviour. Pass an empty string to clear notes.", Category: "meta"},
|
{Name: "annotate_tool", Description: "Persist usage notes, gotchas, or workflow patterns for a specific tool. Notes survive across sessions and are returned by describe_tools. Call this whenever you discover something non-obvious about a tool's behaviour. Pass an empty string to clear notes.", Category: "meta"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func importantDateFromModel(m generatedmodels.ModelPublicImportantDates, memberN
|
|||||||
Title: m.Title.String(),
|
Title: m.Title.String(),
|
||||||
DateValue: m.DateValue.Time(),
|
DateValue: m.DateValue.Time(),
|
||||||
RecurringYearly: m.RecurringYearly,
|
RecurringYearly: m.RecurringYearly,
|
||||||
ReminderDaysBefore: int(m.ReminderDaysBefore.Int64()),
|
ReminderDaysBefore: int(m.ReminderDaysBefore),
|
||||||
Notes: m.Notes.String(),
|
Notes: m.Notes.String(),
|
||||||
CreatedAt: m.CreatedAt.Time(),
|
CreatedAt: m.CreatedAt.Time(),
|
||||||
}
|
}
|
||||||
@@ -418,6 +418,56 @@ func shoppingListFromModel(m generatedmodels.ModelPublicShoppingLists) ext.Shopp
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func planFromModel(m generatedmodels.ModelPublicPlans, tags []string) ext.Plan {
|
||||||
|
var projectID *uuid.UUID
|
||||||
|
if m.ProjectID.Valid {
|
||||||
|
id := m.ProjectID.UUID()
|
||||||
|
projectID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
var dueDate *time.Time
|
||||||
|
if m.DueDate.Valid {
|
||||||
|
t := m.DueDate.Time()
|
||||||
|
dueDate = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedAt *time.Time
|
||||||
|
if m.CompletedAt.Valid {
|
||||||
|
t := m.CompletedAt.Time()
|
||||||
|
completedAt = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastReviewedAt *time.Time
|
||||||
|
if m.LastReviewedAt.Valid {
|
||||||
|
t := m.LastReviewedAt.Time()
|
||||||
|
lastReviewedAt = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
var supersedesPlanID *uuid.UUID
|
||||||
|
if m.SupersedesPlanID.Valid {
|
||||||
|
id := m.SupersedesPlanID.UUID()
|
||||||
|
supersedesPlanID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
return ext.Plan{
|
||||||
|
ID: m.ID.UUID(),
|
||||||
|
Title: m.Title.String(),
|
||||||
|
Description: m.Description.String(),
|
||||||
|
Status: ext.PlanStatus(m.Status.String()),
|
||||||
|
Priority: ext.PlanPriority(m.Priority.String()),
|
||||||
|
ProjectID: projectID,
|
||||||
|
Owner: m.Owner.String(),
|
||||||
|
DueDate: dueDate,
|
||||||
|
CompletedAt: completedAt,
|
||||||
|
ReviewedBy: m.ReviewedBy.String(),
|
||||||
|
LastReviewedAt: lastReviewedAt,
|
||||||
|
SupersedesPlanID: supersedesPlanID,
|
||||||
|
Tags: tags,
|
||||||
|
CreatedAt: m.CreatedAt.Time(),
|
||||||
|
UpdatedAt: m.UpdatedAt.Time(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func learningFromModel(m generatedmodels.ModelPublicLearnings, tags []string) ext.Learning {
|
func learningFromModel(m generatedmodels.ModelPublicLearnings, tags []string) ext.Learning {
|
||||||
var projectID *uuid.UUID
|
var projectID *uuid.UUID
|
||||||
if m.ProjectID.Valid {
|
if m.ProjectID.Valid {
|
||||||
|
|||||||
477
internal/store/plans.go
Normal file
477
internal/store/plans.go
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
|
||||||
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const planColumns = `
|
||||||
|
id, title, description, status, priority, project_id, owner, due_date,
|
||||||
|
completed_at, reviewed_by, last_reviewed_at, supersedes_plan_id, tags::text[], created_at, updated_at`
|
||||||
|
|
||||||
|
func (db *DB) CreatePlan(ctx context.Context, plan ext.Plan) (ext.Plan, error) {
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
insert into plans (title, description, status, priority, project_id, owner, due_date,
|
||||||
|
completed_at, reviewed_by, last_reviewed_at, supersedes_plan_id, tags)
|
||||||
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
returning`+planColumns,
|
||||||
|
strings.TrimSpace(plan.Title),
|
||||||
|
strings.TrimSpace(plan.Description),
|
||||||
|
string(plan.Status),
|
||||||
|
string(plan.Priority),
|
||||||
|
plan.ProjectID,
|
||||||
|
nullableText(plan.Owner),
|
||||||
|
plan.DueDate,
|
||||||
|
plan.CompletedAt,
|
||||||
|
nullableText(plan.ReviewedBy),
|
||||||
|
plan.LastReviewedAt,
|
||||||
|
plan.SupersedesPlanID,
|
||||||
|
plan.Tags,
|
||||||
|
)
|
||||||
|
return scanPlan(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPlan(ctx context.Context, id uuid.UUID) (ext.Plan, error) {
|
||||||
|
row := db.pool.QueryRow(ctx, `select`+planColumns+` from plans where id = $1`, id)
|
||||||
|
plan, err := scanPlan(row)
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return ext.Plan{}, fmt.Errorf("plan not found: %s", id)
|
||||||
|
}
|
||||||
|
return ext.Plan{}, fmt.Errorf("get plan: %w", err)
|
||||||
|
}
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPlanDetail(ctx context.Context, id uuid.UUID) (ext.PlanDetail, error) {
|
||||||
|
plan, err := db.GetPlan(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dependsOn, err := db.listPlansByQuery(ctx, `
|
||||||
|
select`+planColumns+`
|
||||||
|
from plans p
|
||||||
|
join plan_dependencies pd on pd.depends_on_plan_id = p.id
|
||||||
|
where pd.plan_id = $1 order by p.title`, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, fmt.Errorf("get plan depends_on: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks, err := db.listPlansByQuery(ctx, `
|
||||||
|
select`+planColumns+`
|
||||||
|
from plans p
|
||||||
|
join plan_dependencies pd on pd.plan_id = p.id
|
||||||
|
where pd.depends_on_plan_id = $1 order by p.title`, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, fmt.Errorf("get plan blocks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
related, err := db.listPlansByQuery(ctx, `
|
||||||
|
select`+planColumns+`
|
||||||
|
from plans p
|
||||||
|
where p.id in (
|
||||||
|
select plan_b_id from plan_related_plans where plan_a_id = $1
|
||||||
|
union
|
||||||
|
select plan_a_id from plan_related_plans where plan_b_id = $1
|
||||||
|
) order by p.title`, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, fmt.Errorf("get plan related: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
skills, err := db.ListPlanSkills(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, fmt.Errorf("get plan skills: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guardrails, err := db.ListPlanGuardrails(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return ext.PlanDetail{}, fmt.Errorf("get plan guardrails: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ext.PlanDetail{
|
||||||
|
Plan: plan,
|
||||||
|
DependsOn: dependsOn,
|
||||||
|
Blocks: blocks,
|
||||||
|
RelatedPlans: related,
|
||||||
|
Skills: skills,
|
||||||
|
Guardrails: guardrails,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdatePlan(ctx context.Context, id uuid.UUID, u ext.PlanUpdate) (ext.Plan, error) {
|
||||||
|
sets := []string{"updated_at = now()"}
|
||||||
|
args := []any{}
|
||||||
|
|
||||||
|
if u.Title != nil {
|
||||||
|
args = append(args, strings.TrimSpace(*u.Title))
|
||||||
|
sets = append(sets, fmt.Sprintf("title = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.Description != nil {
|
||||||
|
args = append(args, strings.TrimSpace(*u.Description))
|
||||||
|
sets = append(sets, fmt.Sprintf("description = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.Status != nil {
|
||||||
|
args = append(args, strings.TrimSpace(*u.Status))
|
||||||
|
sets = append(sets, fmt.Sprintf("status = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.Priority != nil {
|
||||||
|
args = append(args, strings.TrimSpace(*u.Priority))
|
||||||
|
sets = append(sets, fmt.Sprintf("priority = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.Owner != nil {
|
||||||
|
args = append(args, nullableText(*u.Owner))
|
||||||
|
sets = append(sets, fmt.Sprintf("owner = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.ClearDueDate {
|
||||||
|
sets = append(sets, "due_date = null")
|
||||||
|
} else if u.DueDate != nil {
|
||||||
|
args = append(args, *u.DueDate)
|
||||||
|
sets = append(sets, fmt.Sprintf("due_date = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.ClearCompletedAt {
|
||||||
|
sets = append(sets, "completed_at = null")
|
||||||
|
} else if u.CompletedAt != nil {
|
||||||
|
args = append(args, *u.CompletedAt)
|
||||||
|
sets = append(sets, fmt.Sprintf("completed_at = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.ReviewedBy != nil {
|
||||||
|
args = append(args, nullableText(*u.ReviewedBy))
|
||||||
|
sets = append(sets, fmt.Sprintf("reviewed_by = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.MarkReviewed {
|
||||||
|
sets = append(sets, "last_reviewed_at = now()")
|
||||||
|
}
|
||||||
|
if u.ClearSupersedesPlanID {
|
||||||
|
sets = append(sets, "supersedes_plan_id = null")
|
||||||
|
} else if u.SupersedesPlanID != nil {
|
||||||
|
args = append(args, *u.SupersedesPlanID)
|
||||||
|
sets = append(sets, fmt.Sprintf("supersedes_plan_id = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if u.Tags != nil {
|
||||||
|
args = append(args, *u.Tags)
|
||||||
|
sets = append(sets, fmt.Sprintf("tags = $%d", len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, id)
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
"update plans set %s where id = $%d returning%s",
|
||||||
|
strings.Join(sets, ", "), len(args), planColumns,
|
||||||
|
)
|
||||||
|
|
||||||
|
row := db.pool.QueryRow(ctx, query, args...)
|
||||||
|
plan, err := scanPlan(row)
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return ext.Plan{}, fmt.Errorf("plan not found: %s", id)
|
||||||
|
}
|
||||||
|
return ext.Plan{}, fmt.Errorf("update plan: %w", err)
|
||||||
|
}
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeletePlan(ctx context.Context, id uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `delete from plans where id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete plan: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("plan not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListPlans(ctx context.Context, filter ext.PlanFilter) ([]ext.Plan, error) {
|
||||||
|
args := make([]any, 0, 8)
|
||||||
|
conditions := make([]string, 0, 8)
|
||||||
|
|
||||||
|
if filter.ProjectID != nil {
|
||||||
|
args = append(args, *filter.ProjectID)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(filter.Status); v != "" {
|
||||||
|
args = append(args, v)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("status = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(filter.Priority); v != "" {
|
||||||
|
args = append(args, v)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(filter.Owner); v != "" {
|
||||||
|
args = append(args, v)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("owner = $%d", len(args)))
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(filter.Tag); v != "" {
|
||||||
|
args = append(args, v)
|
||||||
|
conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(filter.Query); v != "" {
|
||||||
|
args = append(args, v)
|
||||||
|
conditions = append(conditions, fmt.Sprintf(
|
||||||
|
"to_tsvector('simple', title || ' ' || coalesce(description, '')) @@ websearch_to_tsquery('simple', $%d)", len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "select" + planColumns + " from plans"
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
query += " where " + strings.Join(conditions, " and ")
|
||||||
|
}
|
||||||
|
query += " order by updated_at desc"
|
||||||
|
if filter.Limit > 0 {
|
||||||
|
args = append(args, filter.Limit)
|
||||||
|
query += fmt.Sprintf(" limit $%d", len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.listPlansByQuery(ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
|
||||||
|
func (db *DB) AddPlanDependency(ctx context.Context, planID, dependsOnPlanID uuid.UUID) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into plan_dependencies (plan_id, depends_on_plan_id)
|
||||||
|
values ($1, $2)
|
||||||
|
on conflict do nothing
|
||||||
|
`, planID, dependsOnPlanID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add plan dependency: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemovePlanDependency(ctx context.Context, planID, dependsOnPlanID uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from plan_dependencies where plan_id = $1 and depends_on_plan_id = $2
|
||||||
|
`, planID, dependsOnPlanID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove plan dependency: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("plan dependency not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Related Plans
|
||||||
|
|
||||||
|
func (db *DB) AddRelatedPlan(ctx context.Context, planAID, planBID uuid.UUID) error {
|
||||||
|
a, b := canonicalPlanPair(planAID, planBID)
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into plan_related_plans (plan_a_id, plan_b_id)
|
||||||
|
values ($1, $2)
|
||||||
|
on conflict do nothing
|
||||||
|
`, a, b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add related plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemoveRelatedPlan(ctx context.Context, planAID, planBID uuid.UUID) error {
|
||||||
|
a, b := canonicalPlanPair(planAID, planBID)
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from plan_related_plans where plan_a_id = $1 and plan_b_id = $2
|
||||||
|
`, a, b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove related plan: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("related plan link not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan Skills
|
||||||
|
|
||||||
|
func (db *DB) AddPlanSkill(ctx context.Context, planID, skillID uuid.UUID) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into plan_skills (plan_id, skill_id) values ($1, $2) on conflict do nothing
|
||||||
|
`, planID, skillID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add plan skill: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemovePlanSkill(ctx context.Context, planID, skillID uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from plan_skills where plan_id = $1 and skill_id = $2
|
||||||
|
`, planID, skillID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove plan skill: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("plan skill link not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListPlanSkills(ctx context.Context, planID uuid.UUID) ([]ext.AgentSkill, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select s.id, s.name, s.description, s.content, s.tags::text[], s.created_at, s.updated_at
|
||||||
|
from agent_skills s
|
||||||
|
join plan_skills ps on ps.skill_id = s.id
|
||||||
|
where ps.plan_id = $1
|
||||||
|
order by s.name
|
||||||
|
`, planID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list plan skills: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var skills []ext.AgentSkill
|
||||||
|
for rows.Next() {
|
||||||
|
var model generatedmodels.ModelPublicAgentSkills
|
||||||
|
var tags []string
|
||||||
|
if err := rows.Scan(&model.ID, &model.Name, &model.Description, &model.Content, &tags, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan plan skill: %w", err)
|
||||||
|
}
|
||||||
|
s := ext.AgentSkill{
|
||||||
|
ID: model.ID.UUID(),
|
||||||
|
Name: model.Name.String(),
|
||||||
|
Description: model.Description.String(),
|
||||||
|
Content: model.Content.String(),
|
||||||
|
Tags: tags,
|
||||||
|
CreatedAt: model.CreatedAt.Time(),
|
||||||
|
UpdatedAt: model.UpdatedAt.Time(),
|
||||||
|
}
|
||||||
|
if s.Tags == nil {
|
||||||
|
s.Tags = []string{}
|
||||||
|
}
|
||||||
|
skills = append(skills, s)
|
||||||
|
}
|
||||||
|
return skills, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan Guardrails
|
||||||
|
|
||||||
|
func (db *DB) AddPlanGuardrail(ctx context.Context, planID, guardrailID uuid.UUID) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into plan_guardrails (plan_id, guardrail_id) values ($1, $2) on conflict do nothing
|
||||||
|
`, planID, guardrailID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add plan guardrail: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RemovePlanGuardrail(ctx context.Context, planID, guardrailID uuid.UUID) error {
|
||||||
|
tag, err := db.pool.Exec(ctx, `
|
||||||
|
delete from plan_guardrails where plan_id = $1 and guardrail_id = $2
|
||||||
|
`, planID, guardrailID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove plan guardrail: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("plan guardrail link not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListPlanGuardrails(ctx context.Context, planID uuid.UUID) ([]ext.AgentGuardrail, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select g.id, g.name, g.description, g.content, g.severity, g.tags::text[], g.created_at, g.updated_at
|
||||||
|
from agent_guardrails g
|
||||||
|
join plan_guardrails pg on pg.guardrail_id = g.id
|
||||||
|
where pg.plan_id = $1
|
||||||
|
order by g.name
|
||||||
|
`, planID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list plan guardrails: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var guardrails []ext.AgentGuardrail
|
||||||
|
for rows.Next() {
|
||||||
|
var model generatedmodels.ModelPublicAgentGuardrails
|
||||||
|
var tags []string
|
||||||
|
if err := rows.Scan(&model.ID, &model.Name, &model.Description, &model.Content, &model.Severity, &tags, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan plan guardrail: %w", err)
|
||||||
|
}
|
||||||
|
g := ext.AgentGuardrail{
|
||||||
|
ID: model.ID.UUID(),
|
||||||
|
Name: model.Name.String(),
|
||||||
|
Description: model.Description.String(),
|
||||||
|
Content: model.Content.String(),
|
||||||
|
Severity: model.Severity.String(),
|
||||||
|
Tags: tags,
|
||||||
|
CreatedAt: model.CreatedAt.Time(),
|
||||||
|
UpdatedAt: model.UpdatedAt.Time(),
|
||||||
|
}
|
||||||
|
if g.Tags == nil {
|
||||||
|
g.Tags = []string{}
|
||||||
|
}
|
||||||
|
guardrails = append(guardrails, g)
|
||||||
|
}
|
||||||
|
return guardrails, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
type planScanner interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanPlan(row planScanner) (ext.Plan, error) {
|
||||||
|
var model generatedmodels.ModelPublicPlans
|
||||||
|
var tags []string
|
||||||
|
err := row.Scan(
|
||||||
|
&model.ID,
|
||||||
|
&model.Title,
|
||||||
|
&model.Description,
|
||||||
|
&model.Status,
|
||||||
|
&model.Priority,
|
||||||
|
&model.ProjectID,
|
||||||
|
&model.Owner,
|
||||||
|
&model.DueDate,
|
||||||
|
&model.CompletedAt,
|
||||||
|
&model.ReviewedBy,
|
||||||
|
&model.LastReviewedAt,
|
||||||
|
&model.SupersedesPlanID,
|
||||||
|
&tags,
|
||||||
|
&model.CreatedAt,
|
||||||
|
&model.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ext.Plan{}, err
|
||||||
|
}
|
||||||
|
if tags == nil {
|
||||||
|
tags = []string{}
|
||||||
|
}
|
||||||
|
return planFromModel(model, tags), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) listPlansByQuery(ctx context.Context, query string, args ...any) ([]ext.Plan, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
plans := make([]ext.Plan, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
plan, err := scanPlan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scan plan: %w", err)
|
||||||
|
}
|
||||||
|
plans = append(plans, plan)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("iterate plans: %w", err)
|
||||||
|
}
|
||||||
|
return plans, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalPlanPair ensures the smaller UUID is always plan_a_id to prevent duplicates.
|
||||||
|
func canonicalPlanPair(a, b uuid.UUID) (uuid.UUID, uuid.UUID) {
|
||||||
|
if strings.Compare(a.String(), b.String()) <= 0 {
|
||||||
|
return a, b
|
||||||
|
}
|
||||||
|
return b, a
|
||||||
|
}
|
||||||
|
|
||||||
344
internal/tools/plans.go
Normal file
344
internal/tools/plans.go
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/config"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/session"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/store"
|
||||||
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlansTool struct {
|
||||||
|
store *store.DB
|
||||||
|
sessions *session.ActiveProjects
|
||||||
|
cfg config.SearchConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlansTool(db *store.DB, sessions *session.ActiveProjects, cfg config.SearchConfig) *PlansTool {
|
||||||
|
return &PlansTool{store: db, sessions: sessions, cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- I/O types ---
|
||||||
|
|
||||||
|
type CreatePlanInput struct {
|
||||||
|
Title string `json:"title" jsonschema:"plan title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Status string `json:"status,omitempty" jsonschema:"draft|active|blocked|completed|cancelled|superseded"`
|
||||||
|
Priority string `json:"priority,omitempty" jsonschema:"low|medium|high|critical"`
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to active session project"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
DueDate string `json:"due_date,omitempty" jsonschema:"RFC3339 timestamp"`
|
||||||
|
SupersedesPlanID *uuid.UUID `json:"supersedes_plan_id,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePlanOutput struct {
|
||||||
|
Plan thoughttypes.Plan `json:"plan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPlanInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"plan id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPlanOutput struct {
|
||||||
|
Plan thoughttypes.PlanDetail `json:"plan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePlanInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"plan id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Status string `json:"status,omitempty" jsonschema:"draft|active|blocked|completed|cancelled|superseded"`
|
||||||
|
Priority string `json:"priority,omitempty" jsonschema:"low|medium|high|critical"`
|
||||||
|
Owner *string `json:"owner,omitempty" jsonschema:"empty string clears the owner"`
|
||||||
|
DueDate string `json:"due_date,omitempty" jsonschema:"RFC3339; omit to keep, 'clear' to remove"`
|
||||||
|
ClearDueDate bool `json:"clear_due_date,omitempty"`
|
||||||
|
CompletedAt string `json:"completed_at,omitempty" jsonschema:"RFC3339; omit to keep, 'clear' to remove"`
|
||||||
|
ClearCompletedAt bool `json:"clear_completed_at,omitempty"`
|
||||||
|
ReviewedBy *string `json:"reviewed_by,omitempty" jsonschema:"empty string clears the reviewer"`
|
||||||
|
MarkReviewed bool `json:"mark_reviewed,omitempty" jsonschema:"set last_reviewed_at to now"`
|
||||||
|
SupersedesPlanID *uuid.UUID `json:"supersedes_plan_id,omitempty"`
|
||||||
|
ClearSupersedesPlanID bool `json:"clear_supersedes_plan_id,omitempty"`
|
||||||
|
Tags *[]string `json:"tags,omitempty" jsonschema:"replaces all tags when provided; pass [] to clear"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePlanOutput struct {
|
||||||
|
Plan thoughttypes.Plan `json:"plan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeletePlanInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"plan id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeletePlanOutput struct {
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlansInput struct {
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to active session project"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Priority string `json:"priority,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlansOutput struct {
|
||||||
|
Plans []thoughttypes.Plan `json:"plans"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanDependencyInput struct {
|
||||||
|
PlanID uuid.UUID `json:"plan_id" jsonschema:"the plan that depends on another"`
|
||||||
|
DependsOnPlanID uuid.UUID `json:"depends_on_plan_id" jsonschema:"the plan that must complete first"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanRelatedInput struct {
|
||||||
|
PlanAID uuid.UUID `json:"plan_a_id"`
|
||||||
|
PlanBID uuid.UUID `json:"plan_b_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanLinkOutput struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanSkillInput struct {
|
||||||
|
PlanID uuid.UUID `json:"plan_id"`
|
||||||
|
SkillID uuid.UUID `json:"skill_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlanSkillsInput struct {
|
||||||
|
PlanID uuid.UUID `json:"plan_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlanSkillsOutput struct {
|
||||||
|
Skills []thoughttypes.AgentSkill `json:"skills"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanGuardrailInput struct {
|
||||||
|
PlanID uuid.UUID `json:"plan_id"`
|
||||||
|
GuardrailID uuid.UUID `json:"guardrail_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlanGuardrailsInput struct {
|
||||||
|
PlanID uuid.UUID `json:"plan_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlanGuardrailsOutput struct {
|
||||||
|
Guardrails []thoughttypes.AgentGuardrail `json:"guardrails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
|
||||||
|
func (t *PlansTool) Create(ctx context.Context, req *mcp.CallToolRequest, in CreatePlanInput) (*mcp.CallToolResult, CreatePlanOutput, error) {
|
||||||
|
title := strings.TrimSpace(in.Title)
|
||||||
|
if title == "" {
|
||||||
|
return nil, CreatePlanOutput{}, errRequiredField("title")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, CreatePlanOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := thoughttypes.Plan{
|
||||||
|
Title: title,
|
||||||
|
Description: strings.TrimSpace(in.Description),
|
||||||
|
Status: thoughttypes.PlanStatus(defaultString(strings.TrimSpace(in.Status), string(thoughttypes.PlanStatusDraft))),
|
||||||
|
Priority: thoughttypes.PlanPriority(defaultString(strings.TrimSpace(in.Priority), string(thoughttypes.PlanPriorityMedium))),
|
||||||
|
Owner: strings.TrimSpace(in.Owner),
|
||||||
|
SupersedesPlanID: in.SupersedesPlanID,
|
||||||
|
Tags: normalizeStringSlice(in.Tags),
|
||||||
|
}
|
||||||
|
if project != nil {
|
||||||
|
plan.ProjectID = &project.ID
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.DueDate); v != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, CreatePlanOutput{}, errInvalidField("due_date", "invalid due_date", "use RFC3339 format")
|
||||||
|
}
|
||||||
|
plan.DueDate = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := t.store.CreatePlan(ctx, plan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, CreatePlanOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, CreatePlanOutput{Plan: created}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) Get(ctx context.Context, _ *mcp.CallToolRequest, in GetPlanInput) (*mcp.CallToolResult, GetPlanOutput, error) {
|
||||||
|
detail, err := t.store.GetPlanDetail(ctx, in.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetPlanOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, GetPlanOutput{Plan: detail}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) Update(ctx context.Context, _ *mcp.CallToolRequest, in UpdatePlanInput) (*mcp.CallToolResult, UpdatePlanOutput, error) {
|
||||||
|
u := thoughttypes.PlanUpdate{
|
||||||
|
ReviewedBy: in.ReviewedBy,
|
||||||
|
MarkReviewed: in.MarkReviewed,
|
||||||
|
ClearDueDate: in.ClearDueDate,
|
||||||
|
ClearCompletedAt: in.ClearCompletedAt,
|
||||||
|
ClearSupersedesPlanID: in.ClearSupersedesPlanID,
|
||||||
|
SupersedesPlanID: in.SupersedesPlanID,
|
||||||
|
Tags: in.Tags,
|
||||||
|
Owner: in.Owner,
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.Title); v != "" {
|
||||||
|
u.Title = &v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.Description); v != "" {
|
||||||
|
u.Description = &v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.Status); v != "" {
|
||||||
|
u.Status = &v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.Priority); v != "" {
|
||||||
|
u.Priority = &v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.DueDate); v != "" && !in.ClearDueDate {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, UpdatePlanOutput{}, errInvalidField("due_date", "invalid due_date", "use RFC3339 format")
|
||||||
|
}
|
||||||
|
u.DueDate = &parsed
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(in.CompletedAt); v != "" && !in.ClearCompletedAt {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, UpdatePlanOutput{}, errInvalidField("completed_at", "invalid completed_at", "use RFC3339 format")
|
||||||
|
}
|
||||||
|
u.CompletedAt = &parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := t.store.UpdatePlan(ctx, in.ID, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, UpdatePlanOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, UpdatePlanOutput{Plan: plan}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) Delete(ctx context.Context, _ *mcp.CallToolRequest, in DeletePlanInput) (*mcp.CallToolResult, DeletePlanOutput, error) {
|
||||||
|
if err := t.store.DeletePlan(ctx, in.ID); err != nil {
|
||||||
|
return nil, DeletePlanOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, DeletePlanOutput{Deleted: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListPlansInput) (*mcp.CallToolResult, ListPlansOutput, error) {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListPlansOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := thoughttypes.PlanFilter{
|
||||||
|
Limit: normalizeLimit(in.Limit, t.cfg),
|
||||||
|
Status: strings.TrimSpace(in.Status),
|
||||||
|
Priority: strings.TrimSpace(in.Priority),
|
||||||
|
Owner: strings.TrimSpace(in.Owner),
|
||||||
|
Tag: strings.TrimSpace(in.Tag),
|
||||||
|
Query: strings.TrimSpace(in.Query),
|
||||||
|
}
|
||||||
|
if project != nil {
|
||||||
|
filter.ProjectID = &project.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
plans, err := t.store.ListPlans(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListPlansOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, ListPlansOutput{Plans: plans}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) AddDependency(ctx context.Context, _ *mcp.CallToolRequest, in PlanDependencyInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if in.PlanID == in.DependsOnPlanID {
|
||||||
|
return nil, PlanLinkOutput{}, errInvalidField("depends_on_plan_id", "a plan cannot depend on itself", "use a different plan id")
|
||||||
|
}
|
||||||
|
if err := t.store.AddPlanDependency(ctx, in.PlanID, in.DependsOnPlanID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) RemoveDependency(ctx context.Context, _ *mcp.CallToolRequest, in PlanDependencyInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.RemovePlanDependency(ctx, in.PlanID, in.DependsOnPlanID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) AddRelated(ctx context.Context, _ *mcp.CallToolRequest, in PlanRelatedInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if in.PlanAID == in.PlanBID {
|
||||||
|
return nil, PlanLinkOutput{}, errInvalidField("plan_b_id", "a plan cannot be related to itself", "use a different plan id")
|
||||||
|
}
|
||||||
|
if err := t.store.AddRelatedPlan(ctx, in.PlanAID, in.PlanBID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) RemoveRelated(ctx context.Context, _ *mcp.CallToolRequest, in PlanRelatedInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.RemoveRelatedPlan(ctx, in.PlanAID, in.PlanBID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) AddSkill(ctx context.Context, _ *mcp.CallToolRequest, in PlanSkillInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.AddPlanSkill(ctx, in.PlanID, in.SkillID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) RemoveSkill(ctx context.Context, _ *mcp.CallToolRequest, in PlanSkillInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.RemovePlanSkill(ctx, in.PlanID, in.SkillID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) ListSkills(ctx context.Context, _ *mcp.CallToolRequest, in ListPlanSkillsInput) (*mcp.CallToolResult, ListPlanSkillsOutput, error) {
|
||||||
|
skills, err := t.store.ListPlanSkills(ctx, in.PlanID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListPlanSkillsOutput{}, err
|
||||||
|
}
|
||||||
|
if skills == nil {
|
||||||
|
skills = []thoughttypes.AgentSkill{}
|
||||||
|
}
|
||||||
|
return nil, ListPlanSkillsOutput{Skills: skills}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) AddGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in PlanGuardrailInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.AddPlanGuardrail(ctx, in.PlanID, in.GuardrailID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) RemoveGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in PlanGuardrailInput) (*mcp.CallToolResult, PlanLinkOutput, error) {
|
||||||
|
if err := t.store.RemovePlanGuardrail(ctx, in.PlanID, in.GuardrailID); err != nil {
|
||||||
|
return nil, PlanLinkOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, PlanLinkOutput{OK: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PlansTool) ListGuardrails(ctx context.Context, _ *mcp.CallToolRequest, in ListPlanGuardrailsInput) (*mcp.CallToolResult, ListPlanGuardrailsOutput, error) {
|
||||||
|
guardrails, err := t.store.ListPlanGuardrails(ctx, in.PlanID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListPlanGuardrailsOutput{}, err
|
||||||
|
}
|
||||||
|
if guardrails == nil {
|
||||||
|
guardrails = []thoughttypes.AgentGuardrail{}
|
||||||
|
}
|
||||||
|
return nil, ListPlanGuardrailsOutput{Guardrails: guardrails}, nil
|
||||||
|
}
|
||||||
83
internal/types/plan.go
Normal file
83
internal/types/plan.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlanStatusDraft PlanStatus = "draft"
|
||||||
|
PlanStatusActive PlanStatus = "active"
|
||||||
|
PlanStatusBlocked PlanStatus = "blocked"
|
||||||
|
PlanStatusCompleted PlanStatus = "completed"
|
||||||
|
PlanStatusCancelled PlanStatus = "cancelled"
|
||||||
|
PlanStatusSuperseded PlanStatus = "superseded"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanPriority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlanPriorityLow PlanPriority = "low"
|
||||||
|
PlanPriorityMedium PlanPriority = "medium"
|
||||||
|
PlanPriorityHigh PlanPriority = "high"
|
||||||
|
PlanPriorityCritical PlanPriority = "critical"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status PlanStatus `json:"status"`
|
||||||
|
Priority PlanPriority `json:"priority"`
|
||||||
|
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
ReviewedBy string `json:"reviewed_by,omitempty"`
|
||||||
|
LastReviewedAt *time.Time `json:"last_reviewed_at,omitempty"`
|
||||||
|
SupersedesPlanID *uuid.UUID `json:"supersedes_plan_id,omitempty"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanDetail enriches Plan with all related records, returned by get_plan.
|
||||||
|
type PlanDetail struct {
|
||||||
|
Plan
|
||||||
|
DependsOn []Plan `json:"depends_on"`
|
||||||
|
Blocks []Plan `json:"blocks"`
|
||||||
|
RelatedPlans []Plan `json:"related_plans"`
|
||||||
|
Skills []AgentSkill `json:"skills"`
|
||||||
|
Guardrails []AgentGuardrail `json:"guardrails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanFilter struct {
|
||||||
|
Limit int
|
||||||
|
ProjectID *uuid.UUID
|
||||||
|
Status string
|
||||||
|
Priority string
|
||||||
|
Owner string
|
||||||
|
Tag string
|
||||||
|
Query string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanUpdate describes a partial update; nil pointer fields are not touched.
|
||||||
|
type PlanUpdate struct {
|
||||||
|
Title *string
|
||||||
|
Description *string
|
||||||
|
Status *string
|
||||||
|
Priority *string
|
||||||
|
Owner *string // "" to clear
|
||||||
|
DueDate *time.Time // nil = no change
|
||||||
|
ClearDueDate bool // true = set NULL (takes priority over DueDate)
|
||||||
|
CompletedAt *time.Time
|
||||||
|
ClearCompletedAt bool
|
||||||
|
ReviewedBy *string // "" to clear
|
||||||
|
MarkReviewed bool // sets last_reviewed_at = now()
|
||||||
|
SupersedesPlanID *uuid.UUID
|
||||||
|
ClearSupersedesPlanID bool
|
||||||
|
Tags *[]string // nil = no change; replace when non-nil
|
||||||
|
}
|
||||||
@@ -89,6 +89,32 @@ Do not abandon the project scope or retry without a project. The project simply
|
|||||||
- Do not base64-encode a file to pass it to `save_file` if an `amcs://files/{id}` URI is already available from a prior `upload_file` or HTTP upload.
|
- Do not base64-encode a file to pass it to `save_file` if an `amcs://files/{id}` URI is already available from a prior `upload_file` or HTTP upload.
|
||||||
- When saving, choose the narrowest correct scope: project if project-specific, global if not.
|
- When saving, choose the narrowest correct scope: project if project-specific, global if not.
|
||||||
|
|
||||||
|
## Plans
|
||||||
|
|
||||||
|
Plans are structured, trackable work items linked to projects. Use plans for multi-step goals, workstreams, or anything that needs an owner, due date, status lifecycle, or explicit dependency tracking.
|
||||||
|
|
||||||
|
- **Status lifecycle**: `draft` → `active` → `blocked` | `completed` | `cancelled` | `superseded`
|
||||||
|
- **Priority**: `low`, `medium` (default), `high`, `critical`
|
||||||
|
- Create plans with `create_plan` (required: `title`; optional: `description`, `status`, `priority`, `project`, `owner`, `due_date`, `supersedes_plan_id`, `tags`).
|
||||||
|
- Retrieve a full plan with `get_plan` — returns the plan plus `depends_on`, `blocks`, `related_plans`, `skills`, and `guardrails` in a single call.
|
||||||
|
- Partially update a plan with `update_plan` (only provided fields change). Use `mark_reviewed: true` to stamp `last_reviewed_at` without manually passing a timestamp.
|
||||||
|
- List and filter with `list_plans` (project/status/priority/owner/tag/query).
|
||||||
|
- Delete permanently with `delete_plan`.
|
||||||
|
|
||||||
|
**Dependencies** (directional — "A cannot proceed until B is done"):
|
||||||
|
- `add_plan_dependency` / `remove_plan_dependency` using `plan_id` and `depends_on_plan_id`.
|
||||||
|
- `get_plan` returns `depends_on` (plans this plan waits on) and `blocks` (plans waiting on this one).
|
||||||
|
|
||||||
|
**Related plans** (bidirectional — thematically linked, no ordering):
|
||||||
|
- `add_related_plan` / `remove_related_plan` using `plan_a_id` and `plan_b_id` (order does not matter).
|
||||||
|
|
||||||
|
**Plan skills and guardrails** (agent behaviour scoped to a plan):
|
||||||
|
- `add_plan_skill` / `remove_plan_skill` / `list_plan_skills`
|
||||||
|
- `add_plan_guardrail` / `remove_plan_guardrail` / `list_plan_guardrails`
|
||||||
|
- Load plan skills and guardrails alongside project skills/guardrails when working within a specific plan's scope.
|
||||||
|
|
||||||
|
**Freshness**: use `last_reviewed_at` and `reviewed_by` to track whether a plan is current. Set `mark_reviewed: true` on `update_plan` after reviewing a plan so staleness is visible in `list_plans` results.
|
||||||
|
|
||||||
## Tool Annotations
|
## Tool Annotations
|
||||||
|
|
||||||
As you learn non-obvious behaviours, gotchas, or workflow patterns for individual tools, persist them with `annotate_tool`:
|
As you learn non-obvious behaviours, gotchas, or workflow patterns for individual tools, persist them with `annotate_tool`:
|
||||||
@@ -109,4 +135,4 @@ Notes are returned by `describe_tools` in future sessions. Annotate whenever you
|
|||||||
|
|
||||||
## Short Operational Form
|
## Short Operational Form
|
||||||
|
|
||||||
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store raw/durable notes with `capture_thought`, and store curated durable lessons with `add_learning`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.
|
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store raw/durable notes with `capture_thought`, store curated durable lessons with `add_learning`, and track structured multi-step goals with `create_plan`. Use `get_plan` to load a plan's full context including dependencies, related plans, and linked skills/guardrails. Stamp `last_reviewed_at` on plans you review with `update_plan mark_reviewed: true`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
92
schema/plans.dbml
Normal file
92
schema/plans.dbml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
Table plans {
|
||||||
|
id uuid [pk, default: `gen_random_uuid()`]
|
||||||
|
title text [not null]
|
||||||
|
description text [not null, default: '']
|
||||||
|
status text [not null, default: 'draft'] // draft, active, blocked, completed, cancelled, superseded
|
||||||
|
priority text [not null, default: 'medium'] // low, medium, high, critical
|
||||||
|
project_id uuid [ref: > projects.guid]
|
||||||
|
owner text
|
||||||
|
due_date timestamptz
|
||||||
|
completed_at timestamptz
|
||||||
|
reviewed_by text
|
||||||
|
last_reviewed_at timestamptz
|
||||||
|
supersedes_plan_id uuid [ref: > plans.id]
|
||||||
|
tags "text[]" [not null, default: `'{}'`]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
updated_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
project_id
|
||||||
|
status
|
||||||
|
priority
|
||||||
|
owner
|
||||||
|
due_date
|
||||||
|
last_reviewed_at
|
||||||
|
tags [type: gin]
|
||||||
|
title [type: gin]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directional: plan_id cannot proceed until depends_on_plan_id is complete
|
||||||
|
Table plan_dependencies {
|
||||||
|
id serial [pk]
|
||||||
|
plan_id uuid [not null, ref: > plans.id]
|
||||||
|
depends_on_plan_id uuid [not null, ref: > plans.id]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(plan_id, depends_on_plan_id) [unique]
|
||||||
|
plan_id
|
||||||
|
depends_on_plan_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bidirectional: store with plan_a_id < plan_b_id to avoid duplicates
|
||||||
|
Table plan_related_plans {
|
||||||
|
id serial [pk]
|
||||||
|
plan_a_id uuid [not null, ref: > plans.id]
|
||||||
|
plan_b_id uuid [not null, ref: > plans.id]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(plan_a_id, plan_b_id) [unique]
|
||||||
|
plan_a_id
|
||||||
|
plan_b_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table plan_skills {
|
||||||
|
id serial [pk]
|
||||||
|
plan_id uuid [not null, ref: > plans.id]
|
||||||
|
skill_id uuid [not null, ref: > agent_skills.id]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(plan_id, skill_id) [unique]
|
||||||
|
plan_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table plan_guardrails {
|
||||||
|
id serial [pk]
|
||||||
|
plan_id uuid [not null, ref: > plans.id]
|
||||||
|
guardrail_id uuid [not null, ref: > agent_guardrails.id]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(plan_id, guardrail_id) [unique]
|
||||||
|
plan_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-file refs (for relspecgo merge)
|
||||||
|
Ref: plans.project_id > projects.guid [delete: set null]
|
||||||
|
Ref: plans.supersedes_plan_id > plans.id [delete: set null]
|
||||||
|
Ref: plan_dependencies.plan_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_dependencies.depends_on_plan_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_related_plans.plan_a_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_related_plans.plan_b_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_skills.plan_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_skills.skill_id > agent_skills.id [delete: cascade]
|
||||||
|
Ref: plan_guardrails.plan_id > plans.id [delete: cascade]
|
||||||
|
Ref: plan_guardrails.guardrail_id > agent_guardrails.id [delete: cascade]
|
||||||
@@ -313,6 +313,21 @@ export const api = {
|
|||||||
dry_run: input?.dry_run ?? false
|
dry_run: input?.dry_run ?? false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
plans: {
|
||||||
|
list: async (params?: { status?: string; priority?: string; project_id?: string; limit?: number }) => {
|
||||||
|
const filters: ResolveSpecFilter[] = [];
|
||||||
|
if (params?.status) filters.push({ column: 'status', operator: 'eq', value: params.status });
|
||||||
|
if (params?.priority) filters.push({ column: 'priority', operator: 'eq', value: params.priority });
|
||||||
|
if (params?.project_id) filters.push({ column: 'project_id', operator: 'eq', value: params.project_id });
|
||||||
|
|
||||||
|
const rows = await rsReadMany<Omit<import('./types').Plan, 'tags'> & { tags?: unknown }>('plans', {
|
||||||
|
filters,
|
||||||
|
limit: params?.limit ?? 500,
|
||||||
|
sort: [{ column: 'updated_at', direction: 'desc' }]
|
||||||
|
});
|
||||||
|
return rows.map((row) => ({ ...row, tags: normalizeTags(row.tags) }));
|
||||||
|
}
|
||||||
|
},
|
||||||
stats: async () => {
|
stats: async () => {
|
||||||
type StatsThoughtRow = {
|
type StatsThoughtRow = {
|
||||||
metadata?: {
|
metadata?: {
|
||||||
|
|||||||
197
ui/src/components/plans/PlansPage.svelte
Normal file
197
ui/src/components/plans/PlansPage.svelte
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { GridlerFull, type GridlerColumn } from "@warkypublic/svelix";
|
||||||
|
import { GlobalStateStore } from "../../shellState";
|
||||||
|
import { adminGridTheme } from "../../gridTheme";
|
||||||
|
import type { Plan } from "../../types";
|
||||||
|
|
||||||
|
let selectedPlan = $state<Plan | null>(null);
|
||||||
|
let gridTotal = $state<number | null>(null);
|
||||||
|
|
||||||
|
const plansDataSourceOptions = {
|
||||||
|
url: "/api/rs",
|
||||||
|
authToken: GlobalStateStore.getState().session.authToken,
|
||||||
|
schema: "public",
|
||||||
|
entity: "plans",
|
||||||
|
uniqueID: "id",
|
||||||
|
sort: [{ column: "updated_at", direction: "desc" }],
|
||||||
|
} as unknown as {
|
||||||
|
url: string;
|
||||||
|
authToken?: string;
|
||||||
|
schema: string;
|
||||||
|
entity: string;
|
||||||
|
uniqueID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridlerColumn[] = [
|
||||||
|
{ id: "title", title: "Title", dataKey: "title", width: 340 },
|
||||||
|
{ id: "status", title: "Status", dataKey: "status", width: 120 },
|
||||||
|
{ id: "priority", title: "Priority", dataKey: "priority", width: 110 },
|
||||||
|
{ id: "owner", title: "Owner", dataKey: "owner", width: 160 },
|
||||||
|
{ id: "due_date", title: "Due", dataKey: "due_date", width: 180, format: "datetime" },
|
||||||
|
{ id: "last_reviewed_at", title: "Reviewed", dataKey: "last_reviewed_at", width: 180, format: "datetime" },
|
||||||
|
{ id: "updated_at", title: "Updated", dataKey: "updated_at", width: 180, format: "datetime" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeTags(value: unknown): string[] {
|
||||||
|
if (Array.isArray(value)) return value.map((t) => String(t).trim()).filter(Boolean);
|
||||||
|
if (typeof value !== "string" || !value.trim()) return [];
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
||||||
|
return trimmed.slice(1, -1).split(",").map((t) => t.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
|
||||||
|
}
|
||||||
|
return trimmed.split(",").map((t) => t.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlan(rowData: Record<string, unknown>): Plan {
|
||||||
|
return {
|
||||||
|
id: String(rowData.id ?? ""),
|
||||||
|
title: typeof rowData.title === "string" ? rowData.title : "",
|
||||||
|
description: typeof rowData.description === "string" ? rowData.description : "",
|
||||||
|
status: (typeof rowData.status === "string" ? rowData.status : "draft") as Plan["status"],
|
||||||
|
priority: (typeof rowData.priority === "string" ? rowData.priority : "medium") as Plan["priority"],
|
||||||
|
project_id: typeof rowData.project_id === "string" ? rowData.project_id : undefined,
|
||||||
|
owner: typeof rowData.owner === "string" && rowData.owner ? rowData.owner : undefined,
|
||||||
|
due_date: typeof rowData.due_date === "string" ? rowData.due_date : undefined,
|
||||||
|
completed_at: typeof rowData.completed_at === "string" ? rowData.completed_at : undefined,
|
||||||
|
reviewed_by: typeof rowData.reviewed_by === "string" ? rowData.reviewed_by : undefined,
|
||||||
|
last_reviewed_at: typeof rowData.last_reviewed_at === "string" ? rowData.last_reviewed_at : undefined,
|
||||||
|
supersedes_plan_id: typeof rowData.supersedes_plan_id === "string" ? rowData.supersedes_plan_id : undefined,
|
||||||
|
tags: normalizeTags(rowData.tags),
|
||||||
|
created_at: String(rowData.created_at ?? ""),
|
||||||
|
updated_at: String(rowData.updated_at ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRowClick(_row: number, rowData: Record<string, unknown> | undefined) {
|
||||||
|
selectedPlan = rowData ? normalizePlan(rowData) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridEvent(
|
||||||
|
type: string,
|
||||||
|
_item?: unknown,
|
||||||
|
_column?: unknown,
|
||||||
|
_coords?: unknown,
|
||||||
|
detail?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
if (type !== "page_loaded" && type !== "load") return;
|
||||||
|
const total = detail?.total;
|
||||||
|
if (typeof total === "number") gridTotal = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string): string {
|
||||||
|
if (!value) return "—";
|
||||||
|
return new Date(value).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusClasses: Record<string, string> = {
|
||||||
|
draft: "bg-slate-700/60 text-slate-300",
|
||||||
|
active: "bg-cyan-900/60 text-cyan-200",
|
||||||
|
blocked: "bg-amber-900/60 text-amber-200",
|
||||||
|
completed: "bg-emerald-900/60 text-emerald-200",
|
||||||
|
cancelled: "bg-slate-800/60 text-slate-500",
|
||||||
|
superseded: "bg-purple-900/60 text-purple-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityClasses: Record<string, string> = {
|
||||||
|
low: "bg-slate-700/60 text-slate-400",
|
||||||
|
medium: "bg-cyan-900/60 text-cyan-300",
|
||||||
|
high: "bg-amber-900/60 text-amber-300",
|
||||||
|
critical: "bg-rose-900/60 text-rose-300",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4 w-full">
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-white">Plans</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{#if gridTotal === null}
|
||||||
|
Server-backed grid
|
||||||
|
{:else}
|
||||||
|
{gridTotal} plan{gridTotal !== 1 ? "s" : ""}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 xl:grid-cols-[1.6fr_1fr]">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
|
||||||
|
<GridlerFull
|
||||||
|
{columns}
|
||||||
|
theme={adminGridTheme}
|
||||||
|
rowMarkers="number"
|
||||||
|
height={560}
|
||||||
|
width="100%"
|
||||||
|
pageSize={40}
|
||||||
|
dataSource="resolvespec"
|
||||||
|
dataSourceOptions={plansDataSourceOptions}
|
||||||
|
serverSideSearch={true}
|
||||||
|
searchColumns={["title", "description", "status", "priority", "owner"]}
|
||||||
|
{onGridEvent}
|
||||||
|
{onRowClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||||
|
<h3 class="text-sm font-semibold text-white">Plan Inspector</h3>
|
||||||
|
|
||||||
|
{#if !selectedPlan}
|
||||||
|
<p class="mt-3 text-sm text-slate-500">
|
||||||
|
Select a plan row to inspect details and relationships.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-3 space-y-3 text-sm text-slate-300">
|
||||||
|
<p class="text-base font-semibold text-slate-100">{selectedPlan.title}</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span class={`inline-flex items-center rounded-lg px-2.5 py-0.5 text-xs font-medium ${statusClasses[selectedPlan.status] ?? "bg-slate-700/60 text-slate-300"}`}>
|
||||||
|
{selectedPlan.status}
|
||||||
|
</span>
|
||||||
|
<span class={`inline-flex items-center rounded-lg px-2.5 py-0.5 text-xs font-medium ${priorityClasses[selectedPlan.priority] ?? "bg-slate-700/60 text-slate-300"}`}>
|
||||||
|
{selectedPlan.priority}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3 space-y-1">
|
||||||
|
<p><strong class="text-slate-100">Owner:</strong> {selectedPlan.owner || "—"}</p>
|
||||||
|
<p><strong class="text-slate-100">Due:</strong> {formatDate(selectedPlan.due_date)}</p>
|
||||||
|
<p><strong class="text-slate-100">Completed:</strong> {formatDate(selectedPlan.completed_at)}</p>
|
||||||
|
<p><strong class="text-slate-100">Last reviewed:</strong> {formatDate(selectedPlan.last_reviewed_at)}</p>
|
||||||
|
<p><strong class="text-slate-100">Reviewed by:</strong> {selectedPlan.reviewed_by || "—"}</p>
|
||||||
|
<p><strong class="text-slate-100">Created:</strong> {formatDate(selectedPlan.created_at)}</p>
|
||||||
|
<p><strong class="text-slate-100">Updated:</strong> {formatDate(selectedPlan.updated_at)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedPlan.description}
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Description</p>
|
||||||
|
<p class="mt-2 whitespace-pre-wrap text-slate-300">{selectedPlan.description}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedPlan.tags.length > 0}
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Tags</p>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{#each selectedPlan.tags as tag}
|
||||||
|
<span class="rounded-md bg-white/10 px-2 py-0.5 text-xs text-slate-300">{tag}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedPlan.project_id || selectedPlan.supersedes_plan_id}
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3 space-y-1">
|
||||||
|
{#if selectedPlan.project_id}
|
||||||
|
<p><strong class="text-slate-100">Project:</strong> <span class="font-mono text-xs text-slate-400">{selectedPlan.project_id}</span></p>
|
||||||
|
{/if}
|
||||||
|
{#if selectedPlan.supersedes_plan_id}
|
||||||
|
<p><strong class="text-slate-100">Supersedes:</strong> <span class="font-mono text-xs text-slate-400">{selectedPlan.supersedes_plan_id}</span></p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import FilesPage from '../files/FilesPage.svelte';
|
import FilesPage from '../files/FilesPage.svelte';
|
||||||
import GuardrailsPage from '../guardrails/GuardrailsPage.svelte';
|
import GuardrailsPage from '../guardrails/GuardrailsPage.svelte';
|
||||||
import LearningsPage from '../learnings/LearningsPage.svelte';
|
import LearningsPage from '../learnings/LearningsPage.svelte';
|
||||||
|
import PlansPage from '../plans/PlansPage.svelte';
|
||||||
import MaintenancePage from '../maintenance/MaintenancePage.svelte';
|
import MaintenancePage from '../maintenance/MaintenancePage.svelte';
|
||||||
import DashboardPage from '../dashboard/DashboardPage.svelte';
|
import DashboardPage from '../dashboard/DashboardPage.svelte';
|
||||||
import ProjectsPage from '../projects/ProjectsPage.svelte';
|
import ProjectsPage from '../projects/ProjectsPage.svelte';
|
||||||
@@ -41,6 +42,8 @@
|
|||||||
<ThoughtsPage />
|
<ThoughtsPage />
|
||||||
{:else if currentPage === 'learnings'}
|
{:else if currentPage === 'learnings'}
|
||||||
<LearningsPage />
|
<LearningsPage />
|
||||||
|
{:else if currentPage === 'plans'}
|
||||||
|
<PlansPage />
|
||||||
{:else if currentPage === 'skills'}
|
{:else if currentPage === 'skills'}
|
||||||
<SkillsPage />
|
<SkillsPage />
|
||||||
{:else if currentPage === 'guardrails'}
|
{:else if currentPage === 'guardrails'}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
{ id: 'projects', label: 'Projects', description: 'Browse and manage projects.' },
|
{ id: 'projects', label: 'Projects', description: 'Browse and manage projects.' },
|
||||||
{ id: 'thoughts', label: 'Thoughts', description: 'Search and inspect thoughts.' },
|
{ id: 'thoughts', label: 'Thoughts', description: 'Search and inspect thoughts.' },
|
||||||
{ id: 'learnings', label: 'Learnings', description: 'Curated insights and outcomes.' },
|
{ id: 'learnings', label: 'Learnings', description: 'Curated insights and outcomes.' },
|
||||||
|
{ id: 'plans', label: 'Plans', description: 'Structured plans and workstreams.' },
|
||||||
{ id: 'skills', label: 'Skills', description: 'Agent skill registry.' },
|
{ id: 'skills', label: 'Skills', description: 'Agent skill registry.' },
|
||||||
{ id: 'guardrails', label: 'Guardrails', description: 'Agent guardrail registry.' },
|
{ id: 'guardrails', label: 'Guardrails', description: 'Agent guardrail registry.' },
|
||||||
{ id: 'files', label: 'Files', description: 'Stored file inventory.' },
|
{ id: 'files', label: 'Files', description: 'Stored file inventory.' },
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export type NavItem = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'learnings' | 'skills' | 'guardrails' | 'files' | 'maintenance';
|
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'learnings' | 'plans' | 'skills' | 'guardrails' | 'files' | 'maintenance';
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -181,6 +181,24 @@ export type Learning = {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Plan = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status: 'draft' | 'active' | 'blocked' | 'completed' | 'cancelled' | 'superseded';
|
||||||
|
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
project_id?: string;
|
||||||
|
owner?: string;
|
||||||
|
due_date?: string;
|
||||||
|
completed_at?: string;
|
||||||
|
reviewed_by?: string;
|
||||||
|
last_reviewed_at?: string;
|
||||||
|
supersedes_plan_id?: string;
|
||||||
|
tags: string[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type MaintenanceTask = {
|
export type MaintenanceTask = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user