devflow-kit 3.7.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 (184) hide show
  1. package/CHANGELOG.md +359 -1
  2. package/README.md +11 -11
  3. package/dist/agents/code.md +2 -4
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +6 -7
  6. package/dist/agents/git.md +1 -1
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +2 -4
  9. package/dist/agents/review.md +4 -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 +34 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +240 -54
  20. package/dist/cli/commands/memory.js +2 -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 +2 -2
  24. package/dist/commands/bug-analysis.md +24 -35
  25. package/dist/commands/code-review.md +32 -36
  26. package/dist/commands/debug.md +4 -4
  27. package/dist/commands/dynamic-build.md +25 -24
  28. package/dist/commands/dynamic-plan.md +4 -4
  29. package/dist/commands/dynamic-profile.md +4 -4
  30. package/dist/commands/dynamic-tickets.md +4 -4
  31. package/dist/commands/explore.md +5 -5
  32. package/dist/commands/implement.md +52 -81
  33. package/dist/commands/plan.md +5 -5
  34. package/dist/commands/release.md +4 -4
  35. package/dist/commands/research.md +5 -5
  36. package/dist/commands/resolve.md +28 -30
  37. package/dist/commands/self-review.md +4 -12
  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 +11 -5
  47. package/dist/core/evidence-policy.js +1 -1
  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 +10 -2
  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 +58 -14
  56. package/dist/core/migrations.js +1 -13
  57. package/dist/core/observations.js +2 -2
  58. package/dist/core/plugins.js +15 -15
  59. package/dist/core/pool.js +26 -0
  60. package/dist/core/project-paths.js +103 -34
  61. package/dist/core/queue-drain.js +2 -2
  62. package/dist/core/tracked-areas.js +35 -0
  63. package/dist/core/tracker.js +1 -1
  64. package/dist/{learning-off → curation-off}/agents/code.md +2 -4
  65. package/dist/{learning-off → curation-off}/agents/fix.md +6 -7
  66. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  67. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  68. package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
  69. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  70. package/dist/{learning-off → curation-off}/commands/code-review.md +29 -33
  71. package/dist/{learning-off → curation-off}/commands/debug.md +1 -1
  72. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +22 -21
  73. package/dist/{learning-off → curation-off}/commands/explore.md +1 -1
  74. package/dist/{learning-off → curation-off}/commands/implement.md +49 -78
  75. package/dist/{learning-off → curation-off}/commands/plan.md +1 -1
  76. package/dist/{learning-off → curation-off}/commands/resolve.md +25 -27
  77. package/dist/{learning-off → curation-off}/commands/self-review.md +1 -9
  78. package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
  79. package/dist/hud/config.js +1 -1
  80. package/dist/hud/index.js +6 -6
  81. package/dist/hud/render.js +3 -3
  82. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  83. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  84. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  85. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  86. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  87. package/dist/targets/claude-code/installer.js +15 -15
  88. package/dist/targets/claude-code/language-stamp.js +5 -5
  89. package/dist/targets/claude-code/post-install.js +2 -2
  90. package/dist/targets/claude-code/tracker-servers.js +1 -1
  91. package/package.json +3 -2
  92. package/src/assets/agents/_partials/_code.mds +11 -11
  93. package/src/assets/agents/code.mds +2 -2
  94. package/src/assets/agents/curate.md +8 -8
  95. package/src/assets/agents/debug.mds +6 -6
  96. package/src/assets/agents/design.mds +18 -18
  97. package/src/assets/agents/fix.mds +6 -5
  98. package/src/assets/agents/git.mds +1 -1
  99. package/src/assets/agents/harden.mds +7 -7
  100. package/src/assets/agents/knowledge.mds +23 -23
  101. package/src/assets/agents/refactor.mds +2 -2
  102. package/src/assets/agents/research.mds +15 -15
  103. package/src/assets/agents/review.mds +43 -43
  104. package/src/assets/agents/skim.mds +11 -11
  105. package/src/assets/agents/triage.mds +22 -22
  106. package/src/assets/agents/validate.md +13 -26
  107. package/src/assets/commands/_partials/_decisions.mds +13 -13
  108. package/src/assets/commands/_partials/_engine.mds +8 -8
  109. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  110. package/src/assets/commands/_partials/_preamble.mds +4 -4
  111. package/src/assets/commands/_partials/_settings.mds +2 -2
  112. package/src/assets/commands/_partials/_wave.mds +7 -7
  113. package/src/assets/commands/bug-analysis.mds +37 -48
  114. package/src/assets/commands/code-review.mds +46 -50
  115. package/src/assets/commands/debug.mds +14 -14
  116. package/src/assets/commands/dynamic-build.mds +38 -37
  117. package/src/assets/commands/dynamic-plan.mds +22 -22
  118. package/src/assets/commands/dynamic-profile.mds +2 -2
  119. package/src/assets/commands/dynamic-tickets.mds +8 -8
  120. package/src/assets/commands/explore.mds +20 -20
  121. package/src/assets/commands/implement.mds +77 -98
  122. package/src/assets/commands/plan.mds +27 -27
  123. package/src/assets/commands/release.mds +12 -12
  124. package/src/assets/commands/research.mds +19 -19
  125. package/src/assets/commands/resolve.mds +56 -50
  126. package/src/assets/commands/self-review.mds +20 -20
  127. package/src/assets/mds/git/_ops.mds +3 -1
  128. package/src/assets/mds/git/_ops_pr.mds +1 -1
  129. package/src/assets/mds/git/_ops_steps.mds +10 -4
  130. package/src/assets/mds/git/_pr.mds +7 -4
  131. package/src/assets/rules/quality.md +1 -0
  132. package/src/assets/scripts/hooks/background-memory-update +2 -2
  133. package/src/assets/scripts/hooks/capture-prompt +12 -12
  134. package/src/assets/scripts/hooks/capture-question +11 -11
  135. package/src/assets/scripts/hooks/capture-turn +12 -12
  136. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  137. package/src/assets/scripts/hooks/curation-move +293 -0
  138. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  139. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  140. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  141. package/src/assets/scripts/hooks/git-marker +81 -0
  142. package/src/assets/scripts/hooks/json-helper.cjs +134 -51
  143. package/src/assets/scripts/hooks/json-parse +1 -1
  144. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  145. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
  146. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  147. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  149. package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
  150. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
  151. package/src/assets/scripts/hooks/memory-worker +1 -1
  152. package/src/assets/scripts/hooks/pre-compact-memory +6 -16
  153. package/src/assets/scripts/hooks/queue-append +228 -103
  154. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  155. package/src/assets/scripts/hooks/session-start-context +143 -69
  156. package/src/assets/scripts/hooks/session-start-memory +3 -43
  157. package/src/assets/scripts/lib/project-config.cjs +12 -5
  158. package/src/assets/scripts/resolve-base.cjs +1043 -0
  159. package/src/assets/scripts/resolve-settings.cjs +42 -18
  160. package/src/assets/skills/agent-references/SKILL.md +5 -2
  161. package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
  162. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  163. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
  164. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  165. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  166. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  167. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  168. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  169. package/templates/lint-pack/.lint-pack.conf +86 -0
  170. package/templates/lint-pack/README.md +190 -0
  171. package/templates/lint-pack/lint-pack.sh +905 -0
  172. package/dist/core/learning-queue-cleanup.js +0 -27
  173. package/dist/core/learning-store.js +0 -61
  174. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  175. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  176. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  177. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  178. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  179. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  180. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  181. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  182. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  183. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  184. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -1,17 +1,17 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # SessionStart Hook: Cross-Feature Context Injection
