diff --git a/go.mod b/go.mod index 51f2784b..15d90ce7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/sonh/qs v0.7.0 github.com/teamwork/desksdkgo v1.1.1 github.com/teamwork/spacessdkgo v0.0.0-20260518181558-a6af69d00abb - github.com/teamwork/twapi-go-sdk v1.29.1 + github.com/teamwork/twapi-go-sdk v1.29.2 ) require ( @@ -84,7 +84,7 @@ require ( golang.org/x/mod v0.37.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect + golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/go.sum b/go.sum index 5452feed..e12cc144 100644 --- a/go.sum +++ b/go.sum @@ -165,8 +165,8 @@ github.com/teamwork/desksdkgo v1.1.1 h1:ivmBqxTnTYgZrjpGvWXaK9DEdFyH1qL60FSY7HMX github.com/teamwork/desksdkgo v1.1.1/go.mod h1:Mgvw83q8iqHr7Sm9xV1iI/T89o3ObaPU3ChMJheRzwA= github.com/teamwork/spacessdkgo v0.0.0-20260518181558-a6af69d00abb h1:bQluDjySZeC5etnWgjk4WFRy0PvzGDw8XEBd4JJYWCQ= github.com/teamwork/spacessdkgo v0.0.0-20260518181558-a6af69d00abb/go.mod h1:jfE0RLsZuk/3Glzs5bJ95pNb92emV7uXZYgoGSLQ76I= -github.com/teamwork/twapi-go-sdk v1.29.1 h1:JOZF+kesZKKlXgfHjo5GDEMHqIc5llpeLivwvfH5wKI= -github.com/teamwork/twapi-go-sdk v1.29.1/go.mod h1:akTysmeHa9IACt/jMtpRZRy+aLHFlJWge27vqNLpgsc= +github.com/teamwork/twapi-go-sdk v1.29.2 h1:oUR/FQsAs73tuoVLprV5BegbC5PpoboXjhgfM4OaLR8= +github.com/teamwork/twapi-go-sdk v1.29.2/go.mod h1:5aKvss5ZuvwWlxJqzC4rO5QwziNcLbgLFfRbzP1eu0E= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= @@ -260,8 +260,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/twprojects/tasks.go b/internal/twprojects/tasks.go index e19984b2..10aa60e6 100644 --- a/internal/twprojects/tasks.go +++ b/internal/twprojects/tasks.go @@ -1000,6 +1000,10 @@ func TaskGet(engine *twapi.Engine) toolsets.ToolWrapper { var taskGetRequest projects.TaskGetRequest taskGetRequest.Filters.IncludeRelatedTasks = true + // This endpoint serves a deleted task with 200, so without this the tool + // answers one as an ordinary task and the model has no way to tell. + taskGetRequest.Filters.HideDeleted = true + // The related-task filter reports *active* subtasks, dependencies and // predecessors only, so a task whose subtasks are all done answers with an // empty subTaskIds — indistinguishable from a task that never had any. The @@ -1036,9 +1040,12 @@ func TaskGet(engine *twapi.Engine) toolsets.ToolWrapper { // back, with the two completed-work flags it gates, when the selection // names either attribute. relatedTasks := taskFieldsNeedRelatedTasks(taskGetRequest.Fields.Task) - taskGetRequest.Filters = projects.TaskRequestFilters{ - IncludeRelatedTasks: relatedTasks, - IncludeCompletedPredecessors: relatedTasks, + taskGetRequest.Filters = projects.TaskGetRequestFilters{ + TaskRequestFilters: projects.TaskRequestFilters{ + IncludeRelatedTasks: relatedTasks, + IncludeCompletedPredecessors: relatedTasks, + }, + HideDeleted: true, } return helpers.NewRawToolResult(ctx, engine, taskGetRequest, "failed to get task", helpers.WebLinkerWithIDPathBuilder("/app/tasks"), diff --git a/internal/twprojects/tasks_prompts.go b/internal/twprojects/tasks_prompts.go index 9a208ee7..21a7724b 100644 --- a/internal/twprojects/tasks_prompts.go +++ b/internal/twprojects/tasks_prompts.go @@ -48,7 +48,10 @@ func TaskSkillsAndRolesPrompt(engine *twapi.Engine) toolsets.ServerPrompt { return nil, fmt.Errorf("task ID must be a positive integer") } - taskResponse, err := projects.TaskGet(ctx, engine, projects.NewTaskGetRequest(taskID)) + taskGetRequest := projects.NewTaskGetRequest(taskID) + taskGetRequest.Filters.HideDeleted = true + + taskResponse, err := projects.TaskGet(ctx, engine, taskGetRequest) if err != nil { return nil, fmt.Errorf("failed to get task: %w", err) } diff --git a/internal/twprojects/tasks_test.go b/internal/twprojects/tasks_test.go index 1037a1d2..25939b91 100644 --- a/internal/twprojects/tasks_test.go +++ b/internal/twprojects/tasks_test.go @@ -513,6 +513,30 @@ func TestTaskGetCarriesCompletedWork(t *testing.T) { } } +// TestTaskGetHidesDeletedTasks pins hideDeleted on both branches of the get. +// The endpoint serves a deleted task with 200 by default, so without the +// parameter the tool hands one back as an ordinary task and nothing in the +// response says it is gone. The sparse branch rebuilds the filters from +// scratch, which is where the flag is easiest to drop. +// +// Asserted on the query string, because the mock replies with the same canned +// body whether the parameter is sent or not. +func TestTaskGetHidesDeletedTasks(t *testing.T) { + for name, args := range map[string]map[string]any{ + "full": {"id": float64(777)}, + "sparse": {"id": float64(777), "fields": []any{"name"}}, + } { + t.Run(name, func(t *testing.T) { + mcpServer, lastURL := testutil.ProjectsMCPServerMockWithRequestURL(t, http.StatusOK, []byte(`{}`)) + testutil.ExecuteToolRequest(t, mcpServer, twprojects.MethodTaskGet.String(), args) + + if got := lastURL.Query().Get("hideDeleted"); got != "true" { + t.Errorf("expected hideDeleted=true but got %q (raw query: %s)", got, lastURL.RawQuery) + } + }) + } +} + // TestTaskSparseFieldsSubTaskIDsCarryRelatedTasks covers subTaskIds the way // TestSparseFieldsPredecessorsCarryRelatedTasks covers predecessors: the API // leaves it empty unless the request also asks for related tasks, and empty