@4-r-c-4-n-4/todo 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BIBLE.md CHANGED
@@ -21,11 +21,24 @@ todo close <id>
21
21
  ### Feature build (parent + children)
22
22
  A parent ticket tracks the feature. Children track subtasks. All children share the parent's branch.
23
23
 
24
- ```
24
+ ```bash
25
25
  todo new "Add OAuth2 login" --type feature
26
26
  todo new "Add /auth/callback route" --type chore --parent <feature-id>
27
27
  todo new "Store session tokens" --type chore --parent <feature-id>
28
- todo work <child-id> # checks out todo/<feature-id> branch
28
+ todo new "Write integration tests" --type chore --parent <feature-id>
29
+
30
+ # First child creates the shared branch
31
+ todo work <child-1-id>
32
+
33
+ # Subsequent children — use todo next (preferred)
34
+ while next=$(todo next <feature-id> 2>/dev/null); do
35
+ # implement $next ...
36
+ git add -A && git commit -m "todo:$next — ..."
37
+ todo close $next --note "..."
38
+ git add .todo/ && git commit -m "todo:$next — close"
39
+ done
40
+
41
+ todo close <feature-id> --note "All subtasks done."
29
42
  ```
30
43
 
31
44
  Children resolve on the parent branch. Close all children before closing the parent.
@@ -71,7 +84,7 @@ Children resolve on the parent branch. Close all children before closing the par
71
84
 
72
85
  ## The Branch Workflow
73
86
 
74
- Step by step:
87
+ ### Standalone ticket
75
88
 
76
89
  ```bash
77
90
  # 1. Start work — creates branch todo/<id>, transitions ticket to active
@@ -97,7 +110,35 @@ git merge --no-ff todo/<id>
97
110
  git branch -d todo/<id>
98
111
  ```
99
112
 
100
- For child tickets: `todo work <child-id>` checks out the parent's branch, not a new one.
113
+ ### Feature build (shared branch)
114
+
115
+ All children share `todo/<parent-id>`. The first `todo work` creates the branch; subsequent children must not trigger a redundant checkout.
116
+
117
+ ```bash
118
+ # First child — creates todo/<parent-id>
119
+ todo work <child-1-id>
120
+ git add -A && git commit -m "todo:<child-1-id> — ..."
121
+ todo close <child-1-id> --note "..."
122
+ git add .todo/ && git commit -m "todo:<child-1-id> — close"
123
+
124
+ # Remaining children — todo next handles activation cleanly
125
+ while next=$(todo next <parent-id> 2>/dev/null); do
126
+ # implement ...
127
+ git add -A && git commit -m "todo:$next — ..."
128
+ todo close $next --note "..."
129
+ git add .todo/ && git commit -m "todo:$next — close"
130
+ done
131
+
132
+ # Close parent
133
+ todo close <parent-id> --note "All children done."
134
+ git add .todo/ && git commit -m "todo:<parent-id> — close"
135
+ git checkout main && git merge --no-ff todo/<parent-id>
136
+ git branch -d todo/<parent-id>
137
+ ```
138
+
139
+ `todo next <parent-id>` finds the first open child (in creation order), activates it on the current branch without any git checkout, prints its ID to stdout and a summary to stderr. Exits 1 when all children are done — that's what stops the `while` loop.
140
+
141
+ If you need manual control instead of a loop, use `todo work --skip-branch <child-id>` to activate a child without a redundant checkout. Do NOT use plain `todo work` for subsequent children on a shared branch — it performs a no-op checkout and prints confusing resume output.
101
142
 
102
143
  ---
103
144
 
@@ -138,6 +179,7 @@ Examples:
138
179
  todo:a1b2c3d4 — fix null pointer in auth handler
139
180
  todo:a1b2c3d4 — close
140
181
  todo:e5f6a7b8 — add /auth/callback route
182
+ todo:e8e874e9 — plan: 4 subtasks
141
183
  ```
142
184
 
143
185
  The `<id>` is the full 8-char ticket ID. Always include it. This ties commits to tickets and enables commit-based dedup and linking.
@@ -156,6 +198,10 @@ The `<id>` is the full 8-char ticket ID. Always include it. This ties commits to
156
198
 
157
199
  5. **Not committing parent ticket after adding children** — Parent `relationships.children` is updated when you create a child. Commit `.todo/` after creating children.
158
200
 