4
- # Always-on hook that injects learning context as additionalContext. Sections
5
- # 1–2 are gated by the `features.learning` switch in ~/.devflow/manifest.json,
4
+ # Always-on hook that injects curation context as additionalContext. Sections
5
+ # 1–2 are gated by the `features.curation` switch in ~/.devflow/manifest.json,
6
6
  # which this checkout's project.json / config.json can only narrow
7
7
  # (D-FEATURES-NARROW-ONLY) — this hook itself is never disabled. They and the .gitignore carve-out run only in a git project that is
8
- # not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
8
+ # not the home directory (D-HOOKS-GIT-ONLY), and they read the curation ledger of
9
9
  # the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
10
10
  #
11
11
  # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines),
12
12
  # then the path of the decisions index when it lists entries.
13
- # Section 2: Learning maintenance directive — when captured turns are pending in
14
- # the learning queue (or a crashed run left a stale .processing batch), instructs
13
+ # Section 2: Curation maintenance directive — when captured turns are pending in
14
+ # the curation queue (or a crashed run left a stale .processing batch), instructs
15
15
  # the main model to spawn the background Curate agent with the resolved model.
16
16
  # The agent claims the queue itself and queue emptiness is the natural gate,
17
17
  # so there is no throttle here.
@@ -23,6 +23,11 @@
23
23
  # Section 4: Legacy install notice — one line when this repository still carries
24
24
  # a retired project-local devflow install (its .claude/settings.json registers
25
25
  # devflow hooks), pointing at `devflow uninstall --scope local`.
26
+ # Section 4b: Curation move skipped notice — one fixed line when the one-time move of
27
+ # the old ledger folder left the project alone because git tracks files in a curation
28
+ # data folder (`tracked`) or did not answer (`unknown`); the `curation-move` shim sets
29
+ # DF_CURATION_MOVE_SKIPPED, and on a session that starts, resumes or is cleared the line
30
+ # tells the main model to tell the user once.
26
31
  # Section 5: Compaction resume directive — on a `compact` SessionStart, one fixed
27
32
  # line telling the model to re-read the devflow command it was running from the
28
33
  # phase in progress (D-COMPACT-RESUME-DIRECTIVE).
