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
@@ -1,55 +1,65 @@
1
- // src/assets/scripts/hooks/lib/learning-store.cjs
1
+ // src/assets/scripts/hooks/lib/curation-store.cjs
2
2
  //
3
- // The v2 learning store: the one place the learning plumbing reads, validates,
3
+ // The v2 curation store: the one place the curation plumbing reads, validates,
4
4
  // projects and writes the decisions log, the ledger and their side files.
5
5
  //
6
6
  // DESIGN: plumbing only. No function here prints or calls process.exit. Work that
7
7
  // can fail on its input or on the lock returns a Result — { ok: true, value } or
8
8
  // { ok: false, error: { kind, message } } — and its caller prints it: json-helper.cjs
9
- // for the ops, or the `devflow learning` CLI through src/core/learning-store.ts. A throw
9
+ // for the ops, or the `devflow curation` CLI through src/core/curation-store.ts. A throw
10
10
  // means a broken invariant or an I/O failure, never an expected outcome; a lock
11
11
  // held by withDecisionsLock is released on every path, the throw included.
12
12
  //
13
- // Loading: node built-ins and three sibling libs only. This module never requires
13
+ // Loading: node built-ins and four sibling libs only (the path helpers, the lock, the
14
+ // path guard and the tracked-areas check). This module never requires
14
15
  // decisions-format.cjs or render-decisions.cjs at load time: both require it, for
15
16
  // the status list and the one-line and inactive-note text they share with `list`.
16
17
  //
17
- // Files under <root>/.devflow/learning/:
18
+ // The one-time move of the old ledger folder to its new name is not here: it lives in
19
+ // curation-move.cjs, which requires this module. The store holds only the guard that
20
+ // keeps its writers off an unfinished move (withDecisionsLock reads the move journal),
21
+ // so a store write loads no move code.
22
+ //
23
+ // Files under <root>/.devflow/curation/:
18
24
  // decisions-log.jsonl observation rows — the content authority
19
25
  // decisions-ledger.jsonl anchored rows — projections of log rows
20
26
  // decisions-log.archive.jsonl rotated-out observation rows (D-ROTATE-UNREFERENCED)
21
27
  // decisions-history.jsonl prior content versions (D-CONTENT-HISTORY)
22
28
  // *.rejected.jsonl quarantined malformed lines (D-QUARANTINE-MALFORMED)
23
29
  // *.pre-v2.jsonl one-time copies of the v1 files (D-V1-BACKUP-ONCE)
24
- // .decisions.lock/ the one learning lock (D-ONE-LEARNING-LOCK)
30
+ // .decisions.lock/ the one curation lock (D-ONE-LEARNING-LOCK)
25
31
  // .pending-turns.jsonl the queue the capture hooks append to
26
32
  // .pending-turns.processing the claimed batch, and .pending-turns.owner its
27
33
  // owner's token (D-OWNED-CLAIM)
28
34
  //
29
35
  // TS COUNTERPARTS: src/core/observations.ts mirrors the status lists (D201), and
30
- // src/core/learning-store.ts transcribes the functions the CLI calls from their
36
+ // src/core/curation-store.ts transcribes the functions the CLI calls from their
31
37
  // JSDoc here (D-LEARNING-STORE-SEAM) — change both together.
32
38
 
33
39
  'use strict';
34
40
 
35
41
  const crypto = require('crypto');
36
42
  const fs = require('fs');
43
+ const os = require('os');
37
44
  const path = require('path');
38
45
  const { execFileSync } = require('child_process');
39
46
 
40
47
  const {
41
- getLearningDir,
42
- getLearningPendingTurnsPath,
43
- getLearningPendingTurnsProcessingPath,
44
- getLearningClaimOwnerPath,
48
+ LEGACY_NAMES,
49
+ getCurationDir,
50
+ getCurationPendingTurnsPath,
51
+ getCurationPendingTurnsProcessingPath,
52
+ getCurationClaimOwnerPath,
45
53
  getDecisionsLedgerPath,
46
54
  getDecisionsLogPath,
47
55
  getDecisionsArchivePath,
48
56
  getDecisionsHistoryPath,
49
57
  getDecisionsLockDir,
58
+ curationMoveJournalPath,
50
59
  } = require('./project-paths.cjs');
51
60
  const { acquireMkdirLock, releaseLock } = require('./mkdir-lock.cjs');
52
61
  const { safePath } = require('./safe-path.cjs');
62
+ const { inspectTrackedAreaOnce } = require('./tracked-areas.cjs');
53
63
 
54
64
  // ---------------------------------------------------------------------------
55
65
  // Constants
