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
@@ -2,7 +2,7 @@
2
2
  # ensure-root-gitignore — single source of truth for the project root .gitignore
3
3
  # rules that govern .devflow/.
4
4
  #
5
- # .devflow/ holds per-developer runtime state (memory, learning, docs, locks) —
5
+ # .devflow/ holds per-developer runtime state (memory, curation, docs, locks) —
6
6
  # local by default. FOUR exceptions are shared via git:
7
7
  # 1. Feature knowledge bases under .devflow/features/: index.md and every
8
8
  # {slug}/KNOWLEDGE.md (tracked, committed by the Knowledge agent at workflow end).
@@ -22,9 +22,9 @@
22
22
  # so git never descends and later negations are dead. Hence the multi-line block.
23
23
  #
24
24
  # Sourced by:
25
- # - ensure-devflow-init (reached via the memory/learning hooks)
25
+ # - ensure-devflow-init (reached via the memory/curation hooks)
26
26
  # - session-start-context (always-on, memory-independent — covers memory-off
27
- # projects that still use learning/knowledge)
27
+ # projects that still use curation/knowledge)
28
28
  # Both reach this one writer so the rule is applied identically everywhere; this
29
29
  # decouples git-tracking of .devflow/ from any single feature toggle.
30
30
  #
@@ -201,7 +201,7 @@ mkdir -p "$_ERG_DEVFLOW_DIR" 2>/dev/null || return 1
201
201
  # DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE in
202
202
  # src/targets/claude-code/post-install.ts.
203
203
  printf -v _ERG_BLOCK_NO_CI '%s\n' \
204
- '# Devflow runtime data — local by default (memory, learning, docs, locks).' \
204
+ '# Devflow runtime data — local by default (memory, curation, docs, locks).' \
205
205
  '# Shared via git: feature knowledge bases under .devflow/features/ (index.md and' \
206
206
  '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority),' \
207
207
  '# .devflow/policy.json (retired; presence only) and .devflow/project.json (team settings).' \
@@ -133,3 +133,84 @@ df_file_below() {
133
133
  fi
134
134
  return 1
135
135
  }
