Commit5425a49bRecorded2 Aug 2026Repositoryfolio

Widen ids to 6 hex, sort project files, add a from: disambiguator

Message

WIDER IDS. generate-task-id now emits six hex, not four. Four is a 65,536 space; with no uniqueness check, 1291 task lines predicts ~12.5 colliding ids and the estate had 14 -- the arithmetic working exactly as designed. Six gives 16,777,216 and predicts ~0.05 at the same corpus size.

Accept short, mint long: nothing in folio validates id shape and length was decided in exactly one place, so all 1274 existing 4-hex ids stay valid and every reference to them keeps resolving. There is no migration; the two widths coexist, and there is a test asserting they do.

SORTED PROJECT FILES. store-project-files returned raw directory-list (readdir) order. Every id-addressed mutation searches that list and stops at the first match, so with a duplicated id the VICTIM was chosen by readdir -- unstable across machines and unreproducible between runs. Sorting does not fix ambiguity; it makes the ambiguity deterministic, so a reproduction stays reproducible.

A from: DISAMBIGUATOR. complete, reopen, update and move take an optional from naming the project (or "inbox") holding the task meant, and the refusal message now ends by suggesting it. This addresses a duplicated id WITHOUT renaming anything, so no reference in any note, brief or commit message breaks. It is what makes t-bf97's three live copies closable.

A scope that still matches two is still refused; a scope naming the wrong project finds nothing and mutates nothing. Sabotaging the scope filter reddens six of the nine qualifier tests while the two scope-independent controls stay green.

Also corrects a comment on move-task-in-store! that asserted ids are globally unique. They are not, and I wrote that claim myself.

Changed
 src/folio/id.sgl           |  16 +++++++++++++--
 src/folio/store.sgl        |  18 ++++++++++++++---
 src/folio/tools.sgl        | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------
 test/test-id-ambiguity.sgl | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 307 insertions(+), 55 deletions(-)
