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
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ tskflwctl research new "Storage options" --created 2026-06-24 # backdate: the i
# read
tskflwctl task list # active tasks (--all / --status / --epic / --tag)
tskflwctl task list --revisit-due # deferred tasks whose snooze date has arrived
tskflwctl task list --unblocked # active tasks whose derived dependency gate is clear
tskflwctl task show <slug> # metadata + body (--section <name> / --frontmatter-only to narrow)
tskflwctl task info <slug> --json # token-cheap metadata: path, status, epic, ac:{checked,total} (no body)
tskflwctl task path <slug> # just the absolute file path — $EDITOR "$(tskflwctl task path <slug>)"
tskflwctl task blockers <slug> # actionable blocker frontier (--causal for the full closure)
tskflwctl task unblocks <slug> # all transitive downstream tasks and their current graph state
tskflwctl epic list # rollup: done/total per epic
tskflwctl epic show <id> --section goal # epic body section (or --frontmatter-only); epic path <id> for the file
tskflwctl audit list # open audits (--all / --closed / --deferred)
Expand All @@ -130,6 +133,9 @@ tskflwctl task ac <slug> # numbered acceptance criter
tskflwctl task ac <slug> --tracked 3 --reason "carried by <id>" # …or --defer/--wontfix/--na: why it is unmet, not just that it is
tskflwctl task start|next|ready|complete|defer|deprecate <slug>... # defer takes --until <date>
tskflwctl task defer <slug> --until 2026-09-01 # snooze (revisit_at); on a TTY, prompts for the date
tskflwctl task depend add <slug> --on <prerequisite>... # guarded repository-global edge add
tskflwctl task depend remove <slug> --on <prerequisite>... # idempotent guarded edge removal
tskflwctl task depend migrate # convert safe legacy dependency fields repo-wide
tskflwctl audit finding <slug> <code> --status "tracked by <id>" --note "how" # status + resolution, one atomic write
tskflwctl audit close|reopen|defer <slug>...
tskflwctl research set <slug> --description "…" --tags a,b # settable fields only; `schema research` lists them
Expand Down Expand Up @@ -240,15 +246,21 @@ field in place and stamp the dates atomically — no file moves (`lint --fix`
re-normalizes a hand-edited drift). Errors carry semantic exit codes — `10`
not-found, `11` validation, `13` ambiguous, `14` conflict (e.g. a name already taken).

**Task-dependency read foundation.** Task frontmatter and JSON may carry `depends_on`,
a sorted set of stable task IDs representing repository-global prerequisites. This
release reads, validates, and explains that graph but intentionally exposes no public
dependency mutation command yet. Generic task creation, `task set` (even `--force`),
`task edit`, and `lint --fix` cannot add, remove, or reinterpret dependency fields;
guarded `task depend add/remove` operations are the next slice. Ordinary `lint` reports
all graph defects. Exactly resolved legacy `blocked_by`/`dependencies`/`blocks` values
are visible JSON/human advisories with exit zero, while missing, ambiguous, cyclic, or
self-referential legacy projections remain validation errors. See
**Task-dependency graph.** Task frontmatter and JSON may carry `depends_on`, a sorted
set of stable task IDs representing repository-global prerequisites. Guarded
`task depend add/remove` operations resolve references inside one authoritative
repository snapshot, reject broken or cyclic results, support authoritative dry runs,
and emit structured receipts. `task depend migrate` converts safe legacy
`blocked_by`/`dependencies`/`blocks` fields repository-wide and can resume after a
reported sound durable prefix; it also removes explicitly empty legacy keys. Resolved
legacy edges already participate in blocker/downstream reads and derived gates, while
health remains degraded until migration makes them canonical. Generic task creation, `task set` (even `--force`),
`task edit`, and `lint --fix` cannot add, remove, or reinterpret graph-owned fields.
Use `task blockers`, `task unblocks`, and `task list --unblocked` for explanatory and
dispatch-oriented reads; mutations and the eligibility selector fail closed on an
unsound graph, while diagnostic queries report the queried task's derived state, health,
and attributable problems.
Ordinary `lint` reports every graph defect. See
[`ADR-0006`](./planning/adrs/0006-adopt-threads-as-task-dags.md) for the model and rollout.

