qqq-cli 0.6.0

Local-first task queue for coding agent sessions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
# qqq reference

[← README](../README.md)

Detailed behavior and flags for qqq. For a first run, start with the
[quick start](../README.md#try-it).

- [Install from source]#install-from-source
- [List and search]#list-and-show · [Add and edit]#add-and-edit
- [Archive]#archive-and-unarchive · [Reopen]#reopen-work
- [Editor]#built-in-editor · [TUI]#task-tui
- [Dependencies and images]#dependencies-and-images
- [Ownership and recovery]#agents-and-recovery
- [Config and aliases]#config-and-aliases · [Herdr]#herdr
- [JSON error contract]#json-error-contract
- [Backup, restore, deletion, health]#data
- [Development]#development · [Publishing]#publish-to-cratesio

## Install from source

Requires Rust 1.85+ and a C compiler. This guide follows the development branch;
published releases may lag features described here.

```sh
git clone https://github.com/hktonylee/qqq-cli.git
cd qqq-cli
cargo install --path . --locked --force
```

Task commands use the nearest `.qqq` directory in the current directory or its
parents. A missing `qqq.db` there causes an error. `qqq config` needs no project DB.

## List and show

```sh
qqq list
qqq list --oneline
qqq show 1
qqq show -1                         # newest task; -2 is second newest
qqq list --max-completed 10
qqq list --all                      # bypass configured completion limit
qqq list --query "parser error"     # search full descriptions, ignoring case
qqq list --status new --status error
qqq list --query "parser" --status in_progress --all
qqq list --filter 'like(task_name, "%auth%") and priority > 0'
qqq list --tag frontend --tag "UI review"
qqq list --watch
qqq list --watch --json
qqq list --include-archived
```

`list` shows status, priority, and dependency tree; roots and siblings follow
creation ID order. In a terminal with known width, it shows full descriptions
with wrapped, aligned continuation lines; piped output keeps first-line previews.
`show` adds messages, images, ownership history and Herdr link. Negative indexes
work with `show`, `edit`, `archive`, `unarchive`, and `reopen`; `-1` selects newest task.

`--oneline` limits terminal and watch views to each description's first line.
JSON and search still use full descriptions.

`--max-completed N` keeps N most recent completions plus unfinished tasks;
`0` hides completed tasks. Hidden parents display children as roots. `--all`
bypasses configured limit; it conflicts with `--max-completed`.

`--query TEXT` matches any part of a full description, including later lines,
using Unicode lowercase comparison. Repeat `--status` to match any listed
status: `new`, `in_progress`, `completed`, or `error`. Query and statuses combine
with AND. Search runs after completed-display limit; use `--all` to search all
completed tasks. Visible ancestors of matches appear as `[context]` rows even
when they do not match filters. JSON stays flat and marks those ancestors with
`context_only: true`; direct matches keep existing task fields. Empty filtered
results return `[]` in JSON or `No matching tasks.` in human output.

`list --filter EXPR` and `next --filter EXPR` compile a typed Luau expression
to SQLite with bound values. List filters combine with query/status and retain
visible ancestor context. See [filter guide](filter.md),
[default variables](filter-variables.md),
[SQLite functions](filter-functions.md).

`list --tag LABEL` and `next --tag LABEL` match whole stored labels with exact
case/Unicode. Repeat to require every label; tags combine with other filters
using AND. Boundary whitespace is trimmed. Invalid labels fail before project
preflight or DB access. `has_tag("LABEL")` offers the same membership test in
Luau expressions; exactly one valid string literal is required. See
[tag selectors and named views](filter.md#list-behavior).

`--watch` prints initial list, then refreshes after DB commits. Terminal output
redraws; pipes, `TERM=dumb` and JSON append snapshots. Ctrl-C stops watching.

Default list, watch, TUI, and claim queue hide archived tasks. Use
`list --include-archived`, `list --watch --include-archived`, or
`tui --include-archived` to browse them. Human lists label archived rows
`[archived]`; JSON includes `archived: true` without changing description.
`--all` changes completed-task limit only; hidden archived completions do not
consume that limit.

Global `--json` and `--human` work before or after commands; they conflict.
Without either flag, recognized agent callers receive JSON: a nonblank UTF-8
`CODEX_THREAD_ID` or `CODEX_SESSION_ID`, or an exact current Herdr pane reporting
an agent name/session. Detection does not discover other agents at project cwd
or infer agents from pipes, `QQQ_SESSION`, or harness overrides. Missing Herdr,
failed lookup, and panes without agent metadata keep readable output. Explicit
output flags supplied directly skip optional caller lookup. Use `--human` to override automatic
JSON. Help/version remain text; TUI/editor rendering remains interactive.

JSON lists stay flat, preserve
full descriptions and `parent_id`; watch prints one array per line. Empty lists
return `[]`; no ready task returns `null`. Errors use stderr (exit 1 for runtime,
2 for arguments). JSON failures follow [error contract](#json-error-contract).
`NO_COLOR=1` or `TERM=dumb` disables color.

## JSON error contract

Failed commands in JSON mode (global `--json` or recognized agent default)
write one JSON object plus newline to
stderr. Error prose and terminal UI never appear on stdout. Runtime failures
exit 1; argument-parser failures exit 2. Existing success payloads stay unchanged.

```sh
qqq --json show 999 >result.json 2>error.json
```

```json
{"code":"TASK_NOT_FOUND","message":"Task 999 not found","details":{"task_id":999,"command":"show"}}
```

`code` is stable machine classification; branch on code and details.
`message` is readable diagnostic text; wording can change.
`details` is always an object; fields depend on failure and can grow over time.
Runtime errors include `command` when known. Omitted fields mean unavailable;
explicit `null` indicates absent current content or optional recovery file.
Parser and alias failures can occur before command resolution.

| Code | Meaning | Relevant details |
| --- | --- | --- |
| `TASK_NOT_FOUND` | Task ID or creation-order reference unavailable | `task_id` or `task_reference` |
| `OWNERSHIP_MISMATCH` | Active task belongs to different owner | `task_id`, `actual_status`, `expected_statuses` |
| `INVALID_TRANSITION` | State or dependency blocks operation | `task_id`, `actual_status`, `expected_statuses`; archive guards add `actual_archived`, `expected_archived`; dependency guards add `reason`, `blocking_child_id` |
| `INVALID_ARGUMENT` | Flag/value, alias or owner-resolution input invalid | `argument`, `reason`; alias errors add `alias` |
| `INVALID_FILTER` | Luau filter invalid or failed during evaluation | `argument: "--filter"`; runtime failure adds `reason: "evaluation_failed"` |
| `DB_BUSY` | SQLite busy/locked; retry after competing transaction finishes | `sqlite_code` (5 or 6), `sqlite_extended_code` |
| `CONTENT_CONFLICT` | Loaded content changed or task removed during edit | `task_id`, `expected_revision`, `current_revision`, `reason` (`revision_changed` or `task_removed`) |
| `PROJECT_NOT_FOUND` | Project DB unavailable | `command` when resolved |
| `CONFIG_ERROR` | Config cannot load or requested config operation failed | `path` |
| `IO_ERROR` | Filesystem or input/output operation failed | `io_kind`, `os_code` when available; explicit guards may add `path`, `task_id`, `reason` |
| `EDITOR_ERROR` | EDITOR missing, failed to start or exited unsuccessfully | `exit_code` when available |
| `DISPATCH_ERROR` | Herdr lookup, transport or dispatch failed | `reason`, `exit_code` when available; dispatch failures add `task_id`, `actual_status`, `delivery_possible` |
| `DATABASE_ERROR` | Other SQLite or schema failure | `sqlite_code`, `sqlite_extended_code`, or `schema_version`, `expected_schema_version`, `reason` |
| `COMMAND_ERROR` | Failure without narrower public classification | `command`; explicit guards may add `reason` |

Ownership details omit claim keys and owner identity values. Error envelopes
omit full task text, raw Herdr/editor output and arbitrary SQLite trigger text.
Human mode keeps underlying diagnostic context. Existing successful task and
diagnostic payloads keep their own identity/content fields.

Stale-save example:

```json
{"code":"CONTENT_CONFLICT","message":"Content conflict for task 1: expected revision 1, current revision 2; save rejected","details":{"command":"edit","task_id":1,"expected_revision":1,"current_revision":2,"reason":"revision_changed"}}
```

Reload current task, review changes, then explicitly retry with reviewed revision
via `edit --expected-revision N`. `task_removed` has `current_revision: null`;
do not recreate task automatically. Missing task before editor opens remains
`TASK_NOT_FOUND`. `DB_BUSY` permits retry; other codes need caller-specific recovery.

External editor save failures can add `details.recovery` with `local_draft`,
`current_text` and `attachments_manifest` paths. Draft and pending image bytes
remain available for recovery. Nonterminal JSON editor calls suppress editor
terminal output and recovery prose so whole stderr stays parseable.

Help/version still print plain help/version to stdout with exit 0, including
with `--json`. Empty `next` returns successful `null`. Unhealthy `doctor --json`
keeps diagnostic report on stdout with exit 1 and no error envelope.
JSON watch can emit successful snapshots before later failure.

Explicit interactive terminal sessions keep UI/prompts on stderr, followed by
one final JSON error line after terminal restoration if command fails. For
whole-stderr JSON parsing, supply inline fields or nonterminal stdin.
Literal `--json` after `--` or consumed as option value does not enable JSON;
fully expanded aliases determine output mode.

## Add and edit

```sh
qqq add "Fix login"
qqq add "Fix urgent login" --priority 8
qqq add "Fix layout" --tag frontend --tag bug
qqq add --description "Fix login"   # same input, alternative flag
qqq edit 1 --description "Updated details"
qqq edit 1 --priority -5             # lower future claim order
qqq edit 1 --set-tags "frontend, bug" # replace tags; skips editor
qqq edit 1 --set-tags ""              # clear tags
qqq edit -1                         # edit newest task interactively
```

Tags are ordered metadata, displayed before task names as `[frontend] [bug]`.
Repeated `--tag` flags accept one label each; `--set-tags` accepts a comma-separated
replacement list and conflicts with `--edit`. Surrounding spaces are trimmed;
exact duplicates are removed while case/order remain. Labels support Unicode
and internal spaces; empty labels, control characters, commas and square
brackets are rejected. Blank replacement input clears tags. JSON always includes
`tags: []` or a string array and keeps raw description unchanged. Metadata edits
preserve content revision, attachments, status and ownership, including on
completed/archived tasks. Combined edits commit atomically. List `--query` and
TUI filtering continue matching descriptions.

Descriptions preserve whitespace and newlines; blank-only text fails.
Priority defaults to `0` and accepts integers from `-100` through `100`.
Higher priority claims first among ready new tasks; equal priority uses oldest
ID. `list` stays in ID/dependency order. Editing priority on active, completed,
or error tasks keeps status and ownership metadata; an already-owned task still
returns to its owner before new claims.

Task JSON exposes positive `content_revision`. Description or image changes
advance it; messages, status, ownership, priority and dependency changes do not.
Use loaded revision to reject stale direct edits:

```sh
qqq show 1 --json                    # task.content_revision
qqq edit 1 --description "Updated details" --expected-revision 3
```

A stale revision rejects the whole edit, including supplied metadata and images.
Direct edits without `--expected-revision` retain unconditional-save behavior.
An identical description save leaves content revision unchanged.

Read a full description from stdin without opening an editor:

```sh
qqq add --stdin < description.txt
printf '%s\n' 'Fix login' 'Check Unicode: λ' | qqq add --stdin --priority 8
qqq add --stdin --parent 12 --image screenshot.png < description.txt
```

`--stdin` reads UTF-8 to EOF and preserves whitespace, line endings and literal
shell-looking text. Invalid UTF-8 and blank-only descriptions fail. It conflicts
with positional text, `--description` and `--edit`; parent, prerequisite, priority and image
options keep their usual direct-add behavior. Existing add/editor modes stay
available without `--stdin`.

## Atomic batch import

```sh
qqq import plan.json --dry-run --json
qqq import plan.json
cat plan.json | qqq import - --json
```

Version-1 batch schema:

```json
{
  "version": 1,
  "tasks": [
    {
      "key": "tests",
      "description": "Run regression checks\nInclude Unicode: λ",
      "priority": 5,
      "parent": {"key": "feature"}
    },
    {
      "key": "feature",
      "description": "Build feature",
      "priority": 10
    }
  ]
}
```

Each task requires a nonblank unique `key` and nonblank `description`. Keys and
descriptions retain exact whitespace; references match exact keys. Priority
defaults to 0 and accepts integer -100 through 100. Omitted/null parent means
no tree parent. Omitted `tags` defaults to `[]`; supplied tag labels use the same normalization
and validation as add. Import preview/results include normalized tags.
Omitted `depends_on` defaults to `[]`. Each prerequisite uses
the same reference shape as parent; for example `"depends_on": [{"key":"api"},
{"id":12}]`. `{"key":"feature"}` references a task in the same batch,
including one listed later. `{"id":12}` references an existing positive DB task
ID. Reference objects require exactly one reference type. Existing archived
unfinished parents/prerequisites are rejected; archived completed references are allowed, as
with ordinary add.

Missing refs, cycles across parent and prerequisite links, duplicate prerequisites,
parent/prerequisite overlap, duplicate/blank keys, invalid priorities/descriptions,
unknown or duplicate fields, malformed JSON and unsupported versions fail.
An empty `tasks` array is valid and imports nothing. Version 1 has no image
attachment fields; use single-task add's image options for attachments.

Import validates the whole batch before insertion, then commits all tasks in
one transaction. Failed validation or writes leave no partial imported tasks,
messages, events, attachments or consumed IDs. Existing project schema migration
and pending deletion recovery still follow normal command behavior before the
import transaction. Imported tasks start new and unarchived, with ordinary
timestamps/content revisions and no assignment metadata.

Parents and batch-local prerequisites are inserted before their dependents. At each step, the lowest original
input index among available tasks is chosen. This keeps order deterministic for
forward refs and independent tasks. Other writers cannot interleave ID allocation
inside a batch or change an existing dependency's state before its commit.

`--dry-run` validates schema, local links and existing dependencies through a read-only
DB snapshot. It does not create a project, migrate schema, run deletion recovery,
write tasks or allocate/reserve IDs. Older supported DB schemas need a normal
command such as `qqq list` first. Parent state can change after preview; real
import validates it again within its write transaction.

Human output summarizes imported/validated count and keys with IDs or preview
details. JSON returns:

| Field | Meaning |
| --- | --- |
| `version` | Result schema version, currently 1 |
| `dry_run` | Whether this is a validation preview |
| `count` | Number of tasks in the batch |
| `mapping` | Key to committed task ID, in lexicographic key order; empty object for dry-run |
| `creation_order` | Keys in deterministic insertion order |
| `tasks` | Normalized entries in original input order |

Each result entry has `key`, `description`, defaulted `priority`, original
`parent` reference (or null), `depends_on` references (default `[]`), `id`,
resolved `parent_id` and `prerequisite_ids` in source reference order. Dry-run task IDs
are null; existing dependency IDs are known, batch-local dependency IDs remain null. After
commit, allocated task IDs and every resolved dependency ID are returned. For the
example above in an empty project, mapping is `{"feature":1,"tests":2}` and
creation order is `["feature","tests"]`.

## Task recipes

Store reusable workflows in `<project-root>/.qqq-recipes/NAME.json`. Recipe files
are ordinary project files suitable for Git, separate from ignored `.qqq/`
runtime state. Discovery uses nearest `.qqq` project, including from subdirs;
it never searches global folders, uses cwd shadow files or falls back to an outer
project's recipes. Project must already be initialized. Names contain ASCII
letters/digits/underscores/hyphens; pass name without `.json` or path separators.

Copy bundled [bug](../examples/recipes/bug.json) and
[release](../examples/recipes/release.json) examples from qqq source checkout into
your project's `.qqq-recipes/`, or create files using format below.

<!-- recipe-cli-examples -->
```sh
qqq add --template bug --var component=auth --dry-run --json
qqq add --template bug --var component=auth --json
qqq add --template release --var version=0.5.1 --dry-run --json
qqq add --template release --var version=0.5.1 --json
```
<!-- /recipe-cli-examples -->

Each application creates new tasks. Bug example creates one tagged task with
whole description and acceptance criteria. Release example creates `prepare`,
`tests`, `publish`: tests has prepare as parent; publish has prepare as parent
and tests as extra prerequisite. Recipes create queue records; applying release
recipe does not execute tests or publish software.

Version-1 recipe format:

```json
{
  "version": 1,
  "parameters": [
    {"name": "component"},
    {"name": "severity", "default": "normal"}
  ],
  "tasks": [
    {
      "key": "bug",
      "description": "Fix ${component} bug\n\nAcceptance criteria\n- Add regression test\nSeverity: ${severity}",
      "tags": ["bug", "${component}"],
      "priority": 5
    }
  ]
}
```

`version` and `tasks` are required. `parameters` defaults to `[]`. Parameter
names follow ASCII identifier syntax: letter/underscore first, then
letters/digits/underscores. Omitted `default` makes parameter required; supplied
default must be string. Names are case-sensitive. Every required declaration
must receive value, including declarations unused by task text.

Task fields, defaults, parent/prerequisite reference forms, tag validation and
ordering use [atomic import schema](#atomic-batch-import). Acceptance criteria
belong in whole description. Local refs such as `{"key":"prepare"}` resolve to
new IDs from same application; explicitly declared existing refs such as
`"depends_on":[{"id":12}]` retain ID12 and must exist in target project. No image
attachment fields in version1. Empty task arrays are valid no-ops.

Repeat `--var NAME=VALUE`; split occurs at first equals sign. Quote assignments
when values contain spaces or shell metacharacters, for example
`--var 'component=auth = account login'`. Values preserve whitespace, additional
equals signs, quotes, Unicode and newlines. Empty value counts as provided;
expanded descriptions/tags still must pass ordinary task validation.

`${name}` substitutes only descriptions and individual tags. Keys, integer
priorities and reference objects remain literal. Values/defaults are inserted
once without further expansion: value containing `${other}` stays literal text.
`$$` emits `$`; `$${name}` emits literal `${name}`. Other dollar forms such as
`$HOME` or `$(...)` stay literal. Loading recipes never reads environment values,
executes shell commands or evaluates code. Unknown/malformed/unclosed placeholders
fail. JSON itself must use normal JSON string escaping (`\n`, `\"`, `\\`).

Required/duplicate/unknown assignments, duplicate declarations/JSON fields/task
keys, invalid fields/versions, missing refs and graph cycles fail before any task
creation. Expansion happens on parsed strings before shared import validation;
it cannot inject JSON fields. Recipe fields own task data, so `--template`
conflicts with positional text, `--description`, `--stdin`, `--edit`, `--parent`,
`--depends-on`, explicit `--priority`, `--image` and `--tag`. Add's `--var` and
`--dry-run` require `--template`; ordinary add/import behavior stays unchanged.

`--dry-run` validates expanded descriptions, normalized tags and graph against
current read-only DB snapshot. No new tasks, claims, IDs, migration or deletion
recovery. Command-start owner preflight remains independent and may fail a
confirmed dead owner, as with other inspection commands. Real application
rechecks existing references and inserts whole graph in one import transaction;
validation/insertion failures leave batch absent and consume no IDs. Concurrent
applications retain separate complete mappings.

JSON uses import report fields `version`, `dry_run`, `count`, `mapping`,
`creation_order`, `tasks`. Committed mapping is recipe key -> new task ID.
Preview mapping is `{}`; new IDs remain null while original reference objects
and creation_order show graph without reserving IDs. Expanded descriptions/tags
are exact JSON data. Human preview adds full descriptions/tags to graph summary;
terminal controls are escaped. Native agent callers retain automatic JSON,
ordinary shells human output, with existing `--json`/`--human` overrides.

## Archive and unarchive

```sh
qqq archive 12
qqq list --include-archived
qqq show 12
qqq unarchive 12
```

Archive keeps task status, description, priority, messages, images, ownership
history, and dependencies. `show`, `edit`, messages, and image export still work
by ID. Archive/unarchive add history events; repeated commands change nothing.
Events record supplied `--session` or `cli` when absent. In-progress tasks
cannot be archived. An unfinished parent or prerequisite cannot be archived
while a visible unfinished task depends on it. Adding links or unarchiving
unfinished dependents under archived unfinished dependencies also fails.
Archived completed dependencies still release tasks.

## Reopen work

```sh
qqq reopen 12
qqq reopen -1 --session reviewer
```

`reopen` returns a completed task to `new` and records a reopen event. An
in-progress task can also reopen when its owning Herdr agent is absent from a
successful lookup on its saved server. Exact session identity or same terminal
with same agent kind counts as live, including moved panes and changed session
reports. Missing links or saved servers, mismatched claim associations, failed commands and
invalid responses cannot establish absence; task stays unchanged. Legacy links
remain usable because fresh claims replace saved links. New links privately
record current claim association. CLI and TUI Reopen use same checks.

Reopening keeps
description, priority, parent, prerequisites, messages, images, creation time, and prior
history. Reopening clears recorded assignment metadata; saved Herdr link stays
until next claim replaces it. Preflight-generated errors also reopen; ordinary
manual errors use retry. New tasks fail without changes; repeating
`reopen` also fails. Archived tasks require `unarchive` first. A
task with an archived unfinished parent or prerequisite cannot reopen until
dependency is unarchived or completed, or its link removed. Completed descendants
stay completed; new dependents wait for reopened prerequisite to complete again. Reopened tasks return to default
lists, including `--max-completed 0`, and become claimable when dependencies
permit.

## Built-in editor

On a terminal, `add` without text or `edit` without update flags opens built-in
editor. Ctrl-S saves, Esc exits blank draft or confirms discard of nonempty
draft, Ctrl-C exits, Ctrl-W deletes previous word on same line, Ctrl-V pastes
clipboard text or image. Option+Left/Right moves cursor by word. Clipboard paste
needs desktop clipboard support.

Built-in editors collapse pastes over 1,000 characters into one gold
`[Pasted Content N chars]` item. Saving stores full payload in Markdown:

````markdown
```pasteboard
pasted text
```
````

Shift-Up/Down and `qqq edit` restore complete fences as one editable item;
Backspace or Delete removes entire item. Payload containing backticks gets a
longer fence. `NO_COLOR=1` and `TERM=dumb` keep label plain. Existing task text
outside complete `pasteboard` fences stays visible and editable character by
character, regardless of length. Other code fences remain ordinary text. Loading
or saving unchanged text preserves original bytes.

In built-in `qqq add`, Ctrl-S creates or updates task, clears editor, then waits
for next task. Saves commit immediately; exit keeps prior saves. Shift+Up loads
newest task, then older tasks; Shift+Down moves toward newer tasks, then blank
draft. Loaded task places cursor at description end. Header shows selected ID
and status. JSON output returns saved tasks as array on exit. Exit without any
save returns error.

Switching from changed draft asks before discard; N, Enter, or Esc keeps it.
Navigation includes all unarchived tasks, regardless of status; deleted IDs are
skipped. `--parent`, `--depends-on` and `--priority` apply to each new task. `--image` files attach
only to first successful save, including when updating existing task. Inline add,
external editor and `qqq edit` save once.

Use `--edit` (`-e`) to force `$EDITOR`. Without terminal input and stderr,
interactive add/edit uses `$EDITOR` automatically. Editor command must wait
until editing finishes.

```sh
export EDITOR='vim'
qqq add --edit
qqq edit 1 --edit --description "Prefilled draft"
```

Built-in add/edit editor uses stderr; stdout holds result on exit. Cancelling
draft keeps earlier saves. Direct edits preserve omitted fields, ownership and
attachments.

External edits automatically guard the revision loaded before `$EDITOR` opens.
On conflict, qqq shows paths to complete local text and current DB text, plus
current revision. Pending image bytes are copied beside local draft; an
`attachments.json` file lists their names and recovery paths. Newer DB content
and stored images stay intact.

With terminal input, `r` reloads DB text after discard confirmation, `o` saves
local text after overwrite confirmation, and `k` keeps recovery files and exits
with failure. Reload discards pending local images. Overwrite checks the displayed
revision again; another concurrent edit causes another conflict. Without terminal
input, qqq exits with failure and keeps recovery files. Review local/current
files, then retry with `--description` and `--expected-revision` for the current
revision; use `--image` with recovered attachment paths if needed. A removed
task keeps local draft and is never recreated by save.

Built-in edits and TUI saves guard loaded content revision automatically. A
conflict view opens instead of saving. `Tab` switches between complete current
DB text and local draft; arrows, Page-Up/Down and Home/End scroll text. Line breaks
and whitespace remain visible. `Esc` or Ctrl-C closes view while keeping draft,
cursor, collapsed paste blocks and pending image bytes.

`r` offers reload, then `y` confirms discarding local draft; `o` offers overwrite,
then `y` confirms saving local draft against displayed DB revision. `n`, Enter
or Esc cancels confirmation. Further content changes reject overwrite again.
Reload reads latest complete content. A removed task offers local text and keep
only. Retained dirty buffers keep their original loaded revision when switching
tasks; saving or reloading one leaves other drafts intact. Message/status updates
do not cause content conflicts.

## Task TUI

```sh
qqq tui
```

Below 60 columns, compact layout hides task status labels and details/messages.
Task list and editor each use half terminal height, rounded to rows.
From 60 through 149 columns, task list, details/messages and editor stay visible at 35%,
20% and 45% of terminal height, rounded to rows. At 150 columns or wider, list
and details share upper rows: list uses 60% of width, details 40%. Editor spans
full width below them, retaining its height and position between stacked and wide layouts.
Task list shows unarchived tasks by default.
Visible details pane shows `Select task to view details.` for blank drafts; selecting or
saving task keeps pane positions fixed. Small terminals retain usable list and
editor rows.
Details use a double-line box with two spaces inside each side; short panes
retain padding without borders. Text wraps to the inner width.
Details share `show` header and layout: `#ID · Status`, Description, Messages,
Details, Assignment, Images, History and Herdr sections. Fields align their
values; Created/Updated occupy separate rows. Messages follow creation order,
with indented headers and bodies; empty collections show `None`. IDs are bold,
statuses colored, labels/timestamps dim. Styles survive wrapping and scroll.
Each task preview shows at most three wrapped lines; clipped previews end with
`...`. Editor loads full description, including hidden lines.
Selection fills whole row with muted blue background, including wrapped lines;
light text stays readable. Task rows have one leading space. Editor header shows
selected task ID in color and plain modes.
Shift-Up/Down selects tasks in displayed tree order, then new-task draft.
Switching retains unsaved drafts in memory for this TUI session. Returning restores
text, image/paste items, caret and manual editor scroll. Dirty tasks show gold
`[*]` only before the first description preview line (text marker in plain mode).
ID/status columns, wrapping and continuation rows stay fixed when marker toggles;
first-line preview clips at pane edge. Dirty new/child drafts show `[*]` in title.
Reverting edits clears marker. Task list/filter still use saved DB descriptions.
Ctrl-S saves only selected draft; failed save keeps draft and marker. By default, saved task stays open for
further edits or task actions. Freshly loaded and saved tasks place cursor at description
end; editor scrolls to keep cursor visible. Set `tui.after_save_new` to `open_new`
to clear editor after creating task; existing-task edits stay open. Shift-Down past last
displayed task restores general new draft, or opens blank draft when none exists. Esc closes focused filter and clears query,
returning focus to editor while preserving draft. From editor, Esc clears editor
before exiting on next press. Selected tasks and dirty new drafts ask before
discarding unsaved content; `y` clears editor, `n` keeps draft for editing or Ctrl-S save.
Ctrl-C returns selected task to blank draft, asking before discarding edits.
Ctrl-C in dirty new draft asks before exit. After active new draft is approved,
Ctrl-C or Esc exit prompts for every retained dirty draft, including hidden,
archived or deleted tasks and child drafts. Prompts identify draft and preview it;
`y` advances, `n`, Enter or Esc cancels whole exit and preserves all drafts,
including earlier approvals. App exits after every draft is approved. Blank draft
exits immediately only when no retained dirty drafts remain.
Cancelling a deleted task's discard prompt reopens its retained local draft;
Ctrl-S explains removal and keeps draft without recreating task.
Repeated Ctrl-C keeps confirmation open. Ctrl-V or terminal paste inserts text; pasting
image file path attaches image. Saves commit immediately. TUI needs terminal
and writes no stdout, including with `--json`.

Ctrl+P opens or restores child draft under selected task; header shows parent ID.
Current edits stay retained. Each parent has separate child draft; return to parent
and press Ctrl+P to continue it. Ctrl-S creates child with dependency;
Enter or Shift+Enter inserts newline. Navigating away retains child parent context.

Ctrl+L opens tags for the selected task or new draft from editor or filter. Current
labels are prefilled, one per line. Enter or Shift+Enter adds a line; Ctrl-S applies.
Up/Down move caret between tag rows, preserving preferred display column across
short rows. Left/Right move between characters; Home/End jump within current row.
Ctrl-A/Ctrl-E move to line start/end; Alt-Left/Alt-Right move by word. Ctrl-W deletes
the previous word without crossing a line. Typing, terminal paste, Ctrl-V text
paste, Backspace/Delete and Enter edit at caret. Active row and
caret stay visible when tags exceed popup height or width.
Blank lines are ignored; blank input clears tags. Newline/CRLF paste and legacy
comma-separated input are accepted. Ctrl-U deletes the current line while keeping
other labels; delete each line to clear all tags. Esc/Ctrl-C cancels.
Shortcuts share the popup footer; narrow popups use `^S` for Ctrl-S, `^U` for
Ctrl-U and `↵` for Enter. Active input row stays visible in short terminals.
List tags use yellow when color is enabled. Invalid labels stay in the popup. Tag saves
retain unsaved description drafts, caret, scroll, parked buffers and filter
focus. On a new general or child draft, applying tags only changes the draft;
no task enters the DB or queue until Ctrl-S creates it with description, parent,
images and tags in one transaction. Draft tags survive navigation independently
for each buffer. A draft containing only tags is unsaved work and requires discard
confirmation; Ctrl-S still requires a nonempty description. Fresh drafts start
without tags after a save.

Ctrl+K opens a task ID popup from editor or filter. Enter a positive ID, press
Enter to go; invalid or missing IDs keep popup open with an error. Backspace edits
input; Ctrl+U clears it. Esc or Ctrl+C closes popup and preserves selection,
draft, filter and focus. Successful jump clears filter and retains unsaved
task/new/child drafts, including caret and editor scroll. Jumping to archived
task enables archived rows for remaining dashboard session. Jumping to completed
task reveals completed rows when hidden; other jumps and cancellation preserve
that setting. Navigation saves no content and changes no task status or ownership.

Ctrl-H focuses selected task's live Herdr agent and opens Herdr client using
task's linked server. `Ctrl-H Herdr` appears in the shortcut bar only when the
selected task has a saved Herdr link, including while filtering. Detach from client
to return to TUI with draft, selection, filter, cursor and scroll positions preserved. No task is saved or changed.
Missing links, unavailable agents and client errors appear in status bar.
Link task with `qqq herdr link` first if needed. Backspace still edits text.

Task list opens scrolled to bottom. List and details refresh after external DB
commits without keyboard input. Refresh keeps current editor draft, filter query,
and manual scroll positions. Selected task status updates in editor title; task
text stays in editor until reloaded or saved.

Mouse wheel scrolls list, details or editor under pointer. Panes keep separate
scroll positions; scrolling never edits or saves text. PgUp/PgDn scroll selected
task details while editor has focus. Selection changes reset details scroll.
Details clicks leave editor caret and draft unchanged. Shift-Up/Down
returns list to selected task. Editor keys reveal caret after manual scroll.
Left-click task row to load it in editor, including indented or wrapped rows.
Left-click editor text to place caret. Task-row clicks retain unsaved drafts,
using same restoration behavior as Shift-Up/Down.

Ctrl+/ focuses filter and shows `Filter:` with editable caret, even when empty.
Filter bar stays visible while focused, query is nonempty, or completed tasks are
hidden. Empty unfocused query reserves no row when completed tasks are shown.
Right-edge `[✓ Completed]` button includes completed tasks; click to switch to
`[× Completed]` and hide them. Ctrl-T toggles the same state while filter is
focused. Narrow list panes show compact `[✓]` / `[×]`. Visibility resets when TUI
opens again. Toggle preserves query, opened task, unsaved drafts and DB; navigation
uses visible tasks. Unfinished children remain visible when completed parents are
hidden. Type to match any part of full description, ignoring case.
Matching tasks retain visible parent chain;
no matches shows `No matching tasks.`. Backspace edits query. Tab or Enter returns to
editor. `/` or Alt+/ inserts literal `/` into editor. Shift-Up/Down navigates visible
tasks while filter is active. Query changes do not change selected task, unsaved
draft, or DB.
With focused filter or nonempty query, Esc clears query and closes filter focus. Ctrl-C clears
nonempty query, preserving current focus. Selected task and editor draft stay intact, including while
popup or confirmation is open. With empty focused filter, next Ctrl-C closes filter
and returns focus to editor without changing opened task or draft. Next Esc follows
editor clearing flow; later Ctrl-C follows editor cancel flow. Dirty drafts ask
before discard.

Ctrl-G opens centered actions popup for selected task; dashboard stays visible
around it. Prompts, confirmations and errors share popup; small terminals use
compact view, scrolling menu to keep selected action visible.
Actions form three groups separated by empty rows: status (`Complete`,
`Mark error`, optional `Retry error`, `Reopen`), settings (`Priority`,
`Set parent`), and visibility (`Archive`/`Unarchive`). Empty rows are never
selectable. Cyan titles/shortcuts, muted help, gold prompts/warnings and soft-red
errors distinguish popup states. Up/Down selects action, wrapping at ends;
Enter activates selected row. Dark accent tint and `>` mark selection; plain
mode keeps marker. Letter shortcuts activate actions directly. Press `c` to complete owned task,
`r` to retry error task, `o` to reopen completed task or absent Herdr owner's active task, `a` to archive or
unarchive, `p` to set priority (-100..100), or `d` to set parent by positive
task ID or `none`. Retry appears only for error tasks; non-error tasks ignore `r`.
Press `e` for Mark error, enter a nonempty reason, then confirm with `y`.
Only current owner of an in-progress task can mark it error; other states or
owners show an action error and keep draft. Reason is recorded as a task message
with an error event; claim and assignment clear while saved content stays intact.
Error tasks stay out of worker queue until explicitly retried. Reason prompt and
confirmation cancel without changing task. Confirmation previews reason and asks
before discarding an active dirty draft, matching other state actions.
Arrows skip hidden actions; Enter activates displayed selection.
Open menu refreshes Retry availability after external status changes, keeping
selected action when available.
Complete checks current session ownership when activated. Without a matching
claim, popup asks `Force complete task #ID?` and warns
`Complete without matching task owner.` Confirm with `y` to finish a New,
In progress or Error task regardless of owner **after selecting** `[ ] Force complete`
with Space or clicking checkbox/label. Checkbox starts unchecked on every opening;
forced confirmation without selecting it keeps popup and changes nothing. Matching
owner can complete normally with checkbox unchecked, or select force explicitly.
This clears active claim, retains recorded harness/orchestrator metadata and Herdr
link for log lookup, preserves task content and history, and records a standard `complete`
event attributed to explicit session or `manual`. Already-completed tasks fail.
Cancellation preserves draft and DB. If Herdr fails or ownership changes during
normal confirmation, popup keeps exact error plus unchecked force checkbox.
Retry normally with `y`, or select force then confirm with `y`. Force path bypasses
Herdr/owner lookup; it never happens automatically after failure. DB corruption,
invalid input and other unrelated failures keep ordinary error handling.
After filter clears, Esc closes menu or prompt. State changes ask for `y` before
running; priority and parent changes ask when draft has unsaved edits. Successful
action refreshes task and list; rejected action keeps draft and shows DB error.
Error view wraps long messages; Up/Down scrolls, Esc closes it.

## Dependencies and images

Each task has at most one tree parent and any number of extra prerequisites.
A new, unarchived task becomes ready only when its parent (if set) and every
extra prerequisite are completed. `next`, filtered/waiting next, dry-run,
Herdr dispatch and queue diagnostics use the same readiness rule. Extra edges
do not change tree placement or duplicate task rows.

```sh
qqq add "Integrate" --parent 1 --depends-on 2 --depends-on 3
qqq edit 4 --depends-on 5                 # add prerequisite
qqq edit 4 --remove-depends-on 2          # remove one; repeat flag to remove more
qqq edit 4 --clear-depends-on             # clear extras, retain tree parent
qqq edit 4 --clear-depends-on --depends-on 6  # replace extras atomically
qqq edit 4 --set-parent none              # clear tree parent, retain extras
```

IDs must exist. Self links, duplicate additions (including an existing edge),
missing removals, parent/prerequisite overlap, and cycles across both link types
fail atomically. Add/remove of the same ID and clear/remove together fail.
Clear is idempotent. Edits validate the final graph: remove an extra while
making it the parent, or clear the parent while making it an extra, in one
command. Dependency edit flags skip the editor and conflict with `--edit`.
Direct description/status/priority/parent/image changes can share the operation.

Dependency edits and reopening prerequisites affect future claims; active owners
remain assigned, completed descendants keep their status/history, and content
revisions remain unchanged. Completed archived prerequisites satisfy readiness.
Archived unfinished prerequisites follow parent safety: unfinished tasks cannot
link to them, unarchive/reopen validates them, and an unfinished prerequisite
cannot be archived with visible unfinished dependents. Remove such edges before
unarchiving/reopening, or unarchive and finish the prerequisite first.

`show`, Task JSON and TUI details include ordered `prerequisites` entries:
`{"id":2,"status":"new","archived":false}`. Corrupt missing references have
null status/archive values. Human/TUI details mark unfinished entries with
`blocks claim`. Queue explanations include their IDs, status and archived state.
TUI details refresh when prerequisite status changes, retaining unsaved text.

Attach files through `add` or `edit`; repeat `--image` for multiple files:

```sh
qqq edit 2 --image ./before.png --image ./after.jpg
qqq show 2
qqq show 2 --export-image 1 --output ./exported.png
```

Image IDs come from `show`. Export needs selected task's image ID and new output
path. PNG, JPEG, GIF and WebP signatures supported, up to 20 MiB each; signature
check does not fully validate file. Bytes copy into
`.qqq/images/<task_id>/<image_id>.<ext>`. Built-in editor also accepts pasted
image paths or clipboard images. Pasted images become Markdown links in the
description, such as `![before.png](.qqq/images/2/4.png)`. Reloading a task
shows each linked image as one editable item; deleting that item removes its
description link while keeping the attachment available through `show`.

Description, status, parent, prerequisites, priority and image updates save atomically.

## Queue diagnostics

```sh
qqq status
qqq status --include-archived --json
qqq next --explain
qqq next --explain --filter 'priority >= 5' --json
qqq next --explain --tag frontend --tag bug
qqq next --explain --session worker-1
```

`status` summarizes ready, blocked, in-progress, error and completed counts,
then shows unfinished tasks with active owners, immediate parent/prerequisite blockers and
latest activity. JSON includes every task in scope, including completed tasks.
Archived tasks are excluded by default; `--include-archived` includes their rows
and counts. Archived tasks never become ready.

`next --explain` reports selection and every scoped task's eligibility without
claiming, dispatching an agent or changing assignment metadata. It distinguishes
an empty queue, blocked/error/owned queues, mixed unavailable work and ready tasks
excluded by filters. Candidates use priority descending, then ID ascending on
ties. Readiness uses exactly the same rules as real next: new, unarchived, with
a completed parent if set and all extra prerequisites completed.

Global explanation needs no session. Native Codex/Herdr identity discovery and
dispatch config are ignored. Supply `--session`, `QQQ_SESSION` or
`--harness-session` to inspect owned-task reuse; add `--harness-name` when a
public harness session is ambiguous. Exact claim keys take precedence over public
session matching. Existing owned tasks return before queue/filter selection, as
with ordinary next. This differs from `--dry-run`, which always previews queued
candidates. Explanation accepts `--local`, conflicts with `--wait` and
`--dry-run`; next's `--include-archived` requires `--explain`.

Diagnostics open the existing DB read-only, without creating a project, migrating
schema or recovering pending deletion files. Older supported schemas require a
normal command such as `qqq list` before diagnostics. Counts, task rows, blockers,
activity, owner lookup and selection share one SQLite transaction snapshot.
Filter results are evaluated once per task row and reused for counts and selection,
including date functions whose clock values can change between statements.
Reports do not reserve work; subsequent claims may see newer state. Activity age
never expires or reassigns ownership. Owners still explicitly complete or release
their claims.

### Diagnostic JSON

Both commands return an object with these fields:

| Field | Meaning |
| --- | --- |
| `include_archived` | Requested archive scope |
| `counts.total` | Number of scoped tasks |
| `counts.new` | Scoped tasks with `new` status |
| `counts.ready` | Unfiltered ready tasks |
| `counts.blocked` | New tasks that are not ready; includes archived new tasks when included |
| `counts.matching_ready` | Ready tasks matching supplied filter; same as ready without a filter |
| `counts.in_progress`, `.error`, `.completed` | Scoped task counts for each status |
| `counts.archived` | Archived subset of total; not an additional status category |
| `state` | `empty`, `ready`, `no_matching_ready`, `blocked`, `error`, `in_progress`, or `no_ready` |
| `tasks` | Scoped diagnostic rows in ascending creation ID order |
| `explanation` | Selection diagnostics for `--explain`; null for status |

`ready + blocked = new`. `blocked`, `error` and `in_progress` states describe
queues containing only that unfinished class; completed history does not hide
those states. Mixed unavailable or completed-only queues use `no_ready`.

Each row contains the normal `task` object plus:

| Field | Meaning |
| --- | --- |
| `ready` | Shared readiness predicate, independent of filter |
| `matches_filter` | Combined Luau/tag selector result, independent of readiness; true without selectors |
| `queue_rank` | One-based rank among all ready tasks by priority/ID; null otherwise |
| `reasons` | Zero or more eligibility/exclusion codes |
| `owner` | Recorded claim key for an active task; null otherwise |
| `blockers` | Unfinished/missing parent and extra prerequisites: ID, status and archived flag; missing refs have null status/flag |
| `latest_activity` | Latest task update, message or event: `at`, `source`, `id`, `session`, `action` |

Reason codes: `archived`, `parent_not_completed`, `prerequisite_not_completed`, `in_progress`, `error`,
`completed`, `filter_excluded`, `tag_excluded`. Explicit tag exclusion may appear
alongside dependency blockers. When tags match but Luau fails, reason remains
`filter_excluded`; predicates inside arbitrary Luau expressions use that reason
too. A ready matching task has no reasons.
Latest activity compares task `updated_at` with message/event `created_at`.
Timestamp ties prefer message, then event, then task; higher IDs break ties
within one source. Task activity has null ID/session/action; messages have null
action; events include their action. Updates cover creation and edits as well as
other task changes. Blocker resolution includes archived dependencies regardless of
output scope.

Explanation contains `owner_input`, `resolved_owner` (both null without explicit
context), `ordering: ["priority_desc", "id_asc"]`, `eligible_ids` (matching ready
IDs in selection order), `outcome` and `selection`. Outcome is
`owned_task_reuse`, `ready_candidate`, `empty_queue`,
`no_matching_ready_candidate`, `blocked_queue`, `error_queue`,
`in_progress_queue` or `no_ready_tasks`. Selection is null or
`{"kind":"owned"|"queued","task":...}`. An archived active owned task can appear
in selection while absent from default scoped rows, matching ordinary next.
Task objects retain the existing task JSON format; raw claim keys are exposed
only in diagnostic row `owner`.

## Agents and recovery

Use stable, unique session ID per worker:

```sh
export QQQ_SESSION='worker-1'
qqq next
qqq message 1 "Running tests"
qqq complete 1
qqq next --wait
```

`--session` overrides `QQQ_SESSION`. Each owner gets one active task; repeated
`next` returns it. Claims never expire. `--wait` blocks until work is ready;
without it, empty queue prints `No task available for pickup.` and exits successfully. Error
tasks and blocked children stay out of queue.

`next --dry-run` previews queued candidate without claiming or dispatching an
agent. Existing claims are skipped; no session or Herdr identity required.
Preview retains stored `new` status and metadata. Combine with `--filter`,
`--wait`, `--local`, and `--json`. Preview does not reserve task; another worker
can claim it afterward.

Preview opens DB read-only, leaves deletion staging untouched. Older schemas
return `DATABASE_ERROR` with reason `migration_required`; run `qqq list` to
upgrade before previewing. `--wait` keeps preview waiting for queued candidate
without reserving it or creating upgrade snapshots. Strict read-only commands
refuse WAL mode or existing WAL sidecars with reason `unsafe_read_only`,
because SQLite read-only opens can create WAL/shm files. Stop project writers,
use `sqlite3 .qqq/qqq.db 'PRAGMA journal_mode=DELETE;'`, then retry. Waiting
preview also rechecks journaling state before each read transaction.

Without a queued candidate, human preview prints `No task available for pickup.`
Use `qqq status` to inspect blocked, active, and error tasks.
JSON preview returns `null` when no candidate is available.

```sh
qqq next --dry-run --json
```

`next --filter 'priority >= 5'` selects matching new candidates using same
priority/readiness rules. Without `--dry-run`, existing owned task still returns
regardless of filter.
`--tag LABEL` uses the same selection rules; repeated labels require all tags.
Changed selectors never release or transfer existing ownership.
Combine with `--wait` or Herdr dispatch; no matching ready candidate yields no claim.

Without `--session` or `QQQ_SESSION`, owner discovery uses:

1. Exact Herdr pane from `HERDR_PANE_ID` or `HERDR_ENV=1` (requires pane ID).
2. Native Codex `CODEX_THREAD_ID`, then `CODEX_SESSION_ID` fallback.
3. Unique Herdr agent at project root (`foreground_cwd` before `cwd`).

Explicit session IDs and native Codex ownership work without Herdr when dispatch is
disabled or `next --local` is used. Exact Herdr context takes precedence.
`next --local` skips dispatch, still resolves owner.

Automatic claims save detected harness/orchestrator fields and Herdr link when
available. Session flags are optional in these contexts:

| Context | Automatic identity | Saved metadata |
| --- | --- | --- |
| Herdr pane, any reported harness | Pane agent-session identity, or terminal ID plus agent kind | Reported harness name/session, Herdr server name, saved pane link |
| Standalone Codex | `CODEX_THREAD_ID`, then `CODEX_SESSION_ID` | `codex`, displayed Codex session; no orchestrator or Herdr link |
| Unique Herdr agent at project root | Matching pane identity | Reported harness name/session, Herdr server name, saved pane link |
| Dispatched child | Inherited `QQQ_SESSION` resolves reserved claim | Child harness and Herdr link saved before prompt |

Outside Herdr, built-in native environment discovery currently covers Codex.
Other harnesses use reported Herdr pane identity. qqq does not guess sessions
from unrelated environment variables. Explicit session flags remain overrides.

For automatic Codex claims, displayed `harness_session` uses `CODEX_SESSION_ID`
when set, including inside Herdr. Without it, resolved owner ID supplies display.
Claim keys and Herdr links keep resolved identity; `--harness-session` overrides
display. Repeated claims retain saved metadata unless explicit assignment flags
override it.

Completion, release and error marking require owner. Use original session token
or uniquely matching displayed `harness_session`; add `--harness-name` when
public sessions overlap. JSON assignment fields: `harness_name`,
`harness_session`, `orchestrator_name`, `orchestrator_session`.

Normal and force completion retain these four recorded fields and saved Herdr
link. Completion response, JSON lists, `show` and TUI details keep worker/session
information available for log inspection; `qqq herdr find ID` still uses saved
link to locate agent. Active claim ends, so same worker can claim next task even
when completed tasks keep same public session. Release, error marking and reopen
clear current assignment metadata; fresh claim records new worker identity.

Human `next` output omits these four assignment fields. JSON and `show` retain
them for ownership inspection.

### Owner liveness before commands

qqq checks current project’s active owners synchronously before command execution
or TUI/watch startup. No background process or periodic scan. Confirmed dead
harness/orchestrator changes task to `error`, clears assignment, preserves task
contents/history/link, records reason message and `qqq-preflight` error event.
Inspection commands (`status`, `doctor`, `next --explain`, `--dry-run`) also run
cleanup first; preview itself does not claim or edit queued work. When no dead
owner is confirmed, preflight makes no DB writes. It never creates or migrates a
project; writable opens scan after migration.

Native Codex claims bind actual harness ancestor PID, start time, executable and
machine identity privately to current claim. Missing/reused PID or zombie proves
death on same machine; another machine or unavailable process data stays unknown.
Manual session tokens alone do not identify process. Saved Herdr server and agent
identity/terminal establish liveness; stopped/missing saved server or successful
agent list without owner proves death. Failed/malformed/timeout probes stay
unknown unless separate evidence proves stopped server. Probes time out after
two seconds. Claim, link and process guards prevent stale checks from failing
reassigned work.

Use `qqq edit ID --set-status new` to retry errors. `qqq reopen ID` also recovers
auto-failed owners; archive/unarchive preserves that option. TUI actions apply
same rules. Idle TUI/watch/wait does not rescan until another qqq command runs.

### Return or retry work

Return active task owned by `worker-1`:

```sh
qqq edit 1 --set-pending --session worker-1
```

Alternatively, mark active task failed, then retry:

```sh
qqq edit 1 --set-status error --reason "Missing credentials" --session worker-1
qqq edit 1 --set-status new          # retry error task; no session required
```

`--set-pending` equals `--set-status new`. Active tasks require owner; error
tasks can be retried without one. Error marking requires nonblank reason,
clears assignment, records reason as message. Status updates skip editor.

For abandoned active work, inspect task before forcing return:

```sh
qqq show 1
qqq edit 1 --set-status new --force
```

`--force` skips ownership matching and Herdr discovery. It requires literal
`--set-status new`; cannot combine with `--set-pending` or `--edit`. Return/retry
preserves content, dependencies, messages, images and Herdr link.

Session IDs coordinate local agents; they do not authenticate users. Any local
caller can edit task content or append messages regardless of ownership.

## Config and aliases

Config: `~/.config/qqq/config.toml` (under `HOME`).

```sh
qqq config --list
qqq config alias.ls 'list --watch'
qqq config --get alias.ls
qqq config alias.ls                 # same as --get
qqq config display.max-completed 10
qqq config tui.after_save_new open_new
qqq config --unset alias.ls
```

Keys use TOML dotted syntax; quote literal dots: `'alias."with.dot"'`.
Alias values are strings; `herdr.next-to-new-agent` is boolean;
`display.max-completed` is non-negative integer. `tui.after_save_new` accepts
`open_saved` (default) or `open_new`, loaded when `qqq tui` starts. Other values
accept TOML literals, falling back to strings.

Reads do not create files. `--list` shows stored values; `--get` can inspect
invalid settings. Writes validate known settings, preserve comments and unknown
keys. `--json` returns typed values; unset restores default.

Completion limit config affects human lists only; explicit `--max-completed`
affects text and JSON. Missing limit means unlimited.

Example config:

```toml
[display]
max-completed = 10

[tui]
after_save_new = "open_saved"

[alias]
ls = "list"
n = "next"
done = "complete"
bug = "add"
```

`qqq bug "Fix login"` expands to `qqq add "Fix login"`. Aliases support quoting
and chains; built-in commands take precedence. No shell expansion. Cycles,
invalid quoting and `!` aliases fail.

## Herdr

Optional Herdr integration links tasks to live agent panes:

```sh
qqq herdr link 1
qqq herdr find 1
qqq herdr link 1 --agent codex --agent-session session-123 --server work
```

Discovery selects exact caller pane or unique agent at project root. Ambiguous
identity fails before claim. Without agent-session hooks, terminal ID plus agent
kind supplies identity. Active claims retain identity as hooks change.

`link` stores association without changing owner; `find` searches live panes for
saved link identity. Both require Herdr CLI and server. Local explicit `--session`
claims do not auto-link; new-agent dispatch links spawned agent even when caller
uses explicit session.

To dispatch ready work into new Codex agent tab:

```sh
qqq config herdr.next-to-new-agent true
qqq next
```

Dispatch needs `HERDR_ENV=1` and `HERDR_WORKSPACE_ID`. It reuses caller's active
task; otherwise reserves ready task, opens sibling tab, starts agent and submits
prompt. Empty or blocked queues create no tab. `next --local` skips dispatch.
Default: disabled.

Dispatched child inherits `QQQ_SESSION` for reserved claim. Worker commands
`qqq message ID ...`, `qqq complete ID` and `qqq edit ID --set-status error
--reason ...` use that env automatically; generated prompt requires no session
parameter. Keep inherited variable set. Child's saved harness/orchestrator fields
identify spawned agent, separate from dispatch ownership token.

Startup failure releases dispatch reservation; created tabs stay open. Prompt
errors keep claim and link because delivery may have happened. Inspect agent
before retry or forced release.

## Data

Compatibility facts below describe current source checkout. Fixture matrix and
runtime output enforce this table; package version and DB schema are separate.

<!-- compatibility-support:start -->
| Contract | Supported value |
| --- | --- |
| Current DB schema | `13` |
| Normal-open DB schemas | `1–13` |
| Automatic upgrade source schemas | `1–12` |
| Portable snapshot format | `1` |
| Portable snapshot DB schemas | `9–13` |
| Upgrade recovery format | `2` |
| Upgrade recovery source schemas | `1–12` |
| Initialization-only schema | `0` |
| Manual-conversion layouts | `title`, `pending` |
<!-- compatibility-support:end -->

Supported versions require compatible `description`/`new` layout and valid data.
Legacy `title` columns or `pending` status layouts need manual conversion even
when their `user_version` falls inside listed range. Schema 0 initializes an
empty DB; existing schema-0 projects do not qualify for automatic upgrade.
Unknown newer schemas and archive formats are rejected.

Keep `.qqq/` and `.qqq-upgrades/` out of Git. Create portable snapshot while other local qqq
writers run:

<!-- compatibility-portable-example:start -->
```sh
qqq backup ../project-snapshot.tar
mkdir ../restored-project
cd ../restored-project
qqq restore ../project-snapshot.tar
qqq list
qqq doctor
```
<!-- compatibility-portable-example:end -->

Backup contains consistent SQLite data plus every stored image. Tar archive
starts with `manifest.json`, then `qqq.db`, then `images/<task-id>/<image-id>.<ext>`.
Manifest records SHA-256 hashes and sizes; restore checks those, image rows,
SQLite integrity, and foreign keys before installing `.qqq`. Backup refuses to
overwrite destination. Restore works only from new project directory or one
with empty `.qqq`; existing data and nested projects are rejected. Invalid
archives leave project unchanged. Relative snapshot paths resolve from current
directory.

Before automatically upgrading an existing supported DB, qqq saves original
schema and required attachments in sibling `.qqq-upgrades/` directory. Human
stderr prints `Saved pre-upgrade snapshot: <path>` before migration. JSON
result/error shapes stay unchanged; discover archives in that directory.
Names follow `schema-<source>-to-<target>-.upgrade-<unique>.tar`; retries never
overwrite previous records. Manifest records source/target schemas, original
absolute project path, creation time and SHA-256 hashes/byte counts. SQLite
write lock coordinates DB copy and required attachments before migration;
waiting openers recheck schema after acquiring lock. One successful concurrent
upgrade produces one archive. Snapshot is verified, synced and atomically
published before migration changes DB/images. Snapshot failure aborts upgrade
with source intact. Successful
and failed migrations both retain verified snapshots. Current-schema opens,
fresh initialization, diagnostics and dry runs create none.

Upgrade recovery uses distinct manifest format listed in support table. Stop qqq workers, choose
original archive from printed absolute path or `.qqq-upgrades/` inventory.
Example starts in original project, prompts for that path, creates fresh target.
SQLite inspection requires `sqlite3` CLI:

<!-- compatibility-recovery-example:start -->
```sh
printf 'Saved snapshot absolute path: '
IFS= read -r recovery_archive
test -f "$recovery_archive"
recovery_project=$(mktemp -d "${TMPDIR:-/tmp}/qqq-recovered.XXXXXX")
cd "$recovery_project"
printf 'Recovered project: %s\n' "$PWD"
qqq restore --recovery "$recovery_archive"
# Recovery preserves original schema. Inspect data before retrying upgrade.
sqlite3 .qqq/qqq.db 'PRAGMA user_version; PRAGMA integrity_check; PRAGMA foreign_key_check;'
qqq list # saves new pre-upgrade snapshot, retries normal upgrade
qqq doctor
```
<!-- compatibility-recovery-example:end -->

Recovery validates manifest schema/project metadata, SHA256 hashes, SQLite
integrity/foreign keys, task status/claims and attachments before installation.
It accepts original source schemas listed in support table, including
embedded-image schemas 1–5, without
migrating during restore. Pending deletion attachments are captured without
altering live staging, then restored at canonical image paths. Populated target
is rejected. Ordinary `restore` accepts portable format/schema range listed above;
`--recovery` is required for automatic upgrade archives. Retain archives until
recovered data and subsequent upgrade are verified; no automatic cleanup runs.
Use SQLite inspection before `qqq list`, `qqq show`, backup, TUI or another
normal DB-backed command: those commands automatically upgrade restored old DB.
Recovery does not overwrite original project; inspect and verify fresh target
before replacing any live project files.

Preview permanent deletion before confirming it:

```sh
qqq backup ../before-delete.tar
qqq delete 42
qqq delete 42 --yes
```

Only archived tasks without children can be deleted. Use positive task ID;
relative creation indexes can change before confirmation. Preview lists messages,
events, Herdr link, stored images, and paths without changing project data;
`--json` returns same counts with `deleted:false`. `--yes` removes task and
dependent data permanently, including stored image files, then returns
`deleted:true`. Release or complete an active task before archiving it; delete
or reparent every child and remove all prerequisite references before deleting
a task. IDs remain reserved after deletion. Preview refuses legacy DB versions and pending recovery without
changing files; run `qqq list` to migrate or recover before previewing again.
If deletion stops while images are staged, next DB-backed qqq command recovers
them according to committed DB state. Keep backup until recovered project
passes `qqq doctor`.

Check project health without changing DB or attachments:

```sh
qqq doctor
qqq --json doctor
```

Doctor checks SQLite integrity, foreign keys, schema version, combined dependency
graph cycles/duplicate edges/missing refs, image paths, byte counts,
signatures, and orphan files/directories. Healthy project exits 0. Issues print recovery
actions and exit 1; JSON includes `ok`, counts, and `issues` with code, path,
message, and action. Doctor never migrates DB. If SQLite journal/WAL sidecars
exist, stop writers and recover or checkpoint SQLite before rerunning doctor.
Persisted WAL mode without sidecars also defers check (`DB_READ_ONLY_UNSAFE`),
leaving source files untouched. Stop writers, switch to journal_mode=DELETE
through SQLite before retrying.
For damaged DB or images, restore verified snapshot into new directory first;
inspect recovered data before replacing damaged project files. Keep damaged
copy until recovery is verified. For `DELETE_RECOVERY_PENDING`, follow returned
action: fix unsafe staging paths or access errors first. Valid interrupted
deletions recover through normal DB-backed command, such as `qqq list`;
rerun doctor afterward.

To move old root-level `qqq.db`, stop DB writers. Before `qqq init`, check that
`.qqq/qqq.db` does not exist. From project root:

```sh
mkdir -p .qqq
sqlite3 qqq.db ".backup '.qqq/qqq.db'"
qqq list # run updated CLI
```

Check task data before removing old DB. New CLI does not discover root-level
`qqq.db`; `qqq init` without migration creates separate empty DB.

Compatible source schemas listed above migrate to current DB schema after saving
verified original-schema recovery archive outside live store. Upgrade preserves
task/message/event/image IDs, reserved SQLite ID sequences, descriptions,
timestamps, parent/dependency edges, history, stored images and active claim
keys. Existing queue readiness remains consistent with existing dependencies;
new fields use defaults when source schema predates them:

| Added in schema | Field/behavior | Value for older data |
| --- | --- | --- |
| 2 | Parent | `parent_id=NULL` |
| 3, 5 | Ownership/identity | Owner key preserved through renames; active harness identity derives from saved Herdr link or prior claim key; orchestrator identity derives from saved link |
| 4 | Error status | Existing status/history unchanged; no error tasks invented |
| 7 | Priority | `0` |
| 8 | Archived | `false` |
| 9 | Reopen event | Existing events unchanged |
| 10 | Content revision | `1` |
| 11 | Extra prerequisites | Empty; existing parent edges retained |
| 12 | Tags | Empty array `[]` |
| 13 | Private claim process identity | Empty; legacy Herdr links still support liveness checks |

Version 6 moves existing image blobs to `.qqq/images/` before SQLite drops its
`data` column, preserving IDs, metadata and bytes. Portable restore preserves
stored schema; next normal DB open upgrades older versions.
Migration tries `VACUUM` to reclaim old blob pages. If compaction warns, stop
writers and run `sqlite3 .qqq/qqq.db 'VACUUM;'` later.

Coordinate workers sharing project: stop older workers and waiters, upgrade all
binaries, run one normal command such as `qqq list`, verify `qqq doctor`, then
restart workers. Older binaries reject schemas newer than their own supported
version; do not run mixed versions after migration. Claims remain explicit:
migration does not expire, transfer, release or replace active ownership. Original
owner still completes/releases work explicitly; elapsed time does not free claim.
Back up before manual conversion of unsupported legacy layouts.

## Development

Tests require Python 3 for real-terminal coverage, POSIX shell and `sqlite3` CLI
for executable recovery documentation on macOS and Linux.
[Historical compatibility fixtures](../tests/fixtures/compatibility/README.md)
cover every normal-open DB schema and accepted portable snapshot schema. Tests
copy checked-in artifacts; regeneration uses frozen historical sources.
Snapshot validation rejects legacy title/pending layouts and invalid status/claim
relationships before installing DB/images. Human and JSON failures preserve
original projects. Diagnostics and all dry runs never migrate; next dry-run
requires current schema, previews queued candidate without claiming.
Recovery matrix checks original-schema equality, embedded/external images, WAL
and concurrent writer consistency, one archive per concurrent upgrade, retained
records after SQL/image/commit failures, snapshot verification failure and
metadata/claim/checksum rejection before installing restored projects.

```sh
./scripts/check-compatibility-docs.sh
./scripts/check-compatibility.sh
cargo test --locked
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo build --locked --release
```

[CI](../.github/workflows/ci.yml) checks Ubuntu 24.04 and macOS 14, builds release
binary, uploads `qqq-<OS>-<ARCH>` archives (14-day retention). Both CI OSes and
publication require `scripts/check-compatibility.sh`: immutable historical DB
upgrade/restore matrix plus identity, image-storage, dependency/tag CLI and
snapshot regressions. Matrix verifies repeated/concurrent opens, rollback/retry,
field defaults, ownership/readiness and reserved ID sequences. Missing fixture
coverage for a new schema or snapshot format fails gate. Focused
`scripts/check-compatibility-docs.sh` checks marked support table against fixture
metadata, initialized schema and runtime archive metadata (`SCHEMA_VERSION`
sets upgrade target). It executes documented portable/recovery examples in
temporary historical projects. New schemas require matrix fixtures and published
facts to change together; unrelated prose is outside contract check.

### Publish to crates.io

[Release checklist](release-checklist.md) covers compatibility fixtures, recovery,
user-visible schema notes, package/binary versions and existing publication gates.

[Publish workflow](../.github/workflows/publish.yml) checks `v<version>` tag against
`Cargo.toml`, runs checks and package dry run, then publishes `qqq-cli`. Setup:

1. Sign in to crates.io, verify email, create token allowed to publish `qqq-cli`.
2. Add GitHub Actions repo secret `CARGO_REGISTRY_TOKEN`.
3. Choose unused version, update `Cargo.toml` and `Cargo.lock`, commit changes.
4. Push commit and matching tag. Example for version `0.3.0`:

```sh
git push origin master
git tag v0.3.0
git push origin v0.3.0
```

For manual validation, run **Publish to crates.io** with existing `tag` and
default `dry_run=true`. Set `dry_run=false` to publish new version. Published
versions cannot be overwritten.

License: [MIT](../LICENSE).