devflow-kit 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +438 -1
  2. package/README.md +15 -11
  3. package/dist/agents/code.md +9 -15
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +11 -13
  6. package/dist/agents/git.md +1 -1
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +7 -10
  9. package/dist/agents/review.md +6 -4
  10. package/dist/agents/skim.md +2 -2
  11. package/dist/agents/triage.md +2 -2
  12. package/dist/cli/commands/capture.js +1 -1
  13. package/dist/cli/commands/curation-move-io.js +102 -0
  14. package/dist/cli/commands/curation-move-step.js +374 -0
  15. package/dist/cli/commands/{learning.js → curation.js} +302 -99
  16. package/dist/cli/commands/gate-level-prompts.js +426 -0
  17. package/dist/cli/commands/gates.js +299 -0
  18. package/dist/cli/commands/init-seed.js +16 -5
  19. package/dist/cli/commands/init.js +263 -54
  20. package/dist/cli/commands/memory.js +20 -2
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +81 -9
  23. package/dist/cli.js +4 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +77 -73
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +76 -83
  28. package/dist/commands/dynamic-plan.md +5 -5
  29. package/dist/commands/dynamic-profile.md +5 -5
  30. package/dist/commands/dynamic-tickets.md +5 -5
  31. package/dist/commands/explore.md +6 -6
  32. package/dist/commands/implement.md +68 -102
  33. package/dist/commands/plan.md +19 -29
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +29 -31
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/assets.js +20 -20
  39. package/dist/core/curate-tuning-config.js +5 -6
  40. package/dist/core/curation-move-notice.js +84 -0
  41. package/dist/core/curation-move-skip.js +36 -0
  42. package/dist/core/curation-move.js +35 -0
  43. package/dist/core/curation-queue-cleanup.js +134 -0
  44. package/dist/core/curation-store.js +98 -0
  45. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  46. package/dist/core/echo-value.js +48 -0
  47. package/dist/core/evidence-policy.js +18 -6
  48. package/dist/core/feature-switch.js +95 -19
  49. package/dist/core/fs-atomic.js +1 -1
  50. package/dist/core/gate-level.js +221 -0
  51. package/dist/core/git.js +16 -14
  52. package/dist/core/ledger-root.js +82 -22
  53. package/dist/core/linked-path.js +4 -4
  54. package/dist/core/manifest-lock.js +238 -0
  55. package/dist/core/manifest.js +84 -24
  56. package/dist/core/migrations.js +1 -13
  57. package/dist/core/model-discovery.js +7 -1
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +15 -15
  60. package/dist/core/pool.js +26 -0
  61. package/dist/core/project-paths.js +103 -34
  62. package/dist/core/queue-drain.js +2 -2
  63. package/dist/core/tracked-areas.js +35 -0
  64. package/dist/core/tracker.js +6 -34
  65. package/dist/{learning-off → curation-off}/agents/code.md +9 -15
  66. package/dist/{learning-off → curation-off}/agents/fix.md +11 -13
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +7 -10
  68. package/dist/{learning-off → curation-off}/agents/review.md +5 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
  70. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  71. package/dist/{learning-off → curation-off}/commands/code-review.md +74 -70
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +73 -80
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +65 -99
  76. package/dist/{learning-off → curation-off}/commands/plan.md +15 -25
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +26 -28
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
  80. package/dist/hud/config.js +1 -1
  81. package/dist/hud/index.js +6 -6
  82. package/dist/hud/render.js +3 -3
  83. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  84. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +20 -14
  85. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +20 -20
  86. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +20 -20
  87. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +99 -67
  88. package/dist/targets/claude-code/installer.js +15 -15
  89. package/dist/targets/claude-code/language-stamp.js +5 -5
  90. package/dist/targets/claude-code/post-install.js +2 -2
  91. package/dist/targets/claude-code/tracker-servers.js +1 -1
  92. package/package.json +3 -2
  93. package/src/assets/agents/_partials/_code.mds +16 -17
  94. package/src/assets/agents/code.mds +4 -7
  95. package/src/assets/agents/curate.md +12 -14
  96. package/src/assets/agents/debug.mds +6 -6
  97. package/src/assets/agents/design.mds +18 -18
  98. package/src/assets/agents/fix.mds +6 -5
  99. package/src/assets/agents/git.mds +1 -1
  100. package/src/assets/agents/harden.mds +7 -7
  101. package/src/assets/agents/knowledge.mds +23 -23
  102. package/src/assets/agents/refactor.mds +2 -2
  103. package/src/assets/agents/research.mds +15 -15
  104. package/src/assets/agents/review.mds +45 -43
  105. package/src/assets/agents/skim.mds +11 -11
  106. package/src/assets/agents/synthesize.md +2 -2
  107. package/src/assets/agents/triage.mds +22 -22
  108. package/src/assets/agents/validate.md +13 -26
  109. package/src/assets/commands/_partials/_decisions.mds +13 -13
  110. package/src/assets/commands/_partials/_engine.mds +8 -8
  111. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  112. package/src/assets/commands/_partials/_preamble.mds +4 -4
  113. package/src/assets/commands/_partials/_settings.mds +3 -3
  114. package/src/assets/commands/_partials/_wave.mds +17 -24
  115. package/src/assets/commands/bug-analysis.mds +37 -48
  116. package/src/assets/commands/code-review.mds +90 -86
  117. package/src/assets/commands/debug.mds +14 -14
  118. package/src/assets/commands/dynamic-build.mds +78 -78
  119. package/src/assets/commands/dynamic-plan.mds +22 -22
  120. package/src/assets/commands/dynamic-profile.mds +2 -2
  121. package/src/assets/commands/dynamic-tickets.mds +8 -8
  122. package/src/assets/commands/explore.mds +20 -20
  123. package/src/assets/commands/implement.mds +92 -118
  124. package/src/assets/commands/plan.mds +41 -51
  125. package/src/assets/commands/release.mds +12 -12
  126. package/src/assets/commands/research.mds +19 -19
  127. package/src/assets/commands/resolve.mds +56 -50
  128. package/src/assets/commands/self-review.mds +20 -20
  129. package/src/assets/mds/git/_ops.mds +3 -1
  130. package/src/assets/mds/git/_ops_pr.mds +1 -1
  131. package/src/assets/mds/git/_ops_steps.mds +13 -7
  132. package/src/assets/mds/git/_pr.mds +7 -4
  133. package/src/assets/mds/tracker/_common.mds +23 -12
  134. package/src/assets/mds/tracker/_github.mds +2 -9
  135. package/src/assets/mds/tracker/_jira.mds +2 -15
  136. package/src/assets/mds/tracker/_linear.mds +2 -15
  137. package/src/assets/rules/quality.md +1 -0
  138. package/src/assets/scripts/ci-wait.cjs +22 -4
  139. package/src/assets/scripts/claude-md-audit.cjs +66 -51
  140. package/src/assets/scripts/hooks/background-memory-update +406 -97
  141. package/src/assets/scripts/hooks/capture-prompt +12 -12
  142. package/src/assets/scripts/hooks/capture-question +11 -11
  143. package/src/assets/scripts/hooks/capture-turn +12 -12
  144. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  145. package/src/assets/scripts/hooks/curation-move +293 -0
  146. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  147. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  148. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  149. package/src/assets/scripts/hooks/git-marker +81 -0
  150. package/src/assets/scripts/hooks/json-helper.cjs +145 -51
  151. package/src/assets/scripts/hooks/json-parse +19 -4
  152. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  153. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
  154. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  155. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  156. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  157. package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
  158. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
  159. package/src/assets/scripts/hooks/memory-permission +358 -0
  160. package/src/assets/scripts/hooks/memory-worker +1 -1
  161. package/src/assets/scripts/hooks/pre-compact-memory +6 -16
  162. package/src/assets/scripts/hooks/queue-append +228 -103
  163. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  164. package/src/assets/scripts/hooks/session-start-context +145 -71
  165. package/src/assets/scripts/hooks/session-start-memory +27 -46
  166. package/src/assets/scripts/lib/project-config.cjs +31 -9
  167. package/src/assets/scripts/resolve-base.cjs +1043 -0
  168. package/src/assets/scripts/resolve-settings.cjs +238 -46
  169. package/src/assets/skills/agent-references/SKILL.md +9 -2
  170. package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
  171. package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
  172. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  173. package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
  174. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
  175. package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
  176. package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
  177. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  178. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  179. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  180. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  181. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  182. package/templates/lint-pack/.lint-pack.conf +86 -0
  183. package/templates/lint-pack/README.md +190 -0
  184. package/templates/lint-pack/lint-pack.sh +905 -0
  185. package/dist/core/learning-queue-cleanup.js +0 -27
  186. package/dist/core/learning-store.js +0 -61
  187. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  188. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  189. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  190. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  191. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  192. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  193. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  194. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  195. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  196. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  197. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -54,7 +54,7 @@ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
