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
@@ -6,35 +6,73 @@
6
6
  # under a lock. Used by capture-prompt, capture-turn, and capture-question so the
7
7
  # per-feature dual-queue-append logic exists in exactly one place.
8
8
  #
9
- # Source-order requirement: source json-parse (for _HAS_JQ) and learning-lock (for
10
- # learning_lock_acquire/learning_lock_release, which itself requires get-mtime) BEFORE
9
+ # Source-order requirement: source json-parse (for _HAS_JQ) and curation-lock (for
10
+ # curation_lock_acquire/curation_lock_release, which itself requires get-mtime) BEFORE
11
11
  # calling queue_append_row or queue_append_both, and define log/dbg. This file
12
- # sources its sibling git-marker itself, for df_no_symlink_below. Sourcing it only
13
- # defines functions -- no side effects until they are called.
12
+ # sources its siblings git-marker (for df_no_symlink_below) and curation-move (for the
13
+ # ledger folder's names, df_curation_move_names) itself. Sourcing it only defines
14
+ # functions -- no side effects until they are called.
14
15
  #
15
16
  # queue_append_row <root> <queue_file> <role> <content> <ts>
16
17
  # Appends one JSONL row {role, content, ts} to <queue_file>, which lies below
17
- # <root>: the project root for the memory queue, the ledger root for the learning
18
+ # <root>: the project root for the memory queue, the ledger root for the curation
18
19
  # queue. When the queue, or a folder between <root> and it, is a symbolic link,
19
20
  # nothing is written: the skip is logged once and the call returns 0, as a
20
- # skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it
21
- # creates the queue's folder if absent, and the file with mode 0600 (umask 077).
21
+ # skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it builds the
22
+ # row first (a row that fails to build writes nothing), then makes one builtin
23
+ # append under umask 077, so a queue it creates has mode 0600. The umask is the
24
+ # caller's again when the append returns, so a folder made later in the same hook
25
+ # has the mode its parent's umask gives. A ledger queue follows the creator rule
26
+ # (git-marker, df_ledger_queue): it is appended to in the
27
+ # old learning/ folder while that exists, which is never created here; the new
28
+ # curation/ folder is created only when the old name is absent. An append that
29
+ # finds the old folder gone (the one-time move ran meanwhile) looks again and goes
30
+ # on in the new folder, so a capture never drops a row and never waits. Any other
31
+ # queue's folder is created if absent.
32
+ # A queue in an area that git tracks, or that git did not answer for, is paused: before
33
+ # the row is built, queue_tracked_area asks for the area the queue lies in, and a queue
34
+ # that is not untracked gets no row, no folder and no file. The skip is logged once. The
35
+ # check runs on every call, whether or not the queue's folder exists.
22
36
  # After appending, truncates from 200 to the newest 100 lines under a lock
23
- # (learning_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
37
+ # (curation_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
24
38
  # The append itself is intentionally lock-free (accepted-class race shared with
25
39
  # the pre-existing memory design -- see the design doc's "Append-vs-claim race"
26
40
  # note).
27
41
  #
28
- # queue_append_both <memory_root> <memory_queue> <learning_root> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
29
- # Calls queue_append_row for each queue whose *_enabled flag is "true", with the
30
- # root that queue lies below. Each queue is gated independently -- callers
31
- # compute memory_enabled/learning_enabled themselves (see queue_read_gates below
32
- # for the at-most-one-fork read of both).
42
+ # queue_append_both <memory_root> <memory_queue> <curation_root> <curation_queue> <memory_enabled> <curation_enabled> <role> <content> <ts>
43
+ # Builds the row once, then appends it as queue_append_row does to each queue whose
44
+ # *_enabled flag is "true", with the root that queue lies below. Each queue is gated
45
+ # independently -- callers compute memory_enabled/curation_enabled themselves (see
46
+ # queue_read_gates below for the at-most-one-fork read of both). With neither queue
47
+ # enabled it builds nothing. A queue whose area is paused (see queue_tracked_area) reads
48
+ # as not enabled for the call, and nothing is built for it.
49
+ #
50
+ # queue_tracked_area <root> <curation|memory|both> [git|node]
51
+ # Does git track files in the data folders of the area at <root>? Sets
52
+ # DF_TRACKED_AREA_CURATION or DF_TRACKED_AREA_MEMORY (one variable per area) to
53
+ # `tracked`, `untracked` or `unknown`, and always returns 0. The area `both` is one
54
+ # call over the union of the two: it sets both variables, and a listing in it is
55
+ # followed by one call per area, so each variable names its own area. A root is asked
56
+ # once per area in a process. The shell twin of lib/tracked-areas.cjs, held to its
57
+ # answers by tests/decisions/tracked-area-parity.test.ts:
58
+ # - no `.git` entry (a file, a folder or a link) at the root: `untracked`, no call;
59
+ # - a root that is not an absolute path, whose physical parent holds a colon, or that
60
+ # can not be entered: `unknown`;
61
+ # - git mode (the default): one `git ls-files -z` over the area's pathspecs, with the
62
+ # leaf's environment scrub, a ceiling at the root's parent and the fsmonitor command
63
+ # off. Only the first byte of the listing is read. A byte read means `tracked`,
64
+ # whatever git's status (git ends on a closed pipe). With no byte, git status 0
65
+ # means `untracked`, and any other status `unknown`;
66
+ # - node mode: the json-helper `tracked-area` op, which bounds the call in time. Without
67
+ # node it runs as git mode; an answer that is not one of the three words is `unknown`.
68
+ # The capture hooks ask in git mode, with no process but git. The session start asks in
69
+ # node mode, so a hung git is cut.
33
70
  #