@@ -75,9 +80,9 @@ source "$SCRIPT_DIR/json-parse" || { echo "session-start-context: failed to sour
75
80
  if [ "$_JSON_AVAILABLE" = "false" ]; then exit 0; fi
76
81
 
77
82
  # One subprocess yields both fields: the working directory and the SessionStart
78
- # `source` (startup|resume|clear|compact) that Section 5 gates on, so the directive
79
- # costs no fork of its own. Sections 3 and 4 keep reading `source` themselves, where
80
- # they gate (startup|clear), so neither depends on this value.
83
+ # `source` (startup|resume|clear|compact) that Sections 4b (startup|resume|clear) and 5
84
+ # (compact) gate on, so neither costs a fork of its own. Sections 3 and 4 keep reading
85
+ # `source` themselves, where they gate (startup|clear), so neither depends on this value.
81
86
  _SC_FIELDS=$(printf '%s' "$INPUT" | json_extract_cwd_field "source")
82
87
  CWD="${_SC_FIELDS%%$'\001'*}"
83
88
  SESSION_SOURCE="${_SC_FIELDS#*$'\001'}"
@@ -91,7 +96,7 @@ dbg "CWD=$CWD"
91
96
 
92
97
  # Normal logging, set up before the carve-out below: its skips, and those of the
93
98
  # ensure-root-gitignore it sources, are written with log(), and with memory and
94
- # learning both off this hook is the only one that reaches the carve-out.
99
+ # curation both off this hook is the only one that reaches the carve-out.
95
100
  source "$SCRIPT_DIR/hook-log-init" "session-start-context"
96
101
 
97
102
  # Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
@@ -121,7 +126,7 @@ fi
121
126
 
122
127
  # Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
123
128
  # git-project session regardless of feature toggles, so memory-off projects
124
- # (learning/knowledge only) still get .devflow/ ignored — this is the
129
+ # (curation/knowledge only) still get .devflow/ ignored — this is the
125
130
  # memory-independent path that fixes the gitignore/memory coupling. Single
126
131
  # source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
127
132
  # block context injection. Marker keeps it O(1). A `.devflow` that is a symbolic
@@ -138,7 +143,7 @@ fi
138
143
  CONTEXT=""
139
144
 
140
145
  # The devflow machine root, always $HOME/.devflow (D-ONE-HOME): Section 3's
141
- # tracker files and the manifest the learning gate reads are machine-wide, not
146
+ # tracker files and the manifest the curation gate reads are machine-wide, not
142
147
  # project-scoped (the project's own .devflow is PROJECT_DEVFLOW_DIR below).
143
148
  TRACKER_DEVFLOW_DIR="$HOME/.devflow"
144
149
 
@@ -163,12 +168,12 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
163
168
  # quote, a backtick, `$` or `;` suppresses the directive that embeds it rather
164
169
  # than interpolating an unproven value. That is the fail-closed direction — the
165
170
  # directive is an optimisation — but it must not be a SILENT one: a refused
166
- # Learning root with pending work emits the fixed `--- LEARNING PAUSED ---`
171
+ # Curation root with pending work emits the fixed `--- CURATION PAUSED ---`
167
172
  # notice instead (Section 2), which interpolates nothing, so the user learns why
168
173
  # the queue never drains rather than finding it capped at 200 rows one day.
169
174
  #
170
175
  # `+` is admitted: Claude Code names a worktree for a slash branch with it
171
- # (`feat/x` → `feat+x`), so refusing it would silence learning in exactly the
176
+ # (`feat/x` → `feat+x`), so refusing it would silence curation in exactly the
172
177
  # linked worktrees users open most, and it is inert inside a double-quoted prompt
173
178
  # string — it closes nothing, escapes nothing and expands nothing.
174
179
  #
@@ -191,8 +196,8 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
191
196
  # worktree's root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
192
197
  # checked for them, not the checkout's own; only Section 3 interpolates
193
198
  # $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
194
- # ~/.devflow shape suppress the Learning directive as well — a value Section 2
195
- # never embeds, silently disabling the whole learning pipeline for any machine
199
+ # ~/.devflow shape suppress the Curation directive as well — a value Section 2
200
+ # never embeds, silently disabling the whole curation pipeline for any machine
196
201
  # whose home directory carries a space. A gate must refuse a sink its value
197
202
  # actually reaches and no other, or the fail-closed direction stops being the
198
203
  # safe one.
@@ -222,40 +227,73 @@ esac
222
227
 
223
228
  PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
224
229
  LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
225
- LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
230
+ # The ledger folder's names come from the move shim (curation-move), the one shell place
231
+ # that holds them. Sourcing it only defines functions. The source is guarded by a builtin
232
+ # readability test, because bash 3.2 ends a `set -e` shell at a `source` of a missing file,
233
+ # even inside `|| true`. Without the shim the hook cannot name the folder: the move and the
234
+ # curation sections are skipped, and every other section runs.
235
+ [ -r "$SCRIPT_DIR/curation-move" ] && source "$SCRIPT_DIR/curation-move" 2>/dev/null || true
236
+ CURATION_NAMES_OK="true"
237
+ df_curation_move_names 2>/dev/null || CURATION_NAMES_OK=""
238
+ CURATION_DIR="$LEDGER_DEVFLOW_DIR/$DF_CM_NEW_FOLDER"
239
+ if [ -z "$CURATION_NAMES_OK" ]; then
240
+ CURATION_DIR=""
241
+ log "Curation skipped: the ledger folder names are unavailable (curation-move is missing)"
242
+ fi
226
243
 
227
- # --- Learning gate: the machine switch, narrowed by this checkout ---
244
+ # --- Curation gate: the machine switch, narrowed by this checkout ---
228
245
  # D-FEATURES-NARROW-ONLY (see queue-append): ~/.devflow/manifest.json's
229
- # features.learning, narrowed by PROJECT_ROOT's project.json / config.json, is
246
+ # features.curation, narrowed by PROJECT_ROOT's project.json / config.json, is
230
247
  # read by the same helper the capture hooks use, so the directive and the queue
231
- # it drains can never disagree about whether learning is on. The manifest lives
248
+ # it drains can never disagree about whether curation is on. The manifest lives
232
249
  # under the machine root captured above, not the project-scoped
233
250
  # PROJECT_DEVFLOW_DIR. Neither file present, or neither holding a `false`, costs
234
251
  # no fork (D-GATES-FAST-PATH).
235
252
  source "$SCRIPT_DIR/queue-append" || { echo "session-start-context: failed to source queue-append" >&2; exit 1; }
236
253
  queue_read_gates "$TRACKER_DEVFLOW_DIR/manifest.json" "$PROJECT_ROOT"
237
- LEARNING_ENABLED="$_QG_LEARNING"
238
- # Learning is project work: off outside a git project (D-HOOKS-GIT-ONLY above).
239
- [ -n "$PROJECT_OK" ] || LEARNING_ENABLED="false"
254
+ CURATION_ENABLED="$_QG_CURATION"
255
+ # Curation is project work: off outside a git project (D-HOOKS-GIT-ONLY above), and off
256
+ # where the ledger folder has no name.
257
+ [ -n "$PROJECT_OK" ] || CURATION_ENABLED="false"
258
+ [ -n "$CURATION_NAMES_OK" ] || CURATION_ENABLED="false"
259
+
260
+ # --- The one-time ledger folder move (curation-move) ---
261
+ # The old learning/ folder moves to curation/ here, at every session start, resume,
262
+ # clear and compact: on the LEDGER root (the main worktree's, in a linked worktree),
263
+ # before Section 1's heal can create a folder, and whether or not the curation
264
+ # switch is on, because the old folder holds the project's decisions either way. The
265
+ # `retry` word clears the four markers of an earlier attempt (refused, busy because a
266
+ # fresh queue claim held the move, tracked and unknown), so a refused, claimed or skipped
267
+ # root is looked at once per session. A held, refused or skipped move leaves the old
268
+ # folder in place. A skipped move (git tracks files in a data folder or a backup folder,
269
+ # or git did not answer) sets DF_CURATION_MOVE_SKIPPED to `tracked` or `unknown`, and
270
+ # Section 4b tells the user.
271
+ DF_CURATION_MOVE_SKIPPED=""
272
+ if [ -n "$PROJECT_OK" ]; then
273
+ df_curation_move "$LEDGER_ROOT" retry 2>/dev/null || true
274
+ fi
240
275
 
241
276
  # --- Section 1: Project Decisions TL;DR ---
242
- if [ "$LEARNING_ENABLED" = "true" ]; then
243
- # Heal older installs that have .devflow/ but not .devflow/learning/, never
277
+ if [ "$CURATION_ENABLED" = "true" ]; then
278
+ # Heal older installs that have .devflow/ but not .devflow/curation/, never
244
279
  # through a symbolic link (D-HOOKS-NO-SYMLINK, git-marker).
245
- if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
246
- if df_no_symlink_below "$LEDGER_ROOT" "$LEARNING_DIR"; then
247
- mkdir -p "$LEARNING_DIR" 2>/dev/null || true
248
- else
249
- log "Skipped: a symbolic link sits on the path to $LEARNING_DIR; nothing written"
280
+ # The creator rule (git-marker, df_make_ledger_folder): the folder made is the new one
281
+ # only when the old name is absent, so a new folder never hides an old ledger, and a
282
+ # creator never makes the old folder. A link on the path is the one refusal logged.
283
+ if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$CURATION_DIR" ]; then
284
+ _SC_MAKE_RC=0
285
+ df_make_ledger_folder "$LEDGER_ROOT" "$LEDGER_DEVFLOW_DIR" "$DF_CM_OLD_FOLDER" "$DF_CM_NEW_FOLDER" || _SC_MAKE_RC=$?
286
+ if [ "$_SC_MAKE_RC" -eq 2 ]; then
287
+ log "Skipped: a symbolic link sits on the path to $DF_LEDGER_FOLDER; nothing written"
250
288
  fi
251
289
  fi
252
- if [ -d "$LEARNING_DIR" ]; then
253
- # D-HOOKS-NO-SYMLINK (git-marker): this section and the next read the learning
290
+ if [ -d "$CURATION_DIR" ]; then
291
+ # D-HOOKS-NO-SYMLINK (git-marker): this section and the next read the curation
254
292
  # files into the context, or name them to the model and the Curate agent, so
255
293
  # each is read only where no symbolic link sits on its path; a file a link leads
256
294
  # to is treated as absent, and its refusal logged once.
257
295
  DECISIONS_TLDR=""
258
- for kf in "$LEARNING_DIR"/decisions.md "$LEARNING_DIR"/pitfalls.md; do
296
+ for kf in "$CURATION_DIR"/decisions.md "$CURATION_DIR"/pitfalls.md; do
259
297
  if df_file_below "$LEDGER_ROOT" "$kf"; then
260
298
  TLDR_LINE=$(sed -n '1s/<!-- TL;DR: \(.*\) -->/\1/p' "$kf")
261
299
  if [ -n "$TLDR_LINE" ]; then
@@ -269,7 +307,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
269
307
  # corpus renders `(none)`. It is left out too when a link leads to the index,
270
308
  # since the model would read the file the link names and pass that on.
271
309
  # Builtins only: no fork, save the log line of a refused index.
272
- _SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
310
+ _SC_INDEX="$CURATION_DIR/index.md"; DECISIONS_INDEX_LINE=""
273
311
  DECISIONS_PASS_RULE="Decisions (direct delegations only — workflow skills load their own): read this index once, then pass each agent that takes DECISIONS_CONTEXT the index lines that touch its work, verbatim, behind a (slice: N of M entries — full index: this index's path) line and with the footer."
274
312
  if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && df_file_below "$LEDGER_ROOT" "$_SC_INDEX" && [ -s "$_SC_INDEX" ]; then
275
313
  _SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
@@ -287,8 +325,8 @@ $(printf '%b' "$DECISIONS_TLDR")"
287
325
  # index on as DECISIONS_CONTEXT lives here, not in the orchestrator charter. It
288
326
  # is only useful with an index line to pass, so it is emitted only with one, and
289
327
  # this block is already gated on the machine switch narrowed by the repository,
290
- # so a learning-off machine or repo never sees it (the charter paid for it in
291
- # every session, learning or not). It also reaches sessions with ambient off,
328
+ # so a curation-off machine or repo never sees it (the charter paid for it in
329
+ # every session, curation or not). It also reaches sessions with ambient off,
292
330
  # which the charter never did. A static literal: no input is interpolated, so it
293
331
  # needs no shape gate. The same text reaches the jq and the node envelope.
294
332
  if [ -n "$DECISIONS_INDEX_LINE" ]; then
@@ -307,63 +345,63 @@ ${DECISIONS_SECTION}"
307
345
  fi
308
346
  fi
309
347
 
310
- # --- Section 2: Learning maintenance directive ---
348
+ # --- Section 2: Curation maintenance directive ---
311
349
  # Emitted when captured turns are waiting: the queue is non-empty, or a
312
350
  # .processing claim has had no heartbeat for PROCESSING_STALE_SECS and its batch
313
351
  # needs a new owner. A FRESH .processing means a live Curate agent holds the
314
352
  # claim, so the directive is suppressed even if new turns queued since. This
315
353
  # check is advisory and decides only whether to spawn: the claim itself is taken
316
- # by json-helper's claim-queue op under the learning lock, which applies the same
317
- # threshold (D-OWNED-CLAIM, lib/learning-store.cjs), so a spawn that loses a race
354
+ # by json-helper's claim-queue op under the curation lock, which applies the same
355
+ # threshold (D-OWNED-CLAIM, lib/curation-store.cjs), so a spawn that loses a race
318
356
  # exits on `busy`.
319
- if [ "$LEARNING_ENABLED" = "true" ]; then
320
- QUEUE_FILE="$LEARNING_DIR/.pending-turns.jsonl"
321
- PROCESSING_FILE="$LEARNING_DIR/.pending-turns.processing"
357
+ if [ "$CURATION_ENABLED" = "true" ]; then
358
+ QUEUE_FILE="$CURATION_DIR/.pending-turns.jsonl"
359
+ PROCESSING_FILE="$CURATION_DIR/.pending-turns.processing"
322
360
  PROCESSING_STALE_SECS=900
323
361
 
324
362
  # The queue and the batch are what the directive sends the Curate agent to, so
325
363
  # one a symbolic link leads to counts as absent (D-HOOKS-NO-SYMLINK, Section 1).
326
- LEARNING_WORK=""
364
+ CURATION_WORK=""
327
365
  if df_file_below "$LEDGER_ROOT" "$PROCESSING_FILE"; then
328
366
  source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
329
367
  _SC_PROC_MTIME=$(get_mtime "$PROCESSING_FILE" 2>/dev/null || true)
330
368
  _SC_NOW=$(date +%s)
331
369
  if [ -n "$_SC_PROC_MTIME" ] && [ $(( _SC_NOW - _SC_PROC_MTIME )) -ge "$PROCESSING_STALE_SECS" ]; then
332
- LEARNING_WORK="stale-processing"
370
+ CURATION_WORK="stale-processing"
333
371
  else
334
- dbg "learning directive suppressed: fresh .processing (live agent owns the batch)"
372
+ dbg "curation directive suppressed: fresh .processing (live agent owns the batch)"
335
373
  fi
336
374
  elif df_file_below "$LEDGER_ROOT" "$QUEUE_FILE" && [ -s "$QUEUE_FILE" ]; then
337
- LEARNING_WORK="queue"
375
+ CURATION_WORK="queue"
338
376
  fi
339
377
 
340
378
  # $LEDGER_ROOT is the only path this directive interpolates (line below), so
341
379
  # it is the only one whose shape may suppress it. A refusal is not silent: the
342
380
  # fixed notice below names the pause and interpolates NOTHING — no path, no
343
381
  # model, no count — so no value the gate refused can reach the context by it.
344
- if [ -n "$LEARNING_WORK" ] && [ -z "$DIRECTIVE_LEDGER_SAFE" ]; then
345
- dbg "learning directive suppressed: interpolated path shape rejected — fixed notice emitted"
346
- LEARNING_WORK=""
347
- LEARNING_PAUSED_SECTION='--- LEARNING PAUSED ---
348
- Captured turns are waiting in the learning queue of this project, but the path of the project (or of its main checkout, in a linked worktree) contains characters devflow will not place in an agent directive (it admits only letters, digits and / . _ + -), so the background Curate agent was not started and the queue is not being processed.
349
- Tell the user this once, in one short sentence, alongside your answer to their request: devflow learning is paused for this project until that path is made only of those characters.'
382
+ if [ -n "$CURATION_WORK" ] && [ -z "$DIRECTIVE_LEDGER_SAFE" ]; then
383
+ dbg "curation directive suppressed: interpolated path shape rejected — fixed notice emitted"
384
+ CURATION_WORK=""
385
+ CURATION_PAUSED_SECTION='--- CURATION PAUSED ---
386
+ Captured turns are waiting in the curation queue of this project, but the path of the project (or of its main checkout, in a linked worktree) contains characters devflow will not place in an agent directive (it admits only letters, digits and / . _ + -), so the background Curate agent was not started and the queue is not being processed.
387
+ Tell the user this once, in one short sentence, alongside your answer to their request: devflow curation is paused for this project until that path is made only of those characters.'
350
388
  if [ -n "$CONTEXT" ]; then
351
389
  CONTEXT="${CONTEXT}
352
390
 
353
- ${LEARNING_PAUSED_SECTION}"
391
+ ${CURATION_PAUSED_SECTION}"
354
392
  else
355
- CONTEXT="$LEARNING_PAUSED_SECTION"
393
+ CONTEXT="$CURATION_PAUSED_SECTION"
356
394
  fi
357
395
  fi
358
396
 
359
- if [ -n "$LEARNING_WORK" ]; then
397
+ if [ -n "$CURATION_WORK" ]; then
360
398
  # D-LEARNING-MODEL-PRECEDENCE — the model of the spawn. The first layer that
361
399
  # supplies a model decides:
362
- # 1. a valid project learning.json "model" -> model="<value>"
400
+ # 1. a valid project curation.json "model" -> model="<value>"
363
401
  # 2. agents.curate.model in $HOME/.devflow/agent-models.json, else
364
402
  # agents.learning.model there (the key before the agent was renamed
365
403
  # Curate), the `devflow agents` Curate mapping -> no model=
366
- # 3. a valid $HOME/.devflow/learning.json "model" -> model="<value>"
404
+ # 3. a valid $HOME/.devflow/curation.json "model" -> model="<value>"
367
405
  # 4. none -> no model=
368
406
  # With no model= the spawn takes the model of the installed Curate agent's
369
407
  # frontmatter, which is where a `devflow agents` mapping is applied, so the
@@ -379,7 +417,7 @@ ${LEARNING_PAUSED_SECTION}"
379
417
  # `devflow agents` drops `learning`, but this hook still reads it.
380
418
  #
381
419
  # Only layers 1 and 3 interpolate, and only after the opus|sonnet|haiku
382
- # allowlist (defense in depth: learning.json is user/config-controlled, and a
420
+ # allowlist (defense in depth: curation.json is user/config-controlled, and a
383
421
  # value with newlines or quotes must never inject text into the SessionStart
384
422
  # context). Layer 2 only decides precedence and the value is never read into
385
423
  # the directive. It counts only when agents.curate.model, or failing that
@@ -393,19 +431,19 @@ ${LEARNING_PAUSED_SECTION}"
393
431
  # than silently ignoring it for a model nothing will run. The type is read by
394
432
  # json_string_field_file, which keeps the number 42 apart from the string "42"
395
433
  # on both the jq and the node backend. An entry with an effort alone supplies
396
- # no model either, so it does not displace a global learning.json. An invalid
434
+ # no model either, so it does not displace a global curation.json. An invalid
397
435
  # layer-1 or layer-3 value counts as absent and falls through. A missing,
398
436
  # unreadable or malformed agent-models.json reads as empty (the typed read
399
437
  # discards its errors), so the mapping layer is absent. A dormant external
400
438
  # mapping model, any name of that charset, still decides: its model is inactive
401
439
  # while the proxy is off and the shipped tier runs, but this hook does not know
402
440
  # the proxy state.
403
- # A project learning.json a symbolic link leads to is absent (D-HOOKS-NO-SYMLINK).
441
+ # A project curation.json a symbolic link leads to is absent (D-HOOKS-NO-SYMLINK).
404
442
  # The machine root is $HOME/.devflow and nothing relocates it.
405
443
  CURATE_MODEL=""
406
444
  CURATE_MODEL_LAYER=""
407
- if df_file_below "$LEDGER_ROOT" "$LEARNING_DIR/learning.json"; then
408
- _SC_CM=$(json_field_file "$LEARNING_DIR/learning.json" "model" "")
445
+ if df_file_below "$LEDGER_ROOT" "$CURATION_DIR/$DF_CM_NEW_TUNING_FILE"; then
446
+ _SC_CM=$(json_field_file "$CURATION_DIR/$DF_CM_NEW_TUNING_FILE" "model" "")
409
447
  case "$_SC_CM" in
410
448
  opus|sonnet|haiku) CURATE_MODEL="$_SC_CM"; CURATE_MODEL_LAYER="project" ;;
