devflow-kit 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +438 -1
  2. package/README.md +15 -11
  3. package/dist/agents/code.md +9 -15
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +11 -13
  6. package/dist/agents/git.md +1 -1
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +7 -10
  9. package/dist/agents/review.md +6 -4
  10. package/dist/agents/skim.md +2 -2
  11. package/dist/agents/triage.md +2 -2
  12. package/dist/cli/commands/capture.js +1 -1
  13. package/dist/cli/commands/curation-move-io.js +102 -0
  14. package/dist/cli/commands/curation-move-step.js +374 -0
  15. package/dist/cli/commands/{learning.js → curation.js} +302 -99
  16. package/dist/cli/commands/gate-level-prompts.js +426 -0
  17. package/dist/cli/commands/gates.js +299 -0
  18. package/dist/cli/commands/init-seed.js +16 -5
  19. package/dist/cli/commands/init.js +263 -54
  20. package/dist/cli/commands/memory.js +20 -2
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +81 -9
  23. package/dist/cli.js +4 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +77 -73
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +76 -83
  28. package/dist/commands/dynamic-plan.md +5 -5
  29. package/dist/commands/dynamic-profile.md +5 -5
  30. package/dist/commands/dynamic-tickets.md +5 -5
  31. package/dist/commands/explore.md +6 -6
  32. package/dist/commands/implement.md +68 -102
  33. package/dist/commands/plan.md +19 -29
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +29 -31
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/assets.js +20 -20
  39. package/dist/core/curate-tuning-config.js +5 -6
  40. package/dist/core/curation-move-notice.js +84 -0
  41. package/dist/core/curation-move-skip.js +36 -0
  42. package/dist/core/curation-move.js +35 -0
  43. package/dist/core/curation-queue-cleanup.js +134 -0
  44. package/dist/core/curation-store.js +98 -0
  45. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  46. package/dist/core/echo-value.js +48 -0
  47. package/dist/core/evidence-policy.js +18 -6
  48. package/dist/core/feature-switch.js +95 -19
  49. package/dist/core/fs-atomic.js +1 -1
  50. package/dist/core/gate-level.js +221 -0
  51. package/dist/core/git.js +16 -14
  52. package/dist/core/ledger-root.js +82 -22
  53. package/dist/core/linked-path.js +4 -4
  54. package/dist/core/manifest-lock.js +238 -0
  55. package/dist/core/manifest.js +84 -24
  56. package/dist/core/migrations.js +1 -13
  57. package/dist/core/model-discovery.js +7 -1
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +15 -15
  60. package/dist/core/pool.js +26 -0
  61. package/dist/core/project-paths.js +103 -34
  62. package/dist/core/queue-drain.js +2 -2
  63. package/dist/core/tracked-areas.js +35 -0
  64. package/dist/core/tracker.js +6 -34
  65. package/dist/{learning-off → curation-off}/agents/code.md +9 -15
  66. package/dist/{learning-off → curation-off}/agents/fix.md +11 -13
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +7 -10
  68. package/dist/{learning-off → curation-off}/agents/review.md +5 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
  70. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  71. package/dist/{learning-off → curation-off}/commands/code-review.md +74 -70
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +73 -80
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +65 -99
  76. package/dist/{learning-off → curation-off}/commands/plan.md +15 -25
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +26 -28
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
  80. package/dist/hud/config.js +1 -1
  81. package/dist/hud/index.js +6 -6
  82. package/dist/hud/render.js +3 -3
  83. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  84. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +20 -14
  85. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +20 -20
  86. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +20 -20
  87. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +99 -67
  88. package/dist/targets/claude-code/installer.js +15 -15
  89. package/dist/targets/claude-code/language-stamp.js +5 -5
  90. package/dist/targets/claude-code/post-install.js +2 -2
  91. package/dist/targets/claude-code/tracker-servers.js +1 -1
  92. package/package.json +3 -2
  93. package/src/assets/agents/_partials/_code.mds +16 -17
  94. package/src/assets/agents/code.mds +4 -7
  95. package/src/assets/agents/curate.md +12 -14
  96. package/src/assets/agents/debug.mds +6 -6
  97. package/src/assets/agents/design.mds +18 -18
  98. package/src/assets/agents/fix.mds +6 -5
  99. package/src/assets/agents/git.mds +1 -1
  100. package/src/assets/agents/harden.mds +7 -7
  101. package/src/assets/agents/knowledge.mds +23 -23
  102. package/src/assets/agents/refactor.mds +2 -2
  103. package/src/assets/agents/research.mds +15 -15
  104. package/src/assets/agents/review.mds +45 -43
  105. package/src/assets/agents/skim.mds +11 -11
  106. package/src/assets/agents/synthesize.md +2 -2
  107. package/src/assets/agents/triage.mds +22 -22
  108. package/src/assets/agents/validate.md +13 -26
  109. package/src/assets/commands/_partials/_decisions.mds +13 -13
  110. package/src/assets/commands/_partials/_engine.mds +8 -8
  111. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  112. package/src/assets/commands/_partials/_preamble.mds +4 -4
  113. package/src/assets/commands/_partials/_settings.mds +3 -3
  114. package/src/assets/commands/_partials/_wave.mds +17 -24
  115. package/src/assets/commands/bug-analysis.mds +37 -48
  116. package/src/assets/commands/code-review.mds +90 -86
  117. package/src/assets/commands/debug.mds +14 -14
  118. package/src/assets/commands/dynamic-build.mds +78 -78
  119. package/src/assets/commands/dynamic-plan.mds +22 -22
  120. package/src/assets/commands/dynamic-profile.mds +2 -2
  121. package/src/assets/commands/dynamic-tickets.mds +8 -8
  122. package/src/assets/commands/explore.mds +20 -20
  123. package/src/assets/commands/implement.mds +92 -118
  124. package/src/assets/commands/plan.mds +41 -51
  125. package/src/assets/commands/release.mds +12 -12
  126. package/src/assets/commands/research.mds +19 -19
  127. package/src/assets/commands/resolve.mds +56 -50
  128. package/src/assets/commands/self-review.mds +20 -20
  129. package/src/assets/mds/git/_ops.mds +3 -1
  130. package/src/assets/mds/git/_ops_pr.mds +1 -1
  131. package/src/assets/mds/git/_ops_steps.mds +13 -7
  132. package/src/assets/mds/git/_pr.mds +7 -4
  133. package/src/assets/mds/tracker/_common.mds +23 -12
  134. package/src/assets/mds/tracker/_github.mds +2 -9
  135. package/src/assets/mds/tracker/_jira.mds +2 -15
  136. package/src/assets/mds/tracker/_linear.mds +2 -15
  137. package/src/assets/rules/quality.md +1 -0
  138. package/src/assets/scripts/ci-wait.cjs +22 -4
  139. package/src/assets/scripts/claude-md-audit.cjs +66 -51
  140. package/src/assets/scripts/hooks/background-memory-update +406 -97
  141. package/src/assets/scripts/hooks/capture-prompt +12 -12
  142. package/src/assets/scripts/hooks/capture-question +11 -11
  143. package/src/assets/scripts/hooks/capture-turn +12 -12
  144. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  145. package/src/assets/scripts/hooks/curation-move +293 -0
  146. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  147. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  148. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  149. package/src/assets/scripts/hooks/git-marker +81 -0
  150. package/src/assets/scripts/hooks/json-helper.cjs +145 -51
  151. package/src/assets/scripts/hooks/json-parse +19 -4
  152. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  153. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
  154. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  155. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  156. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  157. package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
  158. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
  159. package/src/assets/scripts/hooks/memory-permission +358 -0
  160. package/src/assets/scripts/hooks/memory-worker +1 -1
  161. package/src/assets/scripts/hooks/pre-compact-memory +6 -16
  162. package/src/assets/scripts/hooks/queue-append +228 -103
  163. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  164. package/src/assets/scripts/hooks/session-start-context +145 -71
  165. package/src/assets/scripts/hooks/session-start-memory +27 -46
  166. package/src/assets/scripts/lib/project-config.cjs +31 -9
  167. package/src/assets/scripts/resolve-base.cjs +1043 -0
  168. package/src/assets/scripts/resolve-settings.cjs +238 -46
  169. package/src/assets/skills/agent-references/SKILL.md +9 -2
  170. package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
  171. package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
  172. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  173. package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
  174. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
  175. package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
  176. package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
  177. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  178. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  179. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  180. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  181. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  182. package/templates/lint-pack/.lint-pack.conf +86 -0
  183. package/templates/lint-pack/README.md +190 -0
  184. package/templates/lint-pack/lint-pack.sh +905 -0
  185. package/dist/core/learning-queue-cleanup.js +0 -27
  186. package/dist/core/learning-store.js +0 -61
  187. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  188. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  189. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  190. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  191. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  192. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  193. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  194. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  195. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  196. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  197. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -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,16 +2,17 @@
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
  //