136
+
137
+ # df_ledger_folder <devflow_dir> <old> [<new>] — sets DF_LEDGER_FOLDER to the path of
138
+ # the ledger folder a creator may create or write into: <devflow_dir>/<old> while that
139
+ # name is present (a folder, a file or a link, dangling or not), else
140
+ # <devflow_dir>/<new>. <new> defaults to <old>, so a caller that names only the old
141
+ # folder gets the old folder's path in every state.
142
+ #
143
+ # The creator rule: a creator makes the new folder only when the old name is absent.
144
+ # A new folder made beside a present old one would hide the old ledger from every
145
+ # reader, so the session-start heal, the ensure-devflow-init scaffold and queue
146
+ # append all take their folder from here. It answers a path and creates nothing; the
147
+ # caller checks the path with df_no_symlink_below and creates it.
148
+ #
149
+ # Zero forks: builtin tests and parameter expansion only. It always returns 0; read
150
+ # the answer from DF_LEDGER_FOLDER, not from a command substitution.
151
+ df_ledger_folder() {
152
+ local _dir="$1" _old="$2" _new="${3:-$2}"
153
+ if [ -e "$_dir/$_old" ] || [ -L "$_dir/$_old" ]; then
154
+ DF_LEDGER_FOLDER="$_dir/$_old"
155
+ else
156
+ DF_LEDGER_FOLDER="$_dir/$_new"
157
+ fi
158
+ return 0
159
+ }
160
+
161
+ # df_ledger_queue <queue_file> <old> [<new>] — sets DF_LEDGER_QUEUE to the queue file
162
+ # a creator may append to. A queue file whose folder is named <old> or <new> is
163
+ # answered under the folder df_ledger_folder picks for that folder's parent; a queue
164
+ # in any other folder (the memory queue) and a path with fewer than two parts come
165
+ # back unchanged. <new> defaults to <old>. Creates nothing; zero forks; returns 0.
166
+ df_ledger_queue() {
167
+ local _file="$1" _old="$2" _new="${3:-$2}" _folder="" _parent="" _name=""
168
+ DF_LEDGER_QUEUE="$_file"
169
+ case "$_file" in */*/*) ;; *) return 0 ;; esac
170
+ _folder="${_file%/*}"
171
+ _parent="${_folder%/*}"
172
+ _name="${_folder##*/}"
173
+ case "$_name" in
174
+ "$_old"|"$_new")
175
+ df_ledger_folder "$_parent" "$_old" "$_new"
176
+ DF_LEDGER_QUEUE="$DF_LEDGER_FOLDER/${_file##*/}"
177
+ ;;
178
+ esac
179
+ return 0
180
+ }
181
+
182
+ # df_make_ledger_folder <root> <devflow_dir> <old> <new> — the one creator of the
183
+ # ledger folder for a hook. It sets DF_LEDGER_FOLDER as df_ledger_folder does, then
184
+ # makes the NEW folder and only the new one, and only where the old name is absent at
185
+ # the probe and no link sits on the way down from <root> (df_no_symlink_below). Where
186
+ # the old name is present it makes nothing, so a creator never makes the old folder:
187
+ # a `mkdir -p` on it would bring back an empty old folder beside the new one after a
188
+ # move had renamed it away. It logs nothing, so each hook words its own skip once.
189
+ # A predicate: it returns 0 when nothing is left to make or the folder was made, 2 for
190
+ # a link on the path (nothing is made), and 1 for an empty argument or a failed mkdir.
191
+ # Call it in an `if` or an `||` list, never as a plain statement under `set -e`.
192
+ #
193
+ # The remaining window is the gap between the probe and the mkdir. A move that
194
+ # finishes inside it leaves no folder this time, and the next event makes the new one.
195
+ # An old folder that appears inside it, from an older install or by hand, leaves the
196
+ # new folder beside it, and the next move merges the two. No data is lost either way:
197
+ # the helper only ever makes an empty folder, and never writes into or removes one.
198
+ df_make_ledger_folder() {
199
+ local _root="$1" _dir="$2" _old="$3" _new="$4"
200
+ [ -n "$_root" ] && [ -n "$_dir" ] && [ -n "$_old" ] && [ -n "$_new" ] || return 1
201
+ df_ledger_folder "$_dir" "$_old" "$_new"
202
+ [ "$DF_LEDGER_FOLDER" = "$_dir/$_new" ] || return 0
203
+ if ! df_no_symlink_below "$_root" "$DF_LEDGER_FOLDER"; then return 2; fi
204
+ if [ -d "$DF_LEDGER_FOLDER" ]; then return 0; fi
205
+ mkdir -p "$DF_LEDGER_FOLDER" 2>/dev/null || return 1
206
+ return 0
207
+ }
208
+
209
+ # df_global_tuning_file <machine_dir> — sets DF_GLOBAL_TUNING_FILE to the global
210
+ # curate-agent tuning file inside the machine directory (~/.devflow). The shell
211
+ # twin of getGlobalCurateTuningConfigPath in src/core/project-paths.ts: the one
212
+ # place a hook names the file. Zero forks; returns 0.
213
+ df_global_tuning_file() {
214
+ DF_GLOBAL_TUNING_FILE="$1/curation.json"
215
+ return 0
216
+ }
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/assets/scripts/hooks/json-helper.cjs
4
4
  // Provides jq-equivalent operations for hooks when jq is not installed, and the
5
- // learning ops the Curate agent runs from the project root.
5
+ // curation ops the Curate agent runs from the project root.
6
6
  // SECURITY: This is a local CLI helper invoked only by shell hooks and the
7
7
  // Curate agent with controlled arguments. No operation takes a file path: a
8
- // file's content arrives on stdin (json-parse redirects it), and the learning ops
8
+ // file's content arrives on stdin (json-parse redirects it), and the curation ops
9
9
  // build every path from the current directory.
10
10
  // Usage: node json-helper.cjs <operation> [args...]
11
11
  //
@@ -37,13 +37,17 @@
37
37
  // show <anchor|obs_id> Read-only: print one entry as pretty JSON