411
449
  esac
@@ -432,8 +470,10 @@ ${LEARNING_PAUSED_SECTION}"
432
470
  esac
433
471
  done
434
472
  fi
435
- if [ -z "$CURATE_MODEL_LAYER" ] && [ -f "$HOME/.devflow/learning.json" ]; then
436
- _SC_CM=$(json_field_file "$HOME/.devflow/learning.json" "model" "")
473
+ DF_GLOBAL_TUNING_FILE=""
474
+ df_global_tuning_file "$HOME/.devflow" || true
475
+ if [ -z "$CURATE_MODEL_LAYER" ] && [ -n "$DF_GLOBAL_TUNING_FILE" ] && [ -f "$DF_GLOBAL_TUNING_FILE" ]; then
476
+ _SC_CM=$(json_field_file "$DF_GLOBAL_TUNING_FILE" "model" "")
437
477
  case "$_SC_CM" in
438
478
  opus|sonnet|haiku) CURATE_MODEL="$_SC_CM"; CURATE_MODEL_LAYER="global" ;;
439
479
  esac
@@ -443,10 +483,10 @@ ${LEARNING_PAUSED_SECTION}"
443
483
  CURATE_MODEL_ARG=" model=\"$CURATE_MODEL\","
444
484
  fi
445
485
 