54
54
  source "$SCRIPT_DIR/hook-log-init" "pre-compact-memory"
55
55
 
56
56
  # Memory gate: the machine switch, narrowed by this checkout's project.json /
57
- # config.json `features.memory`, read by the helper every memory/learning gate
57
+ # config.json `features.memory`, read by the helper every memory/curation gate
58
58
  # shares (D-FEATURES-NARROW-ONLY, see queue-append).
59
59
  source "$SCRIPT_DIR/queue-append" || { echo "pre-compact-memory: failed to source queue-append" >&2; exit 1; }
60
60
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
@@ -112,27 +112,18 @@ if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
112
112
  dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
113
113
  fi
114
114
 
115
- # Snapshot current WORKING-MEMORY.md (preserves session context through compaction).
116
- # The snapshot goes into the backup the next session start injects, so a working
117
- # memory a symbolic link leads to is treated as absent (D-HOOKS-NO-SYMLINK).
118
- MEMORY_SNAPSHOT=""
119
- if df_file_below "$PROJECT_ROOT" "$MEMORY_DIR/WORKING-MEMORY.md"; then
120
- MEMORY_SNAPSHOT=$(head -c 65536 "$MEMORY_DIR/WORKING-MEMORY.md")
121
- dbg "MEMORY_SNAPSHOT_LENGTH=${#MEMORY_SNAPSHOT}"
122
- fi
123
-
124
115
  # Write backup JSON
125
116
  # D-BACKUP-RENAME: the backup is written whole into a copy beside it, then
126
117
  # renamed over it; it is never truncated and refilled in place. Reason:
127
- # session-start-memory reads it twice, and a read landing between a truncate
128
- # and the write that refills it would meet a partial file; a rename hands every
129
- # read either the previous backup or the new one. SEC-2: the copy is created
118
+ # a read landing between a truncate and the write that refills it would meet a
119
+ # partial file; a rename hands every read either the previous backup or the new
120
+ # one. SEC-2: the copy is created
130
121
  # under umask 077, and only where nothing stands at its name: builtin tests come