12
12
  // Operations:
13
13
  // get-field <field> [default] Read field from stdin JSON
14
14
  // get-string-field <field> Read field from stdin JSON only when it is a string
15
+ // array-length <field> Count of an array field in stdin JSON; `unknown` for any other type
15
16
  // extract-cwd-field <field> Extract cwd + arbitrary field, SOH-byte delimited
16
17
  // session-output <context> [message] Build SessionStart output envelope (+ systemMessage)
17
18
  // prompt-output <context> Build UserPromptSubmit output envelope
@@ -36,13 +37,17 @@
36
37
  // show <anchor|obs_id> Read-only: print one entry as pretty JSON
37
38
  // claim-due Hand out the entries due for maintenance, leased
38
39
  // for a day, after the ref their claims are checked at
39
- // claim-queue Claim the learning queue for this run; prints
40
+ // claim-queue Claim the curation queue for this run; prints
40
41
  // claimed <token>[ takeover] | busy | none
41
42
  // release-claim <token> Release the claim the token owns; prints
42
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
43
48
  //
44
- // Every learning op above except claim-queue and release-claim first refreshes
45
- // 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).
46
51
 
47
52
  'use strict';
48
53
 
@@ -52,21 +57,35 @@ const { constants: { MAX_STRING_LENGTH } } = require('buffer');
52
57
  const op = process.argv[2];
