devflow-kit 3.7.0 → 3.9.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 (205) hide show
  1. package/CHANGELOG.md +479 -1
  2. package/README.md +15 -15
  3. package/dist/agents/code.md +6 -8
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +7 -8
  6. package/dist/agents/git.md +3 -3
  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 +9 -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/curation.js +808 -0
  16. package/dist/cli/commands/gate-level-prompts.js +428 -0
  17. package/dist/cli/commands/gates.js +35 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +244 -57
  20. package/dist/cli/commands/memory.js +60 -13
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +157 -17
  23. package/dist/cli.js +2 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +40 -44
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +26 -25
  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 +53 -82
  33. package/dist/commands/plan.md +6 -6
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +39 -41
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/area-pause-notice.js +44 -0
  39. package/dist/core/assets.js +20 -20
  40. package/dist/core/curate-tuning-config.js +5 -6
  41. package/dist/core/curation-move-notice.js +84 -0
  42. package/dist/core/curation-move-skip.js +35 -0
  43. package/dist/core/curation-move.js +35 -0
  44. package/dist/core/curation-queue-cleanup.js +135 -0
  45. package/dist/core/curation-store.js +99 -0
  46. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  47. package/dist/core/echo-value.js +11 -5
  48. package/dist/core/evidence-policy.js +11 -9
  49. package/dist/core/feature-switch.js +95 -19
  50. package/dist/core/fs-atomic.js +1 -1
  51. package/dist/core/gate-level.js +10 -2
  52. package/dist/core/git.js +16 -14
  53. package/dist/core/ledger-root.js +82 -22
  54. package/dist/core/linked-path.js +4 -4
  55. package/dist/core/manifest-lock.js +238 -0
  56. package/dist/core/manifest.js +58 -14
  57. package/dist/core/migrations.js +1 -13
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +19 -19
  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 +159 -0
  64. package/dist/core/tracker.js +1 -1
  65. package/dist/{learning-off → curation-off}/agents/code.md +6 -8
  66. package/dist/{learning-off → curation-off}/agents/fix.md +7 -8
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  68. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +8 -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 +37 -41
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +23 -22
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +50 -79
  76. package/dist/{learning-off → curation-off}/commands/plan.md +2 -2
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +36 -38
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +63 -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/post-resolution-summary.md +12 -11
  84. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  85. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  86. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  87. package/dist/skills/git/references/publication-gate.md +6 -7
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  89. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  90. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  91. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  92. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  94. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  95. package/dist/targets/claude-code/installer.js +15 -15
  96. package/dist/targets/claude-code/language-stamp.js +5 -5
  97. package/dist/targets/claude-code/post-install.js +2 -2
  98. package/dist/targets/claude-code/tracker-servers.js +1 -1
  99. package/package.json +3 -2
  100. package/src/assets/agents/_partials/_code.mds +11 -11
  101. package/src/assets/agents/code.mds +6 -6
  102. package/src/assets/agents/curate.md +8 -8
  103. package/src/assets/agents/debug.mds +6 -6
  104. package/src/assets/agents/design.mds +18 -18
  105. package/src/assets/agents/evaluate.md +6 -12
  106. package/src/assets/agents/fix.mds +7 -6
  107. package/src/assets/agents/git.mds +3 -3
  108. package/src/assets/agents/harden.mds +7 -7
  109. package/src/assets/agents/knowledge.mds +23 -23
  110. package/src/assets/agents/refactor.mds +2 -2
  111. package/src/assets/agents/research.mds +15 -15
  112. package/src/assets/agents/review.mds +43 -43
  113. package/src/assets/agents/skim.mds +11 -11
  114. package/src/assets/agents/triage.mds +29 -22
  115. package/src/assets/agents/validate.md +30 -50
  116. package/src/assets/commands/_partials/_decisions.mds +22 -20
  117. package/src/assets/commands/_partials/_engine.mds +8 -8
  118. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  119. package/src/assets/commands/_partials/_preamble.mds +4 -4
  120. package/src/assets/commands/_partials/_settings.mds +3 -3
  121. package/src/assets/commands/_partials/_wave.mds +7 -7
  122. package/src/assets/commands/bug-analysis.mds +37 -48
  123. package/src/assets/commands/code-review.mds +53 -57
  124. package/src/assets/commands/debug.mds +14 -14
  125. package/src/assets/commands/dynamic-build.mds +38 -37
  126. package/src/assets/commands/dynamic-plan.mds +22 -22
  127. package/src/assets/commands/dynamic-profile.mds +2 -2
  128. package/src/assets/commands/dynamic-tickets.mds +8 -8
  129. package/src/assets/commands/explore.mds +20 -20
  130. package/src/assets/commands/implement.mds +77 -98
  131. package/src/assets/commands/plan.mds +27 -27
  132. package/src/assets/commands/release.mds +12 -12
  133. package/src/assets/commands/research.mds +19 -19
  134. package/src/assets/commands/resolve.mds +66 -60
  135. package/src/assets/commands/self-review.mds +20 -20
  136. package/src/assets/mds/git/_ops.mds +9 -2
  137. package/src/assets/mds/git/_ops_pr.mds +21 -11
  138. package/src/assets/mds/git/_ops_steps.mds +20 -4
  139. package/src/assets/mds/git/_pr.mds +21 -18
  140. package/src/assets/mds/git/_references.mds +6 -7
  141. package/src/assets/mds/tracker/_github.mds +1 -4
  142. package/src/assets/mds/tracker/_jira.mds +1 -4
  143. package/src/assets/mds/tracker/_linear.mds +1 -4
  144. package/src/assets/rules/quality.md +1 -0
  145. package/src/assets/scripts/ci-wait.cjs +389 -92
  146. package/src/assets/scripts/hooks/background-memory-update +81 -3
  147. package/src/assets/scripts/hooks/capture-prompt +12 -12
  148. package/src/assets/scripts/hooks/capture-question +11 -11
  149. package/src/assets/scripts/hooks/capture-turn +12 -12
  150. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  151. package/src/assets/scripts/hooks/curation-move +293 -0
  152. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  153. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  154. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  155. package/src/assets/scripts/hooks/git-marker +81 -0
  156. package/src/assets/scripts/hooks/json-helper.cjs +176 -51
  157. package/src/assets/scripts/hooks/json-parse +1 -1
  158. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  159. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +308 -122
  160. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  161. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  162. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  163. package/src/assets/scripts/hooks/lib/render-decisions.cjs +42 -22
  164. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +452 -0
  165. package/src/assets/scripts/hooks/memory-permission +10 -2
  166. package/src/assets/scripts/hooks/memory-worker +12 -1
  167. package/src/assets/scripts/hooks/pre-compact-memory +17 -16
  168. package/src/assets/scripts/hooks/queue-append +444 -102
  169. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  170. package/src/assets/scripts/hooks/session-start-context +198 -72
  171. package/src/assets/scripts/hooks/session-start-memory +42 -43
  172. package/src/assets/scripts/lib/project-config.cjs +12 -5
  173. package/src/assets/scripts/pr-evidence.cjs +5 -2
  174. package/src/assets/scripts/resolve-base.cjs +1043 -0
  175. package/src/assets/scripts/resolve-settings.cjs +283 -45
  176. package/src/assets/scripts/verify-evidence.cjs +307 -75
  177. package/src/assets/skills/agent-references/SKILL.md +11 -2
  178. package/src/assets/skills/agent-references/references/commands/implement-levels.md +29 -0
  179. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  180. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +25 -0
  181. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  182. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  183. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  184. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  185. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  186. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  187. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  188. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  189. package/templates/lint-pack/.lint-pack.conf +86 -0
  190. package/templates/lint-pack/README.md +190 -0
  191. package/templates/lint-pack/lint-pack.sh +905 -0
  192. package/dist/cli/commands/learning.js +0 -554
  193. package/dist/core/learning-queue-cleanup.js +0 -27
  194. package/dist/core/learning-store.js +0 -61
  195. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  196. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  197. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  198. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  199. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  200. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  201. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  202. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  203. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  204. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  205. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -29,6 +29,9 @@