**Research** is the thinnest kind, and the omissions are the point: no status and
Expand Down
16 changes: 12 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,18 @@ adapter capabilities rather than leaked persistence.
`TaskGraph` is an immutable read projection over one repository scan. It owns graph
health (`healthy`/`degraded`/`broken`), SCC-based cycle attribution, derived lifecycle
role and gate state, sound completion, topology, downstream impact, and separately
named causal-blocker and action-frontier projections. The analyzer uses only taskflow
types and owned deterministic algorithms; a graph package cannot leak into domain,
persistence, or wire contracts. Eligibility is read from derived state, never inferred
from an empty blocker list.
named causal-blocker and action-frontier projections. It also resolves ordinary task
references inside that immutable snapshot, so dependency planners never pre-resolve a
slug through persistence and carry a stale choice into the guarded callback. Service
dependency use cases emit taskflow-owned, adapter-neutral receipts for edge set
operations and the repository-wide legacy migration; typed failures retain any sound
durable prefix for explicit recovery. The analyzer uses only taskflow types and owned
deterministic algorithms; a graph package cannot leak into domain, persistence, or
wire contracts. Exactly resolved legacy edges participate in diagnostic traversal and
derived gates before migration, so a degraded snapshot cannot issue a false all-clear;
present-but-empty legacy keys still keep health degraded until migration removes them.
Eligibility is read from the queried task's explicit derived state, never inferred from
an empty blocker list, and `task list --unblocked` fails closed unless the snapshot is healthy.
Per-space failures remain data in the projection; the CLI renders the complete sweep
before applying its partial-failure exit policy. Pure; unit-testable without fs.
- **`internal/store`** — the secondary adapter: tasks as
Expand Down
3 changes: 3 additions & 0 deletions docs/cli/tskflwctl_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ Work with tasks
* [tskflwctl](tskflwctl.md) - Local-first planning CLI (tasks, epics, audits, research) over markdown
* [tskflwctl task ac](tskflwctl_task_ac.md) - List a task's acceptance criteria, or check/uncheck one by index
* [tskflwctl task append](tskflwctl_task_append.md) - Append a section to a task's body (atomic; agent-facing)
* [tskflwctl task blockers](tskflwctl_task_blockers.md) - Explain the actionable blockers for a task
* [tskflwctl task complete](tskflwctl_task_complete.md) - Move task(s) to completed
* [tskflwctl task defer](tskflwctl_task_defer.md) - Move task(s) to deferred (optionally with a revisit date)
* [tskflwctl task depend](tskflwctl_task_depend.md) - Change repository-global task dependencies through the graph guard
* [tskflwctl task deprecate](tskflwctl_task_deprecate.md) - Move task(s) to deprecated
* [tskflwctl task edit](tskflwctl_task_edit.md) - Open a task in your editor (whole file; re-validated on save)
* [tskflwctl task info](tskflwctl_task_info.md) - Show a task's metadata + file path + acceptance tally (no body)
Expand All @@ -43,4 +45,5 @@ Work with tasks
* [tskflwctl task set](tskflwctl_task_set.md) - Set one or more frontmatter fields (validated, single atomic write)
* [tskflwctl task show](tskflwctl_task_show.md) - Show a task's metadata and body
* [tskflwctl task start](tskflwctl_task_start.md) - Move task(s) to in-progress
* [tskflwctl task unblocks](tskflwctl_task_unblocks.md) - Show every task transitively downstream of this task

45 changes: 45 additions & 0 deletions docs/cli/tskflwctl_task_blockers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## tskflwctl task blockers

Explain the actionable blockers for a task

### Synopsis

Explain a task's current derived role, gate, eligibility, and actionable blocker frontier. --causal selects the full forensic closure. Resolved legacy constraints participate in both projections, while graph health still reports degraded until they are migrated.

```
tskflwctl task blockers <task> [flags]
```

### Examples

```
tskflwctl task blockers deploy
tskflwctl task blockers deploy --causal --json
```

### Options

```
--causal show the full causal blocker closure instead of the actionable frontier
-h, --help help for blockers
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task](tskflwctl_task.md) - Work with tasks

32 changes: 32 additions & 0 deletions docs/cli/tskflwctl_task_depend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## tskflwctl task depend

Change repository-global task dependencies through the graph guard

### Options

```
-h, --help help for depend
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task](tskflwctl_task.md) - Work with tasks
* [tskflwctl task depend add](tskflwctl_task_depend_add.md) - Add one or more hard prerequisites
* [tskflwctl task depend migrate](tskflwctl_task_depend_migrate.md) - Convert all safe legacy dependency fields to canonical depends_on IDs
* [tskflwctl task depend remove](tskflwctl_task_depend_remove.md) - Remove one or more hard prerequisites

40 changes: 40 additions & 0 deletions docs/cli/tskflwctl_task_depend_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## tskflwctl task depend add

Add one or more hard prerequisites

```
tskflwctl task depend add <task> [flags]
```

### Examples

```
tskflwctl task depend add deploy --on build --on verify
```

### Options

```
-h, --help help for add
--on strings prerequisite task reference (repeat or comma-separate)
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task depend](tskflwctl_task_depend.md) - Change repository-global task dependencies through the graph guard

44 changes: 44 additions & 0 deletions docs/cli/tskflwctl_task_depend_migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## tskflwctl task depend migrate

Convert all safe legacy dependency fields to canonical depends_on IDs

### Synopsis

Convert every legacy blocked_by, dependencies, and blocks field occurrence to canonical depends_on IDs and remove the legacy keys. Present-but-empty legacy keys are also removed. The repository-wide plan writes dependents before legacy blocks owners so every durable prefix remains conservative and a retry converges after interruption.

```
tskflwctl task depend migrate [flags]
```

### Examples

```
tskflwctl task depend migrate --dry-run --json
tskflwctl task depend migrate
```

### Options

```
-h, --help help for migrate
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task depend](tskflwctl_task_depend.md) - Change repository-global task dependencies through the graph guard

