Skip to content

Commit 5f37db7

Browse files
committed
Add batch child task creation and --ensure idempotency
- Support multiple --child titles in create command for parent tasks - Add --ensure flag for idempotent task creation (root and children) - Update CLI docs and skill guide for new create options - Add tests for batch children, ensure, and output shape stability
1 parent 78832e8 commit 5f37db7

19 files changed

Lines changed: 605 additions & 130 deletions

.tasque/events.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,3 +1275,4 @@
12751275
{"id":"01KJAYD9VMYB1Y291T148QR407","event_id":"01KJAYD9VMYB1Y291T148QR407","ts":"2026-02-25T17:44:07.284Z","actor":"BumpyClock","type":"task.status_set","task_id":"tsq-5bdc110f","payload":{"closed_at":"2026-02-25T17:44:07.284Z","status":"closed"}}
12761276
{"id":"01KJAYDA0KWXEDJH636T0479WT","event_id":"01KJAYDA0KWXEDJH636T0479WT","ts":"2026-02-25T17:44:07.443Z","actor":"BumpyClock","type":"task.status_set","task_id":"tsq-a6ba78a7","payload":{"closed_at":"2026-02-25T17:44:07.443Z","status":"closed"}}
12771277
{"id":"01KJAZ632FY0YK44GJX9S0HHAN","event_id":"01KJAZ632FY0YK44GJX9S0HHAN","ts":"2026-02-25T17:57:39.535Z","actor":"BumpyClock","type":"task.status_set","task_id":"tsq-96wvfdq0","payload":{"closed_at":"2026-02-25T17:57:39.535Z","status":"closed"}}
1278+
{"id":"01KJE2TG3N12REK0HPSG6K02ER","event_id":"01KJE2TG3N12REK0HPSG6K02ER","ts":"2026-02-26T22:58:57.269Z","actor":"Aditya Sharma","type":"task.created","task_id":"tsq-g99k9p5d","payload":{"description":null,"discovered_from":null,"external_ref":null,"id":"tsq-g99k9p5d","kind":"task","parent_id":null,"planning_state":"needs_planning","priority":3,"status":"open","title":"Refactor src/cli/commands/task.rs into smaller command modules"}}

AGENTS-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Relation types:
7373
## CLI Contract
7474
- `tsq init`
7575
- `tsq init --install-skill|--uninstall-skill [--skill-targets ...]`
76-
- `tsq create "Title" [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
76+
- `tsq create [<title>] [--child <title> ...] [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--ensure] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
7777
- `tsq show <id>`
7878
- `tsq list [--status ...] [--assignee ...] [--external-ref <ref>] [--discovered-from <id>] [--kind ...] [--planning <needs_planning|planned>] [--dep-type <blocks|starts_after>] [--dep-direction <in|out|any>] [--tree]`
7979
- `tsq ready [--lane <planning|coding>]`
@@ -170,4 +170,4 @@ Error:
170170
- run `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test --quiet`. Fix any issues that arise.
171171
- use a fix forward approach and avoid unnecessary complexity of backward compatibility in mind. We are in active development.
172172
- keep the codebase organized and modular. Refactor as needed to improve readability and maintainability.
173-
- Lookup if a refactor task already exists before creating a new one. If it doesn't create one so we can track it.
173+
- Lookup if a refactor task already exists before creating a new one. If it doesn't create one so we can track it.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Open blocker:
8787

8888
## CLI Contract (V1)
8989
- `tsq init`
90-
- `tsq create "Title" [-p 0..3] [--parent <id>] [--json]`
90+
- `tsq create [<title>] [--child <title> ...] [-p 0..3] [--parent <id>] [--ensure] [--json]`
9191
- `tsq show <id> [--json]`
9292
- `tsq list [--status <s>] [--assignee <a>] [--json]`
9393
- `tsq ready [--json]`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Commands:
9292
- `tsq` (no args, TTY): open read-only TUI (List/Board views)
9393
- `tsq init`
9494
- `tsq init --install-skill|--uninstall-skill [--skill-targets ...]`
95-
- `tsq create "Title" [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
95+
- `tsq create [<title>] [--child <title> ...] [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--ensure] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
9696
- `tsq show <id>`
9797
- `tsq list [--status ...] [--assignee ...] [--external-ref <ref>] [--discovered-from <id>] [--kind ...] [--planning <needs_planning|planned>] [--dep-type <blocks|starts_after>] [--dep-direction <in|out|any>] [--tree]`
9898
- `tsq ready [--lane <planning|coding>]`