29
29
  # for the next run (D-QUEUE-NO-ORPHAN-DELETE)
30
30
  # No parser: neither jq nor node on PATH → exits with no LLM run; the queue and any .processing batch are left in place
31
31
  # Parser fails: jq or node exits non-zero while it reads the queue or the batch → exits with no LLM run; the turns are kept
32
+ # Paused area: git tracks files in the memory folder, or a `.git` entry stands and git does not answer → exits with
33
+ # result=skip and no LLM run, and reads, claims, writes and notices nothing; asked at the start, and again
34
+ # after the run, where a late answer leaves the staged file unswapped and the batch in .processing
32
35
 
33
36
  set -e
34
37
 
@@ -128,6 +131,59 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
128
131
  # The staged-path check, the settings JSON with the worker's Edit allow and deny rules, the
129
132
  # result line and the one-time notice (needs df_no_symlink_below from resolve-project-root).
130
133
  source "$SCRIPT_DIR/memory-permission" || { echo "background-memory-update: failed to source memory-permission" >&2; exit 1; }
134
+ source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
135
+
136
+ # --- The memory area check: git tracks files in the memory folder, or does not answer ---
137
+ # A folder that git tracks holds the repository's content, not devflow's data. While git
138
+ # tracks files in the memory folder of this checkout, or a `.git` entry stands and git does
139
+ # not answer, the memory area is paused, and the worker reads and writes nothing there.
140
+ # Sets DF_TRACKED_AREA_MEMORY to `tracked`, `untracked` or `unknown` for the project root in
141
+ # $1: queue_tracked_area in git mode, run in a background subshell that answers by its exit
142
+ # status (10 untracked, 11 tracked), so that a git that hangs can be cut and no file is made.
143
+ # Every call asks git afresh: the second ask of a run must see what changed during the run.
144
+ # The bound is DEVFLOW_BG_AREA_PROBE_SECS (default 5, at most 60; tests override it as they
145
+ # do DEVFLOW_BG_WATCHDOG_SECS), kept by the watchdog's background-and-kill idiom: a SIGKILL
146
+ # to the probe's process group (set -m) after a sleep, and a SIGKILL cancel of the watchdog,
147
+ # so no exit trap runs in either subshell (the one `Run:` line, above). A probe that is
148
+ # cut, or ends in any other status, reads `unknown`: the area stays paused. The notices of
149
+ # memory-permission read this variable, so the worker leaves one only after this check has
150
+ # asked and found the area free. Always returns 0.
151
+ probe_memory_area() {
152
+ local _root="${1:-}" _secs _pid _wd_pid _status=1
153
+ DF_TRACKED_AREA_MEMORY="unknown"
154
+ _secs="${DEVFLOW_BG_AREA_PROBE_SECS:-5}"
155
+ case "$_secs" in
156
+ ''|*[!0123456789]*|????*) _secs=5 ;;
157
+ esac
158
+ if [ "$_secs" -lt 1 ] || [ "$_secs" -gt 60 ]; then _secs=5; fi
159
+ set -m
160
+ (
161
+ _QTA_ASKED_MEMORY=""
162
+ queue_tracked_area "$_root" memory git
163
+ case "$DF_TRACKED_AREA_MEMORY" in
164
+ untracked) exit 10 ;;
165
+ tracked) exit 11 ;;
166
+ esac
167
+ exit 12
168
+ ) </dev/null >/dev/null 2>&1 &
169
+ _pid=$!
170
+ set +m
171
+ (
172
+ sleep "$_secs"
173
+ kill -KILL -"$_pid" 2>/dev/null || true
174
+ ) </dev/null >/dev/null 2>&1 &
175
+ _wd_pid=$!
176
+ if wait "$_pid" 2>/dev/null; then _status=0; else _status=$?; fi
177
+ # SIGKILL, not SIGTERM, as for the other watchdogs: no trap can run in the subshell.
178
+ kill -KILL "$_wd_pid" 2>/dev/null || true
179
+ wait "$_wd_pid" 2>/dev/null || true
180
+ case "$_status" in
181
+ 10) DF_TRACKED_AREA_MEMORY="untracked" ;;
182
+ 11) DF_TRACKED_AREA_MEMORY="tracked" ;;
183
+ esac
184
+ return 0
185
+ }
186
+
131
187
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
132
188
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
133
189
  # One canonical root feeds every path below, the staged path in the prompt and in the