131
122
  # first, so an entry already there, a link to a FIFO or a device included, is
132
123
  # never opened (noclobber alone would open one, as no regular file stands there),
133
124
  # and noclobber then makes the create itself exclusive. The rename gives the
134
- # backup the copy's inode and mode, so the backup, which holds the working
135
- # memory, is 0600 whatever the caller's umask (as in queue-append). A write or
125
+ # backup the copy's inode and mode, so the Git-state backup is 0600 whatever
126
+ # the caller's umask (as in queue-append). A write or
136
127
  # rename that fails removes whatever holds the copy's name and keeps the
137
128
  # previous backup; the bootstrap below still runs.
138
129
  #
@@ -157,7 +148,6 @@ elif [ ! -e "$BACKUP_TMP" ] && [ ! -L "$BACKUP_TMP" ] && (umask 077 && set -o no
157
148
  --arg status "$GIT_STATUS" \
158
149
  --arg log "$GIT_LOG" \
159
150
  --arg diff "$GIT_DIFF_STAT" \
160
- --arg memory "$MEMORY_SNAPSHOT" \
161
151
  > "$BACKUP_TMP") && mv "$BACKUP_TMP" "$BACKUP_FILE"; then
162
152
  log "Wrote backup: $BACKUP_FILE"
163
153
  dbg "Wrote backup: $BACKUP_FILE"
@@ -6,35 +6,47 @@
6
6
  # under a lock. Used by capture-prompt, capture-turn, and capture-question so the
7
7
  # per-feature dual-queue-append logic exists in exactly one place.
8
8
  #
9
- # Source-order requirement: source json-parse (for _HAS_JQ) and learning-lock (for
10
- # learning_lock_acquire/learning_lock_release, which itself requires get-mtime) BEFORE
9
+ # Source-order requirement: source json-parse (for _HAS_JQ) and curation-lock (for
10
+ # curation_lock_acquire/curation_lock_release, which itself requires get-mtime) BEFORE
11
11
  # calling queue_append_row or queue_append_both, and define log/dbg. This file
12
- # sources its sibling git-marker itself, for df_no_symlink_below. Sourcing it only
13
- # defines functions -- no side effects until they are called.
12
+ # sources its siblings git-marker (for df_no_symlink_below) and curation-move (for the
13
+ # ledger folder's names, df_curation_move_names) itself. Sourcing it only defines
14
+ # functions -- no side effects until they are called.
14
15
  #
15
16
  # queue_append_row <root> <queue_file> <role> <content> <ts>
16
17
  # Appends one JSONL row {role, content, ts} to <queue_file>, which lies below
17
- # <root>: the project root for the memory queue, the ledger root for the learning
18
+ # <root>: the project root for the memory queue, the ledger root for the curation
18
19
  # queue. When the queue, or a folder between <root> and it, is a symbolic link,
19
20
  # nothing is written: the skip is logged once and the call returns 0, as a
20
- # skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it
21
- # creates the queue's folder if absent, and the file with mode 0600 (umask 077).
21
+ # skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it builds the
22
+ # row first (a row that fails to build writes nothing), then makes one builtin
23
+ # append under umask 077, so a queue it creates has mode 0600. The umask is the
24
+ # caller's again when the append returns, so a folder made later in the same hook
25
+ # has the mode its parent's umask gives. A ledger queue follows the creator rule
26
+ # (git-marker, df_ledger_queue): it is appended to in the
27
+ # old learning/ folder while that exists, which is never created here; the new
28
+ # curation/ folder is created only when the old name is absent. An append that
29
+ # finds the old folder gone (the one-time move ran meanwhile) looks again and goes
30
+ # on in the new folder, so a capture never drops a row and never waits. Any other
31
+ # queue's folder is created if absent.
22
32
  # After appending, truncates from 200 to the newest 100 lines under a lock
23
- # (learning_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
33
+ # (curation_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
24
34
  # The append itself is intentionally lock-free (accepted-class race shared with
25
35
  # the pre-existing memory design -- see the design doc's "Append-vs-claim race"
26
36
  # note).
27
37
  #
28
- # queue_append_both <memory_root> <memory_queue> <learning_root> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
29
- # Calls queue_append_row for each queue whose *_enabled flag is "true", with the
30
- # root that queue lies below. Each queue is gated independently -- callers
31
- # compute memory_enabled/learning_enabled themselves (see queue_read_gates below
32
- # for the at-most-one-fork read of both).
38
+ # queue_append_both <memory_root> <memory_queue> <curation_root> <curation_queue> <memory_enabled> <curation_enabled> <role> <content> <ts>
39
+ # Builds the row once, then appends it as queue_append_row does to each queue whose
40
+ # *_enabled flag is "true", with the root that queue lies below. Each queue is gated
41
+ # independently -- callers compute memory_enabled/curation_enabled themselves (see
42
+ # queue_read_gates below for the at-most-one-fork read of both). With neither queue
43
+ # enabled it builds nothing.
33
44
  #
34
45
  # queue_read_gates <manifest_path> [<root>]
35
- # Reads BOTH switches, `memory` and `learning`, in at most ONE subprocess fork
46
+ # Reads BOTH switches, `memory` and `curation` (curation is read under both of
47
+ # its keys, the current one and the pre-rename copy), in at most ONE subprocess fork
36
48
  # (AC-P1 -- at most one gate-read fork per capture hook, not two; none when