34
71
  # queue_read_gates <manifest_path> [<root>]
35
- # Reads BOTH switches, `memory` and `learning`, in at most ONE subprocess fork
72
+ # Reads BOTH switches, `memory` and `curation` (curation is read under both of
73
+ # its keys, the current one and the pre-rename copy), in at most ONE subprocess fork
36
74
  # (AC-P1 -- at most one gate-read fork per capture hook, not two; none when
37
- # the fast paths settle it). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false")
75
+ # the fast paths settle it). Sets _QG_MEMORY and _QG_CURATION ("true"/"false")
38
76
  # in the caller's scope. The two values are newline-separated
39
77
  # rather than using a control-character delimiter: they are always the literal
40
78
  # strings "true"/"false", never arbitrary content, so a plain newline split is
@@ -44,35 +82,43 @@
44
82
  # machine switch is on AND neither repository file sets `features.<name>` to
45
83
  # the literal `false` -- the rule resolve-settings.cjs folds, held to the same
46
84
  # answers by the shared table tests/fixtures/settings-switch-table.ts (TP-49).
47
- # A repository can switch memory or learning off for itself, never back on.
85
+ # A repository can switch memory or curation off for itself, never back on.
48
86
  # machine <manifest_path>, the ~/.devflow/manifest.json `devflow init` and
49
- # `devflow memory|learning --enable/--disable` write. Only an
87
+ # `devflow memory|curation --enable/--disable` write. Only an
50
88
  # explicit boolean `false` switches a feature off -- an absent,
51
89
  # unreadable or malformed manifest, a missing key, or a non-boolean
52
90
  # value leaves it ON (fail-open), the rule
53
91
  # isMachineFeatureOn() applies in the CLI.
54
92
  # project <root>/.devflow/project.json (team-committed) and
55
93
  # personal <root>/.devflow/config.json (this worktree's own), read ONLY under
56
- # `features.memory` / `features.learning`. The retired top-level
57
- # keys (`memory`, `learning`, `decisions`) and `features.decisions`
58
- # narrow nothing (#378: reading them let a feature run in every repo
59
- # but one). An invalid file -- unparseable, over 4096 bytes, a BOM,
60
- # a symlink, a duplicated key -- narrows nothing.
94
+ # `features.memory` and, for curation, `features.curation` or
95
+ # `features.learning` (a narrow-only union: a literal false in
96
+ # either key turns curation off, and each key is parsed on its
97
+ # own). The retired top-level keys (`memory`, `curation`,
98
+ # `learning`, `decisions`) and `features.decisions` narrow nothing
99
+ # (#378: reading them let a feature run in every repo but one). An
100
+ # invalid file -- unparseable, over 4096 bytes, a BOM, a symlink, a
101
+ # duplicated key -- narrows nothing.
61
102
  # <root> is the checkout's toplevel (DF_ROOT, which every caller holds as
62
- # $PROJECT_ROOT), for learning too: project.json is committed per branch and
103
+ # $PROJECT_ROOT), for curation too: project.json is committed per branch and
63
104
  # config.json lives per worktree, so both are read where the session runs, as
64
105
  # resolve-settings.cjs reads them -- never at DF_LEDGER_ROOT, which only says
65
106
  # where the queue lands. Without <root> (or with an empty one) no repository
66
107
  # file is read.
67
108
  #
68
- # D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, learning reads
69
- # `features.learning` when it is a boolean, else the pre-rename
70
- # `features.decisions` -- readManifest's migration precedence
71
- # exactly, so a legacy `decisions: false` switches learning off before any
72
- # command has healed the file. Only a boolean `false` in whichever key decides
73
- # is off, as above.
109
+ # The machine rule for curation (#529), the one isMachineFeatureOn() applies:
110
+ # off when `features.curation` or `features.learning` is a boolean false; else
111
+ # on when either is a boolean true; else, when neither is a boolean, the
112
+ # pre-rename `features.decisions` decides if it is a boolean; else on. A
113
+ # non-boolean value is ignored for that key only, so a disagreeing pair
114
+ # resolves to off.
115
+ #
116
+ # D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, the pre-rename
117
+ # `features.decisions` is read when neither current key is a boolean --
118
+ # readManifest's migration precedence exactly, so a legacy `decisions: false`
119
+ # switches curation off before any command has healed the file.
74
120
  #
75
- # Every hook that gates on memory or learning reads it through here: the
121
+ # Every hook that gates on memory or curation reads it through here: the
76
122
  # capture hooks, session-start-context (Sections 1-2), memory-worker,
77
123
  # session-start-memory, pre-compact-memory and background-memory-update. The
78
124
  # callers pass <manifest_path> as $HOME/.devflow/manifest.json, the machine
@@ -81,80 +127,370 @@
81
127
 
82
128
  # queue_append_row checks its queue with df_no_symlink_below, so this helper sources
83
129
  # git-marker itself rather than trusting every caller to have done so first. One that