SKILLS/tasque/SKILL.md

Lines changed: 172 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,182 @@ description: Operational guide for Tasque (tsq) local task tracking and manageme
55

66
<!-- tsq-managed-skill:v1 -->
77

8-
Graph-based issue tracker that survives conversation compaction. Provides persistent memory for multi-session work with complex dependencies.
9-
10-
**When to use tsq**:
11-
- Work spans multiple sessions or days
12-
- Tasks have dependencies or blockers
13-
- Need to survive conversation compaction
14-
- Exploratory/research work with fuzzy boundaries
15-
- Collaboration with team (git sync)
16-
17-
**When to use native task tool**:
18-
- Single-session linear tasks
19-
- Simple checklist for immediate work
20-
- All context is in current conversation
21-
- Will complete within current session
22-
23-
Durable task tracking via `tsq`.
24-
25-
- Local-first and repo-local (`.tasque/`), so tracking works offline with no external service.
26-
- Append-only JSONL history is git-friendly and auditable across agent sessions.
27-
- Durable restart/replay model survives context compaction and crashes.
28-
- Lane-aware readiness plus typed dependencies makes parallel sub-agent execution explicit and safe.
29-
- Stable `--json` output keeps agent automation predictable.
30-
- Survive context compaction, session restarts, and crashes.
31-
32-
## What to do by default
33-
34-
1. Run `tsq ready --lane planning` and `tsq ready --lane coding`.
35-
2. Pick a task with `tsq show <id>`.
36-
3. If planning is incomplete, collaborate with the user and attach/update spec.
37-
4. Mark planning done: `tsq update <id> --planning planned`.
38-
5. Claim/start work: `tsq update <id> --claim [--assignee <name>]` then `tsq update <id> --status in_progress`.
39-
6. Close when complete: `tsq update <id> --status closed`.
8+
Tasque = durable, local-first task memory for agent work.
9+
Default day-to-day playbook.
10+
11+
## When to use tsq
12+
13+
- Use `tsq` for multi-step, multi-session, dependency-blocked, shared-agent work.
14+
- Use transient checklist for short linear single-session work.
15+
16+
## Session routine (default)
17+
18+
```bash
19+
tsq ready --lane planning
20+
tsq ready --lane coding
21+
tsq list --status blocked
22+
```
23+
24+
Pick one task; inspect context:
25+
26+
```bash
27+
tsq show <id>
28+
```
29+
30+
31+
### 1) Capture new work
32+
33+
```bash
34+
tsq create "Implement <feature>" --kind feature -p 1 --needs-planning
35+
tsq create "Fix <bug>" --kind task -p 1 --needs-planning
36+
```
37+
38+
Planning already done:
39+
40+
```bash
41+
tsq create "Implement <feature>" --planning planned
42+
```
43+
44+
### 2) Split parent into many children (single command)
45+
46+
```bash
47+
tsq create --parent <parent-id> \
48+
--child "Design API contract" \
49+
--child "Implement service logic" \
50+
--child "Add regression tests"
51+
```
52+
53+
Shared defaults for all children:
54+
55+
```bash
56+
tsq create --parent <parent-id> --kind task -p 2 --planning planned \
57+
--child "Wire CLI args" \
58+
--child "Update docs" \
59+
--child "Add integration tests"
60+
```
61+
62+
Safe reruns without duplicate children:
63+
64+
```bash
65+
tsq create --parent <parent-id> --ensure \
66+
--child "Wire CLI args" \
67+
--child "Update docs" \
68+
--child "Add integration tests"
69+
```
70+
71+
### 3) Planning handoff -> coding
72+
73+
```bash
74+
tsq spec attach <id> --text "## Plan\n...\n## Acceptance\n..."
75+
tsq update <id> --planning planned
76+
tsq update <id> --claim --assignee <name>
77+
tsq update <id> --status in_progress
78+
```
79+
80+
### 4) Model deps for parallel agents
81+
82+
Hard blocker (changes readiness):
83+
84+
```bash
85+
tsq dep add <child-id> <blocker-id> --type blocks
86+
```
87+
88+
Soft ordering only:
89+
90+
```bash
91+
tsq dep add <later-id> <earlier-id> --type starts_after
92+
```
93+
94+
Check actionable tasks:
95+
96+
```bash
97+
tsq ready --lane coding
98+
tsq ready --lane planning
99+
```
100+
101+
### 5) Capture discovered follow-up work
102+
103+
```bash
104+
tsq create "Handle edge case <x>" --discovered-from <current-id> --needs-planning
105+
tsq link add <new-id> <current-id> --type relates_to
106+
```
107+
108+
### 5b) Idempotent root/parent create for automation
109+
110+
```bash
111+
tsq create "Implement auth module" --ensure
112+
tsq create --parent <parent-id> --child "Add tests" --ensure
113+
```
114+
115+
`--ensure` returns existing task when same normalized title already exists under the same parent.
116+
117+
### 6) Park / unpark work
118+
119+
```bash
120+
tsq update <id> --status deferred
121+
tsq list --status deferred
122+
tsq update <id> --status open
123+
```
124+
125+
### 7) Resolve duplicate/superseded work
126+
127+
```bash
128+
tsq duplicate <id> --of <canonical-id> --reason "same root issue"
129+
tsq duplicates
130+
tsq merge <source-id...> --into <target-id> --dry-run
131+
tsq merge <source-id...> --into <target-id> --force
132+
tsq supersede <old-id> --with <new-id> --reason "replaced approach"
133+
```
134+
135+
### 8) Close / report
136+
137+
```bash
138+
tsq update <id> --status closed
139+
tsq history <id> --limit 20
140+
tsq list --tree
141+
```
142+
143+
Agent/tool handoff: add `--json`.
144+
145+
## Built-in task authoring checklist
146+
147+
### Minimum quality bar
148+
149+
- Titles: clear, action-oriented (verb + object + scope).
150+
- Set `kind`: `task|feature|epic`.
151+
- Set priority intentionally: `0..3`.
152+
- Add labels with consistent naming.
153+
- Attach spec when scope/acceptance non-trivial.
154+
- Add explicit deps/relations when relevant.
155+
156+
### Parallelization guidance
157+
158+
- Prefer multiple independent tasks over one large task.
159+
- Use `blocks` only when work truly gates another task.
160+
- Use `starts_after` for sequencing without blocking readiness.
161+
- Add discovered work as new tasks via `--discovered-from`.
162+
- Keep each task small enough for one focused agent pass.
163+
164+
### Practical authoring starter
165+
166+
```bash
167+
tsq create "<title>" --kind task -p 2 --needs-planning
168+
tsq spec attach <id> --text "<markdown spec>"
169+
tsq dep add <child> <blocker> --type blocks
170+
tsq link add <src> <dst> --type relates_to
171+
```
40172