38
38
  // claim-due Hand out the entries due for maintenance, leased
39
39
  // for a day, after the ref their claims are checked at
40
- // claim-queue Claim the learning queue for this run; prints
40
+ // claim-queue Claim the curation queue for this run; prints
41
41
  // claimed <token>[ takeover] | busy | none
42
42
  // release-claim <token> Release the claim the token owns; prints
43
43
  // released | not-owner | gone
44
+ // move-ledger <root> Move a project's old ledger folder to the new name,
45
+ // once; prints none | busy | busy claim |
46
+ // skipped tracked | skipped unknown | moved <mode>
47
+ // and its backup, cause and notes
44
48
  //
45
- // Every learning op above except claim-queue and release-claim first refreshes
46
- // the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
49
+ // Every curation op above except claim-queue, release-claim and move-ledger first
50
+ // refreshes the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
47
51
 
48
52
  'use strict';
49
53
 
@@ -53,21 +57,35 @@ const { constants: { MAX_STRING_LENGTH } } = require('buffer');
53
57
  const op = process.argv[2];
54
58
  const args = process.argv.slice(3);
55
59
 
56
- /** The learning modules, once loaded; see learning(). */
57
- let learningModules = null;
60
+ /** The curation modules, once loaded; see curation(). */
61
+ let curationModules = null;
58
62
 
59
63
  /**
60
- * The learning store, loaded on first use and memoized. The generic ops never
64
+ * The curation store, loaded on first use and memoized. The generic ops never
61
65
  * call it, so a hook that falls back from jq to node never pays for loading it,
62
66
  * and the store loads the renderer only when an op renders.
63
67
  *
64
68
  * @returns {{ store: object }}
65
69
  */