37
- # the fast paths settle it). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false")
49
+ # the fast paths settle it). Sets _QG_MEMORY and _QG_CURATION ("true"/"false")
38
50
  # in the caller's scope. The two values are newline-separated
39
51
  # rather than using a control-character delimiter: they are always the literal
40
52
  # strings "true"/"false", never arbitrary content, so a plain newline split is
@@ -44,35 +56,43 @@
44
56
  # machine switch is on AND neither repository file sets `features.<name>` to
45
57
  # the literal `false` -- the rule resolve-settings.cjs folds, held to the same
46
58
  # answers by the shared table tests/fixtures/settings-switch-table.ts (TP-49).
47
- # A repository can switch memory or learning off for itself, never back on.
59
+ # A repository can switch memory or curation off for itself, never back on.
48
60
  # machine <manifest_path>, the ~/.devflow/manifest.json `devflow init` and
49
- # `devflow memory|learning --enable/--disable` write. Only an
61
+ # `devflow memory|curation --enable/--disable` write. Only an
50
62
  # explicit boolean `false` switches a feature off -- an absent,
51
63
  # unreadable or malformed manifest, a missing key, or a non-boolean
52
64
  # value leaves it ON (fail-open), the rule
53
65
  # isMachineFeatureOn() applies in the CLI.
54
66
  # project <root>/.devflow/project.json (team-committed) and
55
67
  # personal <root>/.devflow/config.json (this worktree's own), read ONLY under
56
- # `features.memory` / `features.learning`. The retired top-level
57
- # keys (`memory`, `learning`, `decisions`) and `features.decisions`
58
- # narrow nothing (#378: reading them let a feature run in every repo
59
- # but one). An invalid file -- unparseable, over 4096 bytes, a BOM,
60
- # a symlink, a duplicated key -- narrows nothing.
68
+ # `features.memory` and, for curation, `features.curation` or
69
+ # `features.learning` (a narrow-only union: a literal false in
70
+ # either key turns curation off, and each key is parsed on its
71
+ # own). The retired top-level keys (`memory`, `curation`,
72
+ # `learning`, `decisions`) and `features.decisions` narrow nothing
73
+ # (#378: reading them let a feature run in every repo but one). An
74
+ # invalid file -- unparseable, over 4096 bytes, a BOM, a symlink, a
75
+ # duplicated key -- narrows nothing.
61
76
  # <root> is the checkout's toplevel (DF_ROOT, which every caller holds as
62
- # $PROJECT_ROOT), for learning too: project.json is committed per branch and
77
+ # $PROJECT_ROOT), for curation too: project.json is committed per branch and
63
78
  # config.json lives per worktree, so both are read where the session runs, as
64
79
  # resolve-settings.cjs reads them -- never at DF_LEDGER_ROOT, which only says
65
80
  # where the queue lands. Without <root> (or with an empty one) no repository
66
81
  # file is read.
67
82
  #
68
- # D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, learning reads
69
- # `features.learning` when it is a boolean, else the pre-rename
70
- # `features.decisions` -- readManifest's migration precedence
71
- # exactly, so a legacy `decisions: false` switches learning off before any
72
- # command has healed the file. Only a boolean `false` in whichever key decides
73
- # is off, as above.
83
+ # The machine rule for curation (#529), the one isMachineFeatureOn() applies:
84
+ # off when `features.curation` or `features.learning` is a boolean false; else
85
+ # on when either is a boolean true; else, when neither is a boolean, the
86
+ # pre-rename `features.decisions` decides if it is a boolean; else on. A
87
+ # non-boolean value is ignored for that key only, so a disagreeing pair
88
+ # resolves to off.
74
89
  #
75
- # Every hook that gates on memory or learning reads it through here: the
90
+ # D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, the pre-rename
91
+ # `features.decisions` is read when neither current key is a boolean --
92
+ # readManifest's migration precedence exactly, so a legacy `decisions: false`
93
+ # switches curation off before any command has healed the file.
94
+ #
95
+ # Every hook that gates on memory or curation reads it through here: the
76
96
  # capture hooks, session-start-context (Sections 1-2), memory-worker,
77
97
  # session-start-memory, pre-compact-memory and background-memory-update. The
78
98
  # callers pass <manifest_path> as $HOME/.devflow/manifest.json, the machine
@@ -81,80 +101,179 @@
81
101
 
82
102
  # queue_append_row checks its queue with df_no_symlink_below, so this helper sources
83
103
  # git-marker itself rather than trusting every caller to have done so first. One that
84
- # failed to source is a command not found: the refusing branch, so nothing is written.
85
- source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
104
+ # is missing is a command not found: the refusing branch, so nothing is written.
105
+ # The ledger folder's names come from the move shim (df_curation_move_names), sourced
106
+ # the same way and refused the same way.
107
+ #
108
+ # Each source is guarded by a builtin readability test, because bash 3.2 under `set -e`
109
+ # ends the shell at a `source` of a missing file, even inside `|| true`. Without the test
110
+ # the refusing branch would never run, and the capture hooks would stop here.
111
+ [ -r "${BASH_SOURCE[0]%/*}/git-marker" ] && source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
112
+ [ -r "${BASH_SOURCE[0]%/*}/curation-move" ] && source "${BASH_SOURCE[0]%/*}/curation-move" 2>/dev/null || true
86
113
 