@@ -136,6 +192,7 @@ PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
136
192
  PROJECT_ROOT_CANONICAL=$(cd -P -- "$PROJECT_ROOT" 2>/dev/null && pwd -P) || PROJECT_ROOT_CANONICAL=""
137
193
  if [ -z "$PROJECT_ROOT_CANONICAL" ]; then
138
194
  log "SKIP: project root could not be resolved; no LLM run"
195
+ probe_memory_area "$PROJECT_ROOT"
139
196
  memory_notice_write "$PROJECT_ROOT/.devflow/memory" "$PROJECT_ROOT" path
140
197
  exit 0
141
198
  fi
@@ -152,15 +209,25 @@ OK_FILE="$MEMORY_DIR/.last-refresh-ok"
152
209
 
153
210
  # --- Re-check the memory switch at runtime (defense-in-depth: the feature may
154
211
  # have been disabled since spawn, or narrowed by a checkout that now carries a
155
- # `features.memory: false`). Same helper as every other memory/learning gate
212
+ # `features.memory: false`). Same helper as every other memory/curation gate
156
213
  # (D-FEATURES-NARROW-ONLY, see queue-append).
157
- source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
158
214
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
159
215
  if [ "$_QG_MEMORY" != "true" ]; then
160
216
  log "ABORT: memory disabled (disabled after spawn)"
161
217
  exit 0
162
218
  fi
163
219
 
220
+ # --- Ask whether git tracks files in the memory folder ---
221
+ # The area may have become tracked since the Stop hook spawned this worker. A paused area
222
+ # ends the run here, before the binary is resolved, a link is checked, the lock is taken or
223
+ # a batch is claimed: nothing is read, moved or written, and no notice is left. The run's
224
+ # `Run:` line says skip.
225
+ probe_memory_area "$PROJECT_ROOT"
226
+ if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
227
+ log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing read or written; no LLM run"
228
+ exit 0
229
+ fi
230
+
164
231
  # --- Resolve claude binary ---
165
232
  CLAUDE_BIN=$(command -v claude 2>/dev/null || true)
166
233
  if [ -z "$CLAUDE_BIN" ]; then
@@ -340,7 +407,7 @@ else
340
407
  fi
341
408
  fi
342
409
 
343
- # --- Worker-level lock (300s stale-break — much longer than learning-lock's 30s) ---
410
+ # --- Worker-level lock (300s stale-break — much longer than curation-lock's 30s) ---
344
411
  # This prevents a second worker (spawned 121s later) from double-writing WORKING-MEMORY.md
345
412
  # while the first worker's claude -p call (up to 120s) is still in flight.
346
413
  #
@@ -1126,6 +1193,17 @@ case "$MEMORY_RESULT_DENIALS" in
1126
1193
  *) RESULT_DENIED="yes" ;;
1127
1194
  esac
1128
1195
 
1196
+ # The run took minutes, so the memory area is asked again before anything lands in the
1197
+ # folder. If git tracks files there now, or does not answer, nothing below runs: the staged
1198
+ # file is not checked, moved or removed, no stamp is touched, no denied run is counted and
1199
+ # no notice is left. The batch stays in .processing.
1200
+ probe_memory_area "$PROJECT_ROOT"
1201
+ if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
1202
+ RUN_RESULT="skip"
1203
+ log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing swapped or stamped; the batch stays in .processing"
1204
+ exit 0
1205
+ fi
1206
+
1129
1207
  # A folder or a link at the staged path now was put there during the run: the path was
1130
1208
  # empty at the spawn. It is removed the way it is before a run (memory_staged_remove), and
1131
1209
  # the run ends as failed: the checks below find no staged file, and the batch stays for
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
 
3
- # Learning System: capture-prompt (UserPromptSubmit Hook)
4
- # Dual-append: writes the user turn to BOTH the memory queue and the learning
3
+ # Curation System: capture-prompt (UserPromptSubmit Hook)
4
+ # Dual-append: writes the user turn to BOTH the memory queue and the curation
5
5
  # queue, each independently gated by its own feature flag (AC-F4). Delegates
6
6
  # the actual JSONL-write logic to the shared queue-append helper rather than
7
7
  # duplicating it inline.
@@ -41,18 +41,18 @@ devflow_debug_set_cwd "$CWD"
41
41
  source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
42
42
  df_resolve_roots "$CWD" 2>/dev/null || true
43
43
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
44
- # The learning queue is the repository's, not the checkout's: in a linked worktree
44
+ # The curation queue is the repository's, not the checkout's: in a linked worktree
45
45
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
46
46
  # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
47
47
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
48
48
 
49
- # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
49
+ # The machine-wide manifest (the memory/curation switches, D-FEATURES-NARROW-ONLY
50
50
  # in queue-append) lives at the machine root,
51
51
  # $HOME/.devflow (D-ONE-HOME).
52
52
  DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
53
53
  PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
54
54
  MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