84
- # failed to source is a command not found: the refusing branch, so nothing is written.
85
- source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
130
+ # is missing is a command not found: the refusing branch, so nothing is written.
131
+ # The ledger folder's names come from the move shim (df_curation_move_names), sourced
132
+ # the same way and refused the same way.
133
+ #
134
+ # Each source is guarded by a builtin readability test, because bash 3.2 under `set -e`
135
+ # ends the shell at a `source` of a missing file, even inside `|| true`. Without the test
136
+ # the refusing branch would never run, and the capture hooks would stop here.
137
+ [ -r "${BASH_SOURCE[0]%/*}/git-marker" ] && source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
138
+ [ -r "${BASH_SOURCE[0]%/*}/curation-move" ] && source "${BASH_SOURCE[0]%/*}/curation-move" 2>/dev/null || true
86
139
 
87
- queue_append_row() {
88
- local _qar_root="$1" _qar_file="$2" _qar_role="$3" _qar_content="$4" _qar_ts="$5"
140
+ # The caller's umask, read once per process. A queue is created 0600, so the append
141
+ # runs under umask 077 -- and the rest of the hook must run under the umask its parent
142
+ # gave it, or a folder the next append makes would be 0700. `$(umask)` is one subshell
143
+ # with no exec, and it runs on the first append only; each append gives this value
144
+ # back, so the umask the process holds when it first appends is the one it keeps.
145
+ _QAR_UMASK=""
146
+
147
+ # _qar_build_row <role> <content> <ts> -- sets _QAR_ROW to the JSONL row, built by jq
148
+ # (--arg, --argjson) or node (JSON.stringify), never by string concatenation. A
149
+ # predicate: it succeeds only for a row that built, and a row that fails to build is
150
+ # empty, so nothing partial can reach a queue. Call it in an `if` or an `||` list.
151
+ _qar_build_row() {
152
+ _QAR_ROW=""
153
+ if [ "$_HAS_JQ" = "true" ]; then
154
+ _QAR_ROW=$(jq -n -c --arg role "$1" --arg content "$2" --argjson ts "$3" \
155
+ '{role: $role, content: $content, ts: $ts}') || _QAR_ROW=""
156
+ else
157
+ _QAR_ROW=$(node -e "process.stdout.write(JSON.stringify({role:process.argv[1],content:process.argv[2],ts:parseInt(process.argv[3])}))" \
158
+ -- "$1" "$2" "$3") || _QAR_ROW=""
159
+ fi
160
+ if [ -z "$_QAR_ROW" ]; then
161
+ log "Queue skipped: the row could not be built; nothing written"
162
+ return 1
163
+ fi
164
+ return 0
165
+ }
166
+
167
+ # _qar_write <queue_file> <row> -- one builtin append, under umask 077 so a queue it
168
+ # creates has mode 0600 (SEC-2). The umask is the caller's again before it returns,
169
+ # whether the append worked or not. The redirection error is silenced by 2>/dev/null
170
+ # written BEFORE it. A predicate: it fails when the row was not written.
171
+ _qar_write() {
172
+ local _qar_status=0
173
+ umask 077
174
+ printf '%s\n' "$2" 2>/dev/null >> "$1" || _qar_status=1
175
+ umask "$_QAR_UMASK" 2>/dev/null || true
176
+ return "$_qar_status"
177
+ }
178
+
179
+ # _qar_append <root> <queue_file> <row> -- appends <row> to <queue_file>, which lies
180
+ # below <root>, and sets _QAR_FILE to the queue it reached (the old ledger folder, or
181
+ # the new one after a move). A predicate: it fails when nothing was written -- a link on
182
+ # the path (D-HOOKS-NO-SYMLINK: the skip is logged once) or a queue that could not be
183
+ # written -- and a skipped capture is not an error for the hook.
184
+ _qar_append() {
185
+ local _qar_root="$1" _qar_file="$2" _qar_row="$3"
186
+ local _qar_given="$2" _qar_old="" _qar_new=""
187
+ _QAR_FILE=""
188
+
189
+ # The names the creator rule below needs. A shim that failed to source is a command
190
+ # not found, so nothing is appended: no queue is written without them.
191
+ if ! df_curation_move_names 2>/dev/null; then
192
+ log "Queue skipped: the ledger folder names are unavailable; nothing written"
193
+ return 1
194
+ fi
195
+ _qar_old="$DF_CM_OLD_FOLDER"
196
+ _qar_new="$DF_CM_NEW_FOLDER"
197
+
198
+ # The creator rule (git-marker, df_ledger_queue): a ledger queue is appended to in
199
+ # the old folder while that folder is present, so a new name never hides an old
200
+ # ledger. Any other queue (the memory queue) comes back unchanged. A helper that
201
+ # failed to source is a command not found, so the queue stays as given and the
202
+ # check below, from the same library, refuses it.
203
+ if df_ledger_queue "$_qar_file" "$_qar_old" "$_qar_new"; then _qar_file="$DF_LEDGER_QUEUE"; fi
89
204
 
90
205
  # D-HOOKS-NO-SYMLINK (git-marker): a skipped append is a skipped capture.
91
206
  if ! df_no_symlink_below "$_qar_root" "$_qar_file"; then
92
207
  log "Queue skipped: a symbolic link sits on the path to $_qar_file; nothing written"
93
- return 0
208
+ return 1
209
+ fi
210
+
211
+ # A folder is made only where this append is its creator: never the old ledger
212
+ # folder, which a move may rename away at any moment, and which a make-again would
213
+ # bring back beside the new one. An old folder that is gone leaves the append to
214
+ # fail, and the re-probe below decides again. A folder is made under the caller's
215
+ # umask: only the write below runs under 077.
216
+ case "${_qar_file%/*}" in
217
+ */"$_qar_old") ;;
218
+ *) mkdir -p "${_qar_file%/*}" 2>/dev/null || true ;;
219
+ esac
220
+
221
+ if [ -z "$_QAR_UMASK" ]; then _QAR_UMASK=$(umask) || _QAR_UMASK=""; fi
222
+ if ! _qar_write "$_qar_file" "$_qar_row"; then
223
+ # The old folder may have been moved after the probe. Look again: when the old
224
+ # name is gone, the creator makes the new folder once and the row goes there.
225
+ df_ledger_queue "$_qar_given" "$_qar_old" "$_qar_new" || true
226
+ if [ "$DF_LEDGER_QUEUE" != "$_qar_file" ] \
227
+ && df_no_symlink_below "$_qar_root" "$DF_LEDGER_QUEUE" \
228
+ && mkdir -p "${DF_LEDGER_QUEUE%/*}" 2>/dev/null \
229
+ && _qar_write "$DF_LEDGER_QUEUE" "$_qar_row"; then
230
+ _qar_file="$DF_LEDGER_QUEUE"
231
+ else
232
+ log "Queue append failed: $_qar_file could not be written"
233
+ return 1
234
+ fi
94
235
  fi