87
- queue_append_row() {
88
- local _qar_root="$1" _qar_file="$2" _qar_role="$3" _qar_content="$4" _qar_ts="$5"
114
+ # The caller's umask, read once per process. A queue is created 0600, so the append
115
+ # runs under umask 077 -- and the rest of the hook must run under the umask its parent
116
+ # gave it, or a folder the next append makes would be 0700. `$(umask)` is one subshell
117
+ # with no exec, and it runs on the first append only; each append gives this value
118
+ # back, so the umask the process holds when it first appends is the one it keeps.
119
+ _QAR_UMASK=""
120
+
121
+ # _qar_build_row <role> <content> <ts> -- sets _QAR_ROW to the JSONL row, built by jq
122
+ # (--arg, --argjson) or node (JSON.stringify), never by string concatenation. A
123
+ # predicate: it succeeds only for a row that built, and a row that fails to build is
124
+ # empty, so nothing partial can reach a queue. Call it in an `if` or an `||` list.
125
+ _qar_build_row() {
126
+ _QAR_ROW=""
127
+ if [ "$_HAS_JQ" = "true" ]; then
128
+ _QAR_ROW=$(jq -n -c --arg role "$1" --arg content "$2" --argjson ts "$3" \
129
+ '{role: $role, content: $content, ts: $ts}') || _QAR_ROW=""
130
+ else
131
+ _QAR_ROW=$(node -e "process.stdout.write(JSON.stringify({role:process.argv[1],content:process.argv[2],ts:parseInt(process.argv[3])}))" \
132
+ -- "$1" "$2" "$3") || _QAR_ROW=""
133
+ fi
134
+ if [ -z "$_QAR_ROW" ]; then
135
+ log "Queue skipped: the row could not be built; nothing written"
136
+ return 1
137
+ fi
138
+ return 0
139
+ }
140
+
141
+ # _qar_write <queue_file> <row> -- one builtin append, under umask 077 so a queue it
142
+ # creates has mode 0600 (SEC-2). The umask is the caller's again before it returns,
143
+ # whether the append worked or not. The redirection error is silenced by 2>/dev/null
144
+ # written BEFORE it. A predicate: it fails when the row was not written.
145
+ _qar_write() {
146
+ local _qar_status=0
147
+ umask 077
148
+ printf '%s\n' "$2" 2>/dev/null >> "$1" || _qar_status=1
149
+ umask "$_QAR_UMASK" 2>/dev/null || true
150
+ return "$_qar_status"
151
+ }
152
+
153
+ # _qar_append <root> <queue_file> <row> -- appends <row> to <queue_file>, which lies
154
+ # below <root>, and sets _QAR_FILE to the queue it reached (the old ledger folder, or
155
+ # the new one after a move). A predicate: it fails when nothing was written -- a link on
156
+ # the path (D-HOOKS-NO-SYMLINK: the skip is logged once) or a queue that could not be
157
+ # written -- and a skipped capture is not an error for the hook.
158
+ _qar_append() {
159
+ local _qar_root="$1" _qar_file="$2" _qar_row="$3"
160
+ local _qar_given="$2" _qar_old="" _qar_new=""
161
+ _QAR_FILE=""
162
+
163
+ # The names the creator rule below needs. A shim that failed to source is a command
164
+ # not found, so nothing is appended: no queue is written without them.
165
+ if ! df_curation_move_names 2>/dev/null; then
166
+ log "Queue skipped: the ledger folder names are unavailable; nothing written"
167
+ return 1
168
+ fi
169
+ _qar_old="$DF_CM_OLD_FOLDER"
170
+ _qar_new="$DF_CM_NEW_FOLDER"
171
+
172
+ # The creator rule (git-marker, df_ledger_queue): a ledger queue is appended to in
173
+ # the old folder while that folder is present, so a new name never hides an old
174
+ # ledger. Any other queue (the memory queue) comes back unchanged. A helper that
175
+ # failed to source is a command not found, so the queue stays as given and the
176
+ # check below, from the same library, refuses it.
177
+ if df_ledger_queue "$_qar_file" "$_qar_old" "$_qar_new"; then _qar_file="$DF_LEDGER_QUEUE"; fi
89
178
 
90
179
  # D-HOOKS-NO-SYMLINK (git-marker): a skipped append is a skipped capture.
91
180
  if ! df_no_symlink_below "$_qar_root" "$_qar_file"; then
92
181
  log "Queue skipped: a symbolic link sits on the path to $_qar_file; nothing written"
93
- return 0
182
+ return 1
94
183
  fi
95
- mkdir -p "${_qar_file%/*}" 2>/dev/null || true
96
184
 
97
- if [ ! -f "$_qar_file" ]; then
98
- (umask 077 && touch "$_qar_file") 2>/dev/null || true
99
- fi
185
+ # A folder is made only where this append is its creator: never the old ledger
186
+ # folder, which a move may rename away at any moment, and which a make-again would
187
+ # bring back beside the new one. An old folder that is gone leaves the append to
188
+ # fail, and the re-probe below decides again. A folder is made under the caller's
189
+ # umask: only the write below runs under 077.
190
+ case "${_qar_file%/*}" in
191
+ */"$_qar_old") ;;
192
+ *) mkdir -p "${_qar_file%/*}" 2>/dev/null || true ;;
193
+ esac
100
194
 