55
- LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
55
+ CURATION_DIR="$LEDGER_ROOT/.devflow/curation"
56
56
 
57
57
  if [ -z "$PROMPT" ]; then
58
58
  dbg "EXIT: empty PROMPT"
@@ -61,16 +61,16 @@ fi
61
61
 
62
62
  source "$SCRIPT_DIR/queue-append" || { echo "capture-prompt: failed to source queue-append" >&2; exit 1; }
63
63
 
64
- # --- AC-P1: at most ONE gate-read fork for memory + learning ---
64
+ # --- AC-P1: at most ONE gate-read fork for memory + curation ---
65
65
  # The machine switch, narrowed by this checkout's project.json/config.json
66
66
  # `features` (D-FEATURES-NARROW-ONLY, see queue-append).
67
67
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
68
68
  MEMORY_ENABLED="$_QG_MEMORY"
69
- LEARNING_ENABLED="$_QG_LEARNING"
69
+ CURATION_ENABLED="$_QG_CURATION"
70
70
 
71
- dbg "MEMORY_ENABLED=$MEMORY_ENABLED LEARNING_ENABLED=$LEARNING_ENABLED"
71
+ dbg "MEMORY_ENABLED=$MEMORY_ENABLED CURATION_ENABLED=$CURATION_ENABLED"
72
72
 
73
- if [ "$MEMORY_ENABLED" != "true" ] && [ "$LEARNING_ENABLED" != "true" ]; then
73
+ if [ "$MEMORY_ENABLED" != "true" ] && [ "$CURATION_ENABLED" != "true" ]; then
74
74
  dbg "EXIT: both features disabled"
75
75
  exit 0
76
76
  fi
@@ -82,15 +82,15 @@ source "$SCRIPT_DIR/hook-log-init" "capture-prompt"
82
82
  source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
83
83
 
84
84
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-prompt: failed to source get-mtime" >&2; exit 1; }
85
- source "$SCRIPT_DIR/learning-lock" || { echo "capture-prompt: failed to source learning-lock" >&2; exit 1; }
85
+ source "$SCRIPT_DIR/curation-lock" || { echo "capture-prompt: failed to source curation-lock" >&2; exit 1; }
86
86
 
87
87
  # Each queue is checked from the root it lies below, and its folder created there,
88
88
  # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
89
89
  TS=$(date +%s)
90
90
  queue_append_both \
91
91
  "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
92
- "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
93
- "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
92
+ "$LEDGER_ROOT" "$CURATION_DIR/.pending-turns.jsonl" \
93
+ "$MEMORY_ENABLED" "$CURATION_ENABLED" \
94
94
  "user" "$PROMPT" "$TS"
95
95
 
96
96
  log "Captured user turn (${#PROMPT} chars)"
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
 
3
- # Learning System: capture-question (PostToolUse Hook, matcher: AskUserQuestion)
3
+ # Curation System: capture-question (PostToolUse Hook, matcher: AskUserQuestion)
4
4
  # Captures each answered question as a {role:"qa"} row into BOTH the memory and
5
- # learning queues -- high decision-signal content that free-running Stop-hook
5
+ # curation queues -- high decision-signal content that free-running Stop-hook
6
6
  # turns miss (the model's own paraphrase of an answer is lossy; the raw Q&A
7
7
  # pair is not).
8
8
  #
@@ -60,18 +60,18 @@ devflow_debug_set_cwd "$CWD"
60
60
  source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
61
61
  df_resolve_roots "$CWD" 2>/dev/null || true
62
62
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
63
- # The learning queue is the repository's, not the checkout's: in a linked worktree
63
+ # The curation queue is the repository's, not the checkout's: in a linked worktree
64
64
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
65
65
  # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
66
66
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
67
67
 
68
- # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
68
+ # The machine-wide manifest (the memory/curation switches, D-FEATURES-NARROW-ONLY
69
69
  # in queue-append) lives at the machine root,
70
70
  # $HOME/.devflow (D-ONE-HOME).
71
71
  DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
72
72
  PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
73
73
  MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
74
- LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
74
+ CURATION_DIR="$LEDGER_ROOT/.devflow/curation"
75
75
 
76
76
  # --- Parse questions + answers into "question<TAB>answer" rows (one subprocess) ---
77
77
  # Defensive against: tool_response absent or a plain string (error case), missing
@@ -123,14 +123,14 @@ fi
123
123
 
124
124
  source "$SCRIPT_DIR/queue-append" || { echo "capture-question: failed to source queue-append" >&2; exit 1; }
125
125
 
126
- # --- AC-P1: at most ONE gate-read fork for memory + learning ---
126
+ # --- AC-P1: at most ONE gate-read fork for memory + curation ---
127
127
  # The machine switch, narrowed by this checkout's project.json/config.json
128
128
  # `features` (D-FEATURES-NARROW-ONLY, see queue-append).
129
129
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
130
130
  MEMORY_ENABLED="$_QG_MEMORY"
131
- LEARNING_ENABLED="$_QG_LEARNING"
131
+ CURATION_ENABLED="$_QG_CURATION"
132
132
 
133
- if [ "$MEMORY_ENABLED" != "true" ] && [ "$LEARNING_ENABLED" != "true" ]; then
133
+ if [ "$MEMORY_ENABLED" != "true" ] && [ "$CURATION_ENABLED" != "true" ]; then
134
134
  dbg "EXIT: both features disabled"
135
135
  exit 0
136
136
  fi
@@ -138,7 +138,7 @@ fi
138
138
  source "$SCRIPT_DIR/hook-log-init" "capture-question"
139
139
  source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
140
140
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-question: failed to source get-mtime" >&2; exit 1; }
141
- source "$SCRIPT_DIR/learning-lock" || { echo "capture-question: failed to source learning-lock" >&2; exit 1; }
141
+ source "$SCRIPT_DIR/curation-lock" || { echo "capture-question: failed to source curation-lock" >&2; exit 1; }
142
142
 