66
- function learning() {
67
- if (learningModules === null) {
68
- learningModules = { store: require('./lib/learning-store.cjs') };
70
+ function curation() {
71
+ if (curationModules === null) {
72
+ curationModules = { store: require('./lib/curation-store.cjs') };
69
73
  }
70
- return learningModules;
74
+ return curationModules;
75
+ }
76
+
77
+ /** The ledger move module, once loaded; see curationMove(). */
78
+ let curationMoveModule = null;
79
+
80
+ /**
81
+ * The ledger move module, loaded on first use and memoized. Only the move op calls
82
+ * it, so no other curation op loads the move code.
83
+ *
84
+ * @returns {object}
85
+ */
86
+ function curationMove() {
87
+ if (curationMoveModule === null) curationMoveModule = require('./lib/curation-move.cjs');
88
+ return curationMoveModule;
71
89
  }
72
90
 
73
91
  /** The bytes each read of stdin asks for. */
@@ -84,7 +102,7 @@ const STDIN_WAIT_CELL = new Int32Array(new SharedArrayBuffer(4));
84
102
 
85
103
  /**
86
104
  * Read stdin to its end, keeping at most `maxBytes`. Every op that reads stdin
87
- * reads it here, so the generic ops and the learning ops cannot read it two ways.
105
+ * reads it here, so the generic ops and the curation ops cannot read it two ways.
88
106
  * Never throws: a failure is a refusal.
89
107
  *
90
108
  * D-STDIN-FD0: stdin is read from file descriptor 0 itself, in reads repeated
@@ -168,14 +186,16 @@ function parseArgs(argList) {
168
186
  }
169
187
 
170
188
  // ---------------------------------------------------------------------------
171
- // Learning-op adapter
189
+ // Curation-op adapter
172
190
  // ---------------------------------------------------------------------------
173
191
 
174
192
  /**
175
- * Print a learning op's Result: `format(value)` and a newline on stdout, or the
193
+ * Print a curation op's Result: `format(value)` and a newline on stdout, or the
176
194
  * error message and a newline on stderr. Returns the exit code for the op to set
177
195
  * as process.exitCode, so the process exits once, after the op has returned and
178
- * every lock it took is released.
196
+ * every lock it took is released. The error message keeps its line structure (a
197
+ * refusal lists one problem per line), and each line has no control character, as it
198
+ * may name a file the repository holds. At most REFUSAL_MAX_LINES lines print.
179
199
  *
180
200
  * @param {{ ok: true, value: unknown } | { ok: false, error: { message: string } }} result
181
201
  * @param {(value: any) => string} format - the stdout text for the value
@@ -186,10 +206,43 @@ function emit(result, format) {
186
206
  process.stdout.write(`${format(result.value)}\n`);
187
207
  return 0;
188
208
  }
189
- process.stderr.write(`${result.error.message}\n`);
209
+ process.stderr.write(`${refusalText(result.error.message)}\n`);
190
210
  return 1;
191
211
  }
192
212
 
213
+ /** The most lines of one refusal message that print. */
214
+ const REFUSAL_MAX_LINES = 200;
215
+
216
+ /**
217
+ * A refusal message with each line scrubbed (singleLine) and the line structure kept.
218
+ * Past REFUSAL_MAX_LINES lines, the rest become one `... N more lines` line.
219
+ *
220
+ * @param {string} message
221
+ * @returns {string}
222
+ */
223
+ function refusalText(message) {
224
+ const { singleLine } = curation().store;
225
+ const lines = String(message).split('\n');
226
+ if (lines.length <= REFUSAL_MAX_LINES) return lines.map(line => singleLine(line)).join('\n');
227
+ const shown = lines.slice(0, REFUSAL_MAX_LINES).map(line => singleLine(line));
228
+ return [...shown, `... ${lines.length - REFUSAL_MAX_LINES} more lines`].join('\n');
229
+ }
230
+
231
+ /**
232
+ * The first line of move-ledger's answer: the state, with `moved` followed by its mode,
233
+ * `busy` followed by `claim` when a fresh queue claim holds the move, and `skipped`
234
+ * followed by its reason (`tracked` or `unknown`).
235
+ *
236
+ * @param {{ state: string, mode?: string, reason?: string }} moved
237
+ * @returns {string}
238
+ */
239
+ function moveAnswerWord(moved) {
240
+ if (moved.state === 'moved') return `moved ${moved.mode}`;
241
+ if (moved.state === 'busy' && moved.reason === 'claim') return 'busy claim';
242
+ if (moved.state === 'skipped') return `skipped ${moved.reason}`;
243
+ return moved.state;
244
+ }
245
+
193
246
  /**
194
247
  * Refuse a malformed command line: print `<op>: usage: <usage>` on stderr and exit 1,
195
248
  * before the op takes any lock.
@@ -202,11 +255,11 @@ function exitWithUsage(usage) {
202
255
  process.exit(1);
203
256
  }
204
257
 
205
- /** The most stdin a learning op reads: far above any valid input, so a runaway writer is refused, not parsed. */
258
+ /** The most stdin a curation op reads: far above any valid input, so a runaway writer is refused, not parsed. */
206
259
  const STDIN_JSON_MAX_BYTES = 64 * 1024;
207
260
 
208
261
  /**
209
- * A learning op's stdin as one JSON object: the one way text reaches a learning
262
+ * A curation op's stdin as one JSON object: the one way text reaches a curation
210
263
  * op, so no field of it ever passes through argv or a shell word. Never throws.
211
264
  *
212
265
  * @param {string} opName - for the message
@@ -235,30 +288,30 @@ const PUT_MODES = new Map([['--create', 'create'], ['--update', 'update'], ['--r
235
288
  const ENTRY_TYPES = new Set(['decision', 'pitfall']);
236
289
 
237
290
  /**
238
- * The learning ops whose run sends the claim heartbeat first (D-OWNED-CLAIM).
291
+ * The curation ops whose run sends the claim heartbeat first (D-OWNED-CLAIM).
239
292
  * claim-queue and release-claim manage the claim themselves, and the generic ops
240
- * never touch it. A new learning op joins this set.
293
+ * never touch it. A new curation op joins this set.
241
294
  */
242
- const LEARNING_OPS = new Set([
295
+ const CURATION_OPS = new Set([
243
296
  'assign-anchor', 'retire-anchor', 'restore-anchor', 'refresh-anchor', 'rotate-observations',
244
297
  'put-observation', 'list', 'show', 'claim-due',
245
298
  ]);
246
299
 
247
300
  /** Send the claim heartbeat; a failure is reported on stderr and never stops the op. */
248
301
  function heartbeat(root) {
249
- const beat = learning().store.touchClaim(root);
302
+ const beat = curation().store.touchClaim(root);
250
303
  if (!beat.ok) process.stderr.write(`${beat.error.message}\n`);
251
304
  }
252
305
 
253
- // The learning ops run from the project root and take no path to a learning file:
306
+ // The curation ops run from the project root and take no path to a curation file:
254
307
  // each builds its paths from the current directory. Every op that writes takes the
255
- // store's one learning lock through withDecisionsLock (D-ONE-LEARNING-LOCK) and
256
- // refuses, creating nothing, when .devflow/learning/ is absent (D-NO-STRAY-TREE)
308
+ // store's one curation lock through withDecisionsLock (D-ONE-LEARNING-LOCK) and
309
+ // refuses, creating nothing, when .devflow/curation/ is absent (D-NO-STRAY-TREE)
257
310
  // or when it, or .devflow, is a symbolic link (D-NO-LINKED-TREE).
258
311
  // A locked body returns its Result; emit prints it once the lock is released.
259
312
  if (require.main === module) {
260
313
  try {
261
- if (LEARNING_OPS.has(op)) heartbeat(process.cwd());
314
+ if (CURATION_OPS.has(op)) heartbeat(process.cwd());
262
315
  switch (op) {
263
316
  case 'get-field': {
264
317
  const input = JSON.parse(readStdin());
@@ -338,7 +391,6 @@ try {
338
391
  console.log(JSON.stringify({
339
392
  timestamp: data.ts || '',
340
393
  trigger: 'pre-compact',
341
- memory_snapshot: data.memory || '',
342
394
  git: {
343
395
  branch: data.branch || '',
344
396
  status: data.status || '',
@@ -353,12 +405,12 @@ try {
353
405
  // assign-anchor <decision|pitfall> <obs_id>
354
406
  // Promote a v2 observation no ledger row carries to the next entry of its
355
407
  // type, skipping each number a tracked file cites (assignAnchor,
356
- // learning-store.cjs: D-LEDGER-REGISTRY, D-E4-SKIP). It never writes the log.
408
+ // curation-store.cjs: D-LEDGER-REGISTRY, D-E4-SKIP). It never writes the log.
357
409
  // stdout: the anchor; stderr: `assign-anchor: skipped <anchor>, cited in
358
410
  // <path>:<line>` for each number skipped
359
411
  // -------------------------------------------------------------------------
360
412
  case 'assign-anchor': {
361
- const { store } = learning();
413
+ const { store } = curation();
362
414
  if (args.length !== 2 || !ENTRY_TYPES.has(args[0]) || !store.OBS_ID_RE.test(args[1])) {
363
415
  exitWithUsage('assign-anchor <decision|pitfall> <obs_id> (run from the project root)');
364
416
  }
@@ -377,12 +429,12 @@ try {
377
429
  // Make an active entry inactive with the one JSON object on stdin its status
378
430
  // takes: { reason } for Retired and Deprecated, { by } for Superseded, and
379
431
  // { at, quote } for Encoded, the quote checked at the verify ref
380
- // (retireAnchor, learning-store.cjs: D-ENCODED-QUOTE).
432
+ // (retireAnchor, curation-store.cjs: D-ENCODED-QUOTE).
381
433
  // stdout: the new status in lower case and the anchor, then `repointed
382
434
  // <anchor>` for each entry re-pointed to the successor
383
435
  // -------------------------------------------------------------------------
384
436
  case 'retire-anchor': {
385
- const { store } = learning();
437
+ const { store } = curation();
386
438
  if (args.length !== 2 || !store.ANCHOR_ID_RE.test(args[0]) || !store.INACTIVE_STATUSES.includes(args[1])) {
387
439
  exitWithUsage('retire-anchor <anchor> <Encoded|Superseded|Retired|Deprecated> (one JSON object on stdin; run from the project root)');
388
440
  }
@@ -400,11 +452,11 @@ try {
400
452
  // Make an inactive entry active again, its notes, last_verified and
401
453
  // last_attempt cleared so it is due for maintenance again, ordered after
402
454
  // integrity problems and legacy entries, or among them when it is one
403
- // (restoreAnchor, learning-store.cjs: D-DUE-ORDER).
455
+ // (restoreAnchor, curation-store.cjs: D-DUE-ORDER).
404
456
  // stdout: restored <anchor>
405
457
  // -------------------------------------------------------------------------
406
458
  case 'restore-anchor': {
407
- const { store } = learning();
459
+ const { store } = curation();
408
460
  if (args.length !== 1 || !store.ANCHOR_ID_RE.test(args[0])) {
409
461
  exitWithUsage('restore-anchor <anchor> (run from the project root)');
410
462
  }
@@ -415,12 +467,12 @@ try {
415
467
  // -------------------------------------------------------------------------
416
468
  // refresh-anchor <anchor> [<anchor>...] [--verified]
417
469
  // Re-project active v2 entries from their log rows, or under --verified stamp
418
- // them verified today; all or nothing (refreshAnchors, learning-store.cjs).
470
+ // them verified today; all or nothing (refreshAnchors, curation-store.cjs).
419
471
  // stdout: `reprojected <anchor>` or `unchanged <anchor>` per anchor, or
420
472
  // `verified <anchor>` under --verified, in the order given
421
473
  // -------------------------------------------------------------------------
422
474
  case 'refresh-anchor': {
423
- const { store } = learning();
475
+ const { store } = curation();
424
476
  const verifiedFlags = args.filter(arg => arg === '--verified').length;
425
477
  const anchors = args.filter(arg => arg !== '--verified');
426
478
  if (verifiedFlags > 1 || anchors.length === 0 || !anchors.every(arg => store.ANCHOR_ID_RE.test(arg))) {
@@ -435,13 +487,13 @@ try {
435
487
  // rotate-observations
436
488
  // Archive the log rows no ledger row carries once 30 days have passed since
437
489
  // their last activity, and delete the usage telemetry's leftovers
438
- // (D-ROTATE-UNREFERENCED, learning-store.cjs). Takes no argument: the log and
490
+ // (D-ROTATE-UNREFERENCED, curation-store.cjs). Takes no argument: the log and
439
491
  // the archive are the project root's.
440
492
  // stdout: rotated <N> observations
441
493
  // -------------------------------------------------------------------------
442
494
  case 'rotate-observations': {
443
495
  if (args.length > 0) exitWithUsage('rotate-observations (no arguments; run from the project root)');
444
- const result = learning().store.rotateObservations(process.cwd());
496
+ const result = curation().store.rotateObservations(process.cwd());
445
497
  process.exitCode = emit(result, ({ rotated }) => `rotated ${rotated} observations`);
446
498
  break;
447
499
  }
@@ -449,7 +501,7 @@ try {
449
501
  // -------------------------------------------------------------------------
450
502
  // put-observation --create|--update|--reinforce
451
503
  // Store one observation from the JSON object on stdin (D-PUT-NOT-MERGE,
452
- // D-PUT-REPROJECTS, learning-store.cjs). Exactly one mode flag; no other argv.
504
+ // D-PUT-REPROJECTS, curation-store.cjs). Exactly one mode flag; no other argv.
453
505
  // stdout: created <id> | updated <id> | unchanged <id> | reinforced <id> <n>,
454
506
  // then one `reprojected <anchor>` line per entry re-projected
455
507
  // -------------------------------------------------------------------------
@@ -459,7 +511,7 @@ try {
459
511
  exitWithUsage('put-observation --create|--update|--reinforce (one JSON object on stdin; run from the project root)');
460
512
  }
461
513
  const input = readStdinJson('put-observation');
462
- const result = input.ok ? learning().store.putObservation(process.cwd(), mode, input.value) : input;
514
+ const result = input.ok ? curation().store.putObservation(process.cwd(), mode, input.value) : input;
463
515
  process.exitCode = emit(result, put => [
464
516
  put.outcome === 'reinforced' ? `reinforced ${put.id} ${put.observations}` : `${put.outcome} ${put.id}`,
465
517
  ...put.reprojected.map(anchorId => `reprojected ${anchorId}`),
@@ -470,25 +522,25 @@ try {
470
522
  // -------------------------------------------------------------------------
471
523
  // list
472
524
  // Print the ledger and the log by section, read-only (readListing and
473
- // formatListing, learning-store.cjs). Takes no argument.
525
+ // formatListing, curation-store.cjs). Takes no argument.
474
526
  // stdout: the ACTIVE, INACTIVE, OBSERVATIONS and INTEGRITY sections, then
475
527
  // MALFORMED when lines were skipped
476
528
  // -------------------------------------------------------------------------
477
529
  case 'list': {
478
530
  if (args.length > 0) exitWithUsage('list (no arguments; run from the project root)');
479
- const { store } = learning();
531
+ const { store } = curation();
480
532
  process.exitCode = emit(store.readListing(process.cwd()), store.formatListing);
481
533
  break;
482
534
  }
483
535
 
484
536
  // -------------------------------------------------------------------------
485
537
  // show <anchor|obs_id>
486
- // Print one entry, read-only (showByKey, learning-store.cjs).
538
+ // Print one entry, read-only (showByKey, curation-store.cjs).
487
539
  // stdout: pretty JSON { key, ledger, log, history_versions, flags }, plus
488
540
  // malformed when lines were skipped
489
541
  // -------------------------------------------------------------------------
490
542
  case 'show': {
491
- const { store } = learning();
543
+ const { store } = curation();
492
544
  if (args.length !== 1 || !(store.ANCHOR_ID_RE.test(args[0]) || store.OBS_ID_RE.test(args[0]))) {
493
545
  exitWithUsage('show <anchor|obs_id> (run from the project root)');
494
546
  }
@@ -499,13 +551,13 @@ try {
499
551
  // -------------------------------------------------------------------------
500
552
  // claim-due
501
553
  // Hand out the entries due for maintenance and lease each for a day
502
- // (claimDue, learning-store.cjs: D-DUE-ORDER, D-VERIFY-REF). Takes no argument.
554
+ // (claimDue, curation-store.cjs: D-DUE-ORDER, D-VERIFY-REF). Takes no argument.
503
555
  // stdout: ref <origin/HEAD|HEAD> <sha12>, or ref none; then one
504
556
  // `<anchor> <reason> <bytes>` line per entry handed out, or due none
505
557
  // -------------------------------------------------------------------------
506
558
  case 'claim-due': {
507
559
  if (args.length > 0) exitWithUsage('claim-due (no arguments; run from the project root)');
508
- const { store } = learning();
560
+ const { store } = curation();
509
561
  process.exitCode = emit(store.claimDue(process.cwd()), ({ ref, due }) => [
510
562
  ref === null ? 'ref none' : `ref ${ref.ref} ${ref.commit.slice(0, 12)}`,
511
563
  ...(due.length > 0 ? due.map(entry => `${store.singleLine(entry.anchor_id)} ${entry.reason} ${entry.bytes}`) : ['due none']),
@@ -515,13 +567,13 @@ try {
515
567
 
516
568
  // -------------------------------------------------------------------------
517
569
  // claim-queue
518
- // Claim the learning queue for this run (D-OWNED-CLAIM, learning-store.cjs).
570
+ // Claim the curation queue for this run (D-OWNED-CLAIM, curation-store.cjs).
519
571
  // Takes no argument; mints the token itself.
520
572
  // stdout: claimed <token> | claimed <token> takeover | busy | none
521
573
  // -------------------------------------------------------------------------
522
574
  case 'claim-queue': {
523
575
  if (args.length > 0) exitWithUsage('claim-queue (no arguments; run from the project root)');
524
- const result = learning().store.claimQueue(process.cwd());
576
+ const result = curation().store.claimQueue(process.cwd());
525
577
  process.exitCode = emit(result, claim => (claim.state === 'claimed'
526
578
  ? `claimed ${claim.token}${claim.takeover ? ' takeover' : ''}`
527
579
  : claim.state));
@@ -530,11 +582,11 @@ try {
530
582
 
531
583
  // -------------------------------------------------------------------------
532
584
  // release-claim <token>
533
- // Release the claim the token owns (D-OWNED-CLAIM, learning-store.cjs).
585
+ // Release the claim the token owns (D-OWNED-CLAIM, curation-store.cjs).
534
586
  // stdout: released | not-owner | gone
535
587
  // -------------------------------------------------------------------------
536
588
  case 'release-claim': {
537
- const { store } = learning();
589
+ const { store } = curation();
538
590
  if (args.length !== 1 || !store.CLAIM_TOKEN_RE.test(args[0])) {
539
591
  exitWithUsage('release-claim <token> (the 16 hex characters claim-queue printed)');
540
592
  }
@@ -542,12 +594,43 @@ try {
542
594
  break;
543
595
  }
544
596
 
597
+ // -------------------------------------------------------------------------
598
+ // move-ledger <root>
599
+ // Move the project's old ledger folder to the new name, once (moveLedger,
600
+ // curation-move.cjs). Not a curation op: it sends no heartbeat, because a
601
+ // heartbeat would refresh a claim the move has to judge stale. Unlike the
602
+ // curation ops it takes the root as an argument, so a caller outside the project
603
+ // root can name it; the root stays one argv word, never a shell word. The two
604
+ // folder names are the path helpers' own: no argument names them.
605
+ // stdout: none | busy | busy claim | skipped tracked | skipped unknown | moved <mode>,
606
+ // then `backup <name>` when a backup was made, `cause <word>` for `skipped unknown`
607
+ // (one fixed word that says why git did not answer), and one `note <text>` line for
608
+ // each note. `busy claim` means a fresh queue claim holds the move, which can last 15
609
+ // minutes; plain `busy` means a lock does. `skipped tracked` means git tracks files in
610
+ // a curation data folder, `skipped unknown` that git did not answer: nothing changed.
611
+ // Every line is scrubbed here (singleLine):
612
+ // a note may name a file the repository holds, and no line may carry a control
613
+ // character or break into two.
614
+ // -------------------------------------------------------------------------
615
+ case 'move-ledger': {
616
+ if (args.length !== 1) exitWithUsage('move-ledger <root> (the root is an absolute path)');
617
+ const { store } = curation();
618
+ process.exitCode = emit(curationMove().moveLedger(args[0]), moved => [
619
+ moveAnswerWord(moved),
620
+ ...(moved.state === 'moved' && moved.backup !== null ? [`backup ${moved.backup}`] : []),
621
+ ...(moved.state === 'skipped' && moved.cause !== undefined ? [`cause ${moved.cause}`] : []),
622
+ ...moved.notes.map(note => `note ${note}`),
623
+ ].map(line => store.singleLine(line)).join('\n'));
624
+ break;
625
+ }
626
+
545
627
  default:
546
628
  process.stderr.write(`json-helper: unknown operation "${op}"\n`);
547
629
  process.exit(1);
548
630
  }
549
631
  } catch (err) {
550
- process.stderr.write(`json-helper error: ${err && err.message ? err.message : String(err)}\n`);
632
+ const reason = err && err.message ? err.message : String(err);
633
+ process.stderr.write(`json-helper error: ${curationModules === null ? reason : curationModules.store.singleLine(reason)}\n`);
551
634
  process.exit(1);
552
635
  }
553
636
  } // end if (require.main === module)
@@ -170,7 +170,7 @@ json_prompt_output() {
170
170
  # Build pre-compact backup JSON. Usage: json_backup_construct --arg ts X --arg branch Y ...
171
171
  json_backup_construct() {
172
172
  if [ "$_HAS_JQ" = "true" ]; then
173
- jq -n "$@" '{timestamp: $ts, trigger: "pre-compact", memory_snapshot: $memory, git: {branch: $branch, status: $status, log: $log, diff_stat: $diff}}'
173
+ jq -n "$@" '{timestamp: $ts, trigger: "pre-compact", git: {branch: $branch, status: $status, log: $log, diff_stat: $diff}}'
174
174
  else
175
175
  node "$_JSON_HELPER" backup-construct "$@"
176
176
  fi