101
- if [ "$_HAS_JQ" = "true" ]; then
102
- jq -n -c --arg role "$_qar_role" --arg content "$_qar_content" --argjson ts "$_qar_ts" \
103
- '{role: $role, content: $content, ts: $ts}' >> "$_qar_file"
104
- else
105
- node -e "process.stdout.write(JSON.stringify({role:process.argv[1],content:process.argv[2],ts:parseInt(process.argv[3])})+String.fromCharCode(10))" \
106
- -- "$_qar_role" "$_qar_content" "$_qar_ts" >> "$_qar_file"
195
+ if [ -z "$_QAR_UMASK" ]; then _QAR_UMASK=$(umask) || _QAR_UMASK=""; fi
196
+ if ! _qar_write "$_qar_file" "$_qar_row"; then
197
+ # The old folder may have been moved after the probe. Look again: when the old
198
+ # name is gone, the creator makes the new folder once and the row goes there.
199
+ df_ledger_queue "$_qar_given" "$_qar_old" "$_qar_new" || true
200
+ if [ "$DF_LEDGER_QUEUE" != "$_qar_file" ] \
201
+ && df_no_symlink_below "$_qar_root" "$DF_LEDGER_QUEUE" \
202
+ && mkdir -p "${DF_LEDGER_QUEUE%/*}" 2>/dev/null \
203
+ && _qar_write "$DF_LEDGER_QUEUE" "$_qar_row"; then
204
+ _qar_file="$DF_LEDGER_QUEUE"
205
+ else
206
+ log "Queue append failed: $_qar_file could not be written"
207
+ return 1
208
+ fi
107
209
  fi
210
+ _QAR_FILE="$_qar_file"
211
+ return 0
212
+ }
108
213
 
109
- # Overflow safety: >200 lines -> keep newest 100, locked to prevent a multi-session race.
110
- if [ -f "$_qar_file" ]; then
111
- local _qar_lines
112
- _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
113
- if [ "$_qar_lines" -gt 200 ]; then
114
- local _qar_lock="${_qar_file}.lock"
115
- if learning_lock_acquire "$_qar_lock" 2; then
116
- _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
117
- if [ "$_qar_lines" -gt 200 ]; then
118
- local _qar_tmp="${_qar_file}.tmp.$$"
119
- # SEC-2: the trimmed copy is created under umask 077, so the renamed
120
- # queue keeps mode 0600. mv replaces the inode, so the queue takes the
121
- # copy's mode, and a chmod after the mv would leave a window. The copy is
122
- # created only where nothing stands at its name: builtin tests come first,
123
- # so an entry already there, a link to a FIFO or a device included, is
124
- # never opened (noclobber alone would open one, as no regular file stands
125
- # there), and noclobber then makes the create itself exclusive. Such an
126
- # entry is removed instead, and the trim waits for a later append.
127
- if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
128
- log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
129
- dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
130
- else
131
- rm -f "$_qar_tmp" 2>/dev/null || true
132
- log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
133
- fi
214
+ # _qar_trim_overflow <queue_file> -- overflow safety: past 200 lines, keep the newest
215
+ # 100, locked to prevent a multi-session race.
216
+ _qar_trim_overflow() {
217
+ local _qar_file="$1" _qar_lines="" _qar_lock="" _qar_tmp=""
218
+ [ -f "$_qar_file" ] || return 0
219
+ _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
220
+ if [ "$_qar_lines" -gt 200 ]; then
221
+ _qar_lock="${_qar_file}.lock"
222
+ if curation_lock_acquire "$_qar_lock" 2; then
223
+ _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
224
+ if [ "$_qar_lines" -gt 200 ]; then
225
+ _qar_tmp="${_qar_file}.tmp.$$"
226
+ # SEC-2: the trimmed copy is created under umask 077, so the renamed
227
+ # queue keeps mode 0600. mv replaces the inode, so the queue takes the
228
+ # copy's mode, and a chmod after the mv would leave a window. The copy is
229
+ # created only where nothing stands at its name: builtin tests come first,
230
+ # so an entry already there, a link to a FIFO or a device included, is
231
+ # never opened (noclobber alone would open one, as no regular file stands
232
+ # there), and noclobber then makes the create itself exclusive. Such an
233
+ # entry is removed instead, and the trim waits for a later append.
234
+ if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
235
+ log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
236
+ dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
237
+ else
238
+ rm -f "$_qar_tmp" 2>/dev/null || true
239
+ log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
134
240
  fi
135
- learning_lock_release "$_qar_lock"
136
241
  fi
242
+ curation_lock_release "$_qar_lock"
137
243
  fi
138
244
  fi
245
+ return 0
246
+ }
247
+
248
+ queue_append_row() {
249
+ _qar_build_row "$3" "$4" "$5" || return 0
250
+ if _qar_append "$1" "$2" "$_QAR_ROW"; then _qar_trim_overflow "$_QAR_FILE"; fi
251
+ return 0
139
252
  }
140
253
 
