aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-14 07:29:57 -0500
committerCraig Jennings <c@cjennings.net>2026-05-14 07:29:57 -0500
commit99252c04a5a0113947681dfb4276116d4aa69983 (patch)
treefbbbbdc64a92a02e3a9e36144dcece7cc9b7bd82
parent3abac9b797e31429b55cd08f5d102275c25b75a4 (diff)
downloadrulesets-99252c04a5a0113947681dfb4276116d4aa69983.tar.gz
rulesets-99252c04a5a0113947681dfb4276116d4aa69983.zip
feat(todo-cleanup): add --sync-child-priority mode for drifted children
When a parent task in todo.org gets reprioritized, its children frequently keep their original (lower) priority cookies, which then mismatches the parent's new importance. The new mode walks every heading with a priority cookie and bumps any direct child whose own cookie is lower (D ranks below A in org's default scheme). Down-only: parents are never bumped up to a child's priority. Priority-less parents and priority-less children are both left alone — sync does not invent priorities. Children opt out by carrying the :no-sync: literal tag, useful for Follow-up:/Spike: sub-tasks that are deliberately deprioritized. The tag match is literal regex against the heading line rather than going through org-get-tags, because org's default tag character class excludes hyphens — :no-sync: would not be parsed as a real tag in batch mode without a custom org-tag-re. org-map-entries visits headings in document order, so a multi-level chain [#A] → [#B] → [#D] collapses to the top priority in one pass: the middle bumps to [#A] before the walk reaches the leaf. wrap-it-up.org Step 3 now invokes --sync-child-priority after --archive-done. --check-child-priority is the report-only alias (--sync-child-priority --check) for previewing before applying. Default cadence is auto-apply, same as --archive-done.
-rw-r--r--.ai/scripts/tests/test-todo-cleanup.el181
-rw-r--r--.ai/scripts/todo-cleanup.el180
-rw-r--r--.ai/workflows/wrap-it-up.org26
3 files changed, 373 insertions, 14 deletions
diff --git a/.ai/scripts/tests/test-todo-cleanup.el b/.ai/scripts/tests/test-todo-cleanup.el
index 9d11b5c..2a9184d 100644
--- a/.ai/scripts/tests/test-todo-cleanup.el
+++ b/.ai/scripts/tests/test-todo-cleanup.el
@@ -30,8 +30,16 @@
;;; Harness
(defun tc-test--reset (&optional check)
- (setq tc-fixes 0 tc-archived 0 tc-issues nil
- tc-check-only (and check t) tc-archive-done t tc-current-file nil))
+ (setq tc-fixes 0 tc-archived 0 tc-bumped 0 tc-issues nil
+ tc-check-only (and check t)
+ tc-archive-done t tc-sync-child-priority nil
+ tc-current-file nil))
+
+(defun tc-test--reset-sync (&optional check)
+ (setq tc-fixes 0 tc-archived 0 tc-bumped 0 tc-issues nil
+ tc-check-only (and check t)
+ tc-archive-done nil tc-sync-child-priority t
+ tc-current-file nil))
(defun tc-test--drop-buffer (file)
(let ((buf (find-buffer-visiting file)))
@@ -315,5 +323,174 @@ from the heading line through (not including) the next level-1 heading or EOF."
;; something actually moved
(should (> (plist-get out :archived) 0)))))
+;;; ---------------------------------------------------------------------------
+;;; Sync-child-priority harness + fixtures
+
+(defun tc-test--sync (content &optional runs check)
+ "Write CONTENT to a temp .org file, run `--sync-child-priority' RUNS times
+\(default 1\). Return a plist: :result final file contents, :bumped count from
+the last run, :issues from the last run. CHECK non-nil ⇒ --check (preview)."
+ (let ((file (make-temp-file "tc-test-sync-" nil ".org"))
+ last-bumped last-issues)
+ (unwind-protect
+ (progn
+ (with-temp-file file (insert content))
+ (dotimes (_ (or runs 1))
+ (tc-test--reset-sync check)
+ (tc-process-file file)
+ (setq last-bumped tc-bumped last-issues tc-issues)
+ (tc-test--drop-buffer file))
+ (list :result (with-temp-buffer (insert-file-contents file)
+ (buffer-string))
+ :bumped last-bumped
+ :issues last-issues))
+ (tc-test--drop-buffer file)
+ (delete-file file))))
+
+(defun tc-test--priority-of (content heading-substring)
+ "Return the priority letter (a string like \"A\") on the first heading line
+in CONTENT that contains HEADING-SUBSTRING, or nil if the heading has no
+priority cookie."
+ (with-temp-buffer
+ (insert content)
+ (goto-char (point-min))
+ (let (found-line found-prio)
+ (while (and (not found-line) (re-search-forward "^\\*+ .*$" nil t))
+ (let ((line (match-string 0)))
+ (when (string-match-p (regexp-quote heading-substring) line)
+ (setq found-line line)
+ (when (string-match "\\[#\\([A-Z]\\)\\]" line)
+ (setq found-prio (match-string 1 line))))))
+ (unless found-line
+ (error "no heading containing %S" heading-substring))
+ found-prio)))
+
+(defun tc-test--sync-bumped-headings (issues)
+ "Return the heading texts of every `:kind' sync-bumped or sync-would entry
+in ISSUES, in document order."
+ (mapcar (lambda (i) (plist-get i :child-heading))
+ (cl-remove-if-not
+ (lambda (i) (memq (plist-get i :kind) '(sync-bumped sync-would)))
+ (reverse issues))))
+
+(defconst tc-test--sync-basic "\
+* Open Work
+** TODO [#B] Parent
+*** TODO [#D] Drifted child
+*** TODO [#B] Already in sync
+")
+
+(defconst tc-test--sync-multi "\
+* Open Work
+** TODO [#B] Parent
+*** TODO [#A] Higher-priority child stays
+*** TODO [#B] Equal-priority child stays
+*** TODO [#C] Lower-priority child bumps
+*** TODO [#D] Way-lower-priority child bumps
+*** TODO Priority-less child stays
+")
+
+(defconst tc-test--sync-no-sync-tag "\
+* Open Work
+** TODO [#B] Parent
+*** TODO [#D] Regular drifted child
+*** TODO [#D] Follow-up: opted-out :no-sync:
+")
+
+(defconst tc-test--sync-priority-less-parent "\
+* Open Work
+** TODO Parent with no priority
+*** TODO [#D] Child with priority should not move
+")
+
+(defconst tc-test--sync-cascade "\
+* Open Work
+** TODO [#A] Top
+*** TODO [#B] Middle
+**** TODO [#D] Leaf
+")
+
+(defconst tc-test--sync-no-change "\
+* Open Work
+** TODO [#B] Parent
+*** TODO [#A] Child higher
+*** TODO [#B] Child equal
+")
+
+;;; ---------------------------------------------------------------------------
+;;; Sync-child-priority tests
+
+(ert-deftest tc-sync-bumps-lower-priority-child ()
+ (let* ((out (tc-test--sync tc-test--sync-basic))
+ (res (plist-get out :result)))
+ (should (= 1 (plist-get out :bumped)))
+ (should (equal "B" (tc-test--priority-of res "Drifted child")))
+ (should (equal "B" (tc-test--priority-of res "Already in sync")))
+ (should (equal "B" (tc-test--priority-of res "Parent")))))
+
+(ert-deftest tc-sync-leaves-higher-and-equal-children-alone ()
+ (let* ((out (tc-test--sync tc-test--sync-multi))
+ (res (plist-get out :result)))
+ (should (= 2 (plist-get out :bumped)))
+ (should (equal "A" (tc-test--priority-of res "Higher-priority child")))
+ (should (equal "B" (tc-test--priority-of res "Equal-priority child")))
+ (should (equal "B" (tc-test--priority-of res "Lower-priority child")))
+ (should (equal "B" (tc-test--priority-of res "Way-lower-priority child")))
+ (should-not (tc-test--priority-of res "Priority-less child"))))
+
+(ert-deftest tc-sync-skips-no-sync-tagged-child ()
+ (let* ((out (tc-test--sync tc-test--sync-no-sync-tag))
+ (res (plist-get out :result)))
+ (should (= 1 (plist-get out :bumped)))
+ (should (equal "B" (tc-test--priority-of res "Regular drifted child")))
+ (should (equal "D" (tc-test--priority-of res "Follow-up: opted-out")))))
+
+(ert-deftest tc-sync-leaves-priority-less-parent-alone ()
+ (let ((out (tc-test--sync tc-test--sync-priority-less-parent)))
+ (should (= 0 (plist-get out :bumped)))
+ (should (equal tc-test--sync-priority-less-parent (plist-get out :result)))))
+
+(ert-deftest tc-sync-cascades-through-multiple-levels ()
+ (let* ((out (tc-test--sync tc-test--sync-cascade))
+ (res (plist-get out :result)))
+ ;; one pass should collapse [#A] → [#B] → [#D] to all [#A] because
+ ;; org-map-entries visits the parent first, bumps the middle, then visits
+ ;; the (now bumped) middle and bumps its leaf
+ (should (= 2 (plist-get out :bumped)))
+ (should (equal "A" (tc-test--priority-of res "Top")))
+ (should (equal "A" (tc-test--priority-of res "Middle")))
+ (should (equal "A" (tc-test--priority-of res "Leaf")))))
+
+(ert-deftest tc-sync-no-change-when-all-children-at-or-above-parent ()
+ (let ((out (tc-test--sync tc-test--sync-no-change)))
+ (should (= 0 (plist-get out :bumped)))
+ (should (equal tc-test--sync-no-change (plist-get out :result)))))
+
+(ert-deftest tc-sync-is-idempotent ()
+ (dolist (fixture (list tc-test--sync-basic
+ tc-test--sync-multi
+ tc-test--sync-no-sync-tag
+ tc-test--sync-priority-less-parent
+ tc-test--sync-cascade
+ tc-test--sync-no-change))
+ (let ((once (plist-get (tc-test--sync fixture 1) :result))
+ (twice (plist-get (tc-test--sync fixture 2) :result)))
+ (should (equal once twice)))))
+
+(ert-deftest tc-sync-check-mode-previews-without-writing ()
+ (let ((out (tc-test--sync tc-test--sync-basic 1 t)))
+ (should (= 1 (plist-get out :bumped)))
+ (should (equal tc-test--sync-basic (plist-get out :result)))
+ (should (member "Drifted child"
+ (tc-test--sync-bumped-headings (plist-get out :issues))))))
+
+(ert-deftest tc-sync-check-mode-is-idempotent ()
+ (let ((once (tc-test--sync tc-test--sync-cascade 1 t))
+ (twice (tc-test--sync tc-test--sync-cascade 2 t)))
+ (should (equal tc-test--sync-cascade (plist-get once :result)))
+ (should (equal tc-test--sync-cascade (plist-get twice :result)))
+ (should (= 2 (plist-get once :bumped)))
+ (should (= 2 (plist-get twice :bumped)))))
+
(provide 'test-todo-cleanup)
;;; test-todo-cleanup.el ends here
diff --git a/.ai/scripts/todo-cleanup.el b/.ai/scripts/todo-cleanup.el
index 4988db0..97be96e 100644
--- a/.ai/scripts/todo-cleanup.el
+++ b/.ai/scripts/todo-cleanup.el
@@ -1,12 +1,14 @@
;;; todo-cleanup.el --- Auto-fix and audit for todo.org hygiene -*- lexical-binding: t; -*-
;;
;; Usage:
-;; emacs --batch -q -l todo-cleanup.el todo.org # apply hygiene fixes in place
-;; emacs --batch -q -l todo-cleanup.el --check todo.org # hygiene report only
-;; emacs --batch -q -l todo-cleanup.el --archive-done todo.org # archive completed subtrees
-;; emacs --batch -q -l todo-cleanup.el --archive-done --check todo.org # preview the archive
+;; emacs --batch -q -l todo-cleanup.el todo.org # apply hygiene fixes in place
+;; emacs --batch -q -l todo-cleanup.el --check todo.org # hygiene report only
+;; emacs --batch -q -l todo-cleanup.el --archive-done todo.org # archive completed subtrees
+;; emacs --batch -q -l todo-cleanup.el --archive-done --check todo.org # preview the archive
+;; emacs --batch -q -l todo-cleanup.el --sync-child-priority todo.org # bump children whose priority drifted below the parent's
+;; emacs --batch -q -l todo-cleanup.el --check-child-priority todo.org # preview the sync (same as --sync-child-priority --check)
;;
-;; Two independent modes:
+;; Three independent modes:
;;
;; * Default (hygiene). Designed for the wrap-it-up workflow: cheap, idempotent,
;; safe to run every session.
@@ -31,6 +33,19 @@
;; skipped with a message. Only direct level-2 children move — a DONE entry
;; nested under an open parent stays put. Archiving is consequential, so it's
;; never run by default; it does *not* also run the hygiene passes.
+;;
+;; * --sync-child-priority (opt-in). Walks every heading with a priority cookie
+;; ([#A]-[#D]) and, for each of its direct child headings whose own priority
+;; is lower (later in the alphabet — D is lower than A), bumps the child's
+;; cookie to match the parent's. Down-only: parents are never adjusted to
+;; match a child. Children with no priority cookie at all are left alone, as
+;; are parents with no priority cookie. A child can opt out of being bumped
+;; by carrying the `:no-sync:' tag — useful for `Follow-up:'/`Spike:' children
+;; that are deliberately deprioritized. Because the walk visits parents
+;; before their descendants in document order, a multi-level chain
+;; ([#A] → [#B] → [#D]) collapses to the top priority in a single pass.
+;; --check-child-priority is the report-only alias for --sync-child-priority
+;; --check.
(require 'org)
(require 'cl-lib)
@@ -41,11 +56,19 @@
(defconst tc-done-states '("DONE" "CANCELLED")
"TODO keywords that mark an entry as completed for `--archive-done'.")
+(defconst tc--priority-cookie-regexp "\\[#\\([A-Z]\\)\\]"
+ "Regexp matching an org priority cookie. Match group 1 is the letter.")
+
+(defconst tc-no-sync-tag "no-sync"
+ "Org tag a child heading carries to opt out of `--sync-child-priority'.")
+
(defvar tc-fixes 0)
(defvar tc-archived 0)
+(defvar tc-bumped 0)
(defvar tc-issues nil)
(defvar tc-check-only nil)
(defvar tc-archive-done nil)
+(defvar tc-sync-child-priority nil)
(defvar tc-current-file nil)
;;; ---------------------------------------------------------------------------
@@ -227,18 +250,123 @@ are reported but not performed."
tc-issues)))))))))
;;; ---------------------------------------------------------------------------
+;;; --sync-child-priority mode
+
+(defun tc--heading-priority-letter ()
+ "Return the priority letter (a character) on the heading at point, or nil
+if the heading has no priority cookie."
+ (save-excursion
+ (org-back-to-heading t)
+ (let ((eol (line-end-position)))
+ (when (re-search-forward tc--priority-cookie-regexp eol t)
+ (string-to-char (match-string 1))))))
+
+(defun tc--priority-lower-p (child parent)
+ "Non-nil when CHILD priority letter ranks lower than PARENT — i.e. later in
+the alphabet, since A is highest in org's default priority scheme."
+ (and child parent (> child parent)))
+
+(defun tc--heading-has-no-sync-tag-p ()
+ "Non-nil when the heading line at point carries `:no-sync:' as a trailing
+tag-style marker. Uses a literal regex match rather than `org-get-tags'
+because org's default tag character class (`org-tag-re') excludes hyphens —
+`no-sync' isn't recognized as a real org tag in batch mode unless the user
+has extended that regex. The literal `:no-sync:' is what wrap-up sessions
+actually type, so match it directly: any `:no-sync:' preceded by whitespace
+on the heading line counts."
+ (save-excursion
+ (org-back-to-heading t)
+ (let ((line (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position))))
+ (string-match-p (format "[ \t]:%s:" (regexp-quote tc-no-sync-tag))
+ line))))
+
+(defun tc--set-heading-priority (letter)
+ "Rewrite the priority cookie on the heading at point to LETTER (a character)."
+ (save-excursion
+ (org-back-to-heading t)
+ (let ((eol (line-end-position)))
+ (when (re-search-forward tc--priority-cookie-regexp eol t)
+ (replace-match (format "[#%c]" letter) t t)))))
+
+(defun tc--direct-children-of-current-heading ()
+ "Return heading positions (beginning of line) of the direct children of the
+heading at point, in document order. Direct children = headings exactly one
+level deeper than the parent."
+ (save-excursion
+ (org-back-to-heading t)
+ (let* ((parent-level (org-current-level))
+ (child-level (1+ parent-level))
+ (subtree-end (save-excursion (org-end-of-subtree t t) (point)))
+ (positions nil))
+ (forward-line 1)
+ (while (re-search-forward "^\\(\\*+\\)[ \t]" subtree-end t)
+ (let ((lvl (length (match-string 1)))
+ (pos (match-beginning 0)))
+ (when (= lvl child-level)
+ (push pos positions))))
+ (nreverse positions))))
+
+(defun tc-sync-child-priority-at-heading ()
+ "If the heading at point carries a priority cookie, bump any direct child
+heading whose own priority is lower, skipping children tagged
+`tc-no-sync-tag'. A priority-less parent is a no-op; priority-less children
+are left untouched (down-only does not invent priorities)."
+ (let ((parent (tc--heading-priority-letter)))
+ (when parent
+ (let ((parent-heading (org-get-heading t t t t)))
+ (dolist (child-pos (tc--direct-children-of-current-heading))
+ (save-excursion
+ (goto-char child-pos)
+ (let ((child (tc--heading-priority-letter)))
+ (when (and child
+ (tc--priority-lower-p child parent)
+ (not (tc--heading-has-no-sync-tag-p)))
+ (let ((child-heading (org-get-heading t t t t))
+ (child-line (line-number-at-pos)))
+ (cl-incf tc-bumped)
+ (if tc-check-only
+ (push (list :kind 'sync-would
+ :file tc-current-file
+ :line child-line
+ :child-heading child-heading
+ :parent-heading parent-heading
+ :from (char-to-string child)
+ :to (char-to-string parent))
+ tc-issues)
+ (tc--set-heading-priority parent)
+ (push (list :kind 'sync-bumped
+ :file tc-current-file
+ :line child-line
+ :child-heading child-heading
+ :parent-heading parent-heading
+ :from (char-to-string child)
+ :to (char-to-string parent))
+ tc-issues)))))))))))
+
+(defun tc-sync-child-priority-in-file ()
+ "Walk every heading in the buffer and run `tc-sync-child-priority-at-heading'.
+`org-map-entries' visits headings in document order, so parents are bumped
+before their descendants — a [#A] → [#B] → [#D] chain collapses in one pass."
+ (org-map-entries #'tc-sync-child-priority-at-heading nil 'file))
+
+;;; ---------------------------------------------------------------------------
;;; Driver + reporting
(defun tc-process-file (file)
(setq tc-current-file (file-name-nondirectory file))
(with-current-buffer (find-file-noselect file)
(org-mode)
- (if tc-archive-done
- (tc-archive-done-in-file)
+ (cond
+ (tc-archive-done
+ (tc-archive-done-in-file))
+ (tc-sync-child-priority
+ (tc-sync-child-priority-in-file))
+ (t
;; Pass 1: auto-fix bogus state logs (or report under --check).
(org-map-entries #'tc-fix-bogus-state-log-in-entry nil 'file)
;; Pass 2: detect orphan planning lines (always report-only).
- (org-map-entries #'tc-detect-orphan-planning-in-entry nil 'file))
+ (org-map-entries #'tc-detect-orphan-planning-in-entry nil 'file)))
(when (and (not tc-check-only) (buffer-modified-p))
(save-buffer))))
@@ -287,8 +415,26 @@ are reported but not performed."
(plist-get i :heading)
(plist-get i :detail)))))))
+(defun tc--emit-sync-report ()
+ (princ (format "todo-cleanup --sync-child-priority: %d child priority cookie(s) %s%s\n"
+ tc-bumped
+ (if tc-check-only "would bump" "bumped")
+ (if tc-check-only " — CHECK MODE (no writes)" "")))
+ (dolist (i (reverse tc-issues))
+ (pcase (plist-get i :kind)
+ ((or 'sync-bumped 'sync-would)
+ (princ (format " %s:%d: [#%s] → [#%s] %s (under: %s)\n"
+ (plist-get i :file)
+ (plist-get i :line)
+ (plist-get i :from)
+ (plist-get i :to)
+ (plist-get i :child-heading)
+ (plist-get i :parent-heading)))))))
+
(defun tc-emit-report ()
- (if tc-archive-done (tc--emit-archive-report) (tc--emit-hygiene-report)))
+ (cond (tc-archive-done (tc--emit-archive-report))
+ (tc-sync-child-priority (tc--emit-sync-report))
+ (t (tc--emit-hygiene-report))))
(defun tc-main ()
;; Strip our flags from `command-line-args-left' so emacs's own arg parser
@@ -299,9 +445,17 @@ are reported but not performed."
(when (member "--archive-done" command-line-args-left)
(setq tc-archive-done t)
(setq command-line-args-left (delete "--archive-done" command-line-args-left)))
+ (when (member "--sync-child-priority" command-line-args-left)
+ (setq tc-sync-child-priority t)
+ (setq command-line-args-left (delete "--sync-child-priority" command-line-args-left)))
+ ;; --check-child-priority is the report-only alias for
+ ;; `--sync-child-priority --check'.
+ (when (member "--check-child-priority" command-line-args-left)
+ (setq tc-sync-child-priority t tc-check-only t)
+ (setq command-line-args-left (delete "--check-child-priority" command-line-args-left)))
(if (null command-line-args-left)
(progn
- (princ "Usage: emacs --batch -q -l todo-cleanup.el [--check] [--archive-done] FILE...\n")
+ (princ "Usage: emacs --batch -q -l todo-cleanup.el [--check] [--archive-done | --sync-child-priority | --check-child-priority] FILE...\n")
(kill-emacs 1))
(let ((files command-line-args-left))
(setq command-line-args-left nil)
@@ -318,7 +472,11 @@ during a test run the trailing args are things like `-f
ert-run-tests-batch-and-exit'."
(and command-line-args-left
(cl-every (lambda (a)
- (cond ((member a '("--check" "--archive-done")) t)
+ (cond ((member a '("--check"
+ "--archive-done"
+ "--sync-child-priority"
+ "--check-child-priority"))
+ t)
((string-prefix-p "-" a) nil)
(t (file-readable-p a))))
command-line-args-left)))
diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org
index 5c1af26..03ee520 100644
--- a/.ai/workflows/wrap-it-up.org
+++ b/.ai/workflows/wrap-it-up.org
@@ -107,6 +107,30 @@ Preview the moves without writing:
emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org
#+end_src
+*** Sync child priorities
+
+#+begin_src bash
+[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --sync-child-priority todo.org
+#+end_src
+
+=--sync-child-priority= walks every heading with a priority cookie =[#A]=–=[#D]= and, for each of its direct child headings whose own priority cookie is /lower/ (later in the alphabet — D is below A), bumps the child to match the parent. Down-only: parents are never bumped up to match a higher-priority child. Children without a priority cookie are left alone, as are parents without one. The walk visits parents before descendants, so a multi-level chain (=[#A]= → =[#B]= → =[#D]=) collapses to the top priority in a single pass. Idempotent.
+
+Opt-out for deliberately-lower children: tag the heading =:no-sync:= (the literal six-character tag, including the hyphen). The script matches the tag literally on the heading line, so it works whether or not the surrounding emacs config has extended =org-tag-re= to allow hyphens.
+
+#+begin_example
+*** TODO [#D] Follow-up: VAD :no-sync:
+#+end_example
+
+Use this for =Follow-up:=, =Spike:=, =Stretch:= sub-tasks that are deliberately deprioritized below their parent — without the tag, the wrap-up would silently bump them back up.
+
+Preview the bumps without writing:
+
+#+begin_src bash
+emacs --batch -q -l .ai/scripts/todo-cleanup.el --check-child-priority todo.org
+#+end_src
+
+(=--check-child-priority= is the report-only alias for =--sync-child-priority --check=.)
+
** Step 3.5: Linear ticket-state hygiene (skip if project doesn't use Linear)
If the project uses Linear and has any tickets currently in *Dev Review* assigned to Craig, sweep them before the wrap-up commit. The check is fast and keeps the board honest — tickets stuck in Dev Review after their PR merges hide actual work-in-progress.
@@ -288,7 +312,7 @@ Before considering wrap-up complete:
- [ ] =.ai/session-context.org= =* Summary= section populated
- [ ] File renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=
- [ ] =.ai/session-context.org= no longer exists
-- [ ] =todo-cleanup.el= ran — hygiene pass + =--archive-done= (if =todo.org= exists at project root)
+- [ ] =todo-cleanup.el= ran — hygiene pass + =--archive-done= + =--sync-child-priority= (if =todo.org= exists at project root)
- [ ] Any orphan-planning-line warnings reviewed (fix or accept)
- [ ] Linear Dev-Review sweep ran; any merged-PR tickets moved to Done or PM Acceptance (skip if project doesn't use Linear)
- [ ] After wrap-up commit + push, =git status --short= is empty OR every remaining line has an explicit user-deferred decision logged in the valediction