@@ -401,17 +411,17 @@ function readTextUnlinked(file, { maxBytes = Infinity } = {}) {
401
411
  * delete them without a trace, and a reader that quarantined would make list,
402
412
  * show and the HUD write files.
403
413
  *
404
- * D-NO-LINKED-READ: a learning file that is itself a symbolic link reads as
414
+ * D-NO-LINKED-READ: a curation file that is itself a symbolic link reads as
405
415
  * missing, and nothing is read through it; the pre-v2 backup skips one the same
406
416
  * way (ensurePreV2Backup), and release-claim reads the claim's owner file the same
407
417
  * way, and only up to CLAIM_OWNER_MAX_BYTES (readClaimOwner). readJsonl and
408
418
  * readClaimOwner read a regular file alone (readTextUnlinked): a directory, a FIFO
409
419
  * or a device where the file belongs reads as missing too, and neither waits on
410
- * one. Reason: a repository can commit any learning file as a link to a file
420
+ * one. Reason: a repository can commit any curation file as a link to a file
411
421
  * elsewhere on the machine, and a read that followed it would put that file's
412
422
  * lines into list and show and, through a rewrite, the quarantine or a render,
413
- * into the project's learning folder; a read that followed one to a FIFO or to
414
- * /dev/zero would wait forever or fill memory, holding the learning lock when a
423
+ * into the project's curation folder; a read that followed one to a FIFO or to
424
+ * /dev/zero would wait forever or fill memory, holding the curation lock when a
415
425
  * writer or release-claim reads. The writers never write through a link either:
416
426
  * a rename replaces one, and an append refuses one.
417
427
  *
@@ -448,11 +458,16 @@ function rejectedPathFor(file) {
448
458
 
449
459
  /**
450
460
  * Append to `file`, creating it when absent. O_NOFOLLOW refuses a symlink planted
451
- * at `file` (ELOOP) instead of writing through it.
461
+ * at `file` (ELOOP) instead of writing through it. A file this call creates gets
462
+ * `mode`, as an explicit open mode (the process umask can only remove bits from it).
463
+ *
464
+ * @param {string} file
465
+ * @param {string} content
466
+ * @param {number} [mode] - the mode of a created file (default 0o666, as the umask leaves it)
452
467
  */
453
- function appendNoFollow(file, content) {
468
+ function appendNoFollow(file, content, mode = 0o666) {
454
469
  const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW || 0);
455
- const fd = fs.openSync(file, flags, 0o666);
470
+ const fd = fs.openSync(file, flags, mode);
456
471
  try {
457
472
  fs.writeFileSync(fd, content);
458
473
  } finally {
@@ -467,17 +482,18 @@ function appendNoFollow(file, content) {
467
482
  *
468
483
  * @param {string} file - the file the lines were read from
469
484
  * @param {Array<{ line: number, text: string }>} rejected
470
- * @param {{ now?: number }} [opts] - now: epoch ms (default Date.now())
485
+ * @param {{ now?: number, mode?: number }} [opts] - now: epoch ms (default Date.now());
486
+ * mode: the mode of a quarantine file this call creates (default as the umask leaves it)
471
487
  * @returns {number} the number of records appended
472
488
  */
473
- function quarantineRejected(file, rejected, { now = Date.now() } = {}) {
489
+ function quarantineRejected(file, rejected, { now = Date.now(), mode } = {}) {
474
490
  if (rejected.length === 0) return 0;
475
491
  const rejectedAt = new Date(now).toISOString();
476
492
  const source = path.basename(file);
477
493
  const content = rejected
478
494
  .map(r => JSON.stringify({ rejected_at: rejectedAt, source, line: r.line, text: r.text }))
479
495
  .join('\n') + '\n';
480
- appendNoFollow(rejectedPathFor(file), content);
496
+ appendNoFollow(rejectedPathFor(file), content, mode);
481
497
  return rejected.length;
482
498
  }
483
499
 
@@ -545,14 +561,14 @@ function writeJsonlAtomic(file, rows) {
545
561
  // ---------------------------------------------------------------------------
546
562
 
547
563
  /**
548
- * True when `<root>/.devflow/learning` exists and is a directory.
564
+ * True when `<root>/.devflow/curation` exists and is a directory.
549
565
  *
550
566
  * @param {string} root - project root
551
567
  * @returns {boolean}
552
568
  */
553
- function hasLearningDir(root) {
569
+ function hasCurationDir(root) {
554
570
  try {
555
- return fs.statSync(getLearningDir(root)).isDirectory();
571
+ return fs.statSync(getCurationDir(root)).isDirectory();
556
572
  } catch (err) {
557
573
  if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return false;
558
574
  throw err;
@@ -560,17 +576,17 @@ function hasLearningDir(root) {
560
576
  }
561
577
 
562
578
  /**
563
- * The error Result of an op run where `<root>/.devflow/learning/` is absent
579
+ * The error Result of an op run where `<root>/.devflow/curation/` is absent
564
580
  * (D-NO-STRAY-TREE).
565
581
  *
566
582
  * @param {string} opName - operation name, for the message
567
583
  * @param {string} root - project root
568
- * @returns {{ ok: false, error: { kind: 'no-learning-dir', message: string } }}
584
+ * @returns {{ ok: false, error: { kind: 'no-curation-dir', message: string } }}
569
585
  */
570
- function noLearningDir(opName, root) {
586
+ function noCurationDir(opName, root) {
571
587
  return {
572
588
  ok: false,
573
- error: { kind: 'no-learning-dir', message: `${opName}: no .devflow/learning/ under ${root} — run from the project root` },
589
+ error: { kind: 'no-curation-dir', message: `${opName}: no .devflow/curation/ under ${root} — run from the project root` },
574
590
  };
575
591
  }
576
592
 
@@ -585,22 +601,22 @@ function isSymbolicLink(file) {
585
601
  }
586
602
 
587
603
  /**
588
- * The first of `<root>/.devflow` and `<root>/.devflow/learning` that is itself a
604
+ * The first of `<root>/.devflow` and `<root>/.devflow/curation` that is itself a
589
605
  * symbolic link, or null when neither is (D-NO-LINKED-TREE).
590
606
  *
591
607
  * @param {string} root - project root
592
608
  * @returns {string|null}
593
609
  */
594
- function linkedLearningFolder(root) {
595
- const learningDir = getLearningDir(root);
596
- for (const dir of [path.dirname(learningDir), learningDir]) {
610
+ function linkedCurationFolder(root) {
611
+ const curationDir = getCurationDir(root);
612
+ for (const dir of [path.dirname(curationDir), curationDir]) {
597
613
  if (isSymbolicLink(dir)) return dir;
598
614
  }
599
615
  return null;
600
616
  }
601
617
 
602
618
  /**
603
- * The error Result of an op run where `.devflow` or `.devflow/learning` under its
619
+ * The error Result of an op run where `.devflow` or `.devflow/curation` under its
604
620
  * root is a symbolic link (D-NO-LINKED-TREE).
605
621
  *
606
622
  * @param {string} opName - operation name, for the message
@@ -614,6 +630,112 @@ function linkedFolder(opName, dir) {
614
630
  };
615
631
  }
616
632
 
633
+ /**
634
+ * The refusal of an op run where git tracks files in the curation data folders under
635
+ * `root` (the curation folder, the old ledger folder and the backup folders), or does
636
+ * not say whether it does, or null where git tracks none. A folder that git tracks is the
637
+ * repository's content, so the curation feature is paused there: the op reads and writes
638
+ * nothing. The messages state the fact and name no command; the root is shown so the
639
+ * folder can be found. The check asks git about the index, not the disk, so it holds
640
+ * whether or not the folder is on disk, and it makes no git call for a root with no `.git`
641
+ * entry. It comes first in every op, before a ledger file is read, a lock is taken or a git
642
+ * call of the op's own is made. The answer is asked once per root for the life of the
643
+ * process (inspectTrackedAreaOnce), so the heartbeat, the op and its lock share one call.
644
+ *
645
+ * The refusal carries `reason`, `tracked` where git tracks files and `unknown` where it
646
+ * did not answer, so a caller that picks a notice reads the field, never the message.
647
+ *
648
+ * @param {string} opName - operation name, for the message
649
+ * @param {string} root - project root
650
+ * @returns {{ ok: false, error: { kind: 'tracked', reason: 'tracked'|'unknown', message: string } } | null}
651
+ */
652
+ function trackedCurationArea(opName, root) {
653
+ const { answer } = inspectTrackedAreaOnce(root, 'curation');
654
+ if (answer === 'untracked') return null;
655
+ const reason = answer === 'tracked' ? 'tracked' : 'unknown';
656
+ const fact = reason === 'tracked' ? 'git tracks files' : 'git did not answer whether it tracks files';
657
+ return {
658
+ ok: false,
659
+ error: { kind: 'tracked', reason, message: `${opName}: ${fact} in the curation data folders under ${root}; nothing was written` },
660
+ };
661
+ }
662
+
663
+ /** The Stats of `file` without following a link, or null when nothing is there. */
664
+ function lstatOrNull(file) {
665
+ try {
666
+ return fs.lstatSync(file);
667
+ } catch (err) {
668
+ if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return null;
669
+ throw err;
670
+ }
671
+ }
672
+
673
+ /** The physical path of `target`, or its resolved path when it does not exist yet. */
674
+ function physicalPath(target) {
675
+ try {
676
+ return fs.realpathSync(target);
677
+ } catch (err) {
678
+ if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return path.resolve(target);
679
+ throw err;
680
+ }
681
+ }
682
+
683
+ /** The home directory: `HOME`, else the passwd entry, else null. */
684
+ function homeDirOrNull() {
685
+ if (process.env.HOME) return process.env.HOME;
686
+ try {
687
+ return os.homedir() || null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Where a root's move journal is, resolved the way moveLedger resolves its machine
695
+ * directory (`$HOME/.devflow`) and the root's key (the root's physical path); null when
696
+ * no home directory is known.
697
+ */
698
+ function moveJournalPathOrNull(root) {
699
+ const home = homeDirOrNull();
700
+ if (home === null) return null;
701
+ return curationMoveJournalPath(path.join(home, '.devflow'), physicalPath(path.resolve(root)));
702
+ }
703
+
704
+ /**
705
+ * Remove a machine-side journal whose old folder is gone: it has no set left to
706
+ * finish. Only a regular file is removed, never a link or any other entry, and nothing
707
+ * is followed. It never throws: a journal that cannot be removed (EACCES, EPERM, EROFS
708
+ * on the machine directory) stays, and the writer goes on, because the journal holds
709
+ * no writer once its old folder is gone. A later write tries again.
710
+ *
711
+ * @param {string} journal
712
+ */
713
+ function dropStaleMoveJournal(journal) {
714
+ try {
715
+ const stat = lstatOrNull(journal);
716
+ if (stat === null || !stat.isFile()) return;
717
+ removeIfPresent(journal);
718
+ } catch {
719
+ // Best effort only: see the note above.
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Whether the journal at `journal` holds store writers off: it exists and the old
725
+ * folder it belongs to still exists. A journal whose old folder is gone is stale and is
726
+ * dropped (dropStaleMoveJournal), and the writer goes on.
727
+ *
728
+ * @param {string} journal
729
+ * @param {string} oldFolder
730
+ * @returns {boolean}
731
+ */
732
+ function moveJournalHoldsWriters(journal, oldFolder) {
733
+ if (lstatOrNull(journal) === null) return false;
734
+ if (lstatOrNull(oldFolder) !== null) return true;
735
+ dropStaleMoveJournal(journal);
736
+ return false;
737
+ }
738
+
617
739
  /** True for a Result: `{ ok: true, … }` or `{ ok: false, error: { … } }`. */
618
740
  function isResult(value) {
619
741
  return isPlainObject(value) && (value.ok === true || (value.ok === false && isPlainObject(value.error)));
@@ -624,50 +746,71 @@ function isResult(value) {
624
746
  * The lock is released on every path, a throw from `fn` included; the throw then
625
747
  * reaches the caller.
626
748
  *
627
- * D-ONE-LEARNING-LOCK: learning writers take one lock, `.decisions.lock`, through
749
+ * D-ONE-LEARNING-LOCK: curation writers take one lock, `.decisions.lock`, through
628
750
  * this wrapper — every write to the log, the ledger, their side files and the
629
751
  * rendered files happens under it. Reason: two writers under two locks can each
630
752
  * read one file, change it and rename their copy over it, and the second rename
631
753
  * silently discards the first one's change.
632
754
  *
633
- * D-NO-STRAY-TREE: a learning writer refuses with `no-learning-dir` when
634
- * `.devflow/learning/` is absent under its root, and the store never creates that
755
+ * D-NO-STRAY-TREE: a curation writer refuses with `no-curation-dir` when
756
+ * `.devflow/curation/` is absent under its root, and the store never creates that
635
757
  * directory or its parent — the lock directory is the only thing made inside it.
636
- * Reason: a writer run from the wrong directory would otherwise create a learning
758
+ * Reason: a writer run from the wrong directory would otherwise create a curation
637
759
  * tree there and write a ledger that no session ever reads.
638
760
  *
639
- * D-NO-LINKED-TREE: a learning writer refuses with `not-a-directory`, changing
640
- * nothing, when `.devflow` or `.devflow/learning` under its root is a symbolic
761
+ * An unfinished ledger move (a move journal in the machine directory, next to the move
762
+ * lock, see moveLedger in curation-move.cjs) makes every writer answer `busy` without taking the lock, so no
763
+ * write lands between two renames of the move's ledger set. The journal is checked
764
+ * again once the lock is held: a move can start, and stop part way through the set,
765
+ * while the writer waits for the lock. Only the machine-side file counts, and only
766
+ * while the old folder it belongs to exists (moveJournalHoldsWriters): a file in the
767
+ * repository's old folder is repository content and is never read. With no home
768
+ * directory no move can have started (moveLedger refuses without a machine directory),
769
+ * so there is no journal to find and the writer goes on.
770
+ *
771
+ * D-NO-LINKED-TREE: a curation writer refuses with `not-a-directory`, changing
772
+ * nothing, when `.devflow` or `.devflow/curation` under its root is a symbolic
641
773
  * link. Reason: a repository can commit either one as a link to a folder elsewhere
642
774
  * on the machine, and a writer that followed it would take its lock there and
643
775
  * rewrite, quarantine, archive, render or delete files in whatever folder the link
644
776
  * names. Refused here, before the lock is taken, so every writer refuses in one
645
777
  * place; the claim heartbeat makes the same check, and read-only paths still read.
646
778
  *
779
+ * A writer also refuses with `tracked`, changing nothing, where git tracks files in the
780
+ * curation data folders under its root, or does not say whether it does
781
+ * (trackedCurationArea). That check follows the link check and comes before the journal,
782
+ * the folder check and the lock, so a paused project gets no lock folder.
783
+ *
647
784
  * @param {string} opName - operation name, for messages
648
785
  * @param {string} root - project root
649
786
  * @param {() => { ok: boolean }} fn - the locked body; it must return a Result
650
787
  * @param {{ timeoutMs?: number, staleMs?: number }} [opts]
651
788
  * @returns {{ ok: true, value?: unknown } | { ok: false, error: { kind: string, message: string } }}
652
- * fn's Result, or an error of kind `not-a-directory`, `no-learning-dir` or `busy`.
789
+ * fn's Result, or an error of kind `not-a-directory`, `tracked`, `no-curation-dir` or `busy`.
653
790
  */
654
791
  function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS, staleMs = LOCK_STALE_MS } = {}) {
655
- const linked = linkedLearningFolder(root);
792
+ const linked = linkedCurationFolder(root);
656
793
  if (linked !== null) return linkedFolder(opName, linked);
657
- if (!hasLearningDir(root)) return noLearningDir(opName, root);
794
+ const paused = trackedCurationArea(opName, root);
795
+ if (paused !== null) return paused;
796
+ const journal = moveJournalPathOrNull(root);
797
+ const oldFolder = path.join(root, '.devflow', LEGACY_NAMES.folder);
798
+ if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
799
+ if (!hasCurationDir(root)) return noCurationDir(opName, root);
658
800
  const lockDir = getDecisionsLockDir(root);
659
801
  let acquired;
660
802
  try {
661
803
  acquired = acquireMkdirLock(lockDir, timeoutMs, staleMs);
662
804
  } catch (err) {
663
- // The learning directory went away between the check and the mkdir.
664
- if (err && err.code === 'ENOENT') return noLearningDir(opName, root);
805
+ // The curation directory went away between the check and the mkdir.
806
+ if (err && err.code === 'ENOENT') return noCurationDir(opName, root);
665
807
  throw err;
666
808
  }
667
809
  if (!acquired) {
668
810
  return { ok: false, error: { kind: 'busy', message: `${opName}: timeout acquiring lock at ${lockDir}` } };
669
811
  }
670
812
  try {
813
+ if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
671
814
  const result = fn();
672
815
  if (!isResult(result)) {
673
816
  throw new TypeError(`${opName}: the locked body must return a Result ({ ok: true, value } or { ok: false, error })`);
@@ -678,6 +821,14 @@ function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_
678
821
  }
679
822
  }
680
823
 
824
+ /** The `busy` Result of a writer that finds an unfinished ledger move's journal. */
825
+ function unfinishedMove(opName, journal) {
826
+ return {
827
+ ok: false,
828
+ error: { kind: 'busy', message: `${opName}: an unfinished ledger move is recorded at ${journal}; it finishes at the next session start or devflow init` },
829
+ };
830
+ }
831
+
681
832
  // ---------------------------------------------------------------------------
682
833
  // State and the ledger registry
683
834
  // ---------------------------------------------------------------------------
@@ -690,7 +841,7 @@ function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_
690
841
  * @param {string} root - project root
691
842
  * @returns {{ ledgerRows: object[], logRows: object[], rejected: { ledger: Array<{ line: number, text: string }>, log: Array<{ line: number, text: string }> } }}
692
843
  */
693
- function readLearningState(root) {
844
+ function readCurationState(root) {
694
845
  const ledger = readJsonl(getDecisionsLedgerPath(root));
695
846
  const log = readJsonl(getDecisionsLogPath(root));
696
847
  return {
@@ -1191,7 +1342,7 @@ function ensurePreV2Backup(root, { logRows = [], ledgerRows = [] } = {}) {
1191
1342
  /** Days of inactivity after which an observation no ledger row carries leaves the log (D-ROTATE-UNREFERENCED). */
1192
1343
  const ROTATE_AGE_DAYS = 30;
1193
1344
 
1194
- /** What an older install's usage telemetry left in the learning directory: a file and a lock directory. */
1345
+ /** What an older install's usage telemetry left in the curation directory: a file and a lock directory. */
1195
1346
  const USAGE_LEFTOVERS = Object.freeze(['.decisions-usage.json', '.decisions-usage.lock']);
1196
1347
 
1197
1348
  /**
@@ -1221,7 +1372,7 @@ function lastActivityMs(row) {
1221
1372
  * anchor_id copied onto a log row can say what to keep, and a dedup by id dropped
1222
1373
  * the newer version of a row whose older version was already archived.
1223
1374
  *
1224
- * Under the learning lock it first deletes the usage leftovers. When no row is
1375
+ * Under the curation lock it first deletes the usage leftovers. When no row is
1225
1376
  * due it writes nothing else. When rows are due it backs up a v1 tree
1226
1377
  * (D-V1-BACKUP-ONCE) and quarantines the log's malformed lines
1227
1378
  * (D-QUARANTINE-MALFORMED) before it appends to the archive and rewrites the log;
@@ -1232,12 +1383,12 @@ function lastActivityMs(row) {
1232
1383
  * @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
1233
1384
  * @returns {{ ok: true, value: { rotated: number, appended: number } } | { ok: false, error: { kind: string, message: string } }}
1234
1385
  * rotated: rows removed from the log; appended: rows added to the archive.
1235
- * Errors are withDecisionsLock's not-a-directory, no-learning-dir and busy.
1386
+ * Errors are withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1236
1387
  */
1237
1388
  function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1238
1389
  return withDecisionsLock('rotate-observations', root, () => {
1239
1390
  for (const name of USAGE_LEFTOVERS) {
1240
- fs.rmSync(path.join(getLearningDir(root), name), { recursive: true, force: true });
1391
+ fs.rmSync(path.join(getCurationDir(root), name), { recursive: true, force: true });
1241
1392
  }
1242
1393
 
1243
1394
  const logPath = getDecisionsLogPath(root);
@@ -1276,7 +1427,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1276
1427
  // ---------------------------------------------------------------------------
1277
1428
 
1278
1429
  /**
1279
- * Drop the observations no entry uses — `devflow learning --clear`.
1430
+ * Drop the observations no entry uses — `devflow curation --clear`.
1280
1431
  *
1281
1432
  * D-CLEAR-UNREFERENCED: clearing removes from the log exactly the rows no ledger
1282
1433
  * row carries (D-LEDGER-REGISTRY), whatever their age or status, and keeps every
@@ -1286,7 +1437,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1286
1437
  * row that is its content authority, and refresh then refused them all — and a
1287
1438
  * malformed ledger line may be the one carrying a row clearing would drop.
1288
1439
  *
1289
- * It runs under the learning lock (D-ONE-LEARNING-LOCK). When a row is dropped it
1440
+ * It runs under the curation lock (D-ONE-LEARNING-LOCK). When a row is dropped it
1290
1441
  * backs up a v1 tree (D-V1-BACKUP-ONCE) and quarantines the log's malformed lines
1291
1442
  * (D-QUARANTINE-MALFORMED) before it rewrites the log; with nothing to drop it
1292
1443
  * writes nothing.
@@ -1295,7 +1446,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1295
1446
  * @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
1296
1447
  * @returns {{ ok: true, value: { cleared: number, kept: number } } | { ok: false, error: { kind: string, message: string } }}
1297
1448
  * cleared: rows removed from the log; kept: rows left in it. Error kinds:
1298
- * ledger-malformed, and withDecisionsLock's not-a-directory, no-learning-dir and busy.
1449
+ * ledger-malformed, and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1299
1450
  */
1300
1451
  function clearUnreferenced(root, { now = Date.now(), timeoutMs } = {}) {
1301
1452
  return withDecisionsLock('clear', root, () => {
@@ -1340,11 +1491,11 @@ function removeEmptyDir(dir) {
1340
1491
  }
1341
1492
 
1342
1493
  /**
1343
- * Remove every learning file — `devflow learning --reset`: the log, the ledger
1494
+ * Remove every curation file — `devflow curation --reset`: the log, the ledger
1344
1495
  * and their side files, the rendered files, the tuning config, and the queue
1345
- * with its claim and owner file — and then the learning directory itself.
1496
+ * with its claim and owner file — and then the curation directory itself.
1346
1497
  *
1347
- * D-RESET-UNDER-LOCK: reset empties the learning directory under the learning
1498
+ * D-RESET-UNDER-LOCK: reset empties the curation directory under the curation
1348
1499
  * lock, sparing only the lock directory, and removes the emptied directory once
1349
1500
  * the lock is released, and only if nothing has arrived in it. Reason: the lock
1350
1501
  * is released by its path, so removing it with the directory would let this
@@ -1352,8 +1503,8 @@ function removeEmptyDir(dir) {
1352
1503
  * and what arrives once the lock is free — a captured turn, or the next writer's
1353
1504
  * lock — belongs to the next run.
1354
1505
  *
1355
- * Like every learning writer it refuses without `.devflow/learning/` and creates
1356
- * nothing (D-NO-STRAY-TREE), refuses a learning directory or a `.devflow` that is a
1506
+ * Like every curation writer it refuses without `.devflow/curation/` and creates
1507
+ * nothing (D-NO-STRAY-TREE), refuses a curation directory or a `.devflow` that is a
1357
1508
  * symbolic link and removes nothing (D-NO-LINKED-TREE), since emptying it would
1358
1509
  * empty whatever directory the link leads to, and waits at most `timeoutMs` for the
1359
1510
  * lock, breaking one a crashed run left behind (D-ONE-LEARNING-LOCK). A symbolic
@@ -1362,18 +1513,18 @@ function removeEmptyDir(dir) {
1362
1513
  * @param {string} root - project root
1363
1514
  * @param {{ timeoutMs?: number }} [opts]
1364
1515
  * @returns {{ ok: true, value: { removed: number } } | { ok: false, error: { kind: string, message: string } }}
1365
- * removed: the entries removed from the learning directory. Errors are
1366
- * withDecisionsLock's not-a-directory, no-learning-dir and busy.
1516
+ * removed: the entries removed from the curation directory. Errors are
1517
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1367
1518
  */
1368
- function resetLearning(root, { timeoutMs } = {}) {
1369
- const learningDir = getLearningDir(root);
1519
+ function resetCuration(root, { timeoutMs } = {}) {
1520
+ const curationDir = getCurationDir(root);
1370
1521
  const lockName = path.basename(getDecisionsLockDir(root));
1371
1522
  const reset = withDecisionsLock('reset', root, () => {
1372
- const entries = fs.readdirSync(learningDir).filter(name => name !== lockName);
1373
- for (const name of entries) fs.rmSync(path.join(learningDir, name), { recursive: true, force: true });
1523
+ const entries = fs.readdirSync(curationDir).filter(name => name !== lockName);
1524
+ for (const name of entries) fs.rmSync(path.join(curationDir, name), { recursive: true, force: true });
1374
1525
  return { ok: true, value: { removed: entries.length } };
1375
1526
  }, { timeoutMs });
1376
- if (reset.ok) removeEmptyDir(learningDir);
1527
+ if (reset.ok) removeEmptyDir(curationDir);
1377
1528
  return reset;
1378
1529
  }
1379
1530
 
@@ -1391,7 +1542,7 @@ const CLAIM_STALE_SECS = 900;
1391
1542
  /** How long a claim waits for the queue's own lock (ms): the wait of queue-append's overflow truncation. */
1392
1543
  const QUEUE_LOCK_TIMEOUT_MS = 2000;
1393
1544
 
1394
- /** Age after which the queue's own lock counts as abandoned (ms): learning-lock's threshold. */
1545
+ /** Age after which the queue's own lock counts as abandoned (ms): curation-lock's threshold. */
1395
1546
  const QUEUE_LOCK_STALE_MS = 30000;
1396
1547
 
1397
1548
  /** A claim token: 16 lowercase hex characters. */
@@ -1462,7 +1613,7 @@ function removeIfPresent(file) {
1462
1613
  * larger than CLAIM_OWNER_MAX_BYTES reads as absent (D-NO-LINKED-READ, at readJsonl).
1463
1614
  */
1464
1615
  function readClaimOwner(root) {
1465
- const text = readTextUnlinked(getLearningClaimOwnerPath(root), { maxBytes: CLAIM_OWNER_MAX_BYTES });
1616
+ const text = readTextUnlinked(getCurationClaimOwnerPath(root), { maxBytes: CLAIM_OWNER_MAX_BYTES });
1466
1617
  if (text === null) return null;
1467
1618
  const token = text.trim();
1468
1619
  return CLAIM_TOKEN_RE.test(token) ? token : null;
@@ -1473,7 +1624,7 @@ function readClaimOwner(root) {
1473
1624
  * queue-append's overflow truncation takes, so a truncation can never rewrite the
1474
1625
  * queue from rows already claimed. link(2) refuses an existing claim; on a
1475
1626
  * filesystem without hard links a rename stands in, which is safe because the
1476
- * caller found the claim path empty under the learning lock. A row a capture hook
1627
+ * caller found the claim path empty under the curation lock. A row a capture hook
1477
1628
  * appends meanwhile lands in the claimed batch or in the queue formed after it,
1478
1629
  * never in neither.
1479
1630
  *
@@ -1516,22 +1667,23 @@ function moveQueueToClaim(queuePath, claimPath) {
1516
1667
  }
1517
1668
 
1518
1669
  /**
1519
- * Claim the learning queue for one Curate run.
1670
+ * Claim the curation queue for one Curate run.
1520
1671
  *
1521
- * D-OWNED-CLAIM: the learning queue is claimed and released only through the
1522
- * claim-queue and release-claim ops, under the learning lock. A claim moves the
1672
+ * D-OWNED-CLAIM: the curation queue is claimed and released only through the
1673
+ * claim-queue and release-claim ops, under the curation lock. A claim moves the
1523
1674
  * queue to .pending-turns.processing by link(2), under the queue's own lock (a
1524
1675
  * rename stands in only where the filesystem has no hard links), sets the claim's
1525
1676
  * mtime to now and records a fresh random token in .pending-turns.owner. A claim
1526
1677
  * younger than CLAIM_STALE_SECS is busy to every other claimant; an older one is
1527
1678
  * taken over with a new token, and the waiting queue is left for the next claim.
1528
1679
  * Release deletes the claim only for the token that owns it, and every json-helper
1529
- * learning op refreshes an existing claim's mtime before it runs, without ever
1680
+ * curation op refreshes an existing claim's mtime before it runs, without ever
1530
1681
  * creating one. Reason: a check-then-mv claim let two runs claim at once and
1531
1682
  * clobber a batch, mv kept the queue's old mtime so a fresh claim could look stale
1532
1683
  * at once, and an unconditional final unlink deleted another run's claim.
1533
1684
  *
1534
- * Without .devflow/learning/ it answers none and creates nothing.
1685
+ * Without .devflow/curation/ it answers none and creates nothing; where git tracks files
1686
+ * in the curation data folders it refuses, kind `tracked`, before it looks (trackedCurationArea).
1535
1687
  *
1536
1688
  * @param {string} root - project root
1537
1689
  * @param {{ now?: number, token?: string, timeoutMs?: number }} [opts]
@@ -1542,17 +1694,19 @@ function moveQueueToClaim(queuePath, claimPath) {
1542
1694
  */
1543
1695
  function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs } = {}) {
1544
1696
  assertClaimToken('claimQueue', token);
1545
- if (!hasLearningDir(root)) return { ok: true, value: { state: 'none' } };
1697
+ const paused = trackedCurationArea('claim-queue', root);
1698
+ if (paused !== null) return paused;
1699
+ if (!hasCurationDir(root)) return { ok: true, value: { state: 'none' } };
1546
1700
  return withDecisionsLock('claim-queue', root, () => {
1547
- const claimPath = getLearningPendingTurnsProcessingPath(root);
1548
- const queuePath = getLearningPendingTurnsPath(root);
1701
+ const claimPath = getCurationPendingTurnsProcessingPath(root);
1702
+ const queuePath = getCurationPendingTurnsPath(root);
1549
1703
  const at = new Date(now);
1550
1704
 
1551
1705
  const claim = regularFileStat(claimPath);
1552
1706
  if (claim === false) return notRegularFile('claim-queue', claimPath);
1553
1707
  if (claim !== null) {
1554
1708
  if (now - claim.mtimeMs < CLAIM_STALE_SECS * 1000) return { ok: true, value: { state: 'busy' } };
1555
- writeFileAtomic(getLearningClaimOwnerPath(root), `${token}\n`);
1709
+ writeFileAtomic(getCurationClaimOwnerPath(root), `${token}\n`);
1556
1710
  fs.utimesSync(claimPath, at, at);
1557
1711
  return { ok: true, value: { state: 'claimed', token, takeover: true } };
1558
1712
  }
@@ -1564,7 +1718,7 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
1564
1718
  const moved = moveQueueToClaim(queuePath, claimPath);
1565
1719
  if (moved !== 'moved') return { ok: true, value: { state: moved } };
1566
1720
  fs.utimesSync(claimPath, at, at);
1567
- writeFileAtomic(getLearningClaimOwnerPath(root), `${token}\n`);
1721
+ writeFileAtomic(getCurationClaimOwnerPath(root), `${token}\n`);
1568
1722
  return { ok: true, value: { state: 'claimed', token, takeover: false } };
1569
1723
  }, { timeoutMs });
1570
1724
  }
@@ -1581,14 +1735,14 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
1581
1735
  * @param {string} token
1582
1736
  * @param {{ timeoutMs?: number }} [opts]
1583
1737
  * @returns {{ ok: true, value: { state: 'released'|'not-owner'|'gone' } } | { ok: false, error: { kind: string, message: string } }}
1584
- * errors include withDecisionsLock's not-a-directory, no-learning-dir and busy
1738
+ * errors include withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy
1585
1739
  * @throws {TypeError} when `token` is not a claim token
1586
1740
  */
1587
1741
  function releaseClaim(root, token, { timeoutMs } = {}) {
1588
1742
  assertClaimToken('releaseClaim', token);
1589
1743
  return withDecisionsLock('release-claim', root, () => {
1590
- const claimPath = getLearningPendingTurnsProcessingPath(root);
1591
- const ownerPath = getLearningClaimOwnerPath(root);
1744
+ const claimPath = getCurationPendingTurnsProcessingPath(root);
1745
+ const ownerPath = getCurationClaimOwnerPath(root);
1592
1746
  const owned = readClaimOwner(root) === token;
1593
1747
 
1594
1748
  const claim = regularFileStat(claimPath);
@@ -1607,18 +1761,21 @@ function releaseClaim(root, token, { timeoutMs } = {}) {
1607
1761
  /**
1608
1762
  * The claim heartbeat (D-OWNED-CLAIM): set an existing claim's mtime to now. It
1609
1763
  * never creates a claim and never follows a symlink at the claim path, and it
1610
- * takes no lock — json-helper sends it before each learning op runs. A claim in a
1611
- * learning tree reached through a symbolic link is left alone (D-NO-LINKED-TREE):
1764
+ * takes no lock — json-helper sends it before each curation op runs. A claim in a
1765
+ * curation tree reached through a symbolic link is left alone (D-NO-LINKED-TREE):
1612
1766
  * `lutimes` follows a linked folder above the claim, and the op that follows
1613
- * refuses that tree anyway.
1767
+ * refuses that tree anyway. A claim in a project whose curation data folders git
1768
+ * tracks, or where git does not answer, is left alone as well (trackedCurationArea).
1614
1769
  *
1615
1770
  * @param {string} root - project root
1616
1771
  * @param {{ now?: number }} [opts] - now: epoch ms (default Date.now())
1617
1772
  * @returns {{ ok: true, value: { touched: boolean } } | { ok: false, error: { kind: 'heartbeat-failed', message: string } }}
1618
1773
  */
1619
1774
  function touchClaim(root, { now = Date.now() } = {}) {
1620
- if (linkedLearningFolder(root) !== null) return { ok: true, value: { touched: false } };
1621
- const claimPath = getLearningPendingTurnsProcessingPath(root);
1775
+ if (linkedCurationFolder(root) !== null || trackedCurationArea('heartbeat', root) !== null) {
1776
+ return { ok: true, value: { touched: false } };
1777
+ }
1778
+ const claimPath = getCurationPendingTurnsProcessingPath(root);
1622
1779
  const at = new Date(now);
1623
1780
  try {
1624
1781
  fs.lutimesSync(claimPath, at, at);
@@ -1950,16 +2107,17 @@ function resolveVerifyRef(root) {
1950
2107
  /**
1951
2108
  * Render decisions.md, pitfalls.md and index.md from `ledgerRows` and write each
1952
2109
  * atomically, the index last. The caller holds .decisions.lock
1953
- * (D-ONE-LEARNING-LOCK), so the learning directory exists. render-decisions.cjs
2110
+ * (D-ONE-LEARNING-LOCK), so the curation directory exists. render-decisions.cjs
1954
2111
  * requires this module at load time, so it is required here, on first use. Prints
1955
2112
  * nothing.
1956
2113
  *
1957
2114
  * @param {string} root - project root
1958
2115
  * @param {object[]} ledgerRows - every ledger row
2116
+ * @param {string} [outputDir] - the folder the files belong in (default: the curation folder)
1959
2117
  */
1960
- function renderAll(root, ledgerRows) {
1961
- const { renderLearningFiles } = require('./render-decisions.cjs');
1962
- for (const file of renderLearningFiles(root, ledgerRows)) writeFileAtomic(file.path, file.content);
2118
+ function renderAll(root, ledgerRows, outputDir = getCurationDir(root)) {
2119
+ const { renderCurationFiles } = require('./render-decisions.cjs');
2120
+ for (const file of renderCurationFiles(root, ledgerRows, outputDir)) writeFileAtomic(file.path, file.content);
1963
2121
  }
1964
2122
 
1965
2123
  // ---------------------------------------------------------------------------
@@ -2118,13 +2276,15 @@ function restoreFirst(id, carriers) {
2118
2276
  * observations: the count the log row holds afterwards; reprojected: the
2119
2277
  * anchors re-projected, in anchor order. Error kinds: invalid-input (with
2120
2278
  * problems), duplicate-log-id, restore-first, cannot-reproject, and
2121
- * withDecisionsLock's not-a-directory, no-learning-dir and busy.
2279
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
2122
2280
  * @throws {TypeError} when `mode` is not a put mode
2123
2281
  */
2124
2282
  function putObservation(root, mode, input, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
2125
2283
  if (!VALIDATION_MODES.includes(mode)) {
2126
2284
  throw new TypeError(`putObservation: mode must be one of ${VALIDATION_MODES.join(', ')}, got '${mode}'`);
2127
2285
  }
2286
+ const paused = trackedCurationArea('put-observation', root);
2287
+ if (paused !== null) return paused;
2128
2288
  const matches = scopeMatches || gitScopeMatcher(root);
2129
2289
  return withDecisionsLock('put-observation', root, () => putUnderLock(root, mode, input, { now, scopeMatches: matches }), { timeoutMs });
2130
2290
  }
@@ -2190,16 +2350,19 @@ function putUnderLock(root, mode, input, { now, scopeMatches }) {
2190
2350
  * The data behind `list`, read-only: buildListing over the ledger and the log as
2191
2351
  * they are, every glob scope checked against the files git tracks. Malformed
2192
2352
  * lines are counted, never quarantined (D-QUARANTINE-MALFORMED). Refuses without
2193
- * .devflow/learning/ (D-NO-STRAY-TREE).
2353
+ * .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
2354
+ * in the curation data folders (trackedCurationArea).
2194
2355
  *
2195
2356
  * @param {string} root - project root
2196
2357
  * @param {{ scopeMatches?: (glob: string) => boolean }} [opts] - default gitScopeMatcher(root)
2197
2358
  * @returns {{ ok: true, value: { active: object[], inactive: object[], observations: object[], integrity: object[], malformed: { ledger: number, log: number } } }
2198
- * | { ok: false, error: { kind: 'no-learning-dir', message: string } }}
2359
+ * | { ok: false, error: { kind: 'tracked'|'no-curation-dir', message: string } }}
2199
2360
  */
2200
2361
  function readListing(root, { scopeMatches } = {}) {
2201
- if (!hasLearningDir(root)) return noLearningDir('list', root);
2202
- const { ledgerRows, logRows, rejected } = readLearningState(root);
2362
+ const paused = trackedCurationArea('list', root);
2363
+ if (paused !== null) return paused;
2364
+ if (!hasCurationDir(root)) return noCurationDir('list', root);
2365
+ const { ledgerRows, logRows, rejected } = readCurationState(root);
2203
2366
  return {
2204
2367
  ok: true,
2205
2368
  value: buildListing(ledgerRows, logRows, { scopeMatches: scopeMatches || gitScopeMatcher(root), rejected }),
@@ -2280,16 +2443,19 @@ function formatListing(listing) {
2280
2443
  * the log and the history as they are. When lines were skipped as malformed
2281
2444
  * (D-QUARANTINE-MALFORMED), a found entry gains `malformed: { ledger, log }` and
2282
2445
  * a not-found message says a skipped line may hold it. Refuses without
2283
- * .devflow/learning/ (D-NO-STRAY-TREE).
2446
+ * .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
2447
+ * in the curation data folders (trackedCurationArea).
2284
2448
  *
2285
2449
  * @param {string} root - project root
2286
2450
  * @param {string} key - an anchor id or an observation id
2287
2451
  * @returns {{ ok: true, value: { key: string, ledger: object[], log: object|null, history_versions: object[], flags: object[], malformed?: { ledger: number, log: number } } }
2288
- * | { ok: false, error: { kind: 'no-learning-dir'|'invalid-key'|'not-found', message: string } }}
2452
+ * | { ok: false, error: { kind: 'tracked'|'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
2289
2453
  */
2290
2454
  function showByKey(root, key) {
2291
- if (!hasLearningDir(root)) return noLearningDir('show', root);
2292
- const { ledgerRows, logRows, rejected } = readLearningState(root);
2455
+ const paused = trackedCurationArea('show', root);
2456
+ if (paused !== null) return paused;
2457
+ if (!hasCurationDir(root)) return noCurationDir('show', root);
2458
+ const { ledgerRows, logRows, rejected } = readCurationState(root);
2293
2459
  const shown = showEntry(key, ledgerRows, logRows, { historyVersions: id => historyVersions(root, id) });
2294
2460
  const malformed = { ledger: rejected.ledger.length, log: rejected.log.length };
2295
2461
  const skipped = malformed.ledger + malformed.log;
@@ -2320,7 +2486,7 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
2320
2486
  /**
2321
2487
  * Hand out the entries maintenance works on next, and lease them.
2322
2488
  *
2323
- * Under the learning lock it flags integrity problems and selects the due
2489
+ * Under the curation lock it flags integrity problems and selects the due
2324
2490
  * entries (D-DUE-ORDER), then stamps each entry it hands out with
2325
2491
  * `last_attempt` = now: claim-due is the one writer of the field the lease
2326
2492
  * reads. A hand-out backs up a v1 tree first (D-V1-BACKUP-ONCE) and quarantines
@@ -2335,11 +2501,13 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
2335
2501
  * now: epoch ms (default Date.now()); scopeMatches: default gitScopeMatcher(root)
2336
2502
  * @returns {{ ok: true, value: { ref: { ref: 'origin/HEAD'|'HEAD', commit: string } | null, due: Array<{ anchor_id: string, reason: string, bytes: number }> } }
2337
2503
  * | { ok: false, error: { kind: string, message: string } }}
2338
- * due is selectDue's answer; errors are withDecisionsLock's not-a-directory,
2339
- * no-learning-dir and busy
2504
+ * due is selectDue's answer; errors are withDecisionsLock's tracked, not-a-directory,
2505
+ * no-curation-dir and busy
2340
2506
  */
2341
2507
  function claimDue(root, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
2342
- if (!hasLearningDir(root)) return noLearningDir('claim-due', root);
2508
+ const paused = trackedCurationArea('claim-due', root);
2509
+ if (paused !== null) return paused;
2510
+ if (!hasCurationDir(root)) return noCurationDir('claim-due', root);
2343
2511
  const matches = scopeMatches || gitScopeMatcher(root);
2344
2512
  const ref = resolveVerifyRef(root);
2345
2513
  warmScopeMatcher(readJsonl(getDecisionsLedgerPath(root)).rows, matches);
@@ -2413,7 +2581,7 @@ function nextAnchorFromLedger(ledgerRows, type) {
2413
2581
 
2414
2582
  /**
2415
2583
  * True when a project-relative path is one the cited-number scan never reads: the
2416
- * learning tree, where every entry cites itself, or anything under an excluded
2584
+ * curation tree, where every entry cites itself, or anything under an excluded
2417
2585
  * directory segment.
2418
2586
  *
2419
2587
  * @param {string} relPath - relative to the project root, either separator style
@@ -2421,7 +2589,7 @@ function nextAnchorFromLedger(ledgerRows, type) {
2421
2589
  */
2422
2590
  function isCitedScanExcluded(relPath) {
2423
2591
  const norm = relPath.split(path.sep).join('/');
2424
- if (norm === '.devflow/learning' || norm.startsWith('.devflow/learning/')) return true;
2592
+ if (norm === '.devflow/curation' || norm.startsWith('.devflow/curation/')) return true;
2425
2593
  return norm.split('/').some(segment => CITED_SCAN_EXCLUDED_SEGMENTS.includes(segment));
2426
2594
  }
2427
2595
 
@@ -2432,7 +2600,7 @@ function isCitedScanExcluded(relPath) {
2432
2600
  * command a repository's config names in `core.fsmonitor` — code chosen by the
2433
2601
  * repository this hook runs inside. The call turns it off for itself
2434
2602
  * (`-c core.fsmonitor=false`), so the listing stays a pure read. Reason: the
2435
- * learning ops run inside any repository a session opens, and a read that ran
2603
+ * curation ops run inside any repository a session opens, and a read that ran
2436
2604
  * the repository's command would execute it with the user's privileges.
2437
2605
  *
2438
2606
  * @param {string} root - project root
@@ -2512,7 +2680,7 @@ function readScannedText(file) {
2512
2680
  * Every anchor the project's files cite as a whole word, each mapped to its first
2513
2681
  * citation: the scan behind D-E4-SKIP. It reads the files git tracks under `root`
2514
2682
  * (D-NO-FSMONITOR) or, outside a git working tree, the files a directory walk
2515
- * finds — never the learning tree, an excluded directory, a symbolic link, a
2683
+ * finds — never the curation tree, an excluded directory, a symbolic link, a
2516
2684
  * binary file or a file over the size cap. A file it cannot read is passed over.
2517
2685
  * It writes nothing and takes no lock.
2518
2686
  *
@@ -2584,10 +2752,10 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
2584
2752
  * Promote a v2 observation to a new ledger entry of `type` — the assign-anchor op.
2585
2753
  *
2586
2754
  * D-E4-SKIP: assign-anchor scans the project's tracked files once, before it takes
2587
- * the learning lock, for every anchor they cite as a whole word, and mints the
2755
+ * the curation lock, for every anchor they cite as a whole word, and mints the
2588
2756
  * first number past the type's highest anchored number that no file cites; it
2589
2757
  * reports each number it skips, skips at most E4_MAX_SKIPS of them and refuses
2590
- * when the next one is cited too. The scan reads neither the learning tree nor
2758
+ * when the next one is cited too. The scan reads neither the curation tree nor
2591
2759
  * .git or a vendored or build directory (node_modules, target, dist), nor a
2592
2760
  * symbolic link, a binary file or a file over 5 MB. Reason: a document can cite a
2593
2761
  * number the ledger has not minted yet, and minting over it silently binds that
@@ -2598,7 +2766,7 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
2598
2766
  * whatever its status (D-LEDGER-REGISTRY). The new row is its projection
2599
2767
  * (toLedgerRowV2) with the type's active status and today as both date and
2600
2768
  * last_verified; it is appended to the ledger and the files are re-rendered, all
2601
- * under the learning lock. The log is never written. Like every writer it backs
2769
+ * under the curation lock. The log is never written. Like every writer it backs
2602
2770
  * up a v1 tree first (D-V1-BACKUP-ONCE) and quarantines the ledger's malformed
2603
2771
  * lines before it rewrites the ledger (D-QUARANTINE-MALFORMED); a refusal writes
2604
2772
  * nothing.
@@ -2612,13 +2780,15 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
2612
2780
  * | { ok: false, error: { kind: string, message: string } }}
2613
2781
  * Error kinds: not-in-log, duplicate-log-id, already-promoted, v1-observation,
2614
2782
  * type-mismatch, cited-numbers-exhausted, and withDecisionsLock's
2615
- * not-a-directory, no-learning-dir and busy.
2783
+ * tracked, not-a-directory, no-curation-dir and busy.
2616
2784
  * @throws {TypeError} for a type other than decision or pitfall, or a malformed obsId
2617
2785
  */
2618
2786
  function assignAnchor(root, type, obsId, { now = Date.now(), timeoutMs, citedAnchors } = {}) {
2619
2787
  if (anchorPrefixFor(type) === null) throw new TypeError(`assignAnchor: type must be 'decision' or 'pitfall', got '${type}'`);
2620
2788
  if (typeof obsId !== 'string' || !OBS_ID_RE.test(obsId)) throw new TypeError('assignAnchor: obsId must be an observation id');
2621
- if (!hasLearningDir(root)) return noLearningDir('assign-anchor', root);
2789
+ const paused = trackedCurationArea('assign-anchor', root);
2790
+ if (paused !== null) return paused;
2791
+ if (!hasCurationDir(root)) return noCurationDir('assign-anchor', root);
2622
2792
  const cited = citedAnchors || collectCitedAnchorIds(root);
2623
2793
  return withDecisionsLock('assign-anchor', root, () => assignUnderLock(root, type, obsId, { now, cited }), { timeoutMs });
2624
2794
  }
@@ -2760,7 +2930,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
2760
2930
  * an inactive entry or a v1 one, and — when re-projecting — an entry with no
2761
2931
  * observation id or whose log row is missing, doubled, v1 or of a type it cannot
2762
2932
  * take refuses the whole batch, every refused anchor listed, nothing written. A
2763
- * batch that changes no row writes nothing. Otherwise, under the learning lock,
2933
+ * batch that changes no row writes nothing. Otherwise, under the curation lock,
2764
2934
  * it backs up a v1 tree (D-V1-BACKUP-ONCE), quarantines the ledger's malformed
2765
2935
  * lines (D-QUARANTINE-MALFORMED), then writes the ledger once and renders once.
2766
2936
  *
@@ -2771,7 +2941,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
2771
2941
  * @returns {{ ok: true, value: { refreshed: Array<{ anchor_id: string, state: 'verified'|'reprojected'|'unchanged' }> } }
2772
2942
  * | { ok: false, error: { kind: string, message: string, problems?: Array<{ anchor_id: string, message: string }> } }}
2773
2943
  * refreshed: each anchor once, in the order given. Error kinds: refused (with
2774
- * problems), and withDecisionsLock's not-a-directory, no-learning-dir and busy.
2944
+ * problems), and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
2775
2945
  * @throws {TypeError} when anchorIds is empty or holds anything but anchor ids
2776
2946
  */
2777
2947
  function refreshAnchors(root, anchorIds, { verified = false, now = Date.now(), timeoutMs } = {}) {
@@ -2954,9 +3124,11 @@ function restoreRefusal(kind, message) {
2954
3124
  * @param {{ verifyRef?: { ref: 'origin/HEAD'|'HEAD', commit: string } | null }} [opts]
2955
3125
  * verifyRef: default resolveVerifyRef(root); null when there is no commit to check
2956
3126
  * @returns {{ ok: true, value: { path: string, quote: string, ref: string, commit: string } }
2957
- * | { ok: false, error: { kind: 'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
3127
+ * | { ok: false, error: { kind: 'tracked'|'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
2958
3128
  */
2959
3129
  function quoteAtRef(root, at, quote, { verifyRef } = {}) {
3130
+ const paused = trackedCurationArea('retire-anchor', root);
3131
+ if (paused !== null) return paused;
2960
3132
  const checkedAt = verifyRef === undefined ? resolveVerifyRef(root) : verifyRef;
2961
3133
  if (checkedAt === null) {
2962
3134
  return retireRefusal('no-verify-ref', 'there is no commit to check the quote at (not a git repository, or no commit yet); nothing was written');
@@ -2991,7 +3163,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
2991
3163
  * Each also sets retired_on to today and clears the other notes; the rest of the
2992
3164
  * row stays as it is, so a v1 entry stays v1. An entry already inactive, an
2993
3165
  * anchor the ledger does not hold or holds twice, and a successor absent or
2994
- * inactive are refused. The input and the quote are checked before the learning
3166
+ * inactive are refused. The input and the quote are checked before the curation
2995
3167
  * lock is taken; under it the ledger is written once and the files are rendered,
2996
3168
  * after a v1 tree is backed up (D-V1-BACKUP-ONCE) and malformed ledger lines are
2997
3169
  * quarantined (D-QUARANTINE-MALFORMED). A refusal writes nothing.
@@ -3008,7 +3180,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
3008
3180
  * kinds: invalid-input (with problems), quoteAtRef's, not-found,
3009
3181
  * duplicate-anchor, already-inactive, successor-not-found,
3010
3182
  * successor-duplicate-anchor, successor-inactive, and withDecisionsLock's
3011
- * not-a-directory, no-learning-dir and busy.
3183
+ * tracked, not-a-directory, no-curation-dir and busy.
3012
3184
  * @throws {TypeError} for a malformed anchorId or a status that is not inactive
3013
3185
  */
3014
3186
  function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeoutMs, verifyRef } = {}) {
@@ -3016,7 +3188,9 @@ function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeout
3016
3188
  if (!INACTIVE_STATUSES.includes(status)) {
3017
3189
  throw new TypeError(`retireAnchor: status must be one of ${INACTIVE_STATUSES.join(', ')}, got '${status}'`);
3018
3190
  }
3019
- if (!hasLearningDir(root)) return noLearningDir('retire-anchor', root);
3191
+ const paused = trackedCurationArea('retire-anchor', root);
3192
+ if (paused !== null) return paused;
3193
+ if (!hasCurationDir(root)) return noCurationDir('retire-anchor', root);
3020
3194
  const problems = retireInputProblems(anchorId, status, input);
3021
3195
  if (problems.length > 0) return invalidInput('retire-anchor', problems);
3022
3196
  let note;
@@ -3078,7 +3252,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
3078
3252
  * claim-due hands it out ahead of every verified entry (D-DUE-ORDER); its content
3079
3253
  * and its date stay, and the files are re-rendered. An entry already active, an
3080
3254
  * anchor the ledger does not hold or holds twice, and a row whose type its anchor
3081
- * does not name are refused. It writes as retireAnchor does, under the learning
3255
+ * does not name are refused. It writes as retireAnchor does, under the curation
3082
3256
  * lock; a refusal writes nothing.
3083
3257
  *
3084
3258
  * @param {string} root - project root
@@ -3087,7 +3261,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
3087
3261
  * @returns {{ ok: true, value: { anchor_id: string, status: 'Accepted'|'Active' } }
3088
3262
  * | { ok: false, error: { kind: string, message: string } }}
3089
3263
  * Error kinds: not-found, duplicate-anchor, already-active, type-mismatch, and
3090
- * withDecisionsLock's not-a-directory, no-learning-dir and busy.
3264
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
3091
3265
  * @throws {TypeError} for a malformed anchorId
3092
3266
  */
3093
3267
  function restoreAnchor(root, anchorId, { now = Date.now(), timeoutMs } = {}) {
@@ -3148,15 +3322,17 @@ module.exports = {
3148
3322
  readJsonl,
3149
3323
  rejectedPathFor,
3150
3324
  quarantineRejected,
3325
+ appendNoFollow,
3151
3326
  readJsonlForWrite,
3152
3327
  writeExclusive,
3153
3328
  writeFileAtomic,
3154
3329
  writeJsonlAtomic,
3155
3330
  // Locking
3156
- hasLearningDir,
3331
+ hasCurationDir,
3332
+ trackedCurationArea,
3157
3333
  withDecisionsLock,
3158
3334
  // State and the ledger registry
3159
- readLearningState,
3335
+ readCurationState,
3160
3336
  ledgerRegistry,
3161
3337
  // Validation
3162
3338
  validateObservationInput,
@@ -3171,7 +3347,7 @@ module.exports = {
3171
3347
  // Rotation, clearing and resetting
3172
3348
  rotateObservations,
3173
3349
  clearUnreferenced,
3174
- resetLearning,
3350
+ resetCuration,
3175
3351
  // The queue claim
3176
3352
  CLAIM_STALE_SECS,
3177
3353
  CLAIM_TOKEN_RE,
@@ -3180,6 +3356,16 @@ module.exports = {
3180
3356
  claimQueue,
3181
3357
  releaseClaim,
3182
3358
  touchClaim,
3359
+ // Primitives curation-move.cjs builds on
3360
+ QUEUE_LOCK_STALE_MS,
3361
+ NO_HARD_LINK_CODES,
3362
+ isPlainObject,
3363
+ readTextUnlinked,
3364
+ removeIfPresent,
3365
+ renderAll,
3366
+ lstatOrNull,
3367
+ physicalPath,
3368
+ homeDirOrNull,
3183
3369
  // Integrity, listing, due selection and show
3184
3370
  integrityFlags,
3185
3371
  buildListing,