143
143
  # Each queue is checked from the root it lies below, and its folder created there,
144
144
  # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
@@ -151,8 +151,8 @@ A: ${A}"
151
151
  TS=$(date +%s)
152
152
  queue_append_both \
153
153
  "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
154
- "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
155
- "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
154
+ "$LEDGER_ROOT" "$CURATION_DIR/.pending-turns.jsonl" \
155
+ "$MEMORY_ENABLED" "$CURATION_ENABLED" \
156
156
  "qa" "$CONTENT" "$TS"
157
157
  _QUESTION_COUNT=$(( _QUESTION_COUNT + 1 ))
158
158
  done <<EOF
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
 
3
- # Learning System: capture-turn (Stop Hook)
3
+ # Curation System: capture-turn (Stop Hook)
4
4
  # Dual-append: writes the assistant turn to BOTH the memory queue and the
5
- # learning queue, each independently gated by its own feature flag (AC-F4). The
5
+ # curation queue, each independently gated by its own feature flag (AC-F4). The
6
6
  # 120s-throttle/nohup-spawn block lives in memory-worker, a separate Stop
7
7
  # hook that Claude Code runs in parallel with this one; the worker tolerates a
8
8
  # queue this hook has not appended to yet (D-QUEUE-NO-ORPHAN-DELETE).
@@ -45,18 +45,18 @@ dbg "ASSISTANT_MSG length=${#ASSISTANT_MSG}"
45
45
  source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
46
46
  df_resolve_roots "$CWD" 2>/dev/null || true
47
47
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
48
- # The learning queue is the repository's, not the checkout's: in a linked worktree
48
+ # The curation queue is the repository's, not the checkout's: in a linked worktree
49
49
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
50
50
  # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
51
51
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
52
52
 
53
- # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
53
+ # The machine-wide manifest (the memory/curation switches, D-FEATURES-NARROW-ONLY
54
54
  # in queue-append) lives at the machine root,
55
55
  # $HOME/.devflow (D-ONE-HOME).
56
56
  DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
57
57
  PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
58
58
  MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
59
- LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
59
+ CURATION_DIR="$LEDGER_ROOT/.devflow/curation"
60
60
 
61
61
  # Skip if empty response
62
62
  if [ -z "$ASSISTANT_MSG" ]; then
@@ -66,16 +66,16 @@ fi
66
66
 
67
67
  source "$SCRIPT_DIR/queue-append" || { echo "capture-turn: failed to source queue-append" >&2; exit 1; }
68
68
 
69
- # --- AC-P1: at most ONE gate-read fork for memory + learning ---
69
+ # --- AC-P1: at most ONE gate-read fork for memory + curation ---
70
70
  # The machine switch, narrowed by this checkout's project.json/config.json
71
71
  # `features` (D-FEATURES-NARROW-ONLY, see queue-append).
72
72
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
73
73
  MEMORY_ENABLED="$_QG_MEMORY"
74
- LEARNING_ENABLED="$_QG_LEARNING"
74
+ CURATION_ENABLED="$_QG_CURATION"
75
75
 
76
- dbg "MEMORY_ENABLED=$MEMORY_ENABLED LEARNING_ENABLED=$LEARNING_ENABLED"
76
+ dbg "MEMORY_ENABLED=$MEMORY_ENABLED CURATION_ENABLED=$CURATION_ENABLED"
77
77
 
78
- if [ "$MEMORY_ENABLED" != "true" ] && [ "$LEARNING_ENABLED" != "true" ]; then
78
+ if [ "$MEMORY_ENABLED" != "true" ] && [ "$CURATION_ENABLED" != "true" ]; then
79
79
  dbg "EXIT: both features disabled"
80
80
  exit 0
81
81
  fi
@@ -88,15 +88,15 @@ source "$SCRIPT_DIR/hook-log-init" "capture-turn"
88
88
  source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
89
89
 
90
90
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-turn: failed to source get-mtime" >&2; exit 1; }
91
- source "$SCRIPT_DIR/learning-lock" || { echo "capture-turn: failed to source learning-lock" >&2; exit 1; }
91
+ source "$SCRIPT_DIR/curation-lock" || { echo "capture-turn: failed to source curation-lock" >&2; exit 1; }
92
92
 
93
93
  # Each queue is checked from the root it lies below, and its folder created there,
94
94
  # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
95
95
  TS=$(date +%s)
96
96
  queue_append_both \
97
97
  "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
98
- "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
99
- "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
98
+ "$LEDGER_ROOT" "$CURATION_DIR/.pending-turns.jsonl" \
99
+ "$MEMORY_ENABLED" "$CURATION_ENABLED" \
100
100
  "assistant" "$ASSISTANT_MSG" "$TS"
101
101
 
102
102
  log "Captured assistant turn (${#ASSISTANT_MSG} chars)"
@@ -8,7 +8,7 @@
8
8
  # break) rather than crashing on a BSD-only stat flag — safe-by-construction,
9
9
  # never a hard failure, but source get-mtime first for accurate staleness.
10
10
 