95
- mkdir -p "${_qar_file%/*}" 2>/dev/null || true
236
+ _QAR_FILE="$_qar_file"
237
+ return 0
238
+ }
96
239
 
97
- if [ ! -f "$_qar_file" ]; then
98
- (umask 077 && touch "$_qar_file") 2>/dev/null || true
240
+ # _qar_trim_overflow <queue_file> -- overflow safety: past 200 lines, keep the newest
241
+ # 100, locked to prevent a multi-session race.
242
+ _qar_trim_overflow() {
243
+ local _qar_file="$1" _qar_lines="" _qar_lock="" _qar_tmp=""
244
+ [ -f "$_qar_file" ] || return 0
245
+ _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
246
+ if [ "$_qar_lines" -gt 200 ]; then
247
+ _qar_lock="${_qar_file}.lock"
248
+ if curation_lock_acquire "$_qar_lock" 2; then
249
+ _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
250
+ if [ "$_qar_lines" -gt 200 ]; then
251
+ _qar_tmp="${_qar_file}.tmp.$$"
252
+ # SEC-2: the trimmed copy is created under umask 077, so the renamed
253
+ # queue keeps mode 0600. mv replaces the inode, so the queue takes the
254
+ # copy's mode, and a chmod after the mv would leave a window. The copy is
255
+ # created only where nothing stands at its name: builtin tests come first,
256
+ # so an entry already there, a link to a FIFO or a device included, is
257
+ # never opened (noclobber alone would open one, as no regular file stands
258
+ # there), and noclobber then makes the create itself exclusive. Such an
259
+ # entry is removed instead, and the trim waits for a later append.
260
+ if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
261
+ log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
262
+ dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
263
+ else
264
+ rm -f "$_qar_tmp" 2>/dev/null || true
265
+ log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
266
+ fi
267
+ fi
268
+ curation_lock_release "$_qar_lock"
269
+ fi
99
270
  fi
271
+ return 0
272
+ }
100
273
 