201
+ 6. **Using plain `todo work` for subsequent children on a shared branch** — After the first child creates `todo/<parent-id>`, calling `todo work <child-N>` again does a redundant checkout and prints misleading "Resumed branch" output. Use `todo next <parent-id>` (preferred) or `todo work --skip-branch <child-N>` instead.
202
+
203
+ 7. **Using `--no-branch` instead of `--skip-branch`** — Commander.js treats `--no-X` as negating the `--X <value>` option. `--no-branch` silently overrides `--branch <name>` rather than setting a new flag. The correct flag is `--skip-branch`.
204
+
159
205
  ---
160
206
 
161
207
  ## CLI Quick Reference
@@ -173,12 +219,17 @@ todo show <id> Show ticket detail
173
219
  --raw
174
220
  todo edit <id> Edit ticket fields
175
221
  --summary --description --type --tags --add-tag --rm-tag
222
+ --parent <id> reparent under a different parent
176
223
  todo transition <id> <state> Transition state
177
224
  --commit --test --note --depends-on --duplicate-of
178
225
  todo close <id> Shorthand: transition to done
179
226
  --commit --test --note --checkout
180
227
  todo work <id> Start/resume work on a ticket
181
228
  --branch --actor
229
+ --skip-branch Activate on current branch, no git ops (orchestrator mode)
230
+ todo next <parent-id> Activate next open child on current branch
231
+ stdout: ticket ID stderr: summary exit 1: all done
232
+ --actor
182
233
  todo analyze <id> Add analysis entry
183
234
  --type blame|hypothesis|evidence|conclusion (required)
184
235
  --content <text> (required)
@@ -193,15 +244,15 @@ todo scan Scan source tree for TODO/FIXME comments
193
244
  todo dedup Find duplicate tickets
194
245
  --strategy fingerprint|file-line|semantic
195
246
  --apply
