summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-27 22:39:50 +0200
committerPaul Buetow <paul@buetow.org>2026-03-27 22:39:50 +0200
commit1575c64b7d40f4a7b462609242bd72885157a383 (patch)
tree31e21bf96769759f16eb4375e27273c824b104c8
parent2ddb334fa671b9c425ca43c8c673c6b36c3ad0ab (diff)
release: v0.27.2v0.27.2main
-rwxr-xr-xcmd/ask/askbin3536143 -> 4321591 bytes
-rw-r--r--docs/usage.md15
-rw-r--r--integrationtests/ask_scope_test.go257
-rw-r--r--integrationtests/ask_test.go2
-rw-r--r--internal/askcli/completion.go153
-rw-r--r--internal/askcli/completion_test.go17
-rw-r--r--internal/askcli/dispatch.go5
-rw-r--r--internal/askcli/dispatch_test.go94
-rw-r--r--internal/askcli/task_scope.go63
-rw-r--r--internal/askcli/task_selector.go2
-rw-r--r--internal/askcli/task_selector_test.go4
-rw-r--r--internal/askcli/taskexec.go23
-rw-r--r--internal/askcli/taskexec_test.go66
-rw-r--r--internal/version.go2
14 files changed, 641 insertions, 62 deletions
diff --git a/cmd/ask/ask b/cmd/ask/ask
index 9d4efad..a7c597a 100755
--- a/cmd/ask/ask
+++ b/cmd/ask/ask
Binary files differ
diff --git a/docs/usage.md b/docs/usage.md
index 8a1c1d1..8824a44 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -125,7 +125,9 @@ cat SOMEFILE.txt | hexai --tps-simulation 20
## Task management
-`ask` is a task management CLI for the current git project. It auto-scopes to `project:<repo> +agent` so all operations are confined to the current project.
+`ask` is a task management CLI for the current git project. By default it auto-scopes to `project:<repo> +agent` so operations are confined to agent-managed project tasks.
+
+Use `ask na <subcommand...>` or `ask no-agent <subcommand...>` to run the same subcommands against project tasks without the `+agent` tag. Those prefixes keep the project scope but replace the default tag filter with `-agent`.
`ask` never exposes Taskwarrior numeric task IDs. Human-facing output uses stable local alias IDs where practical, while `ask info` shows both the alias ID and the UUID. Commands that accept a task selector support either the alias ID or the UUID.
@@ -139,7 +141,9 @@ cat SOMEFILE.txt | hexai --tps-simulation 20
| `ask add depends:<id\|uuid>,<id\|uuid> "description"` | Create task with inline dependencies |
| `ask add priority:H "description"` | Create task with priority |
| `ask add +tag "description"` | Create task with tag |
+| `ask na add "description"` | Create a project task without the `+agent` tag |
| `ask list` | List pending tasks only (alias-ID table) |
+| `ask na list` | List pending project tasks without the `+agent` tag |
| `ask all` | List all tasks including completed/deleted |
| `ask list +READY` | List only ready tasks |
| `ask list +BLOCKED` | List blocked tasks |
@@ -169,15 +173,24 @@ cat SOMEFILE.txt | hexai --tps-simulation 20
# Create a task
ask add priority:H "Implement new feature"
+# Create a non-agent task
+ask na add "Follow up manually"
+
# Create a task with dependencies
ask add +cli depends:0,1 "Implement dependent feature"
# List tasks
ask list +READY limit:5
+# List non-agent tasks
+ask no-agent list
+
# Show alias and UUID for a task
ask info 0
+# Show a non-agent task
+ask na info 0
+
# Start working
ask start 0
diff --git a/integrationtests/ask_scope_test.go b/integrationtests/ask_scope_test.go
new file mode 100644
index 0000000..ef77757
--- /dev/null
+++ b/integrationtests/ask_scope_test.go
@@ -0,0 +1,257 @@
+//go:build integration
+
+package integrationtests
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "codeberg.org/snonux/hexai/internal/askcli"
+)
+
+func scopedAskArgs(scopePrefix string, args ...string) []string {
+ if strings.TrimSpace(scopePrefix) == "" {
+ return append([]string(nil), args...)
+ }
+ scoped := []string{scopePrefix}
+ return append(scoped, args...)
+}
+
+func createTaskInScope(ctx context.Context, scopePrefix, desc string) (taskInfo, error) {
+ stdout, stderr, code := runAsk(ctx, scopedAskArgs(scopePrefix, "add", "+integrationtest", desc))
+ if code != 0 {
+ return taskInfo{}, fmt.Errorf("create task failed (code %d): stdout=%s stderr=%s", code, stdout.String(), stderr.String())
+ }
+
+ id := extractTaskIDFromAddOutput(stdout.String())
+ if id == "" {
+ return taskInfo{}, fmt.Errorf("could not extract task ID from ask add output: %s", stdout.String())
+ }
+
+ info, ok := getTaskInfoInScope(ctx, scopePrefix, id)
+ if !ok {
+ return taskInfo{}, fmt.Errorf("could not resolve task ID %q after ask %s add", id, scopePrefix)
+ }
+ if info.UUID == "" {
+ return taskInfo{}, fmt.Errorf("ask %s info %q did not return a UUID", scopePrefix, id)
+ }
+ return info, nil
+}
+
+func getTaskInfoInScope(ctx context.Context, scopePrefix, selector string) (taskInfo, bool) {
+ stdout, _, code := runAsk(ctx, scopedAskArgs(scopePrefix, "info", selector))
+ if code != 0 {
+ return taskInfo{}, false
+ }
+ return parseTaskInfoText(stdout.String(), selector), true
+}
+
+func exportTaskByUUID(ctx context.Context, uuid string) (askcli.TaskExport, error) {
+ stdout, stderr, code := runTask(ctx, []string{"uuid:" + uuid, "export"})
+ if code != 0 {
+ return askcli.TaskExport{}, fmt.Errorf("task export failed (code %d): stdout=%s stderr=%s", code, stdout.String(), stderr.String())
+ }
+
+ var tasks []askcli.TaskExport
+ if err := json.Unmarshal(stdout.Bytes(), &tasks); err != nil {
+ return askcli.TaskExport{}, fmt.Errorf("parse task export: %w", err)
+ }
+ if len(tasks) != 1 {
+ return askcli.TaskExport{}, fmt.Errorf("expected 1 task, got %d", len(tasks))
+ }
+ return tasks[0], nil
+}
+
+func hasTag(tags []string, want string) bool {
+ for _, tag := range tags {
+ if tag == want {
+ return true
+ }
+ }
+ return false
+}
+
+func hasSelectorLine(output, want string) bool {
+ for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
+ if strings.TrimSpace(line) == want {
+ return true
+ }
+ }
+ return false
+}
+
+func TestNoAgentAddOmitsAgentTag(t *testing.T) {
+ for _, prefix := range []string{"na", "no-agent"} {
+ t.Run(prefix, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
+ defer cancel()
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+
+ desc := fmt.Sprintf("integration test non-agent add %s %d", prefix, time.Now().UnixNano())
+ info, err := createTaskInScope(ctx, prefix, desc)
+ if err != nil {
+ t.Fatalf("failed to create no-agent task: %v", err)
+ }
+ defer deleteTask(ctx, info.UUID)
+
+ task, err := exportTaskByUUID(ctx, info.UUID)
+ if err != nil {
+ t.Fatalf("failed to export task: %v", err)
+ }
+ if task.Description != desc {
+ t.Fatalf("description = %q, want %q", task.Description, desc)
+ }
+ if hasTag(task.Tags, "agent") {
+ t.Fatalf("tags = %v, task should not have agent tag", task.Tags)
+ }
+ if !hasTag(task.Tags, "integrationtest") {
+ t.Fatalf("tags = %v, task should keep explicit integrationtest tag", task.Tags)
+ }
+ if info.ID == "" {
+ t.Fatal("expected alias ID for no-agent task")
+ }
+ })
+ }
+}
+
+func TestNoAgentListSeparatesScopedTasks(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
+ defer cancel()
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+
+ agentDesc := fmt.Sprintf("integration test scoped agent list %d", time.Now().UnixNano())
+ noAgentDesc := fmt.Sprintf("integration test scoped no-agent list %d", time.Now().UnixNano())
+
+ agentUUID, err := createTask(ctx, agentDesc)
+ if err != nil {
+ t.Fatalf("failed to create agent task: %v", err)
+ }
+ defer deleteTask(ctx, agentUUID)
+
+ noAgentInfo, err := createTaskInScope(ctx, "na", noAgentDesc)
+ if err != nil {
+ t.Fatalf("failed to create no-agent task: %v", err)
+ }
+ defer deleteTask(ctx, noAgentInfo.UUID)
+
+ stdout, stderr, code := runAsk(ctx, []string{"list"})
+ if code != 0 {
+ t.Fatalf("ask list failed with code %d: stdout=%s stderr=%s", code, stdout.String(), stderr.String())
+ }
+ if !strings.Contains(stdout.String(), agentDesc) {
+ t.Fatalf("ask list should contain agent task %q: %s", agentDesc, stdout.String())
+ }
+ if strings.Contains(stdout.String(), noAgentDesc) {
+ t.Fatalf("ask list should not contain no-agent task %q: %s", noAgentDesc, stdout.String())
+ }
+
+ for _, prefix := range []string{"na", "no-agent"} {
+ t.Run(prefix, func(t *testing.T) {
+ scopedStdout, scopedStderr, scopedCode := runAsk(ctx, []string{prefix, "list"})
+ if scopedCode != 0 {
+ t.Fatalf("ask %s list failed with code %d: stdout=%s stderr=%s", prefix, scopedCode, scopedStdout.String(), scopedStderr.String())
+ }
+ if !strings.Contains(scopedStdout.String(), noAgentDesc) {
+ t.Fatalf("ask %s list should contain no-agent task %q: %s", prefix, noAgentDesc, scopedStdout.String())
+ }
+ if strings.Contains(scopedStdout.String(), agentDesc) {
+ t.Fatalf("ask %s list should not contain agent task %q: %s", prefix, agentDesc, scopedStdout.String())
+ }
+ })
+ }
+}
+
+func TestNoAgentSelectorCommandsUseScopedTasks(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
+ defer cancel()
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+
+ desc := fmt.Sprintf("integration test scoped info %d", time.Now().UnixNano())
+ info, err := createTaskInScope(ctx, "na", desc)
+ if err != nil {
+ t.Fatalf("failed to create no-agent task: %v", err)
+ }
+ defer deleteTask(ctx, info.UUID)
+
+ _, stderr, code := runAsk(ctx, []string{"info", info.ID})
+ if code == 0 {
+ t.Fatalf("ask info %s unexpectedly succeeded outside no-agent scope", info.ID)
+ }
+ if !strings.Contains(stderr.String(), "current scope") {
+ t.Fatalf("stderr = %q, want current-scope guidance", stderr.String())
+ }
+
+ for _, prefix := range []string{"na", "no-agent"} {
+ t.Run(prefix, func(t *testing.T) {
+ stdout, scopedStderr, scopedCode := runAsk(ctx, []string{prefix, "info", info.ID})
+ if scopedCode != 0 {
+ t.Fatalf("ask %s info failed with code %d: stdout=%s stderr=%s", prefix, scopedCode, stdout.String(), scopedStderr.String())
+ }
+ if !strings.Contains(stdout.String(), "UUID: "+info.UUID) {
+ t.Fatalf("ask %s info output missing UUID %q: %s", prefix, info.UUID, stdout.String())
+ }
+ })
+ }
+
+ stdout, stderr, code := runAsk(ctx, []string{"na", "done", info.ID})
+ if code != 0 {
+ t.Fatalf("ask na done failed with code %d: stdout=%s stderr=%s", code, stdout.String(), stderr.String())
+ }
+
+ task, err := exportTaskByUUID(ctx, info.UUID)
+ if err != nil {
+ t.Fatalf("failed to export completed no-agent task: %v", err)
+ }
+ if task.Status != "completed" {
+ t.Fatalf("status = %q, want completed", task.Status)
+ }
+}
+
+func TestNoAgentCompleteUUIDsUsesScopedTasks(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
+ defer cancel()
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+
+ agentUUID, err := createTask(ctx, fmt.Sprintf("integration test complete uuids agent %d", time.Now().UnixNano()))
+ if err != nil {
+ t.Fatalf("failed to create agent task: %v", err)
+ }
+ defer deleteTask(ctx, agentUUID)
+ agentAlias := mustTaskAlias(t, ctx, agentUUID)
+
+ noAgentInfo, err := createTaskInScope(ctx, "na", fmt.Sprintf("integration test complete uuids no-agent %d", time.Now().UnixNano()))
+ if err != nil {
+ t.Fatalf("failed to create no-agent task: %v", err)
+ }
+ defer deleteTask(ctx, noAgentInfo.UUID)
+
+ defaultStdout, defaultStderr, defaultCode := runAsk(ctx, []string{"complete-uuids"})
+ if defaultCode != 0 {
+ t.Fatalf("ask complete-uuids failed with code %d: stdout=%s stderr=%s", defaultCode, defaultStdout.String(), defaultStderr.String())
+ }
+ if !hasSelectorLine(defaultStdout.String(), agentAlias) || !hasSelectorLine(defaultStdout.String(), agentUUID) {
+ t.Fatalf("default complete-uuids should contain agent selectors: %s", defaultStdout.String())
+ }
+ if hasSelectorLine(defaultStdout.String(), noAgentInfo.ID) || hasSelectorLine(defaultStdout.String(), noAgentInfo.UUID) {
+ t.Fatalf("default complete-uuids should not contain no-agent selectors: %s", defaultStdout.String())
+ }
+
+ for _, prefix := range []string{"na", "no-agent"} {
+ t.Run(prefix, func(t *testing.T) {
+ stdout, stderr, code := runAsk(ctx, []string{prefix, "complete-uuids"})
+ if code != 0 {
+ t.Fatalf("ask %s complete-uuids failed with code %d: stdout=%s stderr=%s", prefix, code, stdout.String(), stderr.String())
+ }
+ if !hasSelectorLine(stdout.String(), noAgentInfo.ID) || !hasSelectorLine(stdout.String(), noAgentInfo.UUID) {
+ t.Fatalf("ask %s complete-uuids should contain no-agent selectors: %s", prefix, stdout.String())
+ }
+ if hasSelectorLine(stdout.String(), agentAlias) || hasSelectorLine(stdout.String(), agentUUID) {
+ t.Fatalf("ask %s complete-uuids should not contain agent selectors: %s", prefix, stdout.String())
+ }
+ })
+ }
+}
diff --git a/integrationtests/ask_test.go b/integrationtests/ask_test.go
index 528021a..483dc7c 100644
--- a/integrationtests/ask_test.go
+++ b/integrationtests/ask_test.go
@@ -377,7 +377,7 @@ func TestAddWithDependsModifier(t *testing.T) {
t.Fatalf("ask add with depends modifier failed with code %d: stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
- id := strings.TrimSpace(stdout.String())
+ id := extractTaskIDFromAddOutput(stdout.String())
info, ok := getTaskInfoFast(ctx, id)
if !ok {
t.Fatalf("ask info %q failed after add", id)
diff --git a/internal/askcli/completion.go b/internal/askcli/completion.go
index 889bbc8..a396029 100644
--- a/internal/askcli/completion.go
+++ b/internal/askcli/completion.go
@@ -16,6 +16,7 @@ var askDepCompletionItems = []fishCompletionItem{
}
func fishSingleSelectorCompletionContext(positional []string) bool {
+ positional = trimTaskScopePrefix(positional)
if len(positional) != 1 {
return false
}
@@ -29,6 +30,7 @@ func fishSingleSelectorCompletionContext(positional []string) bool {
}
func fishDepSelectorCompletionContext(positional []string) bool {
+ positional = trimTaskScopePrefix(positional)
if len(positional) < 2 || positional[0] != "dep" {
return false
}
@@ -44,6 +46,7 @@ func fishDepSelectorCompletionContext(positional []string) bool {
}
func fishAddDependencyModifierCompletionContext(positional []string, current string) bool {
+ positional = trimTaskScopePrefix(positional)
if len(positional) == 0 || positional[0] != "add" {
return false
}
@@ -65,9 +68,15 @@ func FishCompletionFor(binaryPath string) string {
writeFishAddDependencyModifierFunction(&b)
b.WriteString("complete -c ask -f\n")
b.WriteString("complete -c ask -s j -l json -d 'Emit JSON output'\n")
+ for _, item := range []fishCompletionItem{
+ {name: "na", description: "Run against project tasks without +agent"},
+ {name: "no-agent", description: "Run against project tasks without +agent"},
+ } {
+ writeFishCompletionLine(&b, "__ask_needs_root_completion", item)
+ }
for _, entry := range commandRegistry.rootCompletionEntries() {
item := fishCompletionItem{name: entry.name, description: entry.description}
- writeFishCompletionLine(&b, "__ask_needs_root_completion", item)
+ writeFishCompletionLine(&b, "__ask_needs_command_completion", item)
}
for _, item := range askDepCompletionItems {
writeFishCompletionLine(&b, "__ask_in_dep_context", item)
@@ -84,63 +93,110 @@ func writeFishPreamble(b *strings.Builder) {
}
func writeFishContextFunctions(b *strings.Builder) {
+ writeFishPositionalTokensFunction(b)
+ writeFishCommandPositionalsFunction(b)
+ writeFishScopePrefixFunction(b)
writeFishNeedsRootCompletionFunction(b)
+ writeFishNeedsCommandCompletionFunction(b)
writeFishDepContextFunction(b)
writeFishUUIDContextFunction(b)
writeFishDepUUIDContextFunction(b)
writeFishAddDependencyModifierContextFunction(b)
}
+func writeFishPositionalTokensFunction(b *strings.Builder) {
+ b.WriteString("function __ask_positional_tokens\n")
+ b.WriteString(" set -l tokens (commandline -opc)\n")
+ b.WriteString(" set -l positional\n")
+ b.WriteString(" for token in $tokens[2..-1]\n")
+ b.WriteString(" if string match -qr '^-' -- $token\n")
+ b.WriteString(" continue\n")
+ b.WriteString(" end\n")
+ b.WriteString(" set -a positional $token\n")
+ b.WriteString(" end\n")
+ b.WriteString(" for token in $positional\n")
+ b.WriteString(" printf '%s\\n' $token\n")
+ b.WriteString(" end\n")
+ b.WriteString("end\n\n")
+}
+
+func writeFishCommandPositionalsFunction(b *strings.Builder) {
+ b.WriteString("function __ask_command_positionals\n")
+ b.WriteString(" set -l positional (__ask_positional_tokens)\n")
+ b.WriteString(" if test (count $positional) -gt 0\n")
+ b.WriteString(" switch $positional[1]\n")
+ b.WriteString(" case na no-agent\n")
+ b.WriteString(" for token in $positional[2..-1]\n")
+ b.WriteString(" printf '%s\\n' $token\n")
+ b.WriteString(" end\n")
+ b.WriteString(" return 0\n")
+ b.WriteString(" end\n")
+ b.WriteString(" end\n")
+ b.WriteString(" for token in $positional\n")
+ b.WriteString(" printf '%s\\n' $token\n")
+ b.WriteString(" end\n")
+ b.WriteString("end\n\n")
+}
+
+func writeFishScopePrefixFunction(b *strings.Builder) {
+ b.WriteString("function __ask_scope_prefix\n")
+ b.WriteString(" set -l positional (__ask_positional_tokens)\n")
+ b.WriteString(" if test (count $positional) -eq 0\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" switch $positional[1]\n")
+ b.WriteString(" case na no-agent\n")
+ b.WriteString(" printf '%s\\n' $positional[1]\n")
+ b.WriteString(" return 0\n")
+ b.WriteString(" case '*'\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" return 1\n")
+ b.WriteString("end\n\n")
+}
+
func writeFishNeedsRootCompletionFunction(b *strings.Builder) {
b.WriteString("function __ask_needs_root_completion\n")
- b.WriteString(" set -l tokens (commandline -opc)\n")
- b.WriteString(" if test (count $tokens) -le 1\n")
+ b.WriteString(" set -l positional (__ask_positional_tokens)\n")
+ b.WriteString(" if test (count $positional) -eq 0\n")
b.WriteString(" return 0\n")
b.WriteString(" end\n")
- b.WriteString(" for token in $tokens[2..-1]\n")
- b.WriteString(" if not string match -qr '^-' -- $token\n")
- b.WriteString(" return 1\n")
+ b.WriteString(" return 1\n")
+ b.WriteString("end\n\n")
+}
+
+func writeFishNeedsCommandCompletionFunction(b *strings.Builder) {
+ b.WriteString("function __ask_needs_command_completion\n")
+ b.WriteString(" set -l positional (__ask_positional_tokens)\n")
+ b.WriteString(" if test (count $positional) -eq 0\n")
+ b.WriteString(" return 0\n")
+ b.WriteString(" end\n")
+ b.WriteString(" if test (count $positional) -eq 1\n")
+ b.WriteString(" switch $positional[1]\n")
+ b.WriteString(" case na no-agent\n")
+ b.WriteString(" return 0\n")
b.WriteString(" end\n")
b.WriteString(" end\n")
- b.WriteString(" return 0\n")
+ b.WriteString(" return 1\n")
b.WriteString("end\n\n")
}
func writeFishDepContextFunction(b *strings.Builder) {
b.WriteString("function __ask_in_dep_context\n")
- b.WriteString(" set -l tokens (commandline -opc)\n")
- b.WriteString(" if test (count $tokens) -lt 2\n")
+ b.WriteString(" set -l positional (__ask_command_positionals)\n")
+ b.WriteString(" if test (count $positional) -lt 1\n")
b.WriteString(" return 1\n")
b.WriteString(" end\n")
- b.WriteString(" set -l seen_dep 0\n")
- b.WriteString(" for token in $tokens[2..-1]\n")
- b.WriteString(" if string match -qr '^-' -- $token\n")
- b.WriteString(" continue\n")
- b.WriteString(" end\n")
- b.WriteString(" if test $seen_dep -eq 0\n")
- b.WriteString(" if test $token = dep\n")
- b.WriteString(" set seen_dep 1\n")
- b.WriteString(" else\n")
- b.WriteString(" return 1\n")
- b.WriteString(" end\n")
- b.WriteString(" else\n")
- b.WriteString(" return 1\n")
- b.WriteString(" end\n")
+ b.WriteString(" if test $positional[1] != dep\n")
+ b.WriteString(" return 1\n")
b.WriteString(" end\n")
- b.WriteString(" test $seen_dep -eq 1\n")
+ b.WriteString(" test (count $positional) -eq 1\n")
b.WriteString("end\n\n")
}
func writeFishUUIDContextFunction(b *strings.Builder) {
b.WriteString("function __ask_in_uuid_context\n")
- b.WriteString(" set -l tokens (commandline -opc)\n")
- b.WriteString(" set -l positional\n")
- b.WriteString(" for token in $tokens[2..-1]\n")
- b.WriteString(" if string match -qr '^-' -- $token\n")
- b.WriteString(" continue\n")
- b.WriteString(" end\n")
- b.WriteString(" set -a positional $token\n")
- b.WriteString(" end\n")
+ b.WriteString(" set -l positional (__ask_command_positionals)\n")
b.WriteString(" if test (count $positional) -eq 0\n")
b.WriteString(" return 1\n")
b.WriteString(" end\n")
@@ -161,14 +217,7 @@ func writeFishUUIDContextFunction(b *strings.Builder) {
func writeFishDepUUIDContextFunction(b *strings.Builder) {
b.WriteString("function __ask_in_dep_uuid_context\n")
- b.WriteString(" set -l tokens (commandline -opc)\n")
- b.WriteString(" set -l positional\n")
- b.WriteString(" for token in $tokens[2..-1]\n")
- b.WriteString(" if string match -qr '^-' -- $token\n")
- b.WriteString(" continue\n")
- b.WriteString(" end\n")
- b.WriteString(" set -a positional $token\n")
- b.WriteString(" end\n")
+ b.WriteString(" set -l positional (__ask_command_positionals)\n")
b.WriteString(" if test (count $positional) -lt 2\n")
b.WriteString(" return 1\n")
b.WriteString(" end\n")
@@ -193,14 +242,7 @@ func writeFishDepUUIDContextFunction(b *strings.Builder) {
func writeFishAddDependencyModifierContextFunction(b *strings.Builder) {
b.WriteString("function __ask_in_add_dep_modifier_context\n")
- b.WriteString(" set -l tokens (commandline -opc)\n")
- b.WriteString(" set -l positional\n")
- b.WriteString(" for token in $tokens[2..-1]\n")
- b.WriteString(" if string match -qr '^-' -- $token\n")
- b.WriteString(" continue\n")
- b.WriteString(" end\n")
- b.WriteString(" set -a positional $token\n")
- b.WriteString(" end\n")
+ b.WriteString(" set -l positional (__ask_command_positionals)\n")
b.WriteString(" if test (count $positional) -lt 1\n")
b.WriteString(" return 1\n")
b.WriteString(" end\n")
@@ -220,17 +262,28 @@ func writeFishTaskSelectorFunction(b *strings.Builder, binaryPath string) {
b.WriteString(" set -l ask_bin ")
b.WriteString(quoteFishString(binaryPath))
b.WriteString("\n")
+ b.WriteString(" set -l scope_prefix (__ask_scope_prefix)\n")
+ b.WriteString(" set -l cache_key default\n")
+ b.WriteString(" if test -n \"$scope_prefix\"\n")
+ b.WriteString(" set cache_key $scope_prefix\n")
+ b.WriteString(" end\n")
b.WriteString(" set -l now (date +%s)\n")
- b.WriteString(" if set -q __ask_task_selector_cache_until; and test $__ask_task_selector_cache_until -ge $now\n")
+ b.WriteString(" if set -q __ask_task_selector_cache_until; and test $__ask_task_selector_cache_until -ge $now; and set -q __ask_task_selector_cache_key; and test \"$__ask_task_selector_cache_key\" = \"$cache_key\"\n")
b.WriteString(" printf '%s\\n' $__ask_task_selector_cache\n")
b.WriteString(" return 0\n")
b.WriteString(" end\n")
- b.WriteString(" set -l selectors (command $ask_bin complete-uuids 2>/dev/null)\n")
+ b.WriteString(" set -l selectors\n")
+ b.WriteString(" if test -n \"$scope_prefix\"\n")
+ b.WriteString(" set selectors (command $ask_bin $scope_prefix complete-uuids 2>/dev/null)\n")
+ b.WriteString(" else\n")
+ b.WriteString(" set selectors (command $ask_bin complete-uuids 2>/dev/null)\n")
+ b.WriteString(" end\n")
b.WriteString(" if test $status -ne 0\n")
b.WriteString(" return 1\n")
b.WriteString(" end\n")
b.WriteString(" set -g __ask_task_selector_cache $selectors\n")
b.WriteString(" set -g __ask_task_selector_cache_until (math $now + 2)\n")
+ b.WriteString(" set -g __ask_task_selector_cache_key $cache_key\n")
b.WriteString(" printf '%s\\n' $selectors\n")
b.WriteString("end\n\n")
}
diff --git a/internal/askcli/completion_test.go b/internal/askcli/completion_test.go
index 9762975..f43fa32 100644
--- a/internal/askcli/completion_test.go
+++ b/internal/askcli/completion_test.go
@@ -7,6 +7,11 @@ import (
func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) {
script := FishCompletion()
+ for _, name := range []string{"na", "no-agent"} {
+ if !strings.Contains(script, " -a '"+name+"' ") {
+ t.Fatalf("script missing scope completion for %q", name)
+ }
+ }
for _, name := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "fish", "help"} {
if !strings.Contains(script, " -a '"+name+"' ") {
t.Fatalf("script missing root completion for %q", name)
@@ -17,10 +22,14 @@ func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) {
"complete -c ask -n '__ask_in_dep_context' -a 'add' -d 'Add a dependency'",
"complete -c ask -n '__ask_in_dep_context' -a 'rm' -d 'Remove a dependency'",
"complete -c ask -n '__ask_in_dep_context' -a 'list' -d 'List dependencies'",
+ "function __ask_command_positionals",
+ "function __ask_scope_prefix",
"function __ask_task_selectors",
"function __ask_add_dependency_modifiers",
`set -l ask_bin "ask"`,
- "set -l selectors (command $ask_bin complete-uuids 2>/dev/null)",
+ "set -l selectors",
+ "set selectors (command $ask_bin complete-uuids 2>/dev/null)",
+ "set selectors (command $ask_bin $scope_prefix complete-uuids 2>/dev/null)",
"complete -c ask -n '__ask_in_uuid_context' -a '(__ask_task_selectors)' -d 'Task selector'",
"complete -c ask -n '__ask_in_dep_uuid_context' -a '(__ask_task_selectors)' -d 'Task selector'",
"complete -c ask -n '__ask_in_add_dep_modifier_context' -a '(__ask_add_dependency_modifiers)' -d 'Task dependency'",
@@ -49,9 +58,11 @@ func TestFishSingleSelectorCompletionContext(t *testing.T) {
want bool
}{
{name: "info expects selector", positional: []string{"info"}, want: true},
+ {name: "info expects selector with no-agent prefix", positional: []string{"na", "info"}, want: true},
{name: "annotate expects selector", positional: []string{"annotate"}, want: true},
{name: "priority expects selector", positional: []string{"priority"}, want: true},
{name: "delete expects selector", positional: []string{"delete"}, want: true},
+ {name: "delete expects selector with alias prefix", positional: []string{"no-agent", "delete"}, want: true},
{name: "annotate stops after selector", positional: []string{"annotate", "0"}, want: false},
{name: "priority stops after selector", positional: []string{"priority", "0"}, want: false},
{name: "modify stops after selector", positional: []string{"modify", "0"}, want: false},
@@ -75,6 +86,7 @@ func TestFishDepSelectorCompletionContext(t *testing.T) {
want bool
}{
{name: "dep add first selector", positional: []string{"dep", "add"}, want: true},
+ {name: "dep add first selector with no-agent prefix", positional: []string{"na", "dep", "add"}, want: true},
{name: "dep add second selector", positional: []string{"dep", "add", "0"}, want: true},
{name: "dep add stops after second selector", positional: []string{"dep", "add", "0", "1"}, want: false},
{name: "dep rm first selector", positional: []string{"dep", "rm"}, want: true},
@@ -105,6 +117,7 @@ func TestFishAddDependencyModifierCompletionContext(t *testing.T) {
{name: "add without depends modifier", positional: []string{"add", "task"}, current: "task", want: false},
{name: "add with depends keyword prefix", positional: []string{"add"}, current: "depends", want: true},
{name: "add with depends modifier", positional: []string{"add", "+cli"}, current: "depends:0", want: true},
+ {name: "add with depends modifier and no-agent prefix", positional: []string{"na", "add", "+cli"}, current: "depends:0", want: true},
{name: "add with comma continuation", positional: []string{"add", "+cli"}, current: "depends:0,", want: true},
{name: "non add command", positional: []string{"dep", "add"}, current: "depends:0", want: false},
}
@@ -122,7 +135,7 @@ func TestFishCompletionFor_EmbedsBinaryPath(t *testing.T) {
script := FishCompletionFor(`/tmp/ask "$HOME"`)
for _, line := range []string{
`set -l ask_bin "/tmp/ask \"\$HOME\""`,
- "set -l selectors (command $ask_bin complete-uuids 2>/dev/null)",
+ "set selectors (command $ask_bin complete-uuids 2>/dev/null)",
} {
if !strings.Contains(script, line) {
t.Fatalf("script missing %q", line)
diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go
index d03c340..974ceea 100644
--- a/internal/askcli/dispatch.go
+++ b/internal/askcli/dispatch.go
@@ -45,6 +45,8 @@ func parseGlobalFlags(args []string) ([]string, bool) {
func (d *Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
args, jsonOutput := parseGlobalFlags(args)
d.jsonOutput = jsonOutput
+ scope, args := parseTaskScopePrefix(args)
+ ctx = contextWithTaskScope(ctx, scope)
if len(args) == 0 {
args = []string{"list"}
@@ -59,6 +61,9 @@ func (d *Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reade
func (d *Dispatcher) help(w io.Writer) (int, error) {
_, _ = io.WriteString(w, "ask - task management CLI\n")
+ _, _ = io.WriteString(w, "\nScope prefixes:\n")
+ _, _ = io.WriteString(w, " ask na <subcommand...> Run a subcommand against project tasks without +agent\n")
+ _, _ = io.WriteString(w, " ask no-agent <subcommand...> Alias for ask na\n")
_, _ = io.WriteString(w, "\nSubcommands:\n")
_, _ = io.WriteString(w, " ask add [mods...] [depends:<id|uuid>,...] <description...> Create a new task and print created task <id>\n")
_, _ = io.WriteString(w, " ask list [filters] List active tasks (default)\n")
diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go
index 91f4784..cc5854d 100644
--- a/internal/askcli/dispatch_test.go
+++ b/internal/askcli/dispatch_test.go
@@ -26,6 +26,9 @@ func TestDispatcher_Help(t *testing.T) {
if !strings.Contains(output, "ask - task management CLI") {
t.Fatalf("help missing title: %s", output)
}
+ if !strings.Contains(output, "ask na <subcommand...>") || !strings.Contains(output, "ask no-agent <subcommand...>") {
+ t.Fatalf("help missing no-agent scope prefixes: %s", output)
+ }
if !strings.Contains(output, "ask list") {
t.Fatalf("help missing list subcommand: %s", output)
}
@@ -164,6 +167,97 @@ func TestParseGlobalFlags(t *testing.T) {
}
}
+func TestParseTaskScopePrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ args []string
+ wantScope taskScopeMode
+ wantArgs []string
+ }{
+ {name: "default scope", args: []string{"list"}, wantScope: taskScopeAgent, wantArgs: []string{"list"}},
+ {name: "na prefix", args: []string{"na", "list"}, wantScope: taskScopeNoAgent, wantArgs: []string{"list"}},
+ {name: "no-agent prefix", args: []string{"no-agent", "info", "0"}, wantScope: taskScopeNoAgent, wantArgs: []string{"info", "0"}},
+ {name: "empty args", args: nil, wantScope: taskScopeAgent, wantArgs: nil},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ gotScope, gotArgs := parseTaskScopePrefix(tc.args)
+ if gotScope != tc.wantScope {
+ t.Fatalf("scope = %v, want %v", gotScope, tc.wantScope)
+ }
+ if !reflect.DeepEqual(gotArgs, tc.wantArgs) {
+ t.Fatalf("args = %v, want %v", gotArgs, tc.wantArgs)
+ }
+ })
+ }
+}
+
+func TestDispatcher_NoAgentPrefix_StripsScopePrefix(t *testing.T) {
+ taskJSONFor := func(uuid string) string {
+ return `[{"uuid":"` + uuid + `","description":"Test","status":"pending","priority":"M","tags":[],"urgency":10,"depends":[]}]`
+ }
+
+ tests := []struct {
+ name string
+ args []string
+ wantCalls [][]string
+ }{
+ {
+ name: "na defaults to list",
+ args: []string{"na"},
+ wantCalls: [][]string{{"status:pending", "export"}},
+ },
+ {
+ name: "na list",
+ args: []string{"na", "list"},
+ wantCalls: [][]string{{"status:pending", "export"}},
+ },
+ {
+ name: "no-agent info",
+ args: []string{"no-agent", "info", "test-uuid"},
+ wantCalls: [][]string{{"uuid:test-uuid", "export"}},
+ },
+ {
+ name: "no-agent add",
+ args: []string{"no-agent", "add", "new task description"},
+ wantCalls: [][]string{{"add", "rc.verbose=nothing", "rc.verbose=new-uuid", "new task description"}},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var calls [][]string
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ calls = append(calls, append([]string(nil), args...))
+ switch strings.Join(args, " ") {
+ case "status:pending export":
+ _, _ = io.WriteString(stdout, taskJSONFor("test-uuid"))
+ case "uuid:test-uuid export":
+ _, _ = io.WriteString(stdout, taskJSONFor("test-uuid"))
+ case "add rc.verbose=nothing rc.verbose=new-uuid new task description":
+ _, _ = io.WriteString(stdout, "Created task task-uuid-abc.\n")
+ default:
+ t.Fatalf("unexpected runner args: %v", args)
+ }
+ return 0, nil
+ }})
+
+ var stdout, stderr bytes.Buffer
+ code, err := d.Dispatch(context.Background(), tc.args, nil, &stdout, &stderr)
+ if err != nil {
+ t.Fatalf("Dispatch returned error: %v", err)
+ }
+ if code != 0 {
+ t.Fatalf("Dispatch code = %d, want 0", code)
+ }
+ if !reflect.DeepEqual(calls, tc.wantCalls) {
+ t.Fatalf("runner calls = %#v, want %#v", calls, tc.wantCalls)
+ }
+ })
+ }
+}
+
func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) {
dir := t.TempDir()
oldRoot := taskAliasCacheRoot
diff --git a/internal/askcli/task_scope.go b/internal/askcli/task_scope.go
new file mode 100644
index 0000000..42dd022
--- /dev/null
+++ b/internal/askcli/task_scope.go
@@ -0,0 +1,63 @@
+package askcli
+
+import "context"
+
+type taskScopeMode int
+
+const (
+ taskScopeAgent taskScopeMode = iota
+ taskScopeNoAgent
+)
+
+type taskScopeContextKey struct{}
+
+func contextWithTaskScope(ctx context.Context, scope taskScopeMode) context.Context {
+ if scope == taskScopeAgent {
+ return ctx
+ }
+ return context.WithValue(ctx, taskScopeContextKey{}, scope)
+}
+
+func taskScopeFromContext(ctx context.Context) taskScopeMode {
+ if ctx == nil {
+ return taskScopeAgent
+ }
+ scope, ok := ctx.Value(taskScopeContextKey{}).(taskScopeMode)
+ if !ok {
+ return taskScopeAgent
+ }
+ return scope
+}
+
+func taskScopeFilter(scope taskScopeMode) string {
+ if scope == taskScopeNoAgent {
+ return "-agent"
+ }
+ return "+agent"
+}
+
+func parseTaskScopePrefix(args []string) (taskScopeMode, []string) {
+ if len(args) == 0 {
+ return taskScopeAgent, nil
+ }
+ if isTaskScopePrefix(args[0]) {
+ return taskScopeNoAgent, args[1:]
+ }
+ return taskScopeAgent, args
+}
+
+func isTaskScopePrefix(arg string) bool {
+ switch arg {
+ case "na", "no-agent":
+ return true
+ default:
+ return false
+ }
+}
+
+func trimTaskScopePrefix(args []string) []string {
+ if len(args) == 0 || !isTaskScopePrefix(args[0]) {
+ return args
+ }
+ return args[1:]
+}
diff --git a/internal/askcli/task_selector.go b/internal/askcli/task_selector.go
index 82dc06f..033781c 100644
--- a/internal/askcli/task_selector.go
+++ b/internal/askcli/task_selector.go
@@ -28,7 +28,7 @@ func (d *Dispatcher) resolveTaskSelector(ctx context.Context, selector string, s
tasks, code, err := d.exportTasks(ctx, []string{"uuid:" + resolved.UUID, "export"}, stderr)
if err != nil {
if resolved.UsedAlias && strings.Contains(err.Error(), "task not found") {
- return resolvedTaskSelector{}, nil, 1, fmt.Errorf("alias %q is stale: task %s was not found", selector, resolved.UUID)
+ return resolvedTaskSelector{}, nil, 1, fmt.Errorf("alias %q did not resolve to a task in the current scope", selector)
}
return resolvedTaskSelector{}, nil, code, err
}
diff --git a/internal/askcli/task_selector_test.go b/internal/askcli/task_selector_test.go
index e882a0b..8ced941 100644
--- a/internal/askcli/task_selector_test.go
+++ b/internal/askcli/task_selector_test.go
@@ -123,8 +123,8 @@ func TestHandleInfo_StaleAlias(t *testing.T) {
if code != 1 {
t.Fatalf("info code = %d, want 1 for stale alias", code)
}
- if !strings.Contains(stderr.String(), `alias "0" is stale`) {
- t.Fatalf("stderr = %q, want stale alias message", stderr.String())
+ if !strings.Contains(stderr.String(), `alias "0" did not resolve to a task in the current scope`) {
+ t.Fatalf("stderr = %q, want current-scope alias message", stderr.String())
}
}
diff --git a/internal/askcli/taskexec.go b/internal/askcli/taskexec.go
index b188230..a1ede37 100644
--- a/internal/askcli/taskexec.go
+++ b/internal/askcli/taskexec.go
@@ -34,7 +34,7 @@ func NewExecutor(commandName string) Executor {
}
}
-func (e Executor) taskArgs(repoRoot string, args []string) ([]string, error) {
+func (e Executor) taskArgs(ctx context.Context, repoRoot string, args []string) ([]string, error) {
projectName, err := projectNameFromRoot(repoRoot)
if err != nil {
return nil, err
@@ -42,7 +42,24 @@ func (e Executor) taskArgs(repoRoot string, args []string) ([]string, error) {
// rc.verbose=nothing suppresses Taskwarrior's configuration override
// banner, while rc.confirmation=off keeps non-interactive commands from
// prompting when stdin is unavailable.
- return append([]string{"rc.verbose=nothing", "rc.confirmation=off", "project:" + projectName, "+agent"}, args...), nil
+ if len(args) > 0 && args[0] == "add" {
+ return addTaskArgs(projectName, taskScopeFromContext(ctx), args), nil
+ }
+ scopeFilter := taskScopeFilter(taskScopeFromContext(ctx))
+ return append([]string{"rc.verbose=nothing", "rc.confirmation=off", "project:" + projectName, scopeFilter}, args...), nil
+}
+
+func addTaskArgs(projectName string, scope taskScopeMode, args []string) []string {
+ taskArgs := []string{"rc.verbose=nothing", "rc.confirmation=off", "project:" + projectName, "add"}
+ nextArg := 1
+ for nextArg < len(args) && strings.HasPrefix(args[nextArg], "rc.") {
+ taskArgs = append(taskArgs, args[nextArg])
+ nextArg++
+ }
+ if scope == taskScopeAgent {
+ taskArgs = append(taskArgs, "+agent")
+ }
+ return append(taskArgs, args[nextArg:]...)
}
// Run delegates CLI arguments to Taskwarrior, enforcing agent defaults and error handling.
@@ -56,7 +73,7 @@ func (e Executor) Run(ctx context.Context, args []string, stdin io.Reader, stdou
if err != nil {
return 1, fmt.Errorf("%s: must be run inside a git repository: %w", executor.label(), err)
}
- taskArgs, err := executor.taskArgs(repoRoot, args)
+ taskArgs, err := executor.taskArgs(ctx, repoRoot, args)
if err != nil {
return 1, fmt.Errorf("%s: %w", executor.label(), err)
}
diff --git a/internal/askcli/taskexec_test.go b/internal/askcli/taskexec_test.go
index 2492aae..90da61a 100644
--- a/internal/askcli/taskexec_test.go
+++ b/internal/askcli/taskexec_test.go
@@ -13,7 +13,7 @@ import (
func TestExecutorTaskArgs(t *testing.T) {
exec_ := NewExecutor("ask")
- args, err := exec_.taskArgs("/tmp/work/hexai", []string{"list", "limit:1"})
+ args, err := exec_.taskArgs(context.Background(), "/tmp/work/hexai", []string{"list", "limit:1"})
if err != nil {
t.Fatalf("taskArgs returned error: %v", err)
}
@@ -23,6 +23,44 @@ func TestExecutorTaskArgs(t *testing.T) {
}
}
+func TestExecutorTaskArgs_NoAgentScope(t *testing.T) {
+ exec_ := NewExecutor("ask")
+ ctx := contextWithTaskScope(context.Background(), taskScopeNoAgent)
+ args, err := exec_.taskArgs(ctx, "/tmp/work/hexai", []string{"list", "limit:1"})
+ if err != nil {
+ t.Fatalf("taskArgs returned error: %v", err)
+ }
+ want := []string{"rc.verbose=nothing", "rc.confirmation=off", "project:hexai", "-agent", "list", "limit:1"}
+ if !reflect.DeepEqual(args, want) {
+ t.Fatalf("task args = %v, want %v", args, want)
+ }
+}
+
+func TestExecutorTaskArgs_AddDefaultScope(t *testing.T) {
+ exec_ := NewExecutor("ask")
+ args, err := exec_.taskArgs(context.Background(), "/tmp/work/hexai", []string{"add", "rc.verbose=nothing", "rc.verbose=new-uuid", "new task"})
+ if err != nil {
+ t.Fatalf("taskArgs returned error: %v", err)
+ }
+ want := []string{"rc.verbose=nothing", "rc.confirmation=off", "project:hexai", "add", "rc.verbose=nothing", "rc.verbose=new-uuid", "+agent", "new task"}
+ if !reflect.DeepEqual(args, want) {
+ t.Fatalf("task args = %v, want %v", args, want)
+ }
+}
+
+func TestExecutorTaskArgs_AddNoAgentScope(t *testing.T) {
+ exec_ := NewExecutor("ask")
+ ctx := contextWithTaskScope(context.Background(), taskScopeNoAgent)
+ args, err := exec_.taskArgs(ctx, "/tmp/work/hexai", []string{"add", "rc.verbose=nothing", "rc.verbose=new-uuid", "new task"})
+ if err != nil {
+ t.Fatalf("taskArgs returned error: %v", err)
+ }
+ want := []string{"rc.verbose=nothing", "rc.confirmation=off", "project:hexai", "add", "rc.verbose=nothing", "rc.verbose=new-uuid", "new task"}
+ if !reflect.DeepEqual(args, want) {
+ t.Fatalf("task args = %v, want %v", args, want)
+ }
+}
+
func TestExecutorRun_InjectsProjectFilterAndAgentTag(t *testing.T) {
var gotName string
var gotArgs []string
@@ -53,6 +91,32 @@ func TestExecutorRun_InjectsProjectFilterAndAgentTag(t *testing.T) {
}
}
+func TestExecutorRun_InjectsProjectFilterAndNoAgentTag(t *testing.T) {
+ var gotArgs []string
+ exec_ := Executor{
+ commandName: "ask",
+ findBinary: func() (string, error) { return "/usr/bin/task", nil },
+ detectRepoRoot: func(context.Context) (string, error) { return "/tmp/work/hexai", nil },
+ runCommand: func(_ context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
+ gotArgs = append([]string(nil), args...)
+ return nil
+ },
+ }
+
+ ctx := contextWithTaskScope(context.Background(), taskScopeNoAgent)
+ exitCode, err := exec_.Run(ctx, []string{"list", "limit:1"}, strings.NewReader("in"), &bytes.Buffer{}, &bytes.Buffer{})
+ if err != nil {
+ t.Fatalf("Run returned error: %v", err)
+ }
+ if exitCode != 0 {
+ t.Fatalf("exitCode = %d, want 0", exitCode)
+ }
+ wantArgs := []string{"rc.verbose=nothing", "rc.confirmation=off", "project:hexai", "-agent", "list", "limit:1"}
+ if !reflect.DeepEqual(gotArgs, wantArgs) {
+ t.Fatalf("task args = %v, want %v", gotArgs, wantArgs)
+ }
+}
+
func TestExecutorRun_OutsideGitRepo_IsActionable(t *testing.T) {
exec_ := Executor{
commandName: "ask",
diff --git a/internal/version.go b/internal/version.go
index 5b94bc7..627e8c6 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,4 +1,4 @@
// Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries.
package internal
-const Version = "0.27.1"
+const Version = "0.27.2"