53
58
  const args = process.argv.slice(3);
54
59
 
55
- /** The learning modules, once loaded; see learning(). */
56
- let learningModules = null;
60
+ /** The curation modules, once loaded; see curation(). */
61
+ let curationModules = null;
57
62
 
58
63
  /**
59
- * 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
60
65
  * call it, so a hook that falls back from jq to node never pays for loading it,
61
66
  * and the store loads the renderer only when an op renders.
62
67
  *
63
68
  * @returns {{ store: object }}
64
69
  */
65
- function learning() {
66
- if (learningModules === null) {
67
- learningModules = { store: require('./lib/learning-store.cjs') };
70
+ function curation() {
71
+ if (curationModules === null) {
72
+ curationModules = { store: require('./lib/curation-store.cjs') };
68
73
  }
69
- 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;
70
89
  }
71
90
 
72
91
  /** The bytes each read of stdin asks for. */
@@ -83,7 +102,7 @@ const STDIN_WAIT_CELL = new Int32Array(new SharedArrayBuffer(4));
83
102
 
84
103
  /**
85
104
  * Read stdin to its end, keeping at most `maxBytes`. Every op that reads stdin
86
- * 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.
87
106
  * Never throws: a failure is a refusal.
88
107
  *
89
108
  * D-STDIN-FD0: stdin is read from file descriptor 0 itself, in reads repeated
@@ -167,14 +186,16 @@ function parseArgs(argList) {
167
186
  }
168
187
 
169
188
  // ---------------------------------------------------------------------------
170
- // Learning-op adapter
189
+ // Curation-op adapter
171
190
  // ---------------------------------------------------------------------------
172
191
 
173
192
  /**
174
- * 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
175
194
  * error message and a newline on stderr. Returns the exit code for the op to set
176
195
  * as process.exitCode, so the process exits once, after the op has returned and
177
- * 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.
178
199
  *
179
200
  * @param {{ ok: true, value: unknown } | { ok: false, error: { message: string } }} result
180
201
  * @param {(value: any) => string} format - the stdout text for the value
@@ -185,10 +206,43 @@ function emit(result, format) {
185
206
  process.stdout.write(`${format(result.value)}\n`);
186
207
  return 0;
187
208
  }
188
- process.stderr.write(`${result.error.message}\n`);
209
+ process.stderr.write(`${refusalText(result.error.message)}\n`);
189
210
  return 1;
190
211
  }
191
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
+
192
246
  /**
193
247
  * Refuse a malformed command line: print `<op>: usage: <usage>` on stderr and exit 1,
194
248
  * before the op takes any lock.
@@ -201,11 +255,11 @@ function exitWithUsage(usage) {
201
255
  process.exit(1);
202
256
  }
203
257
 
204
- /** 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. */
205
259
  const STDIN_JSON_MAX_BYTES = 64 * 1024;
206
260
 
207
261
  /**
208
- * 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
209
263
  * op, so no field of it ever passes through argv or a shell word. Never throws.
210
264
  *
211
265
  * @param {string} opName - for the message
@@ -234,30 +288,30 @@ const PUT_MODES = new Map([['--create', 'create'], ['--update', 'update'], ['--r
234
288
  const ENTRY_TYPES = new Set(['decision', 'pitfall']);
235
289
 
236
290
  /**
237
- * 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).
238
292
  * claim-queue and release-claim manage the claim themselves, and the generic ops
239
- * never touch it. A new learning op joins this set.
293
+ * never touch it. A new curation op joins this set.
240
294
  */
241
- const LEARNING_OPS = new Set([
295
+ const CURATION_OPS = new Set([
242
296
  'assign-anchor', 'retire-anchor', 'restore-anchor', 'refresh-anchor', 'rotate-observations',
243
297
  'put-observation', 'list', 'show', 'claim-due',
244
298
  ]);
245
299
 
246
300
  /** Send the claim heartbeat; a failure is reported on stderr and never stops the op. */
247
301
  function heartbeat(root) {
248
- const beat = learning().store.touchClaim(root);
302
+ const beat = curation().store.touchClaim(root);
249
303
  if (!beat.ok) process.stderr.write(`${beat.error.message}\n`);
250
304
  }
251
305
 
252
- // 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:
253
307
  // each builds its paths from the current directory. Every op that writes takes the
254
- // store's one learning lock through withDecisionsLock (D-ONE-LEARNING-LOCK) and
255
- // 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)
256
310
  // or when it, or .devflow, is a symbolic link (D-NO-LINKED-TREE).
257
311
  // A locked body returns its Result; emit prints it once the lock is released.
258
312
  if (require.main === module) {
259
313
  try {
260
- if (LEARNING_OPS.has(op)) heartbeat(process.cwd());
314
+ if (CURATION_OPS.has(op)) heartbeat(process.cwd());
261
315
  switch (op) {
262
316
  case 'get-field': {
263
317
  const input = JSON.parse(readStdin());
@@ -280,6 +334,16 @@ try {
280
334
  break;
281
335
  }
282
336
 
337
+ case 'array-length': {
338
+ // The count of an array field: digits for an array, the word `unknown` for an
339
+ // absent field or any other type, so a caller can tell none from not reported.
340
+ // jq's `length` on an array agrees (json-parse json_array_length_file).
341
+ const input = JSON.parse(readStdin());
342
+ const val = getNestedField(input, args[0]);
343
+ console.log(Array.isArray(val) ? String(val.length) : 'unknown');
344
+ break;
345
+ }
346
+
283
347
  case 'extract-cwd-field': {
284
348
  // Extract cwd and an arbitrary top-level field from hook JSON in one pass.
285
349
  // Outputs: cwd + ASCII SOH (0x01) + field value (no trailing newline).
@@ -327,7 +391,6 @@ try {
327
391
  console.log(JSON.stringify({
328
392
  timestamp: data.ts || '',
329
393
  trigger: 'pre-compact',
330
- memory_snapshot: data.memory || '',
331
394
  git: {
332
395
  branch: data.branch || '',
333
396
  status: data.status || '',
@@ -342,12 +405,12 @@ try {
342
405
  // assign-anchor <decision|pitfall> <obs_id>
343
406
  // Promote a v2 observation no ledger row carries to the next entry of its
344
407
  // type, skipping each number a tracked file cites (assignAnchor,
345
- // 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.
346
409
  // stdout: the anchor; stderr: `assign-anchor: skipped <anchor>, cited in
347
410
  // <path>:<line>` for each number skipped
348
411
  // -------------------------------------------------------------------------
349
412
  case 'assign-anchor': {
350
- const { store } = learning();
413
+ const { store } = curation();
351
414
  if (args.length !== 2 || !ENTRY_TYPES.has(args[0]) || !store.OBS_ID_RE.test(args[1])) {
352
415
  exitWithUsage('assign-anchor <decision|pitfall> <obs_id> (run from the project root)');
353
416
  }
@@ -366,12 +429,12 @@ try {
366
429
  // Make an active entry inactive with the one JSON object on stdin its status
367
430
  // takes: { reason } for Retired and Deprecated, { by } for Superseded, and
368
431
  // { at, quote } for Encoded, the quote checked at the verify ref
369
- // (retireAnchor, learning-store.cjs: D-ENCODED-QUOTE).
432
+ // (retireAnchor, curation-store.cjs: D-ENCODED-QUOTE).
370
433
  // stdout: the new status in lower case and the anchor, then `repointed
371
434
  // <anchor>` for each entry re-pointed to the successor
372
435
  // -------------------------------------------------------------------------
373
436
  case 'retire-anchor': {
374
- const { store } = learning();
437
+ const { store } = curation();
375
438
  if (args.length !== 2 || !store.ANCHOR_ID_RE.test(args[0]) || !store.INACTIVE_STATUSES.includes(args[1])) {
376
439
  exitWithUsage('retire-anchor <anchor> <Encoded|Superseded|Retired|Deprecated> (one JSON object on stdin; run from the project root)');
377
440
  }
@@ -389,11 +452,11 @@ try {
389
452
  // Make an inactive entry active again, its notes, last_verified and
390
453
  // last_attempt cleared so it is due for maintenance again, ordered after
391
454
  // integrity problems and legacy entries, or among them when it is one
392
- // (restoreAnchor, learning-store.cjs: D-DUE-ORDER).
455
+ // (restoreAnchor, curation-store.cjs: D-DUE-ORDER).
393
456
  // stdout: restored <anchor>
394
457
  // -------------------------------------------------------------------------
395
458
  case 'restore-anchor': {
396
- const { store } = learning();
459
+ const { store } = curation();
397
460
  if (args.length !== 1 || !store.ANCHOR_ID_RE.test(args[0])) {
398
461
  exitWithUsage('restore-anchor <anchor> (run from the project root)');
399
462
  }
@@ -404,12 +467,12 @@ try {
404
467
  // -------------------------------------------------------------------------
405
468
  // refresh-anchor <anchor> [<anchor>...] [--verified]
406
469
  // Re-project active v2 entries from their log rows, or under --verified stamp
407
- // them verified today; all or nothing (refreshAnchors, learning-store.cjs).
470
+ // them verified today; all or nothing (refreshAnchors, curation-store.cjs).
408
471
  // stdout: `reprojected <anchor>` or `unchanged <anchor>` per anchor, or
409
472
  // `verified <anchor>` under --verified, in the order given
410
473
  // -------------------------------------------------------------------------
411
474
  case 'refresh-anchor': {
412
- const { store } = learning();
475
+ const { store } = curation();
413
476
  const verifiedFlags = args.filter(arg => arg === '--verified').length;
414
477
  const anchors = args.filter(arg => arg !== '--verified');
415
478
  if (verifiedFlags > 1 || anchors.length === 0 || !anchors.every(arg => store.ANCHOR_ID_RE.test(arg))) {
@@ -424,13 +487,13 @@ try {
424
487
  // rotate-observations
425
488
  // Archive the log rows no ledger row carries once 30 days have passed since
426
489
  // their last activity, and delete the usage telemetry's leftovers
427
- // (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
428
491
  // the archive are the project root's.
429
492
  // stdout: rotated <N> observations
430
493
  // -------------------------------------------------------------------------
431
494
  case 'rotate-observations': {
432
495
  if (args.length > 0) exitWithUsage('rotate-observations (no arguments; run from the project root)');
433
- const result = learning().store.rotateObservations(process.cwd());
496
+ const result = curation().store.rotateObservations(process.cwd());
434
497
  process.exitCode = emit(result, ({ rotated }) => `rotated ${rotated} observations`);
435
498
  break;
436
499
  }
@@ -438,7 +501,7 @@ try {
438
501
  // -------------------------------------------------------------------------
439
502
  // put-observation --create|--update|--reinforce
440
503
  // Store one observation from the JSON object on stdin (D-PUT-NOT-MERGE,
441
- // 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.
442
505
  // stdout: created <id> | updated <id> | unchanged <id> | reinforced <id> <n>,
443
506
  // then one `reprojected <anchor>` line per entry re-projected
444
507
  // -------------------------------------------------------------------------
@@ -448,7 +511,7 @@ try {
448
511
  exitWithUsage('put-observation --create|--update|--reinforce (one JSON object on stdin; run from the project root)');
449
512
  }
450
513
  const input = readStdinJson('put-observation');
451
- 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;
452
515
  process.exitCode = emit(result, put => [
453
516
  put.outcome === 'reinforced' ? `reinforced ${put.id} ${put.observations}` : `${put.outcome} ${put.id}`,
454
517
  ...put.reprojected.map(anchorId => `reprojected ${anchorId}`),
@@ -459,25 +522,25 @@ try {
459
522
  // -------------------------------------------------------------------------
460
523
  // list
461
524
  // Print the ledger and the log by section, read-only (readListing and
462
- // formatListing, learning-store.cjs). Takes no argument.
525
+ // formatListing, curation-store.cjs). Takes no argument.
463
526
  // stdout: the ACTIVE, INACTIVE, OBSERVATIONS and INTEGRITY sections, then
464
527
  // MALFORMED when lines were skipped
465
528
  // -------------------------------------------------------------------------
466
529
  case 'list': {
467
530
  if (args.length > 0) exitWithUsage('list (no arguments; run from the project root)');
468
- const { store } = learning();
531
+ const { store } = curation();
469
532
  process.exitCode = emit(store.readListing(process.cwd()), store.formatListing);
470
533
  break;
471
534
  }
472
535
 
473
536
  // -------------------------------------------------------------------------
474
537
  // show <anchor|obs_id>
475
- // Print one entry, read-only (showByKey, learning-store.cjs).
538
+ // Print one entry, read-only (showByKey, curation-store.cjs).
476
539
  // stdout: pretty JSON { key, ledger, log, history_versions, flags }, plus
477
540
  // malformed when lines were skipped
478
541
  // -------------------------------------------------------------------------
479
542
  case 'show': {
480
- const { store } = learning();
543
+ const { store } = curation();
481
544
  if (args.length !== 1 || !(store.ANCHOR_ID_RE.test(args[0]) || store.OBS_ID_RE.test(args[0]))) {
482
545
  exitWithUsage('show <anchor|obs_id> (run from the project root)');
483
546
  }
@@ -488,13 +551,13 @@ try {
488
551
  // -------------------------------------------------------------------------
489
552
  // claim-due
490
553
  // Hand out the entries due for maintenance and lease each for a day
491
- // (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.
492
555
  // stdout: ref <origin/HEAD|HEAD> <sha12>, or ref none; then one
493
556
  // `<anchor> <reason> <bytes>` line per entry handed out, or due none
494
557
  // -------------------------------------------------------------------------
495
558
  case 'claim-due': {
496
559
  if (args.length > 0) exitWithUsage('claim-due (no arguments; run from the project root)');
497
- const { store } = learning();
560
+ const { store } = curation();
498
561
  process.exitCode = emit(store.claimDue(process.cwd()), ({ ref, due }) => [
499
562
  ref === null ? 'ref none' : `ref ${ref.ref} ${ref.commit.slice(0, 12)}`,
500
563
  ...(due.length > 0 ? due.map(entry => `${store.singleLine(entry.anchor_id)} ${entry.reason} ${entry.bytes}`) : ['due none']),
@@ -504,13 +567,13 @@ try {
504
567
 
505
568
  // -------------------------------------------------------------------------
506
569
  // claim-queue
507
- // 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).
508
571
  // Takes no argument; mints the token itself.
509
572
  // stdout: claimed <token> | claimed <token> takeover | busy | none
510
573
  // -------------------------------------------------------------------------
511
574
  case 'claim-queue': {
512
575
  if (args.length > 0) exitWithUsage('claim-queue (no arguments; run from the project root)');
513
- const result = learning().store.claimQueue(process.cwd());
576
+ const result = curation().store.claimQueue(process.cwd());
514
577
  process.exitCode = emit(result, claim => (claim.state === 'claimed'
515
578
  ? `claimed ${claim.token}${claim.takeover ? ' takeover' : ''}`
516
579
  : claim.state));
@@ -519,11 +582,11 @@ try {
519
582
 
520
583
  // -------------------------------------------------------------------------
521
584
  // release-claim <token>
522
- // 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).
523
586
  // stdout: released | not-owner | gone
524
587
  // -------------------------------------------------------------------------
525
588
  case 'release-claim': {
526
- const { store } = learning();
589
+ const { store } = curation();
527
590
  if (args.length !== 1 || !store.CLAIM_TOKEN_RE.test(args[0])) {
528
591
  exitWithUsage('release-claim <token> (the 16 hex characters claim-queue printed)');
529
592
  }
@@ -531,12 +594,43 @@ try {
531
594
  break;
532
595
  }
533
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
+
534
627
  default:
535
628
  process.stderr.write(`json-helper: unknown operation "${op}"\n`);
536
629
  process.exit(1);
537
630
  }
538
631
  } catch (err) {
539
- 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`);
540
634
  process.exit(1);
541
635
  }
542
636
  } // end if (require.main === module)
@@ -69,6 +69,21 @@ json_string_field_file() {
69
69
  fi
70
70
  }
71
71
 
72
+ # The element count of an array field in a JSON file. Usage: json_array_length_file "/path/to/file.json" "field"
73
+ # Prints the count as digits when the field is an array, and the word `unknown` for an
74
+ # absent field or any other type (a number, string, object, boolean or null), so a
75
+ # caller can tell "none" from "not reported". Prints nothing, and fails, when the file
76
+ # cannot be read or parsed. Silent on stderr and read the same way on both backends
77
+ # (the node path reads the file on stdin, with its stderr redirected first).
78
+ json_array_length_file() {
79
+ local file="$1" field="$2"
80
+ if [ "$_HAS_JQ" = "true" ]; then
81
+ jq -r "(try .$field catch null) | if type == \"array\" then length else \"unknown\" end" "$file" 2>/dev/null
82
+ else
83
+ node "$_JSON_HELPER" array-length "$field" 2>/dev/null < "$file"
84
+ fi
85
+ }
86
+
72
87
  # --- Multi-field batched extraction ---
73
88
 
74
89
  # Extract cwd and an arbitrary top-level field from stdin JSON in a single
@@ -103,9 +118,9 @@ json_extract_cwd_prompt() {
103
118
  # D-SYSTEMMESSAGE-ENVELOPE: the optional second argument is a message shown to the
104
119
  # user, carried in the top-level `systemMessage` key beside (never inside)
105
120
  # `hookSpecificOutput`; it does not enter the model's context. With ONE argument the
106
- # envelope is exactly what every existing caller has always received, byte for byte
107
- # (ensure-proxy, session-start-orchestrator, session-start-memory and Sections 1-5 of
108
- # session-start-context pass one). With two:
121
+ # envelope holds the context alone, byte for byte (ensure-proxy and
122
+ # session-start-orchestrator pass one; session-start-context and session-start-memory
123
+ # pass two). With two:
109
124
  # - a message and a context -> both keys, hookSpecificOutput first;
110
125
  # - a message and no context -> `systemMessage` alone, with NO hookSpecificOutput key;
111
126
  # - an empty message -> the one-argument envelope for that context.
@@ -155,7 +170,7 @@ json_prompt_output() {
155
170
  # Build pre-compact backup JSON. Usage: json_backup_construct --arg ts X --arg branch Y ...
156
171
  json_backup_construct() {
157
172
  if [ "$_HAS_JQ" = "true" ]; then
158
- 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}}'
159
174
  else
160
175
  node "$_JSON_HELPER" backup-construct "$@"
161
176
  fi