Diff
src/folio/id.sglmodified
@@ -27,14 +27,26 @@
27
(random-seed! (current-jiffy))
28
(set! *seeded?* #t)))
29
30
;;; Generate a random 4-hex-char task ID.
+30
;;; Generate a random 6-hex-char task ID.
31
;;;
32
;;; Returns a string like "t-a3f2".
+32
;;; Returns a string like "t-9c1e77".
+33
;;;
+34
;;; SIX, not four. Four hex is a 65,536 space, and with no uniqueness
+35
;;; check anywhere, 1291 task lines predicts ~12.5 colliding ids -- the
+36
;;; estate had 14, which is the arithmetic working exactly as designed.
+37
;;; Six gives 16,777,216 and predicts ~0.05 at the same corpus size.
+38
;;;
+39
;;; ACCEPT SHORT, MINT LONG. Nothing in folio validates id shape and
+40
;;; length is decided only here, so the 1274 existing 4-hex ids stay valid
+41
;;; forever and every reference to them keeps resolving. There is no
+42
;;; migration: the two widths simply coexist.
43
(define (generate-task-id)
44
(: -> string?)
45
(ensure-seeded!)
46
(string-append "t-"
47
(string
+48
(string-ref hex-chars (random-integer 16))
+49
(string-ref hex-chars (random-integer 16))
50
(string-ref hex-chars (random-integer 16))
51
(string-ref hex-chars (random-integer 16))
52
(string-ref hex-chars (random-integer 16))
src/folio/store.sglmodified
@@ -7,6 +7,7 @@
7
(import (sigil core)
8
(sigil struct)
9
(sigil string)
+10
(sigil list)
11
(sigil path)
12
(sigil fs))
13
(export make-folio-store
@@ -72,9 +73,20 @@
73
(: folio-store? -> list?)
74
(let ((dir (store-project-dir store)))
75
(if (directory-exists? dir)
75
(filter (lambda (f) (string-ends-with? f ".md"))
76
(map (lambda (name) (path-join dir name))
77
(or (directory-list dir) '())))
+76
;; SORTED. `directory-list` returns raw readdir order, so without
+77
;; this the order varies by filesystem and shifts whenever a
+78
;; project file is added or removed. Every id-addressed mutation
+79
;; searches this list and stops at the first match, so with a
+80
;; duplicated id the VICTIM was decided by readdir -- unstable
+81
;; across machines and unreproducible between runs.
+82
;;
+83
;; Sorting does not fix ambiguity (that is `refuse-if-ambiguous`).
+84
;; It makes the ambiguity deterministic, so a reproduction stays
+85
;; reproducible.
+86
(list-sort string<?
+87
(filter (lambda (f) (string-ends-with? f ".md"))
+88
(map (lambda (name) (path-join dir name))
+89
(or (directory-list dir) '()))))
90
'())))
91
92
;;; List all .md files in the notes directory, recursively.
src/folio/tools.sglmodified
@@ -57,6 +57,14 @@
57
;; Tool Schemas
58
;; ============================================================
59
+60
;; Shared description for the `from` disambiguator. An id that matches
+61
;; more than one task is refused rather than guessed; `from` is how the
+62
;; caller says which one they meant, WITHOUT renaming anything -- renaming
+63
;; would break every reference to that id in notes, briefs and commit
+64
;; messages.
+65
(define scope-arg-description
+66
"Disambiguator for a duplicated id: the project name (or \"inbox\") holding the task you mean. Only needed when folio refuses an ambiguous id.")
+67
68
(define no-args-schema
69
'((type . "object")
70
(properties . ())
@@ -77,13 +85,15 @@
85
(required . ("text"))))
86
87
(define task-complete-schema
80
'((type . "object")
+88
`((type . "object")
89
(properties . ((id . ((type . "string")
82
(description . "Task ID (e.g., t-a3f2)")))))
+90
(description . "Task ID (e.g., t-a3f2)")))
+91
(from . ((type . "string")
+92
(description . ,scope-arg-description)))))
93
(required . ("id"))))
94
95
(define task-update-schema
86
'((type . "object")
+96
`((type . "object")
97
(properties . ((id . ((type . "string")
98
(description . "Task ID (e.g., t-a3f2)")))
99
(text . ((type . "string")
@@ -96,7 +106,9 @@
106
(tags . ((type . "string")
107
(description . "Set tags (space-separated)")))
108
(waiting . ((type . "string")
99
(description . "What/who this is blocked on")))))
+109
(description . "What/who this is blocked on")))
+110
(from . ((type . "string")
+111
(description . ,scope-arg-description)))))
112
(required . ("id"))))
113
114
(define task-add-schema
@@ -114,11 +126,13 @@
126
(required . ("text"))))
127
128
(define task-move-schema
117
'((type . "object")
+129
`((type . "object")
130
(properties . ((id . ((type . "string")
131
(description . "Task ID to move")))
132
(project . ((type . "string")
121
(description . "Target project name (omit to move to inbox)")))))
+133
(description . "Target project name (omit to move to inbox)")))
+134
(from . ((type . "string")
+135
(description . ,scope-arg-description)))))
136
(required . ("id"))))
137
138
(define projects-schema
@@ -453,12 +467,12 @@
467
(define (make-tool-task-complete store)
468
(lambda (args)
469
(let ((task-id (require-arg args id: "id")))
456
(complete-task-in-store! store task-id))))
+470
(complete-task-in-store! store task-id (dict-ref args from: #f)))))
471
472
(define (make-tool-task-uncomplete store)
473
(lambda (args)
474
(let ((task-id (require-arg args id: "id")))
461
(uncomplete-task-in-store! store task-id))))
+475
(uncomplete-task-in-store! store task-id (dict-ref args from: #f)))))
476
477
(define (make-tool-task-update store)
478
(lambda (args)
@@ -469,7 +483,8 @@
483
(lambda (args)
484
(let* ((task-id (require-arg args id: "id"))
485
(target-project (dict-ref args project: #f)))
472
(move-task-in-store! store task-id target-project))))
+486
(move-task-in-store! store task-id target-project
+487
(dict-ref args from: #f)))))
488
489
;; ============================================================
490
;; Project Handlers
@@ -1342,6 +1357,36 @@
1357
(cons (store-inbox-path store)
1358
(store-project-files store)))))
1359
+1360
;; Does this file belong to `scope`? "inbox" means inbox.md; anything else
+1361
;; is matched against the project file's basename without .md.
+1362
(define (file-in-scope? file-path scope)
+1363
(let ((base (path-basename file-path)))
+1364
(if (equal? scope "inbox")
+1365
(equal? base "inbox.md")
+1366
(equal? base (string-append scope ".md")))))
+1367
+1368
;;; Matches for `task-id`, narrowed to `scope` when one is given.
+1369
;;;
+1370
;;; This is the disambiguator for a duplicated id: rather than renaming a
+1371
;;; task -- which breaks every reference to it in notes, briefs and commit
+1372
;;; messages -- the CALLER says which one it meant. Nothing on disk
+1373
;;; changes, and the refusal message has already told them the names to
+1374
;;; choose from.
+1375
(define (find-tasks-by-id-scoped store task-id scope)
+1376
(: folio-store? string? (maybe string?) -> list?)
+1377
(let ((all (find-all-tasks-by-id store task-id)))
+1378
(if (not scope)
+1379
all
+1380
(filter (lambda (m) (file-in-scope? (car m) scope)) all))))
+1381
+1382
;; The files a mutation may touch, narrowed to `scope` when one is given.
+1383
(define (scoped-files store scope)
+1384
(let ((all (cons (store-inbox-path store)
+1385
(store-project-files store))))
+1386
(if (not scope)
+1387
all
+1388
(filter (lambda (f) (file-in-scope? f scope)) all))))
+1389
1390
;; Refuse an id-addressed mutation when the id resolves to more than one
1391
;; task. Returns #f when it is safe to proceed, or a refusal message.
1392
;;
@@ -1355,8 +1400,9 @@
1400
;; list is unsorted `directory-list` order, so which task wins depends on
1401
;; readdir, and any move can change it by removing the copy that was
1402
;; winning.
1358
(define (refuse-if-ambiguous store task-id verb)
1359
(let ((matches (find-all-tasks-by-id store task-id)))
+1403
(define (refuse-if-ambiguous store task-id verb . opts)
+1404
(let* ((scope (if (pair? opts) (car opts) #f))
+1405
(matches (find-tasks-by-id-scoped store task-id scope)))
1406
(if (< (length matches) 2)
1407
#f
1408
(string-join
@@ -1370,20 +1416,24 @@
1416
(folio-task-text (cdr m))))
1417
matches)
1418
(list ""
1373
"Task ids in this estate are not unique. Give one of them a new id to disambiguate, then retry."))
+1419
"Task ids in this estate are not unique. Retry with project: to say which one you mean --"
+1420
(format " e.g. project: \"~a\""
+1421
(let ((base (path-basename (car (car matches)))))
+1422
(if (equal? base "inbox.md")
+1423
"inbox"
+1424
(substring base 0 (- (string-length base) 3)))))))
1425
"\n"))))
1426
1427
;; Complete a task by ID, searching across inbox and all project files.
1377
(define (complete-task-in-store! store task-id)
1378
(or (refuse-if-ambiguous store task-id "complete")
1379
(let ((files (cons (store-inbox-path store)
1380
(store-project-files store))))
1381
(let loop ((rest files))
1382
(if (null? rest)
1383
(format "Task ~a not found." task-id)
1384
(if (complete-task-in-file! (car rest) task-id)
1385
(format "Completed task ~a." task-id)
1386
(loop (cdr rest))))))))
+1428
(define (complete-task-in-store! store task-id . opts)
+1429
(let ((scope (if (pair? opts) (car opts) #f)))
+1430
(or (refuse-if-ambiguous store task-id "complete" scope)
+1431
(let loop ((rest (scoped-files store scope)))
+1432
(if (null? rest)
+1433
(format "Task ~a not found." task-id)
+1434
(if (complete-task-in-file! (car rest) task-id)
+1435
(format "Completed task ~a." task-id)
+1436
(loop (cdr rest))))))))
1437
1438
;; Try to complete a task in a specific file. Returns #t if found.
1439
(define (complete-task-in-file! file-path task-id)
@@ -1417,16 +1467,15 @@
1467
found?)))
1468
1469
;; Reopen a completed task by ID.
1420
(define (uncomplete-task-in-store! store task-id)
1421
(or (refuse-if-ambiguous store task-id "reopen")
1422
(let ((files (cons (store-inbox-path store)
1423
(store-project-files store))))
1424
(let loop ((rest files))
1425
(if (null? rest)
1426
(format "Task ~a not found." task-id)
1427
(if (uncomplete-task-in-file! (car rest) task-id)
1428
(format "Reopened task ~a." task-id)
1429
(loop (cdr rest))))))))
+1470
(define (uncomplete-task-in-store! store task-id . opts)
+1471
(let ((scope (if (pair? opts) (car opts) #f)))
+1472
(or (refuse-if-ambiguous store task-id "reopen" scope)
+1473
(let loop ((rest (scoped-files store scope)))
+1474
(if (null? rest)
+1475
(format "Task ~a not found." task-id)
+1476
(if (uncomplete-task-in-file! (car rest) task-id)
+1477
(format "Reopened task ~a." task-id)
+1478
(loop (cdr rest))))))))
1479
1480
;; Try to uncomplete a task in a specific file. Returns #t if found.
1481
(define (uncomplete-task-in-file! file-path task-id)
@@ -1459,15 +1508,14 @@
1508
1509
;; Update task metadata by ID.
1510
(define (update-task-in-store! store task-id args)
1462
(or (refuse-if-ambiguous store task-id "update")
1463
(let ((files (cons (store-inbox-path store)
1464
(store-project-files store))))
1465
(let loop ((rest files))
1466
(if (null? rest)
1467
(format "Task ~a not found." task-id)
1468
(if (update-task-in-file! (car rest) task-id args)
1469
(format "Updated task ~a." task-id)
1470
(loop (cdr rest))))))))
+1511
(let ((scope (dict-ref args from: #f)))
+1512
(or (refuse-if-ambiguous store task-id "update" scope)
+1513
(let loop ((rest (scoped-files store scope)))
+1514
(if (null? rest)
+1515
(format "Task ~a not found." task-id)
+1516
(if (update-task-in-file! (car rest) task-id args)
+1517
(format "Updated task ~a." task-id)
+1518
(loop (cdr rest))))))))
1519
1520
;; Try to update a task's metadata and/or text in a specific file.
1521
(define (update-task-in-file! file-path task-id args)
@@ -1500,15 +1548,19 @@
1548
1549
;; Move a task by ID to a target project (or inbox if #f).
1550
;;
1503
;; The id is preserved in every direction. Task ids are globally unique
1504
;; random hex (`generate-task-id`) with no per-project id space, so a move
1505
;; has never needed a new one; renumbering only ever stranded references.
1506
(define (move-task-in-store! store task-id target-project)
1507
(or (refuse-if-ambiguous store task-id "move")
1508
(let ((files (cons (store-inbox-path store)
1509
(store-project-files store))))
1510
;; Find the task in any file
1511
(let loop ((rest files))
+1551
;; The id is preserved in every direction. There is no per-project id
+1552
;; space, so a move never needed a new id; renumbering only ever stranded
+1553
;; held references.
+1554
;;
+1555
;; Ids are NOT globally unique -- an earlier version of this comment said
+1556
;; they were, which was inferred from the absence of scoping and is false.
+1557
;; They are random hex with no uniqueness check, so a duplicate is
+1558
;; possible; `refuse-if-ambiguous` is what keeps a move from guessing.
+1559
(define (move-task-in-store! store task-id target-project . opts)
+1560
(let ((scope (if (pair? opts) (car opts) #f)))
+1561
(or (refuse-if-ambiguous store task-id "move" scope)
+1562
;; Find the task in any file the scope allows
+1563
(let loop ((rest (scoped-files store scope)))
1564
(if (null? rest)
1565
(format "Task ~a not found." task-id)
1566
(let ((task (find-and-remove-task! (car rest) task-id)))
test/test-id-ambiguity.sglmodified
@@ -266,3 +266,179 @@
266
(let ((ids (map folio-task-id (folio-project-tasks (proj dir "target")))))
267
(assert-equal 2 (length ids))
268
(assert-false (equal? (car ids) (cadr ids)))))))))
+269
+270
;; ============================================================
+271
;; Id width and deterministic file order
+272
;; ============================================================
+273
+274
(test-group "ids are minted six hex wide, and short ids still work"
+275
(test "a minted id is t- plus six hex"
+276
(call-with-temp-directory
+277
(lambda (dir)
+278
(let* ((store (make-folio-store dir))
+279
(t (car (begin ((make-tool-inbox-add store) (dict text: "x"))
+280
(read-inbox store)))))
+281
(assert-equal 8 (string-length (folio-task-id t)))
+282
(assert-true (string-starts-with? (folio-task-id t) "t-"))))))
+283
+284
;; accept-short/mint-long: the 1274 existing 4-hex ids must keep resolving
+285
(test "a legacy 4-hex id still parses and still mutates"
+286
(call-with-temp-directory
+287
(lambda (dir)
+288
(let ((store (make-folio-store dir)))
+289
(write-file-string (store-inbox-path store)
+290
"# Inbox\n- [ ] legacy item {id: t-a3f2}\n")
+291
(assert-equal "t-a3f2" (folio-task-id (car (read-inbox store))))
+292
(assert-true (string-contains?
+293
(update-task-in-store! store "t-a3f2" (dict priority: "high"))
+294
"Updated"))
+295
(assert-equal "high" (task-priority (car (read-inbox store))))))))
+296
+297
(test "both widths coexist in one store and resolve independently"
+298
(call-with-temp-directory
+299
(lambda (dir)
+300
(let ((store (make-folio-store dir)))
+301
(write-file-string (store-inbox-path store)
+302
"# Inbox\n- [ ] old {id: t-a3f2}\n- [ ] new {id: t-9c1e77}\n")
+303
(complete-task-in-store! store "t-9c1e77")
+304
(let ((after (read-inbox store)))
+305
(assert-false (folio-task-done? (car after)))
+306
(assert-true (folio-task-done? (cadr after)))))))))
+307
+308
(test-group "project file order is deterministic"
+309
(test "store-project-files is sorted, not readdir order"
+310
(call-with-temp-directory
+311
(lambda (dir)
+312
(let ((store (make-folio-store dir)))
+313
(for-each (lambda (n) (mkproject dir n ""))
+314
(list "zeta" "alpha" "mu" "beta"))
+315
(let ((names (map path-basename (store-project-files store))))
+316
(assert-equal (list "alpha.md" "beta.md" "mu.md" "zeta.md")
+317
names)))))))
+318
+319
;; ============================================================
+320
;; The `from` qualifier: address an ambiguous id without renaming
+321
;; ============================================================
+322
+323
(test-group "from: disambiguates a duplicated id"
+324
(test "the refusal message tells the caller how to retry"
+325
(call-with-temp-directory
+326
(lambda (dir)
+327
(let ((store (make-folio-store dir)))
+328
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+329
(write-file-string (store-inbox-path store)
+330
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+331
(let ((result (complete-task-in-store! store "t-dead")))
+332
(assert-true (string-contains? result "Retry with project:")))))))
+333
+334
(test "from: a project completes only that project's copy"
+335
(call-with-temp-directory
+336
(lambda (dir)
+337
(let ((store (make-folio-store dir)))
+338
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+339
(write-file-string (store-inbox-path store)
+340
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+341
(assert-true (string-contains?
+342
(complete-task-in-store! store "t-dead" "real")
+343
"Completed"))
+344
(assert-true (folio-task-done?
+345
(car (folio-project-tasks (proj dir "real")))))
+346
;; the inbox copy is untouched
+347
(assert-false (folio-task-done? (car (read-inbox store))))))))
+348
+349
(test "from: inbox completes only the inbox copy"
+350
(call-with-temp-directory
+351
(lambda (dir)
+352
(let ((store (make-folio-store dir)))
+353
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+354
(write-file-string (store-inbox-path store)
+355
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+356
(complete-task-in-store! store "t-dead" "inbox")
+357
(assert-true (folio-task-done? (car (read-inbox store))))
+358
(assert-false (folio-task-done?
+359
(car (folio-project-tasks (proj dir "real")))))))))
+360
+361
;; t-bf97 in the real estate: three live open copies, none closable
+362
;; without this.
+363
(test "from: picks one of three copies and leaves the others"
+364
(call-with-temp-directory
+365
(lambda (dir)
+366
(let ((store (make-folio-store dir)))
+367
(mkproject dir "one" "- [ ] copy one {id: t-bf97}\n")
+368
(mkproject dir "two" "- [ ] copy two {id: t-bf97}\n")
+369
(mkproject dir "three" "- [ ] copy three {id: t-bf97}\n")
+370
(assert-true (string-contains?
+371
(complete-task-in-store! store "t-bf97" "two")
+372
"Completed"))
+373
(assert-false (folio-task-done? (car (folio-project-tasks (proj dir "one")))))
+374
(assert-true (folio-task-done? (car (folio-project-tasks (proj dir "two")))))
+375
(assert-false (folio-task-done? (car (folio-project-tasks (proj dir "three")))))))))
+376
+377
(test "from: a scope that still matches two is STILL refused"
+378
(call-with-temp-directory
+379
(lambda (dir)
+380
(let ((store (make-folio-store dir)))
+381
;; two copies inside ONE file -- the scope cannot separate them
+382
(mkproject dir "dup" "- [ ] first copy {id: t-dead}\n- [ ] second copy {id: t-dead}\n")
+383
(assert-true (string-contains?
+384
(complete-task-in-store! store "t-dead" "dup")
+385
"REFUSING"))))))
+386
+387
(test "from: a scope naming the wrong project finds nothing"
+388
(call-with-temp-directory
+389
(lambda (dir)
+390
(let ((store (make-folio-store dir)))
+391
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+392
(mkproject dir "elsewhere" "")
+393
(write-file-string (store-inbox-path store)
+394
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+395
(assert-true (string-contains?
+396
(complete-task-in-store! store "t-dead" "elsewhere")
+397
"not found"))
+398
;; and nothing was mutated
+399
(assert-false (folio-task-done? (car (read-inbox store))))
+400
(assert-false (folio-task-done?
+401
(car (folio-project-tasks (proj dir "real")))))))))
+402
+403
(test "move accepts from: to pick its source"
+404
(call-with-temp-directory
+405
(lambda (dir)
+406
(let ((store (make-folio-store dir)))
+407
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+408
(mkproject dir "target" "")
+409
(write-file-string (store-inbox-path store)
+410
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+411
(assert-true (string-contains?
+412
(move-task-in-store! store "t-dead" "target" "inbox")
+413
"Moved"))
+414
;; the probe moved; the real task stayed put
+415
(assert-equal 0 (length (read-inbox store)))
+416
(assert-equal 1 (length (folio-project-tasks (proj dir "real"))))
+417
(assert-equal "probe item"
+418
(folio-task-text
+419
(car (folio-project-tasks (proj dir "target")))))))))
+420
+421
(test "update accepts from: through its args dict"
+422
(call-with-temp-directory
+423
(lambda (dir)
+424
(let ((store (make-folio-store dir)))
+425
(mkproject dir "real" "- [ ] REAL live task {id: t-dead}\n")
+426
(write-file-string (store-inbox-path store)
+427
"# Inbox\n- [ ] probe item {id: t-dead}\n")
+428
(assert-true (string-contains?
+429
(update-task-in-store! store "t-dead"
+430
(dict priority: "high" from: "real"))
+431
"Updated"))
+432
(assert-equal "high" (task-priority
+433
(car (folio-project-tasks (proj dir "real")))))
+434
(assert-false (task-priority (car (read-inbox store))))))))
+435
+436
;; a unique id must not need the qualifier
+437
(test "from: is never required for a unique id"
+438
(call-with-temp-directory
+439
(lambda (dir)
+440
(let* ((store (make-folio-store dir))
+441
(t (inbox-add! store "solo" '())))
+442
(assert-true (string-contains?
+443
(complete-task-in-store! store (folio-task-id t))
+444
"Completed")))))))