101
- if [ "$_HAS_JQ" = "true" ]; then
102
- jq -n -c --arg role "$_qar_role" --arg content "$_qar_content" --argjson ts "$_qar_ts" \
103
- '{role: $role, content: $content, ts: $ts}' >> "$_qar_file"
274
+ # --- The tracked-area gate: no capture into a folder that git tracks ---
275
+ #
276
+ # A folder that git tracks holds the repository's content, not devflow's data, so a
277
+ # capture writes nothing into it, and also when git did not answer (queue_tracked_area,
278
+ # documented above). The check runs before the row is built and before any folder is
279
+ # made, on every call, whether or not the queue's folder exists.
280
+
281
+ # _qta_specs <curation|memory|both> -- sets the array _QTA_SPECS to the pathspecs of the
282
+ # area: the shell twin of AREA_PATHSPECS in lib/tracked-areas.cjs, in the leaf's order and
283
+ # spelling (tests/decisions/tracked-area-parity.test.ts holds the two equal, element by
284
+ # element). `.devflow` is spelled twice, the second time with the fl ligature (U+FB02,
285
+ # bytes ef ac 82), which a case-folding file system opens as the same name. The `.devflow`
286
+ # entry itself (a link, a gitlink or a plain file) stands over every folder: the character
287
+ # class keeps `glob` from matching it as a leading directory. Fails for any other word.
288
+ _qta_specs() {
289
+ local _qs_fl=$'\xef\xac\x82' _qs_dir="" _qs_curation=() _qs_memory=() _qs_entry=()
290
+ for _qs_dir in ".devflow" ".dev${_qs_fl}ow"; do
291
+ _qs_curation+=(
292
+ ":(icase,literal)${_qs_dir}/learning"
293
+ ":(icase,literal)${_qs_dir}/curation"
294
+ ":(icase,glob)${_qs_dir}/learning.backup-*"
295
+ ":(icase,glob)${_qs_dir}/learning.backup-*/**"
296
+ )
297
+ _qs_memory+=(":(icase,literal)${_qs_dir}/memory")
298
+ _qs_entry+=(":(icase,glob)[.]${_qs_dir#.}")
299
+ done
300
+ case "${1:-}" in
301
+ curation) _QTA_SPECS=("${_qs_curation[@]}" "${_qs_entry[@]}") ;;
302
+ memory) _QTA_SPECS=("${_qs_memory[@]}" "${_qs_entry[@]}") ;;
303
+ both) _QTA_SPECS=("${_qs_curation[@]}" "${_qs_entry[@]}" "${_qs_memory[@]}") ;;
304
+ *) _QTA_SPECS=(); return 1 ;;
305
+ esac
306
+ return 0
307
+ }
308
+
309
+ # _qta_probe <root> <curation|memory|both> <git|node> -- sets _QTA_ANSWER to `tracked`,
310
+ # `untracked` or `unknown`, as queue_tracked_area documents. Always returns 0.
311
+ #
312
+ # The git mode reads one byte of the listing and ends. Under bash 3.2 with errexit set the
313
+ # pipeline sits in an `if` condition (an empty listing makes `read` fail, and a failing
314
+ # condition does not end the shell), and PIPESTATUS is copied in both arms before any other
315
+ # command resets it: the second element is the reader's status, the first git's. Nothing
316
+ # follows the pipeline in an `||` list. The `cd -P` runs inside the subshell, so the
317
+ # caller's directory never changes, and git runs there with the variables that redirect it
318
+ # removed (the list is GIT_REDIRECT_VARIABLES of the leaf), a fixed locale, no optional
319
+ # lock, and the root's physical parent as its ceiling.
320
+ _qta_probe() {
321
+ local _qp_root="${1:-}" _qp_area="${2:-}" _qp_mode="${3:-git}" _qp_out="" _qp_ceiling="" _qp_c="" _qp_st=()
322
+ _QTA_ANSWER="unknown"
323
+ case "$_qp_root" in /*) ;; *) return 0 ;; esac
324
+ if [ ! -e "$_qp_root/.git" ] && [ ! -L "$_qp_root/.git" ]; then
325
+ _QTA_ANSWER="untracked"
326
+ return 0
327
+ fi
328
+ if [ "$_qp_mode" = "node" ] && [ "${_HAS_NODE:-}" = "true" ] && [ -n "${_JSON_HELPER:-}" ]; then
329
+ case "$_qp_area" in curation|memory|both) ;; *) return 0 ;; esac
330
+ _qp_out=$(node "$_JSON_HELPER" tracked-area "$_qp_area" "$_qp_root" 2>/dev/null) || _qp_out=""
331
+ _qp_out="${_qp_out%%$'\n'*}"
332
+ case "$_qp_out" in tracked|untracked|unknown) _QTA_ANSWER="$_qp_out" ;; esac
333
+ return 0
334
+ fi
335
+ _qta_specs "$_qp_area" || return 0
336
+ if (
337
+ cd -P "$_qp_root" >/dev/null 2>&1 || exit 1
338
+ _qp_ceiling="${PWD%/*}"
339
+ [ -n "$_qp_ceiling" ] || _qp_ceiling="/"
340
+ case "$_qp_ceiling" in *:*) exit 1 ;; esac
341
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_NAMESPACE GIT_LITERAL_PATHSPECS GIT_GLOB_PATHSPECS GIT_NOGLOB_PATHSPECS GIT_ICASE_PATHSPECS
342
+ export LC_ALL=C GIT_OPTIONAL_LOCKS=0 GIT_CEILING_DIRECTORIES="$_qp_ceiling"
343
+ exec git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- "${_QTA_SPECS[@]}"
344
+ ) </dev/null 2>/dev/null | { IFS= read -r -n 1 _qp_c; }; then
345
+ _qp_st=("${PIPESTATUS[@]}")
104
346
  else
105
- node -e "process.stdout.write(JSON.stringify({role:process.argv[1],content:process.argv[2],ts:parseInt(process.argv[3])})+String.fromCharCode(10))" \
106
- -- "$_qar_role" "$_qar_content" "$_qar_ts" >> "$_qar_file"
347
+ _qp_st=("${PIPESTATUS[@]}")
348
+ fi
349
+ if [ "${#_qp_st[@]}" -eq 2 ]; then
350
+ if [ "${_qp_st[1]}" -eq 0 ]; then
351
+ _QTA_ANSWER="tracked"
352
+ elif [ "${_qp_st[0]}" -eq 0 ]; then
353
+ _QTA_ANSWER="untracked"
354
+ fi
107
355
  fi
356
+ return 0
357
+ }
358
+
359
+ # _qta_ask <root> <curation|memory> <git|node> -- sets the area's variable, unless this
360
+ # process already asked for that root and area. Always returns 0.
361
+ _qta_ask() {
362
+ case "$2" in
363
+ curation)
364
+ if [ "${_QTA_ASKED_CURATION:-}" != "r$1" ]; then
365
+ _qta_probe "$1" curation "$3"
366
+ DF_TRACKED_AREA_CURATION="$_QTA_ANSWER"
367
+ _QTA_ASKED_CURATION="r$1"
368
+ fi
369
+ ;;
370
+ memory)
371
+ if [ "${_QTA_ASKED_MEMORY:-}" != "r$1" ]; then
372
+ _qta_probe "$1" memory "$3"
373
+ DF_TRACKED_AREA_MEMORY="$_QTA_ANSWER"
374
+ _QTA_ASKED_MEMORY="r$1"
375
+ fi
376
+ ;;
377
+ esac
378
+ return 0
379
+ }
108
380
 
109
- # Overflow safety: >200 lines -> keep newest 100, locked to prevent a multi-session race.
110
- if [ -f "$_qar_file" ]; then
111
- local _qar_lines
112
- _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
113
- if [ "$_qar_lines" -gt 200 ]; then
114
- local _qar_lock="${_qar_file}.lock"
115
- if learning_lock_acquire "$_qar_lock" 2; then
116
- _qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
117
- if [ "$_qar_lines" -gt 200 ]; then
118
- local _qar_tmp="${_qar_file}.tmp.$$"
119
- # SEC-2: the trimmed copy is created under umask 077, so the renamed
120
- # queue keeps mode 0600. mv replaces the inode, so the queue takes the
121
- # copy's mode, and a chmod after the mv would leave a window. The copy is
122
- # created only where nothing stands at its name: builtin tests come first,
123
- # so an entry already there, a link to a FIFO or a device included, is
124
- # never opened (noclobber alone would open one, as no regular file stands
125
- # there), and noclobber then makes the create itself exclusive. Such an
126
- # entry is removed instead, and the trim waits for a later append.
127
- if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
128
- log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
129
- dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
130
- else
131
- rm -f "$_qar_tmp" 2>/dev/null || true
132
- log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
133
- fi
381
+ queue_tracked_area() {
382
+ local _qa_root="${1:-}" _qa_area="${2:-}" _qa_mode="${3:-git}"
383
+ case "$_qa_area" in
384
+ curation|memory)
385
+ _qta_ask "$_qa_root" "$_qa_area" "$_qa_mode"
386
+ ;;
387
+ both)
388
+ if [ "${_QTA_ASKED_CURATION:-}" != "r$_qa_root" ] || [ "${_QTA_ASKED_MEMORY:-}" != "r$_qa_root" ]; then
389
+ _qta_probe "$_qa_root" both "$_qa_mode"
390
+ if [ "$_QTA_ANSWER" = "tracked" ]; then
391
+ # The union listed something: one call per area says which.
392
+ _QTA_ASKED_CURATION=""
393
+ _QTA_ASKED_MEMORY=""
394
+ _qta_ask "$_qa_root" curation "$_qa_mode"
395
+ _qta_ask "$_qa_root" memory "$_qa_mode"
396
+ else
397
+ DF_TRACKED_AREA_CURATION="$_QTA_ANSWER"
398
+ DF_TRACKED_AREA_MEMORY="$_QTA_ANSWER"
399
+ _QTA_ASKED_CURATION="r$_qa_root"
400
+ _QTA_ASKED_MEMORY="r$_qa_root"
134
401
  fi
135
- learning_lock_release "$_qar_lock"
136
402
  fi
137
- fi
403
+ ;;
404
+ esac
405
+ return 0
406
+ }
407
+
408
+ # _qar_gate <memory_root> <memory_on> <curation_root> <curation_on> -- the one gate both
409
+ # append functions use. Sets _QAR_MEMORY_ON and _QAR_CURATION_ON to the flags as given,
410
+ # except that a queue whose area is not `untracked` reads "false" (paused), with one fixed
411
+ # log line. Only an enabled queue is asked about. When both are enabled at one root, one
412
+ # call over the union answers for both (queue_tracked_area `both`). Always returns 0.
413
+ _qar_gate() {
414
+ local _qg_mroot="$1" _qg_mon="$2" _qg_croot="$3" _qg_con="$4"
415
+ if [ "$_qg_mon" = "true" ] && [ "$_qg_con" = "true" ] && [ "$_qg_mroot" = "$_qg_croot" ]; then
416
+ queue_tracked_area "$_qg_mroot" both git
417
+ else
418
+ if [ "$_qg_mon" = "true" ]; then queue_tracked_area "$_qg_mroot" memory git; fi
419
+ if [ "$_qg_con" = "true" ]; then queue_tracked_area "$_qg_croot" curation git; fi
420
+ fi
421
+ if [ "$_qg_mon" = "true" ] && [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
422
+ _qg_mon="false"
423
+ log "Queue skipped: the memory area is paused (git tracks files in its data folders, or did not answer); nothing written"
424
+ fi
425
+ if [ "$_qg_con" = "true" ] && [ "${DF_TRACKED_AREA_CURATION:-unknown}" != "untracked" ]; then
426
+ _qg_con="false"
427
+ log "Queue skipped: the curation area is paused (git tracks files in its data folders, or did not answer); nothing written"
138
428
  fi
429
+ _QAR_MEMORY_ON="$_qg_mon"
430
+ _QAR_CURATION_ON="$_qg_con"
431
+ return 0
432
+ }
433
+
434
+ # _qar_area_of <root> <queue_file> -- sets _QAR_AREA to the area the queue lies in:
435
+ # `memory` below <root>/.devflow/memory, `curation` below the ledger folder (either name),
436
+ # and `both` for a queue in neither, which is paused when either area is.
437
+ _qar_area_of() {
438
+ case "$2" in
439
+ "$1"/.devflow/memory/*) _QAR_AREA="memory" ;;
440
+ "$1"/.devflow/learning/*|"$1"/.devflow/curation/*) _QAR_AREA="curation" ;;
441
+ *) _QAR_AREA="both" ;;
442
+ esac
443
+ return 0
444
+ }
445
+
446
+ queue_append_row() {
447
+ _qar_area_of "$1" "$2"
448
+ case "$_QAR_AREA" in
449
+ memory)
450
+ _qar_gate "$1" true "" false
451
+ if [ "$_QAR_MEMORY_ON" != "true" ]; then return 0; fi
452
+ ;;
453
+ curation)
454
+ _qar_gate "" false "$1" true
455
+ if [ "$_QAR_CURATION_ON" != "true" ]; then return 0; fi
456
+ ;;
457
+ *)
458
+ _qar_gate "$1" true "$1" true
459
+ if [ "$_QAR_MEMORY_ON" != "true" ] || [ "$_QAR_CURATION_ON" != "true" ]; then return 0; fi
460
+ ;;
461
+ esac
462
+ _qar_build_row "$3" "$4" "$5" || return 0
463
+ if _qar_append "$1" "$2" "$_QAR_ROW"; then _qar_trim_overflow "$_QAR_FILE"; fi
464
+ return 0
139
465
  }
140
466
 
141
467
  queue_append_both() {
142
468
  local _qab_memory_root="$1" _qab_memory_queue="$2"
143
- local _qab_learning_root="$3" _qab_learning_queue="$4"
144
- local _qab_memory_enabled="$5" _qab_learning_enabled="$6"
145
- local _qab_role="$7" _qab_content="$8" _qab_ts="$9"
469
+ local _qab_curation_root="$3" _qab_curation_queue="$4"
470
+ local _qab_memory_enabled="$5" _qab_curation_enabled="$6"
471
+ local _qab_role="$7" _qab_content="$8" _qab_ts="$9" _qab_row=""
472
+
473
+ # A paused queue reads as not enabled: the gate comes before the row is built.
474
+ _qar_gate "$_qab_memory_root" "$_qab_memory_enabled" "$_qab_curation_root" "$_qab_curation_enabled"
475
+ _qab_memory_enabled="$_QAR_MEMORY_ON"
476
+ _qab_curation_enabled="$_QAR_CURATION_ON"
477
+ if [ "$_qab_memory_enabled" != "true" ] && [ "$_qab_curation_enabled" != "true" ]; then return 0; fi
478
+ # One row for both queues: it is built once, and each enabled queue takes the same line.
479
+ _qar_build_row "$_qab_role" "$_qab_content" "$_qab_ts" || return 0
480
+ _qab_row="$_QAR_ROW"
146
481
 
147
482
  if [ "$_qab_memory_enabled" = "true" ]; then
148
- queue_append_row "$_qab_memory_root" "$_qab_memory_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
483
+ if _qar_append "$_qab_memory_root" "$_qab_memory_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
149
484
  fi
150
- if [ "$_qab_learning_enabled" = "true" ]; then
151
- queue_append_row "$_qab_learning_root" "$_qab_learning_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
485
+ if [ "$_qab_curation_enabled" = "true" ]; then
486
+ if _qar_append "$_qab_curation_root" "$_qab_curation_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
152
487
  fi
488
+ return 0
153
489
  }
154
490
 
155
491
  # _qg_repo_file_can_narrow <file> -- succeeds when <file> COULD set
156
- # features.memory or features.learning to false, so only the parser can decide
157
- # it; fails when it certainly cannot. Builtins only: no fork.
492
+ # features.memory, features.curation or features.learning to false, so only the
493
+ # parser can decide it; fails when it certainly cannot. Builtins only: no fork.
158
494
  #
159
495
  # D-GATES-FAST-PATH (repository files): the read is bounded -- `-n 4097`, which
160
496
  # bash 3.2 supports -- and `read -d ''` succeeds only when it stops short of the
@@ -164,14 +500,14 @@ queue_append_both() {
164
500
  # symlink and refuses a directory or FIFO, so nothing here can block; the parser
165
501
  # itself refuses the symlink. A `\u` escape is the only JSON spelling of a
166
502
  # letter other than the letter itself, so without one a narrowing file shows
167
- # `"features"`, then `{`, then `"memory"` or `"learning"`, then `false`,
168
- # literally and in that order. `.` spans newlines (POSIX ERE without
503
+ # `"features"`, then `{`, then `"memory"`, `"curation"` or `"learning"`, then
504
+ # `false`, literally and in that order. `.` spans newlines (POSIX ERE without
169
505
  # REG_NEWLINE), so the match can only over-report -- an over-report costs one
170
506
  # fork and the parser's answer, never a wrong one. No `"decisions"` and no
171
507
  # top-level key before `features` is ever matched.
172
508
  _qg_repo_file_can_narrow() {
173
509
  local _qgf_text=""
174
- local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|learning)"[[:space:]]*:[[:space:]]*false'
510
+ local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|curation|learning)"[[:space:]]*:[[:space:]]*false'
175
511
  [ -f "$1" ] || return 1
176
512
  if IFS= read -r -d '' -n 4097 _qgf_text 2>/dev/null < "$1"; then return 1; fi
177
513
  case "$_qgf_text" in *'\u'*) return 0 ;; esac
@@ -180,22 +516,24 @@ _qg_repo_file_can_narrow() {
180
516
 
181
517
  queue_read_gates() {
182
518
  local _qg_manifest="${1:-}" _qg_root="${2:-}" _qg_text="" _qg_parse="false" _qg_repo="false" _qg_fields=""
519
+ local _qg_scripts="${BASH_SOURCE[0]%/*}/.."
183
520
  _QG_MEMORY="true"
184
- _QG_LEARNING="true"
521
+ _QG_CURATION="true"
185
522
 
186
523
  if [ -n "$_qg_manifest" ] && [ -f "$_qg_manifest" ]; then
187
524
  # D-GATES-FAST-PATH: every capture hook and every session start pays this
188
525
  # read, and on most machines both switches are on. Only a JSON boolean
189
- # `false` under a "memory", "learning" or "decisions" key can switch anything
190
- # off, and a `\u` escape is the only JSON spelling of a letter other than the
191
- # letter itself, so without one such a key appears literally. A text holding
192
- # no `\u` and no key-then-`false` sequence therefore cannot switch anything
193
- # off -- valid or not, it reads both-on either way -- and is settled by shell
194
- # builtins (read, case, [[ =~ ]]) with no jq/node fork; everything else takes
195
- # the full parse below. `read -d ''` succeeds only on reaching a NUL byte,
196
- # i.e. before the end of the file, so a text it cut short is parsed in full
197
- # too. An unreadable file reads empty and fails open, as the parser would.
198
- local _qg_re='"(memory|learning|decisions)"[[:space:]]*:[[:space:]]*false'
526
+ # `false` under a "memory", "curation", "learning" or "decisions" key can
527
+ # switch anything off, and a `\u` escape is the only JSON spelling of a
528
+ # letter other than the letter itself, so without one such a key appears
529
+ # literally. A text holding no `\u` and no key-then-`false` sequence
530
+ # therefore cannot switch anything off -- valid or not, it reads both-on
531
+ # either way -- and is settled by shell builtins (read, case, [[ =~ ]]) with
532
+ # no jq/node fork; everything else takes the full parse below. `read -d ''`
533
+ # succeeds only on reaching a NUL byte, i.e. before the end of the file, so a
534
+ # text it cut short is parsed in full too. An unreadable file reads empty
535
+ # and fails open, as the parser would.
536
+ local _qg_re='"(memory|curation|learning|decisions)"[[:space:]]*:[[:space:]]*false'
199
537
  IFS= read -r -d '' _qg_text 2>/dev/null < "$_qg_manifest" && _qg_parse="true"
200
538
  case "$_qg_text" in *'\u'*) _qg_parse="true" ;; esac
201
539
  if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
@@ -216,7 +554,7 @@ queue_read_gates() {
216
554
  # manifest too, read only when the fast path above flagged it (an unflagged
217
555
  # manifest is both-on), so a false in all three layers still costs one fork.
218
556
  # jq cannot see duplicate keys, so this path is node whatever _HAS_JQ says.
219
- local _qg_scripts="${BASH_SOURCE[0]%/*}/.." _qg_manifest_arg=""
557
+ local _qg_manifest_arg=""
220
558
  [ "$_qg_parse" = "true" ] && _qg_manifest_arg="$_qg_manifest"
221
559
  _qg_fields=$(node -e "
222
560
  const S = require(require('path').resolve(process.argv[1], 'resolve-settings.cjs'));
@@ -225,7 +563,7 @@ queue_read_gates() {
225
563
  try { m = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8')); } catch (_) { m = undefined; }
226
564
  }
227
565
  const w = S.foldSettings(Object.assign({ manifest: m }, S.readRepoLayers(process.argv[3]))).switches;
228
- process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.learning.on));
566
+ process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.curation.on));
229
567
  " -- "$_qg_scripts" "$_qg_manifest_arg" "$_qg_root" 2>/dev/null) || _qg_fields=""
230
568
  fi
231
569
 
@@ -237,20 +575,24 @@ queue_read_gates() {
237
575
  # `try` covers a manifest whose top level or `features` is not an object; a
238
576
  # parse failure empties the output, which reads as "not switched off". The
239
577
  # comma produces two raw-output lines (newline-separated) in one jq process.
578
+ # Curation (#529): off when a boolean curation or learning is false; else,
579
+ # when neither is a boolean, off when decisions is false (isMachineFeatureOn).
240
580
  _qg_fields=$(jq -r '
241
581
  (if (try .features.memory catch null) == false then "false" else "true" end),
242
- (if (try (.features | if (.learning | type) == "boolean" then .learning else .decisions end) catch null) == false
582
+ (if (try (.features
583
+ | ([.curation, .learning] | map(select(type == "boolean"))) as $c
584
+ | if ($c | length) > 0 then any($c[]; . == false) else .decisions == false end
585
+ ) catch false)
243
586
  then "false" else "true" end)
244
587
  ' "$_qg_manifest" 2>/dev/null) || _qg_fields=""
245
588
  else
589
+ # No jq: the fold is the resolver's own machineSwitchOn (resolve-settings.cjs),
590
+ # so the machine rule is not copied here. One node fork, as for jq's one process.
246
591
  _qg_fields=$(node -e "
592
+ const S = require(require('path').resolve(process.argv[2], 'resolve-settings.cjs'));
247
593
  const m = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
248
- const f = m !== null && typeof m === 'object' ? m.features : undefined;
249
- const isObj = f !== null && typeof f === 'object';
250
- const on = (k) => !(isObj && f[k] === false);
251
- const learnKey = isObj && typeof f.learning !== 'boolean' ? 'decisions' : 'learning';
252
- process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
253
- " -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
594
+ process.stdout.write(String(S.machineSwitchOn(m, 'memory')) + String.fromCharCode(10) + String(S.machineSwitchOn(m, 'curation')));
595
+ " -- "$_qg_manifest" "$_qg_scripts" 2>/dev/null) || _qg_fields=""
254
596
  fi
255
597
  fi
256
598
 
@@ -258,7 +600,7 @@ queue_read_gates() {
258
600
  case "$_qg_fields" in
259
601
  true$'\n'true|true$'\n'false|false$'\n'true|false$'\n'false)
260
602
  _QG_MEMORY="${_qg_fields%%$'\n'*}"
261
- _QG_LEARNING="${_qg_fields#*$'\n'}"
603
+ _QG_CURATION="${_qg_fields#*$'\n'}"
262
604
  ;;
263
605
  esac
264
606