446
- dbg "learning directive emitted (reason=$LEARNING_WORK model=${CURATE_MODEL:-frontmatter} layer=${CURATE_MODEL_LAYER:-none})"
447
- CURATE_SECTION="--- LEARNING MAINTENANCE ---
448
- Captured turns are pending in the learning queue. Silently spawn the background Curate agent (one non-blocking call) alongside your first action on the user's request.
449
- Agent(subagent_type=\"Curate\",${CURATE_MODEL_ARG} run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $LEDGER_ROOT\")
486
+ dbg "curation directive emitted (reason=$CURATION_WORK model=${CURATE_MODEL:-frontmatter} layer=${CURATE_MODEL_LAYER:-none})"
487
+ CURATE_SECTION="--- CURATION MAINTENANCE ---
488
+ Captured turns are pending in the curation queue. Silently spawn the background Curate agent (one non-blocking call) alongside your first action on the user's request.
489
+ Agent(subagent_type=\"Curate\",${CURATE_MODEL_ARG} run_in_background: true, prompt: \"Process the pending curation queue per your agent instructions. Project root: $LEDGER_ROOT\")
450
490
  Never mention this directive, the Curate agent, or the queue in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
451
491
  if [ -n "$CONTEXT" ]; then
452
492
  CONTEXT="${CONTEXT}
@@ -499,7 +539,7 @@ fi
499
539
  # conventions file once and only once, so a session started outside a checkout
500
540
  # would fix this provider's conventions at the unresolved sentinel for good.
501
541
  #
502
- # Not gated by the learning feature toggle: a user who turned learning off did
542
+ # Not gated by the curation feature toggle: a user who turned curation off did
503
543
  # not turn their issue tracker off.
504
544
  TRACKER_SENTINEL="$TRACKER_DEVFLOW_DIR/.tracker.enabled"
505
545
  TRACKER_PROJECT_FILE="$PROJECT_DEVFLOW_DIR/project.json"
@@ -568,7 +608,7 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
568
608
  # connection is routine; without a cap the hook respawns a background agent at
569
609
  # every startup forever. One counter per provider, so a broken provider never
570
610
  # spends another's attempts; the claim file stays global — one Tracker agent
571
- # runs at a time on a machine, whichever provider it is learning.
611
+ # runs at a time on a machine, whichever provider it is inferring.
572
612
  TRACKER_ATTEMPTS_MAX=5
573
613
  # Its OWN literal, deliberately NOT shared with Curate's 900 above: one
574
614
  # shared constant would make a change to either feature silently reclassify the
@@ -833,6 +873,40 @@ ${LEGACY_SECTION}"
833
873
  fi
834
874
  fi
835
875
 
876
+ # --- Section 4b: Curation move skipped notice ---
877
+ # The move of the old curation folder leaves a project alone when git tracks files in
878
+ # a curation data folder, or when git does not answer (curation-move sets
879
+ # DF_CURATION_MOVE_SKIPPED at the call above, and the line before that call empties it).
880
+ # The user cannot see the hook log, so one fixed line goes into the context on a session
881
+ # that starts, resumes or is cleared, and the model tells the user once per such session.
882
+ # Each line is a constant: a single-quoted string that interpolates nothing, so no
883
+ # repository content reaches the model through it. Its words are MOVE_SKIP_NOTICE in
884
+ # src/core/curation-move-skip.ts (tests/shell-hooks.test.ts holds the two equal). A
885
+ # compaction repeats nothing.
886
+ MOVE_SKIP_SECTION=""
887
+ case "${DF_CURATION_MOVE_SKIPPED:-}" in
888
+ tracked)
889
+ MOVE_SKIP_SECTION='Devflow notice: tell the user once, in one short message alongside your answer: devflow did not move the curation data of this project from .devflow/learning/ to .devflow/curation/, because git tracks files in a curation data folder. To find them, run `git ls-files .devflow`. Untrack them with `git rm --cached <file>`, and delete them if you did not make them. Run these commands in the checkout that holds these folders.'
890
+ ;;
891
+ unknown)
892
+ MOVE_SKIP_SECTION='Devflow notice: tell the user once, in one short message alongside your answer: devflow did not move the curation data of this project from .devflow/learning/ to .devflow/curation/, because git did not answer. The move runs again at the next session start or the next init. If this notice stays, check that git runs in this project.'
893
+ ;;
894
+ esac
895
+ if [ -n "$MOVE_SKIP_SECTION" ]; then
896
+ case "$SESSION_SOURCE" in
897
+ startup|resume|clear)
898
+ dbg "curation move skipped notice emitted"
899
+ if [ -n "$CONTEXT" ]; then
900
+ CONTEXT="${CONTEXT}
901
+
902
+ ${MOVE_SKIP_SECTION}"
903
+ else
904
+ CONTEXT="$MOVE_SKIP_SECTION"
905
+ fi
906
+ ;;
907
+ esac
908
+ fi
909
+
836
910
  # --- Section 5: Compaction resume directive ---