141
254
  queue_append_both() {
142
255
  local _qab_memory_root="$1" _qab_memory_queue="$2"
143
- local _qab_learning_root="$3" _qab_learning_queue="$4"
144
- local _qab_memory_enabled="$5" _qab_learning_enabled="$6"
145
- local _qab_role="$7" _qab_content="$8" _qab_ts="$9"
256
+ local _qab_curation_root="$3" _qab_curation_queue="$4"
257
+ local _qab_memory_enabled="$5" _qab_curation_enabled="$6"
258
+ local _qab_role="$7" _qab_content="$8" _qab_ts="$9" _qab_row=""
259
+
260
+ if [ "$_qab_memory_enabled" != "true" ] && [ "$_qab_curation_enabled" != "true" ]; then return 0; fi
261
+ # One row for both queues: it is built once, and each enabled queue takes the same line.
262
+ _qar_build_row "$_qab_role" "$_qab_content" "$_qab_ts" || return 0
263
+ _qab_row="$_QAR_ROW"
146
264
 
147
265
  if [ "$_qab_memory_enabled" = "true" ]; then
148
- queue_append_row "$_qab_memory_root" "$_qab_memory_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
266
+ if _qar_append "$_qab_memory_root" "$_qab_memory_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
149
267
  fi
150
- if [ "$_qab_learning_enabled" = "true" ]; then
151
- queue_append_row "$_qab_learning_root" "$_qab_learning_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
268
+ if [ "$_qab_curation_enabled" = "true" ]; then
269
+ if _qar_append "$_qab_curation_root" "$_qab_curation_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
152
270
  fi
271
+ return 0
153
272
  }
154
273
 
155
274
  # _qg_repo_file_can_narrow <file> -- succeeds when <file> COULD set
156
- # features.memory or features.learning to false, so only the parser can decide
157
- # it; fails when it certainly cannot. Builtins only: no fork.
275
+ # features.memory, features.curation or features.learning to false, so only the
276
+ # parser can decide it; fails when it certainly cannot. Builtins only: no fork.
158
277
  #
159
278
  # D-GATES-FAST-PATH (repository files): the read is bounded -- `-n 4097`, which
160
279
  # bash 3.2 supports -- and `read -d ''` succeeds only when it stops short of the
@@ -164,14 +283,14 @@ queue_append_both() {
164
283
  # symlink and refuses a directory or FIFO, so nothing here can block; the parser
165
284
  # itself refuses the symlink. A `\u` escape is the only JSON spelling of a
166
285
  # letter other than the letter itself, so without one a narrowing file shows
167
- # `"features"`, then `{`, then `"memory"` or `"learning"`, then `false`,
168
- # literally and in that order. `.` spans newlines (POSIX ERE without
286
+ # `"features"`, then `{`, then `"memory"`, `"curation"` or `"learning"`, then
287
+ # `false`, literally and in that order. `.` spans newlines (POSIX ERE without
169
288
  # REG_NEWLINE), so the match can only over-report -- an over-report costs one
170
289
  # fork and the parser's answer, never a wrong one. No `"decisions"` and no
171
290
  # top-level key before `features` is ever matched.
172
291
  _qg_repo_file_can_narrow() {
173
292
  local _qgf_text=""
174
- local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|learning)"[[:space:]]*:[[:space:]]*false'
293
+ local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|curation|learning)"[[:space:]]*:[[:space:]]*false'
175
294
  [ -f "$1" ] || return 1
176
295
  if IFS= read -r -d '' -n 4097 _qgf_text 2>/dev/null < "$1"; then return 1; fi
177
296
  case "$_qgf_text" in *'\u'*) return 0 ;; esac
@@ -180,22 +299,24 @@ _qg_repo_file_can_narrow() {
180
299
 
181
300
  queue_read_gates() {
182
301
  local _qg_manifest="${1:-}" _qg_root="${2:-}" _qg_text="" _qg_parse="false" _qg_repo="false" _qg_fields=""
302
+ local _qg_scripts="${BASH_SOURCE[0]%/*}/.."
183
303
  _QG_MEMORY="true"
184
- _QG_LEARNING="true"
304
+ _QG_CURATION="true"
185
305
 
186
306
  if [ -n "$_qg_manifest" ] && [ -f "$_qg_manifest" ]; then
187
307
  # D-GATES-FAST-PATH: every capture hook and every session start pays this
188
308
  # read, and on most machines both switches are on. Only a JSON boolean
189
- # `false` under a "memory", "learning" or "decisions" key can switch anything
190
- # off, and a `\u` escape is the only JSON spelling of a letter other than the
191
- # letter itself, so without one such a key appears literally. A text holding
192
- # no `\u` and no key-then-`false` sequence therefore cannot switch anything
193
- # off -- valid or not, it reads both-on either way -- and is settled by shell
194
- # builtins (read, case, [[ =~ ]]) with no jq/node fork; everything else takes
195
- # the full parse below. `read -d ''` succeeds only on reaching a NUL byte,
196
- # i.e. before the end of the file, so a text it cut short is parsed in full
197
- # too. An unreadable file reads empty and fails open, as the parser would.
198
- local _qg_re='"(memory|learning|decisions)"[[:space:]]*:[[:space:]]*false'
309
+ # `false` under a "memory", "curation", "learning" or "decisions" key can
310
+ # switch anything off, and a `\u` escape is the only JSON spelling of a
311
+ # letter other than the letter itself, so without one such a key appears
312
+ # literally. A text holding no `\u` and no key-then-`false` sequence
313
+ # therefore cannot switch anything off -- valid or not, it reads both-on
314
+ # either way -- and is settled by shell builtins (read, case, [[ =~ ]]) with
315
+ # no jq/node fork; everything else takes the full parse below. `read -d ''`
316
+ # succeeds only on reaching a NUL byte, i.e. before the end of the file, so a
317
+ # text it cut short is parsed in full too. An unreadable file reads empty
318
+ # and fails open, as the parser would.
319
+ local _qg_re='"(memory|curation|learning|decisions)"[[:space:]]*:[[:space:]]*false'
199
320
  IFS= read -r -d '' _qg_text 2>/dev/null < "$_qg_manifest" && _qg_parse="true"
200
321
  case "$_qg_text" in *'\u'*) _qg_parse="true" ;; esac
201
322
  if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
@@ -216,7 +337,7 @@ queue_read_gates() {
216
337
  # manifest too, read only when the fast path above flagged it (an unflagged
217
338
  # manifest is both-on), so a false in all three layers still costs one fork.
218
339
  # jq cannot see duplicate keys, so this path is node whatever _HAS_JQ says.
219
- local _qg_scripts="${BASH_SOURCE[0]%/*}/.." _qg_manifest_arg=""
340
+ local _qg_manifest_arg=""
220
341
  [ "$_qg_parse" = "true" ] && _qg_manifest_arg="$_qg_manifest"
221
342
  _qg_fields=$(node -e "
222
343
  const S = require(require('path').resolve(process.argv[1], 'resolve-settings.cjs'));
@@ -225,7 +346,7 @@ queue_read_gates() {
225
346
  try { m = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8')); } catch (_) { m = undefined; }
226
347
  }
227
348
  const w = S.foldSettings(Object.assign({ manifest: m }, S.readRepoLayers(process.argv[3]))).switches;
228
- process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.learning.on));
349
+ process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.curation.on));
229
350
  " -- "$_qg_scripts" "$_qg_manifest_arg" "$_qg_root" 2>/dev/null) || _qg_fields=""