40 changes: 40 additions & 0 deletions docs/cli/tskflwctl_task_depend_remove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## tskflwctl task depend remove

Remove one or more hard prerequisites

```
tskflwctl task depend remove <task> [flags]
```

### Examples

```
tskflwctl task depend remove deploy --on build --on verify
```

### Options

```
-h, --help help for remove
--on strings prerequisite task reference (repeat or comma-separate)
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task depend](tskflwctl_task_depend.md) - Change repository-global task dependencies through the graph guard

2 changes: 2 additions & 0 deletions docs/cli/tskflwctl_task_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tskflwctl task list [flags]
tskflwctl task list
tskflwctl task list -q --tag tui | xargs tskflwctl task start
tskflwctl task list -o table -c slug,status,epic
tskflwctl task list --unblocked --json
tskflwctl task list --revisit-due -q | xargs tskflwctl task next # resume snoozed tasks now due
```

Expand All @@ -27,6 +28,7 @@ tskflwctl task list [flags]
--revisit-due only deferred tasks whose revisit date has arrived (composes with --epic/--tag/-c)
--status string filter by status
--tag string filter by tag
--unblocked only tasks whose derived dependency state is eligible
```

### Options inherited from parent commands
Expand Down
44 changes: 44 additions & 0 deletions docs/cli/tskflwctl_task_unblocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## tskflwctl task unblocks

Show every task transitively downstream of this task

### Synopsis

Show the queried task's current derived state and every transitive downstream task with deterministic shortest paths. Resolved legacy constraints participate in the projection. This is current impact, not a promise that completing the source alone makes every result eligible.

```
tskflwctl task unblocks <task> [flags]
```

### Examples

```
tskflwctl task unblocks build
tskflwctl task unblocks build --json
```

### Options

```
-h, --help help for unblocks
```

### Options inherited from parent commands

```
-C, --chdir string anchor to the planning repo at this path (conflicts with --space)
--color string colorize output: auto|always|never (default "auto")
--dry-run preview the mutation without writing (validation still runs)
--json machine-readable JSON output
--no-color disable colored output (alias for --color=never)
--no-input never prompt; missing required input is an error (for scripts/agents; also TSKFLW_NO_INPUT)
--no-pager do not pipe long human output through a pager
--paginate page long human output through $PAGER (on a TTY), even if disabled in config
--space string select a registered entry point by label (also TSKFLW_SPACE; conflicts with -C)
--theme string color theme name (overrides TSKFLW_THEME and [theme].name in config)
```

### SEE ALSO

* [tskflwctl task](tskflwctl_task.md) - Work with tasks

5 changes: 5 additions & 0 deletions internal/cli/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func WriteError(w io.Writer, err error, asJSON bool) {
payload := wire.ErrorEnvelope{SchemaVersion: wire.SchemaVersion}
payload.Error.Code = errorCodeName(ExitCode(err))
payload.Error.Message = err.Error()
var dependencyErr *dependencyCommandFailure
if errors.As(err, &dependencyErr) {
details := wire.ToDependencyMutationJSON(dependencyErr.receipt, dependencyErr.workspace)
payload.Error.DependencyMutation = &details
}
// Compact, like every other --json envelope (see wire.EncodeJSON): an agent
// parsing the failure shouldn't pay for indentation either.
_ = wire.EncodeJSON(w, payload)
Expand Down
3 changes: 3 additions & 0 deletions internal/cli/integration_golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func TestGolden_MachineContract(t *testing.T) {
{"task_list_name", []string{"-C", fixtureRepo, "task", "list", "--all", "-o", "name"}, nil},
{"task_show_json", []string{"-C", fixtureRepo, "task", "show", "alpha-task", "--json"}, nil},
{"task_acceptance_json", []string{"-C", fixtureRepo, "task", "ac", "alpha-task", "--json"}, nil},
{"task_blockers_json", []string{"-C", fixtureRepo, "task", "blockers", "alpha-task", "--json"}, nil},
{"task_unblocks_json", []string{"-C", fixtureRepo, "task", "unblocks", "gamma-task", "--json"}, nil},
{"task_list_unblocked_json", []string{"-C", fixtureRepo, "task", "list", "--unblocked", "--json"}, nil},
// task info / task path emit an absolute file path → redact the fixture root
// so the committed golden is portable (pins schema_version + shape + tally).
{"task_info_json", []string{"-C", fixtureRepo, "task", "info", "alpha-task", "--json"}, redact},
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestLintReportsLegacyAndCanonicalDependencyDefects(t *testing.T) {
t.Fatalf("dependency defects must fail ordinary lint with exit 11, got %v", err)
}
for _, want := range []string{
"legacy dependency field", targetID, "guarded dependency operations",
"legacy dependency field", targetID, "tskflwctl task depend migrate",
"cannot depend on itself", "advisory finding",
} {
if !strings.Contains(out, want) {
Expand Down
Loading