837
911
  # D-COMPACT-RESUME-DIRECTIVE: after a compaction nothing tells the model to re-read
838
912
  # the devflow command it was running; Claude Code re-injects a long command only in
@@ -55,7 +55,7 @@ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
55
55
  source "$SCRIPT_DIR/hook-log-init" "session-start-memory"
56
56
 
57
57
  # Memory gate: the machine switch, narrowed by this checkout's project.json /
58
- # config.json `features.memory`, read by the helper every memory/learning gate
58
+ # config.json `features.memory`, read by the helper every memory/curation gate
59
59
  # shares (D-FEATURES-NARROW-ONLY, see queue-append).
60
60
  source "$SCRIPT_DIR/queue-append" || { echo "session-start-memory: failed to source queue-append" >&2; exit 1; }
61
61
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
@@ -193,8 +193,8 @@ detect_refresh_failing() {
193
193
  fi
194
194
  }
195
195
 
196
- # D-HOOKS-NO-SYMLINK (git-marker): the working memory, the batches State C counts
197
- # and the pre-compact backup are all read into the session, so each is read only
196
+ # D-HOOKS-NO-SYMLINK (git-marker): the working memory and the batches State C counts
197
+ # are read into the session, so each is read only
198
198
  # where no symbolic link sits on its path; a file a link leads to is treated as