41173
## Required habits
42174

43175
- Keep lifecycle `status` and `planning_state` separate.
44-
- Add dependencies so parallel work is explicit (`blocks` vs `starts_after`).
45-
- Break work into small tasks that can run in parallel across sub-agents.
46-
- Create new tasks for discovered bugs/follow-ups instead of leaving TODOs in chat.
47-
- Prefer `--json` for automation and tool-to-tool handoffs.
176+
- Use deps to make parallel execution explicit.
177+
- Create follow-up tasks; avoid chat TODOs.
178+
- Prefer `--json` for automation.
179+
- Use `--ensure` in scripts to prevent duplicate creates on rerun.
48180

49181
## Read when needed
50182

51-
- Planning/deferred semantics and lane-ready behavior: `references/planning-workflow.md`
52-
- Full CLI command catalog and option matrix: `references/command-reference.md`
53-
- Task authoring checklist and naming/labeling standards: `references/task-authoring-checklist.md`
54-
- JSON schema and storage/durability model: `references/machine-output-and-durability.md`
55-
- Install tasque if it's not available : `npm install -g @bumpyclock/tasque`
183+
- Planning/deferred semantics: `references/planning-workflow.md`
184+
- JSON schema + durability details: `references/machine-output-and-durability.md`
185+
- Full option matrix (edge cases): `references/command-reference.md`
186+
- Install if missing: `npm install -g @bumpyclock/tasque`

SKILLS/tasque/references/command-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Read when: you need exact command syntax or available options.
55
## Core workflow
66

77
- `tsq init`
8-
- `tsq create "Title" [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
8+
- `tsq create [<title>] [--child <title> ...] [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--ensure] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
99
- `tsq show <id>`
1010
- `tsq list [--status ...] [--assignee ...] [--external-ref <ref>] [--discovered-from <id>] [--kind ...] [--planning <needs_planning|planned>] [--dep-type <blocks|starts_after>] [--dep-direction <in|out|any>] [--tree]`
1111
- `tsq update <id> [--title ...] [--status ...] [--priority ...] [--external-ref <ref>] [--clear-external-ref] [--discovered-from <id>] [--clear-discovered-from] [--planning <needs_planning|planned>]`