196
- todo export <id> Export ticket as JSON/markdown
197
- --format json|markdown
247
+ todo export Export tickets as JSON
248
+ --state --type
198
249
  ```
199
250
 
200
251
  ---
201
252
 
202
253
  ## Skill Interface
203
254
 
204
- Agents operate via four named skills. Each maps to a phase of the lifecycle.
255
+ Agents operate via five named skills. Each maps to a phase of the lifecycle.
205
256
 
206
257
  **todo-capture** — Create a ticket from any signal (log, test failure, comment, agent observation). Use `todo new` with `--source` and `--pipe` as appropriate. Always commit the result.
207
258
 
@@ -209,4 +260,6 @@ Agents operate via four named skills. Each maps to a phase of the lifecycle.
209
260
 
210
261
  **todo-analyze** — Build up the understanding of a bug or requirement. Use `todo analyze` with sequenced entries: hypothesis → evidence → conclusion. Reference supporting indices. Commit when done.
211
262
 
212
- **todo-implement** — Run `todo work` to branch, implement, commit with the `todo:<id>` prefix, then `todo close`. Always commit `.todo/` after close.
263
+ **todo-plan** — Decompose a feature or spec into a parent ticket with ordered children. Use `todo new --parent` to wire children. Commit the full structure before handing off. Children are worked sequentially on the parent's branch via `todo next`.
264
+
265
+ **todo-implement** — Run `todo work` to branch, implement, commit with the `todo:<id>` prefix, then `todo close`. Use `todo next` to advance through children on a shared branch. Always commit `.todo/` after close.
package/README.md CHANGED
@@ -1,2 +1,4 @@
1
1
  # todo
2
2
  Making working on work work
3
+
4
+ https://www.npmjs.com/package/@4-r-c-4-n-4/todo
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const init_js_1 = require("./commands/init.js");
11
11
  const link_js_1 = require("./commands/link.js");
12
12
  const list_js_1 = require("./commands/list.js");
13
13
  const new_js_1 = require("./commands/new.js");
14
+ const next_js_1 = require("./commands/next.js");
14
15
  const scan_js_1 = require("./commands/scan.js");
15
16
  const show_js_1 = require("./commands/show.js");
16
17
  const transition_js_1 = require("./commands/transition.js");
@@ -29,6 +30,7 @@ program
29
30
  (0, transition_js_1.registerTransition)(program);
30
31
  (0, close_js_1.registerClose)(program);
31
32
  (0, work_js_1.registerWork)(program);
33
+ (0, next_js_1.registerNext)(program);
32
34
  (0, analyze_js_1.registerAnalyze)(program);
33
35
  (0, link_js_1.registerLink)(program);
34
36
  (0, scan_js_1.registerScan)(program);
@@ -21,12 +21,61 @@ function registerEdit(program) {
21
21
  .option("--tags <tags>", "replace all tags (comma-separated)")
22
22
  .option("--add-tag <tag>", "add one tag")
23
23
  .option("--rm-tag <tag>", "remove one tag")
24
+ .option("--parent <id>", "reparent ticket under a different parent")
24
25
  .action((id, opts) => {
25
26
  const ctx = (0, context_js_1.getContext)(true);
26
27
  const { repoRoot } = ctx;
27
28
  try {
28
29
  const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
29
30
  let changed = false;
31
+ if (opts.parent !== undefined) {
32
+ let newParent;
33
+ try {
34
+ newParent = (0, ticket_js_1.readTicketByPrefix)(repoRoot, opts.parent);
35
+ }
36
+ catch {
37
+ console.error(`Error: parent ticket '${opts.parent}' not found`);
38
+ process.exit(1);
39
+ }
40
+ if (newParent.id === ticket.id) {
41
+ console.error("Error: a ticket cannot be its own parent");
42
+ process.exit(1);
43
+ }
44
+ const oldParentId = ticket.relationships?.parent;
45
+ if (oldParentId !== newParent.id) {
46
+ const now = new Date().toISOString();
47
+ if (oldParentId) {
48
+ try {
49
+ const oldParent = (0, ticket_js_1.readTicketByPrefix)(repoRoot, oldParentId);
50
+ if (oldParent.relationships?.children) {
51
+ const before = oldParent.relationships.children.length;
52
+ oldParent.relationships.children =
53
+ oldParent.relationships.children.filter((c) => c !== ticket.id);
54
+ if (oldParent.relationships.children.length !== before) {
55
+ oldParent.updated_at = now;
56
+ (0, ticket_js_1.writeTicket)(repoRoot, oldParent);
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // old parent missing — nothing to detach from
62
+ }
63
+ }
64
+ if (!newParent.relationships)
65
+ newParent.relationships = {};
66
+ if (!newParent.relationships.children)
67
+ newParent.relationships.children = [];
68
+ if (!newParent.relationships.children.includes(ticket.id)) {
69
+ newParent.relationships.children.push(ticket.id);
70
+ newParent.updated_at = now;
71
+ (0, ticket_js_1.writeTicket)(repoRoot, newParent);
72
+ }
73
+ if (!ticket.relationships)
74
+ ticket.relationships = {};
75
+ ticket.relationships.parent = newParent.id;
76
+ changed = true;
77
+ }
78
+ }
30
79
  if (opts.summary !== undefined) {
31
80
  ticket.summary = opts.summary;
32
81
  changed = true;
@@ -58,8 +58,8 @@ function registerLink(program) {
58
58
  const key = relation;
59
59
  if (!ticket.relationships[key])
60
60
  ticket.relationships[key] = [];
61
- if (!ticket.relationships[key].includes(resolvedTarget)) {
62
- ticket.relationships[key].push(resolvedTarget);
61
+ if (!ticket.relationships[key]?.includes(resolvedTarget)) {
62
+ ticket.relationships[key]?.push(resolvedTarget);
63
63
  }
64
64
  }
65
65
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerNext(program: Command): void;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerNext = registerNext;
4
+ const context_js_1 = require("../context.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const git_js_1 = require("../git.js");
7
+ const state_js_1 = require("../state.js");
8
+ const ticket_js_1 = require("../ticket.js");
9
+ function registerNext(program) {
10
+ program
11
+ .command("next <parent-id>")
12
+ .description("Activate the next open child of a parent ticket on the current branch (orchestrator mode)")
13
+ .option("--actor <name>", "override actor (also reads TODO_ACTOR env)")
14
+ .action((parentId, opts) => {
15
+ const ctx = (0, context_js_1.getContext)(true);
16
+ const { repoRoot } = ctx;
17
+ try {
18
+ // Resolve actor
19
+ let actor;
20
+ if (opts.actor) {
21
+ actor = opts.actor;
22
+ }
23
+ else if (process.env["TODO_ACTOR"]) {
24
+ actor = process.env["TODO_ACTOR"];
25
+ }
26
+ else {
27
+ try {
28
+ actor = (0, git_js_1.getGitUserName)(repoRoot);
29
+ }
30
+ catch {
31
+ actor = "unknown";
32
+ }
33
+ }
34
+ // Load the parent ticket to get the children list (ordered by creation)
35
+ const parent = (0, ticket_js_1.readTicket)(repoRoot, parentId);
36
+ const children = parent.relationships?.children ?? [];
37
+ if (children.length === 0) {
38
+ console.error(`Error: ticket ${parent.id} has no children.`);
39
+ process.exit(1);
40
+ }
41
+ // Find the first child that is not in a terminal state
42
+ const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
43
+ const openIds = new Set(openTickets.map((t) => t.id));
44
+ const nextId = children.find((id) => openIds.has(id));
45
+ if (!nextId) {
46
+ console.error(`All children of ${parent.id} are done. Close the parent with: todo close ${parent.id}`);
47
+ process.exit(1);
48
+ }
49
+ // Activate the child on the current branch (--skip-branch semantics)
50
+ const ticket = (0, ticket_js_1.readTicket)(repoRoot, nextId);
51
+ const currentBranch = (0, git_js_1.getCurrentBranch)(repoRoot);
52
+ const defaultBranch = (0, git_js_1.getDefaultBranch)(repoRoot);
53
+ if (ticket.state !== "active") {
54
+ const now = new Date().toISOString();
55
+ let updated;
56
+ try {
57
+ updated = (0, state_js_1.applyTransition)(ticket, "active", { actor }, repoRoot);
58
+ }
59
+ catch (err) {
60
+ console.error(`Error: ${err.message}`);
61
+ process.exit(1);
62
+ }
63
+ updated.work = {
64
+ branch: currentBranch,
65
+ base_branch: defaultBranch,
66
+ started_at: now,
67
+ started_by: actor,
68
+ };
69
+ updated.updated_at = now;
70
+ (0, ticket_js_1.writeTicket)(repoRoot, updated);
71
+ }
72
+ // Print the ticket ID on stdout — scriptable (while next=$(todo next <parent>))
73
+ process.stdout.write(`${nextId}\n`);
74
+ // Print summary on stderr so it doesn't pollute the captured value
75
+ process.stderr.write(`Activated ${nextId}: ${ticket.summary}\n`);
76
+ }
77
+ catch (err) {
78
+ (0, errors_js_1.handleError)(err);
79
+ }
80
+ });
81
+ }
@@ -11,6 +11,7 @@ function registerWork(program) {
11
11
  .command("work <id>")
12
12
  .description("Start or resume work on a ticket")
13
13
  .option("--branch <name>", "override branch name")
14
+ .option("--skip-branch", "activate ticket without any git branch operations (orchestrator mode)")
14
15
  .option("--actor <name>", "override actor (also reads TODO_ACTOR env)")
15
16
  .action((id, opts) => {
16
17
  const ctx = (0, context_js_1.getContext)(true);
@@ -50,7 +51,31 @@ function registerWork(program) {
50
51
  }
51
52
  }
52
53
  const defaultBranch = (0, git_js_1.getDefaultBranch)(repoRoot);
53
- if ((0, git_js_1.branchExists)(branch, repoRoot)) {
54
+ if (opts.skipBranch) {
55
+ // --no-branch: orchestrator mode — activate on current branch without any git ops
56
+ const currentBranch = (0, git_js_1.getCurrentBranch)(repoRoot);
57
+ if (ticket.state !== "active") {
58
+ const now = new Date().toISOString();
59
+ let updated;
60
+ try {
61
+ updated = (0, state_js_1.applyTransition)(ticket, "active", { actor }, repoRoot);
62
+ }
63
+ catch (err) {
64
+ console.error(`Error: ${err.message}`);
65
+ process.exit(1);
66
+ }
67
+ updated.work = {
68
+ branch: currentBranch,
69
+ base_branch: defaultBranch,
70
+ started_at: now,
71
+ started_by: actor,
72
+ };
73
+ updated.updated_at = now;
74
+ (0, ticket_js_1.writeTicket)(repoRoot, updated);
75
+ }
76
+ console.log(`Activated ticket ${ticket.id} on current branch ${currentBranch}.`);
77
+ }
78
+ else if ((0, git_js_1.branchExists)(branch, repoRoot)) {
54
79
  // Resume
55
80
  (0, git_js_1.checkoutBranch)(branch, repoRoot);
56
81
  // Ensure ticket is active
package/dist/git.js CHANGED
@@ -29,7 +29,11 @@ class GitError extends Error {
29
29
  exports.GitError = GitError;
30
30
  function exec(args, cwd) {
31
31
  try {
32
- return (0, node_child_process_1.execFileSync)("git", args, { encoding: "utf8", cwd }).trim();
32
+ return (0, node_child_process_1.execFileSync)("git", args, {
33
+ encoding: "utf8",
34
+ cwd,
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ }).trim();
33
37
  }
34
38
  catch (err) {
35
39
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@4-r-c-4-n-4/todo",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Git-native work tracking for coding agents",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
7
7
  "todo": "./dist/cli.js"
8
8
  },
9
9
  "engines": {
10
- "node": ">=22"
10
+ "node": ">=20"
11
11
  },
12
12
  "files": [
13
13
  "dist/",