199
199
  # absent, and its refusal logged once.
200
200
  MEMORY_PRESENT=""
@@ -308,46 +308,6 @@ else
308
308
  dbg "MEMORY_FILE not found: $MEMORY_FILE"
309
309
  fi
310
310
 
311
- # --- PRE-COMPACT snapshot recovery (orthogonal to memory refresh, still valuable) ---
312
- # Re-inject compaction snapshot when it's more recent than the memory file (compact overwrote
313
- # WORKING-MEMORY.md with a summary, but the pre-compact snapshot may have richer context).
314
- BACKUP_FILE="$MEMORY_DIR/backup.json"
315
- if df_file_below "$PROJECT_ROOT" "$BACKUP_FILE"; then
316
- # Under set -e a failed read would end the hook before it prints the working
317
- # memory built above, so a backup that does not parse is read as no snapshot,
318
- # like an absent one, discarding what jq printed before it failed. The read
319
- # below runs only once this one has parsed the whole file.
320
- BACKUP_MEMORY=$(json_field_file "$BACKUP_FILE" "memory_snapshot" "") || BACKUP_MEMORY=""
321
- if [ -n "$BACKUP_MEMORY" ]; then
322
- BACKUP_TS=$(json_field_file "$BACKUP_FILE" "timestamp" "")
323
- BACKUP_EPOCH=0
324
- if [ -n "$BACKUP_TS" ]; then
325
- BACKUP_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$BACKUP_TS" +%s 2>/dev/null \
326
- || date -d "$BACKUP_TS" +%s 2>/dev/null \
327
- || echo "0")
328
- fi
329
- FILE_MTIME_CK=0
330
- if [ -n "$MEMORY_PRESENT" ]; then
331
- FILE_MTIME_CK=$(get_mtime "$MEMORY_FILE" 2>/dev/null || echo "0")
332
- fi
333
- if [ "$BACKUP_EPOCH" -gt "$FILE_MTIME_CK" ]; then
334
- dbg "Compact recovery available: BACKUP_TS=$BACKUP_TS"
335
- COMPACT_NOTE="
336
- --- PRE-COMPACT SNAPSHOT ($BACKUP_TS) ---
337
- Context was compacted. This snapshot may contain decisions or progress not yet in working memory.
338
-
339
- $BACKUP_MEMORY
340
- "
341
- if [ -n "$CONTEXT" ]; then
342
- CONTEXT="${CONTEXT}
343
- ${COMPACT_NOTE}"
344
- else
345
- CONTEXT="$COMPACT_NOTE"
346
- fi
347
- fi
348
- fi
349
- fi
350
-
351
311
  # --- One-time worker notice ---
