Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.

## Unreleased

- feat: `task complete` gains `--force`, bypassing the incomplete-checkbox guard. `commands/complete-task.md` has advertised the flag since it was written — `argument-hint` lists `[--force]`, and the abort path tells the operator to "re-run with `--force` to complete anyway" — but only `goal complete` implemented it (`cli.go`, "Complete even if open tasks are linked to this goal"). On the task path the advice was a dead end: the run failed with `incomplete subtasks: N pending` and the only way through was editing the task file to remove the checkboxes, which is lossy — out-of-scope follow-up items have to be relabelled as prose to satisfy a gate that was never meant to be absolute. Hit twice in one session on 2026-08-16, and first reported 2026-08-11.

## v0.110.0

- feat: `work-on-task-assistant` Phase 5 now prompts the `accept edits` permission-mode switch when the extracted workflow assigns cluster/deploy mutations to the operator. Stating the operator/agent split was not enough on its own — observed 2026-08-16, the block correctly said "the **operator** runs the cluster mutations" and the session still spent ~40 minutes handing back command blocks until the owner interrupted. The split says who may run the command; the new line makes the switch that lets the agent run it in-session.
Expand Down
12 changes: 10 additions & 2 deletions commands/complete-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ Mark task as complete using vault-cli. Handles normal and recurring tasks approp
- Print: `❌ Task has incomplete items. Finish them first, or re-run with --force to complete anyway.`
- STOP. Do NOT call `vault-cli task complete`. No interactive prompt.

c. Run vault-cli (incomplete items + FORCE=true, OR no incomplete items):
c. Run vault-cli. Pass `--force` through whenever FORCE=true — the CLI enforces
the same incomplete-checkbox guard, so omitting it makes the flag a no-op and
the run fails with `incomplete subtasks: N pending`:
```bash
vault-cli task complete "{task_name}"
vault-cli task complete "{task_name}" # no incomplete items
vault-cli task complete "{task_name}" --force # incomplete items + FORCE=true
```

**`--force` is for items that were never this task's work** — follow-ups filed
for a separate task, or notes that are checkboxes only by formatting. It is not
a way to close unfinished work. If the unchecked items are genuinely part of the
task, finish them.

d. Show report:
```
✅ Task completed: [[{task_name}]]
Expand Down
18 changes: 10 additions & 8 deletions mocks/complete-operation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ func createCompleteCommand(
vaultName *string,
outputFormat *string,
) *cobra.Command {
return &cobra.Command{
var force bool
cmd := &cobra.Command{
Use: "complete <task-name>",
Short: "Mark a task as complete",
Args: cobra.ExactArgs(1),
Expand Down Expand Up @@ -176,7 +177,7 @@ func createCompleteCommand(
dailyStore,
currentDateTime,
)
result, err := completeOp.Execute(ctx, vault.Path, taskName, vault.Name)
result, err := completeOp.Execute(ctx, vault.Path, taskName, vault.Name, force)
if err != nil {
return result, err
}
Expand All @@ -195,6 +196,10 @@ func createCompleteCommand(
)
},
}

cmd.Flags().
BoolVar(&force, "force", false, "Complete even if the task has incomplete checkboxes")
return cmd
}

func createDeferCommand(
Expand Down
12 changes: 9 additions & 3 deletions pkg/ops/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import (

//counterfeiter:generate -o ../../mocks/complete-operation.go --fake-name CompleteOperation . CompleteOperation
type CompleteOperation interface {
// Execute marks a task as complete. When force is true the incomplete-subtask
// guard is bypassed, mirroring the --force flag on goal complete.
Execute(
ctx context.Context,
vaultPath string,
taskName string,
vaultName string,
force bool,
) (MutationResult, error)
}

Expand Down Expand Up @@ -73,6 +76,7 @@ func (c *completeOperation) Execute(
vaultPath string,
taskName string,
vaultName string,
force bool,
) (MutationResult, error) {
var warnings []string

Expand All @@ -94,9 +98,11 @@ func (c *completeOperation) Execute(
return c.handleRecurringTask(ctx, task, vaultPath, vaultName, warnings)
}

// Check subtask completion for non-recurring tasks
if result, shouldBlock, blockErr := c.checkSubtaskCompletion(ctx, task); shouldBlock {
return result, blockErr
// Check subtask completion for non-recurring tasks, unless forced
if !force {
if result, shouldBlock, blockErr := c.checkSubtaskCompletion(ctx, task); shouldBlock {
return result, blockErr
}
}

// Update task status to completed
Expand Down
55 changes: 54 additions & 1 deletion pkg/ops/complete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var _ = Describe("CompleteOperation", func() {
vaultPath string
taskName string
task *domain.Task
force bool
)

BeforeEach(func() {
Expand All @@ -57,10 +58,11 @@ var _ = Describe("CompleteOperation", func() {
)
mockTaskStorage.FindTaskByNameReturns(task, nil)
mockTaskStorage.WriteTaskReturns(nil)
force = false
})

JustBeforeEach(func() {
result, err = completeOp.Execute(ctx, vaultPath, taskName, "test-vault")
result, err = completeOp.Execute(ctx, vaultPath, taskName, "test-vault", force)
})

Context("success", func() {
Expand Down Expand Up @@ -116,6 +118,57 @@ var _ = Describe("CompleteOperation", func() {
})
})

Context("task with incomplete checkboxes", func() {
BeforeEach(func() {
task = domain.NewTask(
map[string]any{"status": "todo"},
domain.FileMetadata{Name: taskName},
domain.Content("# Tasks\n\n- [x] done\n- [ ] still open\n"),
)
mockTaskStorage.FindTaskByNameReturns(task, nil)
})

Context("without force", func() {
BeforeEach(func() {
force = false
})

It("returns error", func() {
Expect(err).NotTo(BeNil())
})

It("reports the pending count", func() {
Expect(err.Error()).To(ContainSubstring("incomplete subtasks: 1 pending"))
})

It("does not write the task", func() {
Expect(mockTaskStorage.WriteTaskCallCount()).To(Equal(0))
})

It("reports incomplete_items as the reason", func() {
Expect(result.Reason).To(Equal("incomplete_items"))
})
})

Context("force bypasses the incomplete-checkbox check", func() {
BeforeEach(func() {
force = true
})

It("returns no error", func() {
Expect(err).To(BeNil())
})

It("writes the task", func() {
Expect(mockTaskStorage.WriteTaskCallCount()).To(Equal(1))
})

It("marks the task completed", func() {
Expect(task.Status()).To(Equal(domain.TaskStatusCompleted))
})
})
})

Context("task with associated goal", func() {
var goal *domain.Goal

Expand Down
Loading