@nijaru/tk 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,22 +1,20 @@
1
1
  # tk
2
2
 
3
- Task tracker for AI agents. Simple, fast, git-friendly.
3
+ Minimal task tracker. Simple, fast, git-friendly.
4
4
 
5
- **Requires [Bun](https://bun.sh) runtime.**
5
+ - Plain JSON files in `.tasks/`
6
+ - No daemons, no merge conflicts, no viruses
6
7
 
7
8
  ## Install
8
9
 
10
+ Requires [Bun](https://bun.sh) runtime.
11
+
9
12
  ```bash
10
13
  # Install Bun first (if not installed)
11
14
  curl -fsSL https://bun.sh/install | bash
12
15
 
13
16
  # Then install tk globally
14
17
  bun add -g @nijaru/tk
15
-
16
- # Or run from source
17
- git clone https://github.com/nijaru/tk.git
18
- cd tk && bun install
19
- bun run src/cli.ts --help
20
18
  ```
21
19
 
22
20
  ## Quick Start
@@ -35,9 +33,9 @@ myapp-x9k2
35
33
  $ tk block x9k2 a7b3 # tests blocked by auth (just use ref)
36
34
 
37
35
  $ tk ready # what can I work on?
38
- ID PRI STATUS TITLE
39
- ------------------------------------------------------------
40
- myapp-a7b3 p1 open Implement auth
36
+ ID | PRIO | STATUS | TITLE
37
+ -----------------------------------------------------------------
38
+ myapp-a7b3 | p1 | open | Implement auth
41
39
 
42
40
  $ tk start a7b3 # just the ref works everywhere
43
41
  Started: myapp-a7b3
@@ -49,9 +47,9 @@ $ tk done a7b3
49
47
  Completed: myapp-a7b3
50
48
 
51
49
  $ tk ready # tests now unblocked
52
- ID PRI STATUS TITLE
53
- ------------------------------------------------------------
54
- myapp-x9k2 p2 open Write tests
50
+ ID | PRIO | STATUS | TITLE
51
+ -----------------------------------------------------------------
52
+ myapp-x9k2 | p2 | open | Write tests
55
53
  ```
56
54
 
57
55
  ## Commands
@@ -61,17 +59,18 @@ myapp-x9k2 p2 open Write tests
61
59
  | `tk init` | Initialize (project name from directory) |
62
60
  | `tk add <title>` | Create task |
63
61
  | `tk ls` / `tk list` | List tasks |
64
- | `tk ready` | List open + unblocked tasks |
62
+ | `tk ready` | List active/open + unblocked tasks |
65
63
  | `tk show <id>` | Show task details |
66
64
  | `tk start <id>` | Start working (open → active) |
67
65
  | `tk done <id>` | Complete task |
68
66
  | `tk reopen <id>` | Reopen task |
69
67
  | `tk edit <id>` | Edit task |
70
- | `tk log <id> <msg>` | Add log entry |
68
+ | `tk log <id> "<msg>"` | Add log entry |
71
69
  | `tk block <id> <blocker>` | Add dependency (id blocked by blocker) |
72
70
  | `tk unblock <id> <blocker>` | Remove dependency |
73
71
  | `tk rm` / `tk remove <id>` | Delete task |
74
- | `tk clean` | Remove old done tasks (default: 7d) |
72
+ | `tk clean` | Remove old done tasks (default: 14 days) |
73
+ | `tk check` | Check task integrity |
75
74
  | `tk config` | Show/set configuration |
76
75
  | `tk completions <shell>` | Output shell completions (bash, zsh, fish) |
77
76
  | `tk help [command]` | Show help (or command-specific help) |
@@ -120,9 +119,9 @@ tk edit a7b3 --parent - # Clear parent
120
119
  ## Clean Options
121
120
 
122
121
  ```bash
123
- tk clean # Remove done tasks older than 7 days
124
- tk clean --older-than 30d # Custom retention (Nd/Nw/Nm/Nh)
125
- tk clean -a # Remove all done tasks (ignore age)
122
+ tk clean # Remove done tasks older than config (default: 14 days)
123
+ tk clean --older-than 30 # Custom threshold (days)
124
+ tk clean --force # Force clean even if disabled in config
126
125
  ```
127
126
 
128
127
  ## Config
@@ -131,12 +130,26 @@ tk clean -a # Remove all done tasks (ignore age)
131
130
  tk config # Show all config
132
131
  tk config project # Show default project
133
132
  tk config project api # Set default project
134
- tk config project lsmvec --rename cloudlsmvec # Rename all cloudlsmvec-* → lsmvec-*
133
+ tk config project lsmvec --rename cloudlsmvec # Rename cloudlsmvec-* → lsmvec-*
135
134
  tk config alias # List aliases
136
135
  tk config alias web src/web # Add alias
137
136
  tk config alias --rm web # Remove alias
138
137
  ```
139
138
 
139
+ Config file (`.tasks/config.json`):
140
+
141
+ ```json
142
+ {
143
+ "version": 1,
144
+ "project": "myapp",
145
+ "clean_after": 14,
146
+ "defaults": { "priority": 3, "labels": [], "assignees": [] },
147
+ "aliases": {}
148
+ }
149
+ ```
150
+
151
+ - `clean_after`: Days to keep done tasks (default: 14). Set to `false` to disable cleaning.
152
+
140
153
  ## Task IDs
141
154
 
142
155
  Format: `project-ref` (e.g., `myapp-a7b3`, `api-x9k2`, `web-3m8p`)
@@ -226,4 +239,4 @@ tk completions fish | source
226
239
 
227
240
  ## License
228
241
 
229
- MIT
242
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@nijaru/tk",
3
- "version": "0.0.1",
4
- "description": "Task tracker for AI agents",
3
+ "version": "0.0.3",
4
+ "description": "Minimal task tracker",
5
5
  "type": "module",
6
6
  "bin": {
7
- "tk": "./src/cli.ts"
7
+ "tk": "src/cli.ts"
8
8
  },
9
9
  "files": [
10
10
  "src",
@@ -37,7 +37,7 @@
37
37
  "license": "MIT",
38
38
  "repository": {
39
39
  "type": "git",
40
- "url": "https://github.com/nijaru/tk"
40
+ "url": "git+https://github.com/nijaru/tk.git"
41
41
  },
42
42
  "author": "nijaru",
43
43
  "homepage": "https://github.com/nijaru/tk#readme",
package/src/cli.test.ts CHANGED
@@ -243,16 +243,96 @@ describe("tk CLI", () => {
243
243
  const { stdout } = await run(["ls"], testDir);
244
244
  expect(stdout).toContain("No tasks found");
245
245
  });
246
+
247
+ test("sorts by status (active > open > done)", async () => {
248
+ const { stdout: idDone } = await run(["add", "Done task"], testDir);
249
+ await run(["done", idDone.trim()], testDir);
250
+ const { stdout: idActive } = await run(["add", "Active task"], testDir);
251
+ await run(["start", idActive.trim()], testDir);
252
+ await run(["add", "Open task"], testDir);
253
+
254
+ const { stdout } = await run(["ls"], testDir);
255
+ const lines = stdout
256
+ .trim()
257
+ .split("\n")
258
+ .filter((l) => l.includes("task"));
259
+ expect(lines[0]).toContain("Active task");
260
+ expect(lines[1]).toContain("Open task");
261
+ expect(lines[2]).toContain("Done task");
262
+ });
263
+
264
+ test("sorts by priority (p1-4, then p0/none)", async () => {
265
+ await run(["add", "Medium", "-p", "3"], testDir);
266
+ await run(["add", "Urgent", "-p", "1"], testDir);
267
+ await run(["add", "None", "-p", "0"], testDir);
268
+ await run(["add", "Low", "-p", "4"], testDir);
269
+
270
+ const { stdout } = await run(["ls"], testDir);
271
+ const lines = stdout
272
+ .trim()
273
+ .split("\n")
274
+ .filter((l) => /Medium|Urgent|None|Low/.test(l));
275
+ expect(lines[0]).toContain("Urgent");
276
+ expect(lines[1]).toContain("Medium");
277
+ expect(lines[2]).toContain("Low");
278
+ expect(lines[3]).toContain("None");
279
+ });
280
+
281
+ test("hoists overdue tasks to the top of their status group", async () => {
282
+ await run(["add", "Urgent Future", "-p", "1", "--due", "2099-01-01"], testDir);
283
+ await run(["add", "Low Overdue", "-p", "4", "--due", "2020-01-01"], testDir);
284
+
285
+ const { stdout } = await run(["ls"], testDir);
286
+ const lines = stdout
287
+ .trim()
288
+ .split("\n")
289
+ .filter((l) => /Future|Overdue/.test(l));
290
+ expect(lines[0]).toContain("Low Overdue");
291
+ expect(lines[1]).toContain("Urgent Future");
292
+ });
293
+
294
+ test("sorts by due date when priority is equal", async () => {
295
+ await run(["add", "Later", "-p", "3", "--due", "2099-01-02"], testDir);
296
+ await run(["add", "Earlier", "-p", "3", "--due", "2099-01-01"], testDir);
297
+
298
+ const { stdout } = await run(["ls"], testDir);
299
+ const lines = stdout
300
+ .trim()
301
+ .split("\n")
302
+ .filter((l) => /Earlier|Later/.test(l));
303
+ expect(lines[0]).toContain("Earlier");
304
+ expect(lines[1]).toContain("Later");
305
+ });
306
+
307
+ test("sorts done tasks by completion time (newest first)", async () => {
308
+ const { stdout: id1 } = await run(["add", "Done First"], testDir);
309
+ const { stdout: id2 } = await run(["add", "Done Second"], testDir);
310
+ await run(["done", id1.trim()], testDir);
311
+ // Ensure time difference
312
+ await new Promise((r) => setTimeout(r, 10));
313
+ await run(["done", id2.trim()], testDir);
314
+
315
+ const { stdout } = await run(["ls", "-s", "done"], testDir);
316
+ const lines = stdout
317
+ .trim()
318
+ .split("\n")
319
+ .filter((l) => l.includes("Done"));
320
+ expect(lines[0]).toContain("Done Second");
321
+ expect(lines[1]).toContain("Done First");
322
+ });
246
323
  });
247
324
 
248
325
  describe("ready", () => {
249
- test("shows only unblocked open tasks", async () => {
250
- const { stdout: id1 } = await run(["add", "Ready task"], testDir);
326
+ test("shows active and unblocked open tasks", async () => {
327
+ const { stdout: id1 } = await run(["add", "Active task"], testDir);
328
+ await run(["start", id1.trim()], testDir);
329
+ await run(["add", "Open task"], testDir);
251
330
  const { stdout: id2 } = await run(["add", "Blocked task"], testDir);
252
331
  await run(["block", id2.trim(), id1.trim()], testDir);
253
332
 
254
333
  const { stdout } = await run(["ready"], testDir);
255
- expect(stdout).toContain("Ready task");
334
+ expect(stdout).toContain("Active task");
335
+ expect(stdout).toContain("Open task");
256
336
  expect(stdout).not.toContain("Blocked task");
257
337
  });
258
338
 
@@ -298,13 +378,24 @@ describe("tk CLI", () => {
298
378
  expect(task.completed_at).toBeNull();
299
379
  });
300
380
 
301
- test("start errors if not open", async () => {
381
+ test("start errors if already active", async () => {
302
382
  const { stdout: id } = await run(["add", "Task"], testDir);
303
383
  await run(["start", id.trim()], testDir);
304
384
 
305
385
  const { stderr, exitCode } = await run(["start", id.trim()], testDir);
306
386
  expect(exitCode).toBe(1);
307
- expect(stderr).toContain("not open");
387
+ expect(stderr).toContain("already active");
388
+ expect(stderr).toContain("tk done"); // suggests next action
389
+ });
390
+
391
+ test("start errors if already done", async () => {
392
+ const { stdout: id } = await run(["add", "Task"], testDir);
393
+ await run(["done", id.trim()], testDir);
394
+
395
+ const { stderr, exitCode } = await run(["start", id.trim()], testDir);
396
+ expect(exitCode).toBe(1);
397
+ expect(stderr).toContain("already done");
398
+ expect(stderr).toContain("tk reopen"); // suggests next action
308
399
  });
309
400
  });
310
401
 
@@ -458,13 +549,29 @@ describe("tk CLI", () => {
458
549
  const task = JSON.parse(stdout);
459
550
  expect(task.blocked_by).not.toContain(id1.trim());
460
551
  });
552
+
553
+ test("clears parent references on child tasks", async () => {
554
+ const { stdout: parentId } = await run(["add", "Parent"], testDir);
555
+ const { stdout: childId } = await run(["add", "Child", "--parent", parentId.trim()], testDir);
556
+
557
+ // Verify child has parent
558
+ const { stdout: before } = await run(["show", childId.trim(), "--json"], testDir);
559
+ expect(JSON.parse(before).parent).toBe(parentId.trim());
560
+
561
+ // Delete parent
562
+ await run(["rm", parentId.trim()], testDir);
563
+
564
+ // Child's parent should be cleared
565
+ const { stdout: after } = await run(["show", childId.trim(), "--json"], testDir);
566
+ expect(JSON.parse(after).parent).toBeNull();
567
+ });
461
568
  });
462
569
 
463
570
  describe("clean", () => {
464
571
  test("removes completed tasks", async () => {
465
572
  const { stdout: id } = await run(["add", "Task"], testDir);
466
573
  await run(["done", id.trim()], testDir);
467
- await run(["clean", "--all"], testDir);
574
+ await run(["clean", "--force"], testDir);
468
575
 
469
576
  const { exitCode } = await run(["show", id.trim()], testDir);
470
577
  expect(exitCode).toBe(1);
@@ -472,11 +579,26 @@ describe("tk CLI", () => {
472
579
 
473
580
  test("keeps open tasks", async () => {
474
581
  const { stdout: id } = await run(["add", "Open task"], testDir);
475
- await run(["clean", "--all"], testDir);
582
+ await run(["clean", "--force"], testDir);
476
583
 
477
584
  const { exitCode } = await run(["show", id.trim()], testDir);
478
585
  expect(exitCode).toBe(0);
479
586
  });
587
+
588
+ test("--older-than respects age threshold", async () => {
589
+ const { stdout: id } = await run(["add", "Task"], testDir);
590
+ await run(["done", id.trim()], testDir);
591
+
592
+ // Task was just completed, so --older-than 1 should keep it
593
+ await run(["clean", "--older-than", "1"], testDir);
594
+ const { exitCode: stillExists } = await run(["show", id.trim()], testDir);
595
+ expect(stillExists).toBe(0);
596
+
597
+ // --older-than 0 should remove it (0 days = remove all)
598
+ await run(["clean", "--older-than", "0"], testDir);
599
+ const { exitCode: gone } = await run(["show", id.trim()], testDir);
600
+ expect(gone).toBe(1);
601
+ });
480
602
  });
481
603
 
482
604
  describe("init", () => {
@@ -547,6 +669,38 @@ describe("tk CLI", () => {
547
669
  expect(stdout).toContain("api");
548
670
  expect(stdout).toContain("packages/api");
549
671
  });
672
+
673
+ test("renames project and updates references", async () => {
674
+ await run(["init", "-P", "old"], testDir);
675
+
676
+ // Create tasks with references
677
+ const { stdout: id1 } = await run(["add", "Task 1"], testDir);
678
+ const { stdout: id2 } = await run(["add", "Task 2"], testDir);
679
+ await run(["block", id2.trim(), id1.trim()], testDir);
680
+
681
+ // Rename project
682
+ const { stdout, exitCode } = await run(
683
+ ["config", "project", "new", "--rename", "old"],
684
+ testDir,
685
+ );
686
+ expect(exitCode).toBe(0);
687
+ expect(stdout).toContain("Renamed 2 tasks");
688
+ expect(stdout).toContain("old-*");
689
+ expect(stdout).toContain("new-*");
690
+
691
+ // Verify new IDs work
692
+ const newId1 = id1.trim().replace("old-", "new-");
693
+ const { exitCode: showCode } = await run(["show", newId1], testDir);
694
+ expect(showCode).toBe(0);
695
+
696
+ // Verify references updated
697
+ const { stdout: showOut } = await run(
698
+ ["show", id2.trim().replace("old-", "new-"), "--json"],
699
+ testDir,
700
+ );
701
+ const task = JSON.parse(showOut);
702
+ expect(task.blocked_by[0]).toBe(newId1);
703
+ });
550
704
  });
551
705
 
552
706
  describe("error handling", () => {
@@ -586,7 +740,7 @@ describe("tk CLI", () => {
586
740
  test("rejects invalid ID format", async () => {
587
741
  const { stderr, exitCode } = await run(["show", "invalid"], testDir);
588
742
  expect(exitCode).toBe(1);
589
- expect(stderr).toContain("Invalid task ID");
743
+ expect(stderr).toContain("not found");
590
744
  });
591
745
  });
592
746
 
@@ -615,6 +769,22 @@ describe("tk CLI", () => {
615
769
  const tasks = JSON.parse(stdout);
616
770
  expect(Array.isArray(tasks)).toBe(true);
617
771
  });
772
+
773
+ test("log requires quoted message", async () => {
774
+ const { stdout: id } = await run(["add", "Task"], testDir);
775
+ const taskId = id.trim();
776
+
777
+ // Unquoted multiple words should error
778
+ const { stderr, exitCode } = await run(["log", taskId, "word1", "word2"], testDir);
779
+ expect(exitCode).toBe(1);
780
+ expect(stderr).toContain("must be quoted");
781
+
782
+ // Quoted message works (shell passes as single arg)
783
+ await run(["log", taskId, "Quoted message works"], testDir);
784
+ const { stdout } = await run(["show", "--json", taskId], testDir);
785
+ const task = JSON.parse(stdout);
786
+ expect(task.logs[0].msg).toBe("Quoted message works");
787
+ });
618
788
  });
619
789
 
620
790
  describe("ID resolution", () => {
@@ -632,5 +802,175 @@ describe("tk CLI", () => {
632
802
  const { exitCode } = await run(["show", id.trim()], testDir);
633
803
  expect(exitCode).toBe(0);
634
804
  });
805
+
806
+ test("ambiguous ID shows matching tasks", async () => {
807
+ // Create tasks with same ref prefix in different projects
808
+ const { stdout: id1 } = await run(["add", "Task 1", "-P", "api"], testDir);
809
+ const { stdout: id2 } = await run(["add", "Task 2", "-P", "web"], testDir);
810
+ const ref1 = id1.trim().split("-")[1] ?? "";
811
+ const ref2 = id2.trim().split("-")[1] ?? "";
812
+
813
+ // Use first char which might match both
814
+ const prefix = ref1[0] ?? "";
815
+
816
+ // Only test if both refs start with same char (otherwise not ambiguous)
817
+ if (ref2.startsWith(prefix)) {
818
+ const { stderr, exitCode } = await run(["show", prefix], testDir);
819
+ expect(exitCode).toBe(1);
820
+ expect(stderr).toContain("Ambiguous");
821
+ expect(stderr).toContain("matches");
822
+ }
823
+ });
824
+
825
+ test("not found shows clear error", async () => {
826
+ const { stderr, exitCode } = await run(["show", "zzzz"], testDir);
827
+ expect(exitCode).toBe(1);
828
+ expect(stderr).toContain("not found");
829
+ });
830
+ });
831
+
832
+ describe("tk check", () => {
833
+ test("reports all OK when no issues", async () => {
834
+ await run(["add", "Task 1"], testDir);
835
+ await run(["add", "Task 2"], testDir);
836
+
837
+ const { stdout, exitCode } = await run(["check"], testDir);
838
+ expect(exitCode).toBe(0);
839
+ expect(stdout).toContain("2 tasks OK");
840
+ });
841
+
842
+ test("auto-fixes orphaned blocker reference", async () => {
843
+ // Create two tasks and block one by the other
844
+ const { stdout: id1 } = await run(["add", "Blocker"], testDir);
845
+ const { stdout: id2 } = await run(["add", "Blocked"], testDir);
846
+ const blockerId = id1.trim();
847
+ const blockedId = id2.trim();
848
+
849
+ await run(["block", blockedId, blockerId], testDir);
850
+
851
+ // Manually delete blocker file to simulate merge
852
+ const fs = await import("fs");
853
+ const path = await import("path");
854
+ const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
855
+ fs.unlinkSync(blockerFile);
856
+
857
+ // Check should fix it
858
+ const { stdout } = await run(["check"], testDir);
859
+ expect(stdout).toContain("cleaned");
860
+ expect(stdout).toContain("orphaned");
861
+
862
+ // Verify the blocked_by is now empty
863
+ const { stdout: showOut } = await run(["show", "--json", blockedId], testDir);
864
+ const task = JSON.parse(showOut);
865
+ expect(task.blocked_by).toEqual([]);
866
+ });
867
+
868
+ test("auto-fixes orphaned parent reference", async () => {
869
+ // Create parent and child
870
+ const { stdout: parentId } = await run(["add", "Parent"], testDir);
871
+ const parent = parentId.trim();
872
+ const { stdout: childId } = await run(["add", "Child", "--parent", parent], testDir);
873
+ const child = childId.trim();
874
+
875
+ // Manually delete parent file
876
+ const fs = await import("fs");
877
+ const path = await import("path");
878
+ const parentFile = path.join(testDir, ".tasks", `${parent}.json`);
879
+ fs.unlinkSync(parentFile);
880
+
881
+ // Check should fix it
882
+ const { stdout } = await run(["check"], testDir);
883
+ expect(stdout).toContain("cleaned");
884
+ expect(stdout).toContain("orphaned parent");
885
+
886
+ // Verify parent is now null
887
+ const { stdout: showOut } = await run(["show", "--json", child], testDir);
888
+ const task = JSON.parse(showOut);
889
+ expect(task.parent).toBeNull();
890
+ });
891
+
892
+ test("auto-fix happens on show command", async () => {
893
+ // Create two tasks and block one
894
+ const { stdout: id1 } = await run(["add", "Blocker"], testDir);
895
+ const { stdout: id2 } = await run(["add", "Blocked"], testDir);
896
+ const blockerId = id1.trim();
897
+ const blockedId = id2.trim();
898
+
899
+ await run(["block", blockedId, blockerId], testDir);
900
+
901
+ // Manually delete blocker file
902
+ const fs = await import("fs");
903
+ const path = await import("path");
904
+ const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
905
+ fs.unlinkSync(blockerFile);
906
+
907
+ // Show should auto-fix and output cleanup message
908
+ const { stderr } = await run(["show", blockedId], testDir);
909
+ expect(stderr).toContain("cleaned");
910
+ expect(stderr).toContain("orphaned");
911
+ });
912
+
913
+ test("reports unfixable corrupted JSON", async () => {
914
+ await run(["add", "Good task"], testDir);
915
+
916
+ // Create a corrupted JSON file
917
+ const fs = await import("fs");
918
+ const path = await import("path");
919
+ const corruptFile = path.join(testDir, ".tasks", "test-bad1.json");
920
+ fs.writeFileSync(corruptFile, "{ invalid json");
921
+
922
+ const { stdout } = await run(["check"], testDir);
923
+ expect(stdout).toContain("Unfixable");
924
+ expect(stdout).toContain("test-bad1.json");
925
+ });
926
+
927
+ test("reports unfixable invalid task structure", async () => {
928
+ await run(["add", "Good task"], testDir);
929
+
930
+ // Create valid JSON but invalid task structure
931
+ const fs = await import("fs");
932
+ const path = await import("path");
933
+ const badFile = path.join(testDir, ".tasks", "test-bad2.json");
934
+ fs.writeFileSync(badFile, '{"foo": "bar"}');
935
+
936
+ const { stdout } = await run(["check"], testDir);
937
+ expect(stdout).toContain("Unfixable");
938
+ expect(stdout).toContain("test-bad2.json");
939
+ expect(stdout).toContain("Invalid task structure");
940
+ });
941
+
942
+ test("auto-fixes ID mismatch (filename vs content)", async () => {
943
+ // Create a task
944
+ const { stdout: id } = await run(["add", "Test task", "-P", "api"], testDir);
945
+ const taskId = id.trim();
946
+
947
+ // Manually rename the file to create a mismatch
948
+ const fs = await import("fs");
949
+ const path = await import("path");
950
+ const oldPath = path.join(testDir, ".tasks", `${taskId}.json`);
951
+ const newPath = path.join(testDir, ".tasks", "web-x1y2.json");
952
+ fs.renameSync(oldPath, newPath);
953
+
954
+ // Check should fix it
955
+ const { stdout } = await run(["check"], testDir);
956
+ expect(stdout).toContain("cleaned");
957
+ expect(stdout).toContain("ID mismatch");
958
+
959
+ // Verify the content was updated to match filename
960
+ const { stdout: showOut } = await run(["show", "--json", "web-x1y2"], testDir);
961
+ const task = JSON.parse(showOut);
962
+ expect(task.project).toBe("web");
963
+ expect(task.ref).toBe("x1y2");
964
+ });
965
+
966
+ test("check --json returns structured output", async () => {
967
+ await run(["add", "Task"], testDir);
968
+
969
+ const { stdout } = await run(["check", "--json"], testDir);
970
+ const result = JSON.parse(stdout);
971
+ expect(result).toHaveProperty("totalTasks");
972
+ expect(result).toHaveProperty("cleaned");
973
+ expect(result).toHaveProperty("unfixable");
974
+ });
635
975
  });
636
976
  });