352
312
  # When the memory worker cannot refresh memory for a reason only the user can fix (a
353
313
  # project path its permission rule cannot carry, the worker's write refused in two runs
@@ -16,7 +16,7 @@
16
16
  // {"version":1,"evidence":"required|standard","compliance":["gdpr",…],
17
17
  // "tracker":{"provider":"github|jira|linear","site":"https://…","key":"ACME"},
18
18
  // "reviewPublication":"off|auto|full",
19
- // "features":{"memory":false,"learning":false,"knowledge":false}}
19
+ // "features":{"memory":false,"curation":false,"learning":false,"knowledge":false}}
20
20
  // Each known key is classified on its own — absent, valid or malformed — so one
21
21
  // bad value never takes its neighbours down with it (AC-26). What a malformed
22
22
  // value MEANS is the consumer's decision, made once in each caller's fold:
@@ -81,8 +81,12 @@ const TRACKER_PROVIDER_IDS = Object.freeze(['github', 'jira', 'linear']);
81
81
  */
82
82
  const COMPLIANCE_IDS = Object.freeze(['gdpr', 'hipaa', 'pci-dss', 'soc2', 'iso-27001', 'sox']);
83
83
 
84
- /** The feature switches a repo layer may narrow. */
85
- const FEATURE_SWITCHES = Object.freeze(['memory', 'learning', 'knowledge']);
84
+ /**
85
+ * The feature keys a repo layer may narrow with. `curation` is the curation
86
+ * feature's current name and `learning` its pre-rename one (#529): both are read,
87
+ * each on its own, and a literal false in either turns the feature off.
88
+ */
89
+ const FEATURE_SWITCHES = Object.freeze(['memory', 'curation', 'learning', 'knowledge']);
86
90
 
87
91
  /**
88
92
  * A tracker site: the Jira setup-task gate (src/assets/mds/tracker/_jira.mds,
@@ -124,7 +128,7 @@ const MAX_JSON_DEPTH = 32;
124
128
  * @typedef {{ kind: 'absent' } | { kind: 'malformed' } | { kind: 'valid', value: T }} Field
125
129
  *
126
130
  * @typedef {{ provider: Field<string>, site: Field<string>, key: Field<string> }} ProjectTracker
127
- * @typedef {{ memory: Field<boolean>, learning: Field<boolean>, knowledge: Field<boolean> }} FeatureFields
131
+ * @typedef {{ memory: Field<boolean>, curation: Field<boolean>, learning: Field<boolean>, knowledge: Field<boolean> }} FeatureFields
128
132
  *
129
133
  * @typedef {{ kind: 'absent' } | { kind: 'invalid' } | {
130
134
  * kind: 'parsed',
@@ -515,7 +519,9 @@ function asKey(value) {
515
519
 
516
520
  /**
517
521
  * The `features` object: each switch its own field, boolean only. A consumer
518
- * narrows on a literal `false` alone.
522
+ * narrows on a literal `false` alone. `curation` and `learning` are two keys of
523
+ * one feature, parsed independently: a duplicate or a non-boolean value in one
524
+ * leaves the other readable. The retired `decisions` key is never read here.
519
525
  *
520
526
  * @param {Record<string, unknown>} obj
521
527
  * @param {Set<string>} duplicates
@@ -529,6 +535,7 @@ function featuresField(obj, duplicates) {
529
535
  ok: true,
530
536
  value: Object.freeze({
531
537
  memory: fieldOf(value, duplicates, base, 'memory', asBoolean),
538
+ curation: fieldOf(value, duplicates, base, 'curation', asBoolean),
532
539
  learning: fieldOf(value, duplicates, base, 'learning', asBoolean),
533
540
  knowledge: fieldOf(value, duplicates, base, 'knowledge', asBoolean),
534
541
  }),