11
- learning_lock_acquire() {
11
+ curation_lock_acquire() {
12
12
  local lock_dir="$1"
13
13
  local timeout="${2:-3}"
14
14
  local stale_threshold=30
@@ -39,6 +39,6 @@ learning_lock_acquire() {
39
39
  return 1
40
40
  }
41
41
 
42
- learning_lock_release() {
42
+ curation_lock_release() {
43
43
  rmdir "$1" 2>/dev/null || true
44
44
  }
@@ -0,0 +1,293 @@
1
+ #!/bin/bash
2
+ # curation-move -- the one-time ledger folder move for hooks (sourced, not executed).
3
+ #
4
+ # Sourcing it only defines functions -- no side effects until they are called. It is
5
+ # installed with the other hooks and is NOT gated on the curation switch: the old
6
+ # folder holds the project's decisions whether the feature is on or off, and a
7
+ # creator that ran before the move would hide it.
8
+ #
9
+ # df_curation_move <ledger_root> [retry]
10
+ # Moves <ledger_root>/.devflow/learning to .devflow/curation, once, by running the
11
+ # move op of json-helper.cjs (moveLedger in lib/curation-move.cjs). <ledger_root>
12
+ # is the repository's ledger root (DF_LEDGER_ROOT), never the checkout's own.
13
+ # Always returns 0, and never waits: a move another process holds is skipped and
14
+ # tried again at the next event.
15
+ #
16
+ # 1. Probe the old folder with a builtin test. Absent is the steady state: nothing
17
+ # is forked, no key is computed, nothing is logged.
18
+ # 2. Builtin refusals, which do nothing else: the root is not a directory, is a
19
+ # link, is the home directory or the machine directory (~/.devflow), or a link
20
+ # sits on the way down to its .devflow. The op refuses the same set.
21
+ # 3. Compute the marker key (df_curation_move_key) with builtins only, and test the
22
+ # four markers, files at the logs root, never inside the project: curation-move.<key>.skip
23
+ # (an earlier attempt was refused or failed; the refused marker), curation-move.<key>.busy
24
+ # (a fresh queue claim held the move, and a claim can last 15 minutes), and
25
+ # curation-move.<key>.tracked and curation-move.<key>.unknown (the move was skipped: git
26
+ # tracks files in a curation data folder, or git did not answer). A marker means
27
+ # return, with no fork and no log line. A second argument `retry` (session start) removes all four first, so a
28
+ # refusal, a skip and a claim are looked at again once per session; that removal
29
+ # forks only when a marker exists. The HUD and status read every marker but the busy
30
+ # one: a busy move is not a refusal.
31
+ # 4. Fork node once, with the root as one argv word (no path is ever part of the
32
+ # script text); the op knows the two folder names itself. The op answers on its first line:
33
+ # moved <mode> one log line (the mode, then the backup folder and the notes
34
+ # the op reports, when any);
35
+ # busy nothing: no log line and no marker, the next event retries (a
36
+ # lock is held, which does not last);
37
+ # busy claim nothing is logged, and the busy marker is written, so no event
38
+ # forks node again until the next session start;
39
+ # skipped <why> <why> is `tracked` or `unknown`. Nothing in the project changed.
40
+ # One log line (with `unknown`, the fixed cause word from the
41
+ # op's `cause <word>` line), the marker of that reason is
42
+ # written, and DF_CURATION_MOVE_SKIPPED is set to the reason, so
43
+ # the hook that called the shim can tell the user
44
+ # (session-start-context);
45
+ # none nothing (the folder went away after the probe);
46
+ # anything else, or a non-zero exit (a refusal or an I/O error): one log line,
47
+ # and the refused marker is written.
48
+ # Hooks that run under `set -e` call it as a plain statement: every path ends in
49
+ # `return 0`, the marker write and the node call are guarded.
50
+ #
51
+ # DF_CURATION_MOVE_SKIPPED is empty at every call, and holds the reason only after the
52
+ # call that ran node and got a skip.
53
+ #
54
+ # df_curation_move_key <root>
55
+ # Sets DF_CURATION_MOVE_KEY to the marker key of <root>: a 32-bit hash over every
56
+ # byte of the root under LC_ALL=C (each step: hash = 33 * hash + (byte & 255),
57
+ # masked to 32 bits, starting from 5381) as eight lowercase hex characters, an
58
+ # underscore, then the last 60 bytes of the root (all of them when it is shorter)
59
+ # with every byte outside letters and digits replaced by an underscore. The
60
+ # TypeScript twin is curationMoveMarkerKey in src/core/project-paths.ts; status and
61
+ # the HUD read the marker through it (tests/shell-hooks.test.ts holds both to the
62
+ # same answers). Returns 1, setting nothing, for a root of more than 4096 bytes.
63
+ #
64
+ # df_curation_move_plain <text>
65
+ # Sets DF_CURATION_MOVE_PLAIN to <text> with every control character replaced by a
66
+ # space: C0 and DEL, and the C1 controls U+0080-U+009F (the bytes C2 80 to C2 9F in
67
+ # UTF-8). Builtin expansion only, so it forks nothing. node's answer can carry a name
68
+ # the repository holds, and no log line may carry an escape sequence or break in two.
69
+ # It works bytewise under LC_ALL=C, which it puts back as it was, so other UTF-8
70
+ # text, whatever the locale, is kept.
71
+ #
72
+ # df_curation_move_names
73
+ # Sets the names the shell reads: DF_CM_OLD_FOLDER and DF_CM_NEW_FOLDER (the ledger
74
+ # folder before and after the rename) and DF_CM_NEW_TUNING_FILE. They mirror
75
+ # LEGACY_NAMES.folder, CURATION_DIR_NAME and CURATION_TUNING_FILE_NAME in
76
+ # src/core/project-paths.ts and hooks/lib/project-paths.cjs (tests/project-paths.test.ts
77
+ # holds all three to the same values). The old backup prefix and the old tuning file
78
+ # are read only by the node move, so the shell holds no copy of them. Every hook that
79
+ # names a ledger folder (queue-append, ensure-devflow-init, session-start-context)
80
+ # takes the name from here.
81
+ #
82
+ # Source-order requirement: none. This file sources its sibling git-marker itself,
83
+ # for df_no_symlink_below; one that failed to source is a command not found, which
84
+ # the refusal below reads as "a link may sit on the path". The caller defines log()
85
+ # to have the move reported; without it the move is silent.
86
+
87
+ source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
88
+
89
+ df_curation_move_names() {
90
+ DF_CM_OLD_FOLDER="learning"
91
+ DF_CM_NEW_FOLDER="curation"
92
+ DF_CM_NEW_TUNING_FILE="curation.json"
93
+ return 0
94
+ }
95
+
96
+ df_curation_move_key() {
97
+ local _root="$1" _hash=5381 _i=0 _len=0 _char="" _byte=0 _start=0 _hex="" _tail="" _lc_set=0 _lc_old=""
98
+ if [ "${LC_ALL+set}" = "set" ]; then _lc_set=1; _lc_old="$LC_ALL"; fi
99
+ LC_ALL=C
100
+ _len=${#_root}
101
+ if [ "$_len" -gt 4096 ]; then
102
+ if [ "$_lc_set" = 1 ]; then LC_ALL="$_lc_old"; else unset LC_ALL; fi
103
+ return 1
104
+ fi
105
+ while [ "$_i" -lt "$_len" ]; do
106
+ _char="${_root:$_i:1}"
107
+ printf -v _byte '%d' "'$_char"
108
+ _hash=$(( (_hash * 33 + (_byte & 255)) & 4294967295 ))
109
+ _i=$((_i + 1))
110
+ done
111
+ printf -v _hex '%08x' "$_hash"
112
+ if [ "$_len" -gt 60 ]; then _start=$((_len - 60)); fi
113
+ _tail="${_root:$_start}"
114
+ _tail="${_tail//[^A-Za-z0-9]/_}"
115
+ if [ "$_lc_set" = 1 ]; then LC_ALL="$_lc_old"; else unset LC_ALL; fi
116
+ DF_CURATION_MOVE_KEY="${_hex}_${_tail}"
117
+ return 0
118
+ }
119
+
120
+ df_curation_move_plain() {
121
+ local _text="$1" _lc_set=0 _lc_old=""
122
+ if [ "${LC_ALL+set}" = "set" ]; then _lc_set=1; _lc_old="$LC_ALL"; fi
123
+ LC_ALL=C
124
+ _text="${_text//$'\xc2'[$'\x80'-$'\x9f']/ }"
125
+ _text="${_text//[[:cntrl:]]/ }"
126
+ if [ "$_lc_set" = 1 ]; then LC_ALL="$_lc_old"; else unset LC_ALL; fi
127
+ DF_CURATION_MOVE_PLAIN="$_text"
128
+ return 0
129
+ }
130
+
131
+ # The marker paths of the key in DF_CURATION_MOVE_KEY, built here and nowhere else. The
132
+ # refused marker keeps the file suffix `.skip` (getCurationMoveMarkerPath in
133
+ # src/core/curation-move-notice.ts).
134
+ _df_cm_markers() {
135
+ local _stem="$HOME/.devflow/logs/curation-move.${DF_CURATION_MOVE_KEY}"
136
+ DF_CM_REFUSED_MARKER="${_stem}.skip"
137
+ DF_CM_BUSY_MARKER="${_stem}.busy"
138
+ DF_CM_TRACKED_MARKER="${_stem}.tracked"
139
+ DF_CM_UNKNOWN_MARKER="${_stem}.unknown"
140
+ return 0
141
+ }
142
+
143
+ # True when anything stands at $1. A link counts as a marker and is never followed.
144
+ _df_cm_present() {
145
+ [ -e "$1" ] || [ -L "$1" ]
146
+ }
147
+
148
+ # Remove the marker at $1. It forks only when one stands there.
149
+ _df_cm_clear() {
150
+ if _df_cm_present "$1"; then rm -f "$1" 2>/dev/null || true; fi
151
+ return 0
152
+ }
153
+
154
+ # Create the marker at $1 under noclobber, which is put back as it was. A marker that
155
+ # cannot be written is ignored: the next event looks again.
156
+ _df_cm_mark() {
157
+ local _noclobber=0
158
+ case $- in *C*) _noclobber=1 ;; esac
159
+ if ! _df_cm_present "$1"; then
160
+ set -o noclobber
161
+ { : > "$1"; } 2>/dev/null || true
162
+ if [ "$_noclobber" = 0 ]; then set +o noclobber; fi
163
+ fi
164
+ return 0
165
+ }
166
+
167
+ # Log the one line for a move: $1 is the op's whole answer, $2 its first line (`moved <mode>`).
168
+ # The lines after it are `backup <name>` and `note <text>`; at most 32 are read.
169
+ _df_cm_report_moved() {
170
+ local _out="$1" _first="$2" _rest="" _line="" _backup="" _notes="" _n=0
171
+ _rest="${_out#*$'\n'}"
172
+ if [ "$_rest" = "$_out" ]; then _rest=""; fi
173
+ while [ -n "$_rest" ] && [ "$_n" -lt 32 ]; do
174
+ _line="${_rest%%$'\n'*}"
175
+ case "$_line" in
176
+ "backup "*) df_curation_move_plain "${_line#backup }"; _backup=", backup folder $DF_CURATION_MOVE_PLAIN" ;;
177
+ "note "*) df_curation_move_plain "${_line#note }"; _notes="${_notes:+$_notes; }$DF_CURATION_MOVE_PLAIN" ;;
178
+ esac
179
+ if [ "$_line" = "$_rest" ]; then _rest=""; else _rest="${_rest#*$'\n'}"; fi
180
+ _n=$((_n + 1))
181
+ done
182
+ if declare -F log >/dev/null 2>&1; then
183
+ log "Curation move (${_first#moved })${_backup}${_notes:+: $_notes}"
184
+ fi
185
+ return 0
186
+ }
187
+
188
+ # Log the refusal in $1 (the op's first line) and write the refused marker.
189
+ _df_cm_mark_refused() {
190
+ if declare -F log >/dev/null 2>&1; then
191
+ log "Curation move refused: ${1:0:200}; not tried again until the next session start"
192
+ fi
193
+ _df_cm_mark "$DF_CM_REFUSED_MARKER"
194
+ return 0
195
+ }
196
+
197
+ # Set DF_CM_CAUSE to the fixed word of the `cause <word>` line in the op's whole answer $1
198
+ # (lowercase letters and hyphens, at most 32 bytes), or to "" when there is none.
199
+ _df_cm_cause() {
200
+ local _rest="${1#*$'\n'}" _word=""
201
+ DF_CM_CAUSE=""
202
+ if [ "$_rest" = "$1" ]; then return 0; fi
203
+ case "${_rest%%$'\n'*}" in
204
+ "cause "*) _word="${_rest%%$'\n'*}"; _word="${_word#cause }" ;;
205
+ esac
206
+ case "$_word" in
207
+ ""|*[!a-z-]*) ;;
208
+ *) if [ "${#_word}" -le 32 ]; then DF_CM_CAUSE="$_word"; fi ;;
209
+ esac
210
+ return 0
211
+ }
212
+
213
+ # Log the skip for reason $1 (`tracked` or `unknown`), write its marker and record the
214
+ # reason in DF_CURATION_MOVE_SKIPPED. $2 is the op's whole answer, whose cause word the
215
+ # `unknown` line carries. The log words are fixed: they hold nothing from the project.
216
+ _df_cm_mark_skipped() {
217
+ local _why="$1" _words="" _marker=""
218
+ DF_CURATION_MOVE_SKIPPED="$_why"
219
+ case "$_why" in
220
+ tracked)
221
+ _words="git tracks files in the .devflow data folders"
222
+ _marker="$DF_CM_TRACKED_MARKER" ;;
223
+ *)
224
+ _df_cm_cause "${2:-}"
225
+ _words="git did not answer${DF_CM_CAUSE:+ ($DF_CM_CAUSE)}"
226
+ _marker="$DF_CM_UNKNOWN_MARKER" ;;
227
+ esac
228
+ if declare -F log >/dev/null 2>&1; then
229
+ log "Curation move skipped: $_words; not tried again until the next session start"
230
+ fi
231
+ _df_cm_mark "$_marker"
232
+ return 0
233
+ }
234
+
235
+ df_curation_move() {
236
+ local _root="$1" _retry="${2:-}"
237
+ local _devflow="" _back="$PWD" _real="" _home_real="" _machine_real=""
238
+ local _helper="" _out="" _rc=0 _first="" _marker=""
239
+
240
+ DF_CURATION_MOVE_SKIPPED=""
241
+ [ -n "$_root" ] || return 0
242
+ [ -n "${HOME:-}" ] || return 0
243
+ df_curation_move_names
244
+ _devflow="$_root/.devflow"
245
+
246
+ # 1. The steady state: no old folder (a link or a file at its name counts as
247
+ # present, so the op can refuse it).
248
+ if [ ! -e "$_devflow/$DF_CM_OLD_FOLDER" ] && [ ! -L "$_devflow/$DF_CM_OLD_FOLDER" ]; then
249
+ return 0
250
+ fi
251
+
252
+ # 2. The refusals that need no fork. The physical paths are compared, because the
253
+ # root, HOME and a repository's toplevel reach here by different routes (on macOS
254
+ # /var is /private/var): cd -P publishes the resolved path in $PWD.
255
+ [ -d "$_root" ] || return 0
256
+ if [ -L "$_root" ]; then return 0; fi
257
+ if cd -P "$_root" 2>/dev/null; then _real="$PWD"; fi
258
+ if cd -P "$HOME" 2>/dev/null; then _home_real="$PWD"; fi
259
+ if cd -P "$HOME/.devflow" 2>/dev/null; then _machine_real="$PWD"; fi
260
+ cd "$_back" 2>/dev/null || true
261
+ [ -n "$_real" ] || return 0
262
+ if [ "$_real" = "$_home_real" ] || [ "$_real" = "$_machine_real" ]; then return 0; fi
263
+ if ! df_no_symlink_below "$_root" "$_devflow"; then return 0; fi
264
+
265
+ # 3. The markers.
266
+ df_curation_move_key "$_root" || return 0
267
+ _df_cm_markers
268
+ set -- "$DF_CM_REFUSED_MARKER" "$DF_CM_BUSY_MARKER" "$DF_CM_TRACKED_MARKER" "$DF_CM_UNKNOWN_MARKER"
269
+ if [ "$_retry" = "retry" ]; then
270
+ for _marker in "$@"; do _df_cm_clear "$_marker"; done
271
+ fi
272
+ for _marker in "$@"; do
273
+ if _df_cm_present "$_marker"; then return 0; fi
274
+ done
275
+
276
+ # 4. The one fork. 2>&1 so a refusal's message reaches the log line.
277
+ _helper="${BASH_SOURCE[0]%/*}/json-helper.cjs"
278
+ _out=$(node "$_helper" move-ledger "$_root" 2>&1) || _rc=$?
279
+ df_curation_move_plain "${_out%%$'\n'*}"
280
+ _first="$DF_CURATION_MOVE_PLAIN"
281
+
282
+ if [ "$_rc" -eq 0 ]; then
283
+ case "$_first" in
284
+ busy|none) return 0 ;;
285
+ "busy claim") _df_cm_mark "$DF_CM_BUSY_MARKER"; return 0 ;;
286
+ "skipped tracked") _df_cm_mark_skipped tracked; return 0 ;;
287
+ "skipped unknown") _df_cm_mark_skipped unknown "$_out"; return 0 ;;
288
+ "moved "*) _df_cm_report_moved "$_out" "$_first"; return 0 ;;
289
+ esac
290
+ fi
291
+ _df_cm_mark_refused "$_first"
292
+ return 0
293
+ }