230
351
  fi
231
352
 
@@ -237,20 +358,24 @@ queue_read_gates() {
237
358
  # `try` covers a manifest whose top level or `features` is not an object; a
238
359
  # parse failure empties the output, which reads as "not switched off". The
239
360
  # comma produces two raw-output lines (newline-separated) in one jq process.
361
+ # Curation (#529): off when a boolean curation or learning is false; else,
362
+ # when neither is a boolean, off when decisions is false (isMachineFeatureOn).
240
363
  _qg_fields=$(jq -r '
241
364
  (if (try .features.memory catch null) == false then "false" else "true" end),
242
- (if (try (.features | if (.learning | type) == "boolean" then .learning else .decisions end) catch null) == false
365
+ (if (try (.features
366
+ | ([.curation, .learning] | map(select(type == "boolean"))) as $c
367
+ | if ($c | length) > 0 then any($c[]; . == false) else .decisions == false end
368
+ ) catch false)
243
369
  then "false" else "true" end)
244
370
  ' "$_qg_manifest" 2>/dev/null) || _qg_fields=""
245
371
  else
372
+ # No jq: the fold is the resolver's own machineSwitchOn (resolve-settings.cjs),
373
+ # so the machine rule is not copied here. One node fork, as for jq's one process.
246
374
  _qg_fields=$(node -e "
375
+ const S = require(require('path').resolve(process.argv[2], 'resolve-settings.cjs'));
247
376
  const m = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
248
- const f = m !== null && typeof m === 'object' ? m.features : undefined;
249
- const isObj = f !== null && typeof f === 'object';
250
- const on = (k) => !(isObj && f[k] === false);
251
- const learnKey = isObj && typeof f.learning !== 'boolean' ? 'decisions' : 'learning';
252
- process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
253
- " -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
377
+ process.stdout.write(String(S.machineSwitchOn(m, 'memory')) + String.fromCharCode(10) + String(S.machineSwitchOn(m, 'curation')));
378
+ " -- "$_qg_manifest" "$_qg_scripts" 2>/dev/null) || _qg_fields=""
254
379
  fi
255
380
  fi
256
381
 
@@ -258,7 +383,7 @@ queue_read_gates() {
258
383
  case "$_qg_fields" in
259
384
  true$'\n'true|true$'\n'false|false$'\n'true|false$'\n'false)
260
385
  _QG_MEMORY="${_qg_fields%%$'\n'*}"
261
- _QG_LEARNING="${_qg_fields#*$'\n'}"
386
+ _QG_CURATION="${_qg_fields#*$'\n'}"
262
387
  ;;
263
388
  esac
264
389
 
@@ -8,7 +8,7 @@
8
8
  # root, mirroring the TS CLI's getGitRoot() (src/core/git.ts) so the shell
9
9
  # side anchors identically.
10
10
  #
11
- # Sourced by: the memory/learning/session hooks and ensure-devflow-init.
11
+ # Sourced by: the memory/curation/session hooks and ensure-devflow-init.
12
12
  # Sourced helper: uses `return`-free pure functions; _-prefixed locals (never
13
13
  # clobbers caller vars). Safe under `set -e` (git failure is guarded with || true).
14
14
  #
@@ -53,7 +53,7 @@ _df_nongit_root() {
53
53
  # df_resolve_roots <cwd> sets TWO roots in the caller's scope, from ONE git call:
54
54
  # DF_ROOT — the checkout's toplevel: where per-checkout data lives
55
55
  # (memory, the .gitignore carve-out, the knowledge bases).
56
- # DF_LEDGER_ROOT — where the learning ledger and its queue live.
56
+ # DF_LEDGER_ROOT — where the curation ledger and its queue live.
57
57
  #
58
58
  # D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
59
59
  # --worktree`) has its own toplevel, so a ledger anchored there restarts its