npm/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Commands:
9191

9292
- `tsq init`
9393
- `tsq init --install-skill|--uninstall-skill [--skill-targets ...]`
94-
- `tsq create "Title" [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
94+
- `tsq create [<title>] [--child <title> ...] [--kind ...] [-p ...] [--parent <id>] [--external-ref <ref>] [--discovered-from <id>] [--planning <needs_planning|planned>] [--needs-planning] [--ensure] [--id <tsq-xxxxxxxx>] [--body-file <path|->]`
9595
- `tsq show <id>`
9696
- `tsq list [--status ...] [--assignee ...] [--external-ref <ref>] [--discovered-from <id>] [--kind ...] [--planning <needs_planning|planned>] [--dep-type <blocks|starts_after>] [--dep-direction <in|out|any>] [--tree]`
9797
- `tsq ready [--lane <planning|coding>]`

src/app/service.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,7 @@ impl TasqueService {
274274
}
275275

276276
pub fn migrate(&self, branch: &str) -> Result<crate::types::MigrateResult, TsqError> {
277-
crate::app::sync::migrate_to_sync_branch(
278-
&self.ctx.repo_root,
279-
branch,
280-
&self.ctx.actor,
281-
)
277+
crate::app::sync::migrate_to_sync_branch(&self.ctx.repo_root, branch, &self.ctx.actor)
282278
}
283279
}
284280

src/app/service_create_update.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::domain::events::make_event;
77
use crate::domain::ids::next_child_id;
88
use crate::domain::projector::apply_events;
99
use crate::errors::TsqError;
10-
use crate::types::{EventRecord, EventType, PlanningState, Task, TaskStatus};
10+
use crate::types::{EventRecord, EventType, PlanningState, State, Task, TaskStatus};
1111
use once_cell::sync::Lazy;
1212
use regex::Regex;
1313
use serde_json::{Map, Value};
@@ -24,6 +24,13 @@ pub fn create(ctx: &ServiceContext, input: &CreateInput) -> Result<Task, TsqErro
2424
1,
2525
));
2626
}
27+
if input.ensure && input.explicit_id.is_some() {
28+
return Err(TsqError::new(
29+
"VALIDATION_ERROR",
30+
"cannot combine --ensure with --id",
31+
1,
32+
));
33+
}
2734
if input.description.is_some() && input.body_file.is_some() {
2835
return Err(TsqError::new(
2936
"VALIDATION_ERROR",
@@ -57,6 +64,15 @@ pub fn create(ctx: &ServiceContext, input: &CreateInput) -> Result<Task, TsqErro
5764
.as_ref()
5865
.map(|raw| must_resolve_existing(&loaded.state, raw, input.exact_id))
5966
.transpose()?;
67+
if input.ensure
68+
&& let Some(existing) = find_existing_by_parent_and_title(
69+
&loaded.state,
70+
parent_id.as_deref(),
71+
&input.title,
72+
)
73+
{
74+
return Ok(existing);
75+
}
6076
let id = if let Some(parent) = parent_id.as_ref() {
6177
next_child_id(&loaded.state, parent)
6278
} else {
@@ -241,3 +257,25 @@ pub fn update(ctx: &ServiceContext, input: &UpdateInput) -> Result<Task, TsqErro
241257
fn payload_map(value: Value) -> Map<String, Value> {
242258
value.as_object().cloned().unwrap_or_default()
243259
}
260+
261+
fn find_existing_by_parent_and_title(
262+
state: &State,
263+
parent_id: Option<&str>,
264+
title: &str,
265+
) -> Option<Task> {
266+
let normalized_title = crate::app::service_utils::normalize_duplicate_title(title);
267+
let mut matches: Vec<&Task> = state
268+
.tasks
269+
.values()
270+
.filter(|task| task.parent_id.as_deref() == parent_id)
271+
.filter(|task| {
272+
crate::app::service_utils::normalize_duplicate_title(&task.title) == normalized_title
273+
})
274+
.collect();
275+
matches.sort_by(|left, right| {
276+
left.created_at
277+
.cmp(&right.created_at)
278+
.then_with(|| left.id.cmp(&right.id))
279+
});
280+
matches.first().map(|task| (*task).clone())
281+
}

src/app/service_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub struct CreateInput {
4747
pub planning_state: Option<PlanningState>,
4848
pub explicit_id: Option<String>,
4949
pub body_file: Option<String>,
50+
pub ensure: bool,
5051
}
5152

5253
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)