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
@@ -0,0 +1,452 @@
1
+ // src/assets/scripts/hooks/lib/tracked-areas.cjs
2
+ //
3
+ // Does git track any entry under one of devflow's data folders at a root? One
4
+ // `git ls-files` call over a fixed set of pathspecs answers `tracked`, `untracked` or
5
+ // `unknown`, for one area at a time: `curation` (the curation folder, the old ledger
6
+ // folder and the backup folders), `memory` (the memory folder) or `both` (the union of the
7
+ // two, one call, for a caller that serves both areas at one root). The one-time ledger move
8
+ // and the backup sweep ask it for the curation area before they lock, read or rename
9
+ // anything: a folder that git tracks is the repository's content, so they leave it alone.
10
+ //
11
+ // DESIGN: a leaf. It loads node built-ins and project-paths.cjs (which loads node
12
+ // built-ins only) and requires neither the store nor the move module (the move requires
13
+ // the store; the store never requires the move), so either of them, and a check for
14
+ // another folder, can use it. No function here prints or exits, and nothing throws:
15
+ // every failure is the answer `unknown`, with a cause from the closed set UNKNOWN_CAUSES.
16
+ //
17
+ // The folder names come from project-paths.cjs, which owns them: AREA_FOLDERS lists the
18
+ // curation area's data folders, the backup prefix names the folders the move leaves
19
+ // behind, and the memory folder is the one getMemoryDir names. To cover another folder of
20
+ // an area, add it to that area's pathspecs.
21
+ //
22
+ // The decision is the repository's index, never the disk. A root with no `.git` entry
23
+ // (a file or a folder) is not a git working tree and gets no git call. A root with one
24
+ // gets exactly one call, and every failure of that call (git missing, a timeout, a
25
+ // non-zero exit with nothing listed) is `unknown`: the caller cannot tell what git
26
+ // tracks and must change nothing. git's error text is never read.
27
+ //
28
+ // trackedAreaCall splits the check in two: it decides whether a call is due and, when it
29
+ // is, describes it (working directory, argv, environment); a caller that runs git in
30
+ // another way (the TypeScript seam's bounded async runner, the settings resolver's
31
+ // injected exec) executes that description and hands the result to judgeTrackedAreas, so
32
+ // every runner gives the answer this leaf gives.
33
+ //
34
+ // COPIES: the leaf is not the only place that holds some of these lists. The shell twin in
35
+ // the capture gate file (hooks/queue-append) keeps its own copy of the pathspecs
36
+ // (`_qta_specs`) and of the list of git variables it removes (the `unset` line of
37
+ // `_qta_probe`), because it runs git without node. Two folder-to-area maps restate which
38
+ // folder belongs to which area: `_qar_area_of` in that file and `projectDataAreaOf` in
39
+ // src/cli/commands/uninstall.ts. Each copy is held equal to the lists here by
40
+ // tests/decisions/tracked-area-parity.test.ts, so a change to a list here fails that test
41
+ // until every copy follows it.
42
+
43
+ 'use strict';
44
+
45
+ const childProcess = require('child_process');
46
+ const fs = require('fs');
47
+ const path = require('path');
48
+
49
+ const { LEGACY_NAMES, CURATION_DIR_NAME, getMemoryDir } = require('./project-paths.cjs');
50
+
51
+ /** The curation data folders under `.devflow` that the check covers, by the names the path helpers hold. */
52
+ const AREA_FOLDERS = Object.freeze([LEGACY_NAMES.folder, CURATION_DIR_NAME]);
53
+
54
+ /** The memory folder under `.devflow`, by the name getMemoryDir gives it. */
55
+ const MEMORY_FOLDER = path.basename(getMemoryDir(path.sep));
56
+
57
+ /**
58
+ * The spellings a case-folding file system opens as `.devflow` beyond ASCII letter case.
59
+ * git's `icase` folds ASCII letters only, but APFS (the macOS default) compares names by
60
+ * full Unicode case folding, so it also opens `.devflow` (U+FB02 folds to "fl"). The code
61
+ * is written with an escape, so the code point is named.
62
+ */
63
+ const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
64
+
65
+ /**
66
+ * The pathspecs of the curation area, each matched without regard to ASCII letter case,
67
+ * whatever the file system or `core.ignorecase`, and each spelled every way
68
+ * DEVFLOW_SPELLINGS lists:
69
+ * - each folder of AREA_FOLDERS, as a literal path: an index entry at the folder, or
70
+ * anywhere below it, matches
71
+ * - an entry named like a backup folder (`<backup prefix>*`), and every entry below one
72
+ * (`<backup prefix>*` + `/**`): the move leaves its old folder there, and the sweep
73
+ * merges the queue files in it into the new folder. The prefix has no glob character.
74
+ * - an index entry named `.devflow` ITSELF, which git records only for a symlink, a
75
+ * gitlink (submodule) or a plain file, never for a directory: the character class
76
+ * keeps `glob` from matching as a leading directory, so the many files under a
77
+ * normal `.devflow/` are not listed. Committed content then stands at the path where
78
+ * the folders are opened.
79
+ * The ledger move and the backup sweep ask for this set, and its order and spelling stay
80
+ * as they are.
81
+ */
82
+ const TRACKED_AREA_PATHSPECS = Object.freeze([
83
+ ...DEVFLOW_SPELLINGS.flatMap(dir => [
84
+ ...AREA_FOLDERS.map(area => ':(icase,literal)' + dir + '/' + area),
85
+ ':(icase,glob)' + dir + '/' + LEGACY_NAMES.backupPrefix + '*',
86
+ ':(icase,glob)' + dir + '/' + LEGACY_NAMES.backupPrefix + '*/**',
87
+ ]),
88
+ ...DEVFLOW_SPELLINGS.map(dir => ':(icase,glob)[.]' + dir.slice(1)),
89
+ ]);
90
+
91
+ /**
92
+ * The pathspecs of the memory area, matched and spelled as the curation area's are: the
93
+ * memory folder as a literal path (an entry at the folder, or anywhere below it), and the
94
+ * `.devflow` entry itself.
95
+ */
96
+ const MEMORY_AREA_PATHSPECS = Object.freeze([
97
+ ...DEVFLOW_SPELLINGS.map(dir => ':(icase,literal)' + dir + '/' + MEMORY_FOLDER),
98
+ ...DEVFLOW_SPELLINGS.map(dir => ':(icase,glob)[.]' + dir.slice(1)),
99
+ ]);
100
+
101
+ /**
102
+ * The pathspecs of the two areas together: the curation set, then the memory set, each
103
+ * pathspec once (the `.devflow` entry forms stand in both sets). One call over this set
104
+ * answers `tracked` when git tracks anything either area covers.
105
+ */
106
+ const BOTH_AREAS_PATHSPECS = Object.freeze([...new Set([...TRACKED_AREA_PATHSPECS, ...MEMORY_AREA_PATHSPECS])]);
107
+
108
+ /** The pathspecs of each area, by the area's name: the closed set of areas the check knows. */
109
+ const AREA_PATHSPECS = Object.freeze({
110
+ curation: TRACKED_AREA_PATHSPECS,
111
+ memory: MEMORY_AREA_PATHSPECS,
112
+ both: BOTH_AREAS_PATHSPECS,
113
+ });
114
+
115
+ /**
116
+ * Why a check answered `unknown`, as one fixed word. The words come from this list
117
+ * alone: they name a spawn error code, a time out, a signal, an exit status or the step
118
+ * that failed, and never carry git's own text or a path.
119
+ */
120
+ const UNKNOWN_CAUSES = Object.freeze({
121
+ rootNotAbsolute: 'root-not-absolute',
122
+ gitEntryUnreadable: 'git-entry-unreadable',
123
+ ceilingUnusable: 'ceiling-unusable',
124
+ checkFailed: 'check-failed',
125
+ gitNotFound: 'git-not-found',
126
+ gitTimedOut: 'git-timed-out',
127
+ gitStopped: 'git-stopped',
128
+ gitExitStatus: 'git-exit-status',
129
+ gitNotRun: 'git-not-run',
130
+ });
131
+
132
+ /** The longest the git call may run (ms). */
133
+ const TRACKED_AREAS_GIT_TIMEOUT_MS = 5000;
134
+
135
+ /**
136
+ * The most output the git call may return, in bytes. A normal answer is empty; a
137
+ * listing past this bound (ENOBUFS) means entries exist, which reads as tracked.
138
+ */
139
+ const TRACKED_AREAS_GIT_MAX_BUFFER = 4096;
140
+
141
+ /**
142
+ * The variables that make git read another repository, another index or other pathspec
143
+ * rules than the root's own. The call runs without them.
144
+ */
145
+ const GIT_REDIRECT_VARIABLES = Object.freeze([
146
+ 'GIT_DIR',
147
+ 'GIT_WORK_TREE',
148
+ 'GIT_INDEX_FILE',
149
+ 'GIT_COMMON_DIR',
150
+ 'GIT_OBJECT_DIRECTORY',
151
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
152
+ 'GIT_NAMESPACE',
153
+ 'GIT_LITERAL_PATHSPECS',
154
+ 'GIT_GLOB_PATHSPECS',
155
+ 'GIT_NOGLOB_PATHSPECS',
156
+ 'GIT_ICASE_PATHSPECS',
157
+ ]);
158
+
159
+ /**
160
+ * A copy of `base` without the redirecting variables (GIT_REDIRECT_VARIABLES): git then
161
+ * reads the repository, index and pathspec rules of the directory it runs in. Nothing else
162
+ * changes. For a caller that runs git for its own purpose and needs only the scrub.
163
+ *
164
+ * @param {NodeJS.ProcessEnv} base
165
+ * @returns {NodeJS.ProcessEnv}
166
+ */
167
+ function gitEnvWithoutRedirects(base) {
168
+ const env = { ...base };
169
+ for (const name of GIT_REDIRECT_VARIABLES) delete env[name];
170
+ return env;
171
+ }
172
+
173
+ /**
174
+ * The environment of the git call: the process environment without the redirecting
175
+ * variables, with a fixed locale, no optional lock and a ceiling directory. Answers a new
176
+ * object. The ceiling is the folder that holds the root: git reads the root's own `.git`
177
+ * and never looks in a folder above it, so an empty or broken `.git` at the root is not
178
+ * answered from a work tree around the project.
179
+ *
180
+ * @param {NodeJS.ProcessEnv} base
181
+ * @param {string} ceiling - an absolute path with no list separator in it
182
+ * @returns {NodeJS.ProcessEnv}
183
+ */
184
+ function trackedAreasGitEnv(base, ceiling) {
185
+ const env = gitEnvWithoutRedirects(base);
186
+ env.LC_ALL = 'C';
187
+ env.GIT_OPTIONAL_LOCKS = '0';
188
+ env.GIT_CEILING_DIRECTORIES = ceiling;
189
+ return env;
190
+ }
191
+
192
+ /**
193
+ * The answer of one `git ls-files` call, from the result `spawnSync` gave, with the
194
+ * cause when the answer is `unknown` (null otherwise):
195
+ * tracked any output byte, whatever the exit status or error (the call listed an
196
+ * entry), and an output past the buffer cap (ENOBUFS)
197
+ * untracked exit 0 with nothing listed
198
+ * unknown every other result, with the cause: git-not-found (spawn error ENOENT),
199
+ * git-timed-out (ETIMEDOUT), git-not-run (another spawn error, or a result
200
+ * of the wrong shape), git-stopped (a signal ended it), git-exit-status (a
201
+ * non-zero exit with nothing listed)
202
+ * Pure: it reads only the fields `error.code`, `status`, `signal` and `stdout`.
203
+ *
204
+ * @param {unknown} result
205
+ * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
206
+ */
207
+ function judgeTrackedAreas(result) {
208
+ if (result === null || typeof result !== 'object') return unknownBecause(UNKNOWN_CAUSES.gitNotRun);
209
+ const { error, status, signal, stdout } = /** @type {{ error?: { code?: unknown }, status?: unknown, signal?: unknown, stdout?: unknown }} */ (result);
210
+ if (error && error.code === 'ENOBUFS') return { answer: 'tracked', cause: null };
211
+ const listed = (typeof stdout === 'string' || Buffer.isBuffer(stdout)) && stdout.length > 0;
212
+ if (listed) return { answer: 'tracked', cause: null };
213
+ if (error) {
214
+ if (error.code === 'ENOENT') return unknownBecause(UNKNOWN_CAUSES.gitNotFound);
215
+ return unknownBecause(error.code === 'ETIMEDOUT' ? UNKNOWN_CAUSES.gitTimedOut : UNKNOWN_CAUSES.gitNotRun);
216
+ }
217
+ if (status === 0) return { answer: 'untracked', cause: null };
218
+ if (typeof status === 'number') return unknownBecause(UNKNOWN_CAUSES.gitExitStatus);
219
+ return unknownBecause(typeof signal === 'string' ? UNKNOWN_CAUSES.gitStopped : UNKNOWN_CAUSES.gitNotRun);
220
+ }
221
+
222
+ /**
223
+ * @param {string} cause - a value of UNKNOWN_CAUSES
224
+ * @returns {{ answer: 'unknown', cause: string }}
225
+ */
226
+ function unknownBecause(cause) {
227
+ return { answer: 'unknown', cause };
228
+ }
229
+
230
+ /**
231
+ * The answer of {@link judgeTrackedAreas}, without its cause.
232
+ *
233
+ * @param {unknown} result
234
+ * @returns {'tracked' | 'untracked' | 'unknown'}
235
+ */
236
+ function classifyTrackedAreas(result) {
237
+ return judgeTrackedAreas(result).answer;
238
+ }
239
+
240
+ /**
241
+ * Whether anything (a file, a folder, a link) stands at `target`: true, false, or null
242
+ * when the probe failed for a reason other than absence.
243
+ *
244
+ * @param {string} target
245
+ * @returns {boolean | null}
246
+ */
247
+ function entryExists(target) {
248
+ try {
249
+ fs.lstatSync(target);
250
+ return true;
251
+ } catch (err) {
252
+ return err && (err.code === 'ENOENT' || err.code === 'ENOTDIR') ? false : null;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * The pathspecs of `area`, or null when the leaf does not know the area. Only the own
258
+ * names of AREA_PATHSPECS count: a name from the object prototype is no area.
259
+ *
260
+ * @param {unknown} area
261
+ * @returns {readonly string[] | null}
262
+ */
263
+ function pathspecsOf(area) {
264
+ if (typeof area !== 'string' || !Object.prototype.hasOwnProperty.call(AREA_PATHSPECS, area)) return null;
265
+ return AREA_PATHSPECS[/** @type {'curation' | 'memory' | 'both'} */ (area)];
266
+ }
267
+
268
+ /**
269
+ * Whether a git call is due for `area` at `root`, and the call when it is. Makes no
270
+ * call and starts no process: it reads only the `.git` entry at the root and the root's
271
+ * real path. Answers one of:
272
+ *
273
+ * {answer, cause} no call is due: `untracked` (no `.git` entry at the root), or
274
+ * `unknown` with a cause from UNKNOWN_CAUSES (an area the leaf does
275
+ * not know, a root that is not an absolute path string, a `.git`
276
+ * entry that could not be probed, a folder above the root that
277
+ * holds the list separator, or a failure)
278
+ * {cwd, argv, env} the call: run `git` with `argv` and `env` in `cwd` with no shell,
279
+ * bound its time (TRACKED_AREAS_GIT_TIMEOUT_MS) and its output
280
+ * (TRACKED_AREAS_GIT_MAX_BUFFER), and give the result to
281
+ * judgeTrackedAreas
282
+ *
283
+ * The call is `git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- <paths>`.
284
+ * `--no-literal-pathspecs` leads the argv, so a forced literal-pathspec setting cannot
285
+ * turn the magic off and hide an entry. Reading the index makes git run the command a
286
+ * repository's config names in `core.fsmonitor`, so the call turns that off for itself and
287
+ * stays a pure read. The folder above the root's real path is a ceiling directory
288
+ * (trackedAreasGitEnv); a parent path that holds the list separator can not be named as
289
+ * one, and answers `unknown`. Each answer is a new object.
290
+ *
291
+ * @param {unknown} root - an absolute path
292
+ * @param {unknown} area - 'curation', 'memory' or 'both'
293
+ * @param {NodeJS.ProcessEnv} [baseEnv] - the environment the call's own derives from (default: the process environment)
294
+ * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null } | { cwd: string, argv: string[], env: NodeJS.ProcessEnv }}
295
+ */
296
+ function trackedAreaCall(root, area, baseEnv = process.env) {
297
+ const pathspecs = pathspecsOf(area);
298
+ if (typeof root !== 'string' || !path.isAbsolute(root)) return unknownBecause(UNKNOWN_CAUSES.rootNotAbsolute);
299
+ if (pathspecs === null) return unknownBecause(UNKNOWN_CAUSES.checkFailed);
300
+ try {
301
+ const gitEntry = entryExists(path.join(root, '.git'));
302
+ if (gitEntry === null) return unknownBecause(UNKNOWN_CAUSES.gitEntryUnreadable);
303
+ if (gitEntry === false) return { answer: 'untracked', cause: null };
304
+ const ceiling = path.dirname(fs.realpathSync(root));
305
+ if (ceiling.includes(path.delimiter)) return unknownBecause(UNKNOWN_CAUSES.ceilingUnusable);
306
+ return {
307
+ cwd: root,
308
+ argv: ['--no-literal-pathspecs', '-c', 'core.fsmonitor=false', 'ls-files', '-z', '--', ...pathspecs],
309
+ env: trackedAreasGitEnv(baseEnv, ceiling),
310
+ };
311
+ } catch (_) {
312
+ return unknownBecause(UNKNOWN_CAUSES.checkFailed);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * The time bound of one check: `options.timeoutMs` when it is a positive finite number of
318
+ * milliseconds, otherwise TRACKED_AREAS_GIT_TIMEOUT_MS. A zero would mean no bound at all
319
+ * to the spawn call, so it is never passed on.
320
+ *
321
+ * @param {unknown} options
322
+ * @returns {number}
323
+ */
324
+ function timeoutOf(options) {
325
+ const timeoutMs = options !== null && typeof options === 'object' ? /** @type {{ timeoutMs?: unknown }} */ (options).timeoutMs : undefined;
326
+ return typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : TRACKED_AREAS_GIT_TIMEOUT_MS;
327
+ }
328
+
329
+ /**
330
+ * Whether git tracks any entry under the folders of `area` ('curation', 'memory' or 'both',
331
+ * the union of the two) at `root`, or an entry named `.devflow` itself (a link, a gitlink or
332
+ * a file). The index
333
+ * decides, not the disk, and a staged entry counts. The answer carries the cause (a value
334
+ * of UNKNOWN_CAUSES) when it is `unknown`, and null otherwise.
335
+ *
336
+ * untracked no `.git` entry at the root (no git call is made), or git listed nothing
337
+ * tracked git listed an entry
338
+ * unknown a `.git` entry stands and git did not answer (see judgeTrackedAreas),
339
+ * the `.git` entry could not be probed, the root is not an absolute path
340
+ * string, or the area is not one the leaf knows (no git call is made)
341
+ *
342
+ * The call is the one trackedAreaCall describes, run with a time bound (`options.timeoutMs`,
343
+ * default TRACKED_AREAS_GIT_TIMEOUT_MS) and an output bound.
344
+ *
345
+ * @param {unknown} root - an absolute path
346
+ * @param {unknown} area - 'curation', 'memory' or 'both'
347
+ * @param {{ timeoutMs?: number }} [options]
348
+ * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
349
+ */
350
+ function inspectTrackedArea(root, area, options) {
351
+ const call = trackedAreaCall(root, area);
352
+ if (!('argv' in call)) return call;
353
+ try {
354
+ const result = childProcess.spawnSync('git', call.argv, {
355
+ cwd: call.cwd,
356
+ env: call.env,
357
+ stdio: ['ignore', 'pipe', 'ignore'],
358
+ timeout: timeoutOf(options),
359
+ maxBuffer: TRACKED_AREAS_GIT_MAX_BUFFER,
360
+ windowsHide: true,
361
+ shell: false,
362
+ });
363
+ return judgeTrackedAreas(result);
364
+ } catch (_) {
365
+ return unknownBecause(UNKNOWN_CAUSES.checkFailed);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * The answer of {@link inspectTrackedArea}, without its cause.
371
+ *
372
+ * @param {unknown} root - an absolute path
373
+ * @param {unknown} area - 'curation', 'memory' or 'both'
374
+ * @returns {'tracked' | 'untracked' | 'unknown'}
375
+ */
376
+ function checkTrackedArea(root, area) {
377
+ return inspectTrackedArea(root, area).answer;
378
+ }
379
+
380
+ /** The most answers the per-process memo of inspectTrackedAreaOnce holds. */
381
+ const ANSWER_MEMO_MAX_ENTRIES = 64;
382
+
383
+ /** The answers inspectTrackedAreaOnce has given in this process, by area and root, oldest first. */
384
+ const answerMemo = new Map();
385
+
386
+ /**
387
+ * {@link inspectTrackedArea}, asked once per root and area for the life of the process: a
388
+ * later ask for the same root and area gets the first answer and makes no git call, even if
389
+ * the project changed meanwhile. For a one-shot process (json-helper, the render check)
390
+ * whose steps each ask before they read or write: together they cost one git call. A
391
+ * long-lived caller, or one that changes what git tracks, asks inspectTrackedArea. The
392
+ * memo holds at most ANSWER_MEMO_MAX_ENTRIES answers; past that the oldest goes. Each
393
+ * answer is a new object.
394
+ *
395
+ * @param {unknown} root - an absolute path
396
+ * @param {unknown} area - 'curation', 'memory' or 'both'
397
+ * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
398
+ */
399
+ function inspectTrackedAreaOnce(root, area) {
400
+ const key = typeof root === 'string' && typeof area === 'string' ? area + '\0' + root : null;
401
+ const known = key === null ? undefined : answerMemo.get(key);
402
+ if (known !== undefined) return { answer: known.answer, cause: known.cause };
403
+ const answer = inspectTrackedArea(root, area);
404
+ if (key !== null) {
405
+ if (answerMemo.size >= ANSWER_MEMO_MAX_ENTRIES) answerMemo.delete(answerMemo.keys().next().value);
406
+ answerMemo.set(key, Object.freeze({ answer: answer.answer, cause: answer.cause }));
407
+ }
408
+ return answer;
409
+ }
410
+
411
+ /**
412
+ * Whether git tracks any entry under the curation area's folders at `root`: the check the
413
+ * ledger move, the backup sweep and the drains ask. {@link inspectTrackedArea} for the
414
+ * curation area.
415
+ *
416
+ * @param {unknown} root - an absolute path
417
+ * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
418
+ */
419
+ function inspectTrackedAreas(root) {
420
+ return inspectTrackedArea(root, 'curation');
421
+ }
422
+
423
+ /**
424
+ * The answer of {@link inspectTrackedAreas}, without its cause.
425
+ *
426
+ * @param {unknown} root - an absolute path
427
+ * @returns {'tracked' | 'untracked' | 'unknown'}
428
+ */
429
+ function checkTrackedAreas(root) {
430
+ return inspectTrackedAreas(root).answer;
431
+ }
432
+
433
+ module.exports = {
434
+ TRACKED_AREA_PATHSPECS,
435
+ MEMORY_AREA_PATHSPECS,
436
+ AREA_PATHSPECS,
437
+ DEVFLOW_SPELLINGS,
438
+ GIT_REDIRECT_VARIABLES,
439
+ TRACKED_AREAS_GIT_TIMEOUT_MS,
440
+ TRACKED_AREAS_GIT_MAX_BUFFER,
441
+ ANSWER_MEMO_MAX_ENTRIES,
442
+ UNKNOWN_CAUSES,
443
+ gitEnvWithoutRedirects,
444
+ judgeTrackedAreas,
445
+ classifyTrackedAreas,
446
+ trackedAreaCall,
447
+ inspectTrackedArea,
448
+ inspectTrackedAreaOnce,
449
+ checkTrackedArea,
450
+ inspectTrackedAreas,
451
+ checkTrackedAreas,
452
+ };
@@ -16,6 +16,11 @@
16
16
  # memory_notice_* the one-time notice: the worker writes it, a session start reads it
17
17
  # memory_denied_seen the count of denied runs that raises the `denied` notice
18
18
  #
19
+ # The worker writes a notice, or counts a denied run, only while the memory area is known
20
+ # to be free of files that git tracks: DF_TRACKED_AREA_MEMORY (queue_tracked_area, queue-append)
21
+ # must read `untracked` in the calling process. Any other value, an unset one included,
22
+ # is a paused area, and both helpers then write nothing.
23
+ #
19
24
  # Source it after resolve-project-root (it needs df_no_symlink_below and df_file_below)
20
25
  # and, for memory_result_summary, after json-parse. log() is used when the caller has one.
21
26
  #
@@ -241,13 +246,15 @@ memory_result_summary() {
241
246
  # start, once per reason: only where neither the notice nor its `.shown` marker stands,
242
247
  # and where no link sits on the way down from <root>. The builtin tests come first, since
243
248
  # noclobber alone opens a link to a FIFO or a device; noclobber then makes the create
244
- # itself exclusive. Content: the reason. Best effort: a refusal leaves nothing.
249
+ # itself exclusive. Content: the reason. Best effort: a refusal leaves nothing. Nothing is
250
+ # written unless DF_TRACKED_AREA_MEMORY reads `untracked` in this process.
245
251
  memory_notice_write() {
246
252
  local _dir="${1:-}" _root="${2:-}" _reason="${3:-}" _file
247
253
  case " $MEMORY_NOTICE_REASONS " in
248
254
  *" $_reason "*) ;;
249
255
  *) return 0 ;;
250
256
  esac
257
+ [ "${DF_TRACKED_AREA_MEMORY:-}" = "untracked" ] || return 0
251
258
  [ -d "$_dir" ] || return 0
252
259
  _file="$_dir/.worker-notice-$_reason"
253
260
  if ! path_occupied "$_file" && ! path_occupied "$_file.shown" \
@@ -285,9 +292,10 @@ memory_notice_clear() {
285
292
  # worker's write in. The first leaves a marker; a second, with the marker still there,
286
293
  # raises the `denied` notice. A run that swapped the memory in clears both
287
294
  # (memory_notice_clear denied), so only two denied runs with no successful update between
288
- # them count.
295
+ # them count. No marker is left unless DF_TRACKED_AREA_MEMORY reads `untracked` in this process.
289
296
  memory_denied_seen() {
290
297
  local _dir="${1:-}" _root="${2:-}" _mark
298
+ [ "${DF_TRACKED_AREA_MEMORY:-}" = "untracked" ] || return 0
291
299
  [ -d "$_dir" ] || return 0
292
300
  _mark="$_dir/.worker-denied-once"
293
301
  if path_occupied "$_mark"; then
@@ -43,7 +43,7 @@ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
43
43
 
44
44
  # Memory gate: the machine switch, narrowed by this checkout's project.json /
45
45
  # config.json `features.memory`, gates this hook entirely -- read by the helper
46
- # every memory/learning gate shares (D-FEATURES-NARROW-ONLY, see queue-append).
46
+ # every memory/curation gate shares (D-FEATURES-NARROW-ONLY, see queue-append).
47
47
  source "$SCRIPT_DIR/queue-append" || { echo "memory-worker: failed to source queue-append" >&2; exit 1; }
48
48
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
49
49
  MEMORY_ENABLED="$_QG_MEMORY"
@@ -104,6 +104,17 @@ if [ ! -x "$UPDATER" ]; then
104
104
  exit 0
105
105
  fi
106
106
 
107
+ # Memory area check: while git tracks files in the memory folder of this checkout, or a
108
+ # `.git` entry stands and git does not answer, the memory area is paused, and this hook
109
+ # stamps no trigger and spawns no worker. It asks once, in git mode (queue_tracked_area),
110
+ # at the project root, after the throttle so a throttled Stop event costs nothing.
111
+ queue_tracked_area "$PROJECT_ROOT" memory git
112
+ if [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
113
+ log "Memory paused: git tracks files in the memory folder, or did not answer; trigger not stamped, worker not spawned"
114
+ dbg "EXIT: memory area paused"
115
+ exit 0
116
+ fi
117
+
107
118
  dbg "Throttle passed: TRIGGER_AGE=${TRIGGER_AGE}s >= 120s — spawning worker"
108
119
 
109
120
  # Touch trigger BEFORE spawning (prevents concurrent Stop hooks from double-spawning)
@@ -54,7 +54,7 @@ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
54
54
  source "$SCRIPT_DIR/hook-log-init" "pre-compact-memory"
55
55
 
56
56
  # Memory gate: the machine switch, narrowed by this checkout's project.json /
57
- # config.json `features.memory`, read by the helper every memory/learning gate
57
+ # config.json `features.memory`, read by the helper every memory/curation gate
58
58
  # shares (D-FEATURES-NARROW-ONLY, see queue-append).
59
59
  source "$SCRIPT_DIR/queue-append" || { echo "pre-compact-memory: failed to source queue-append" >&2; exit 1; }
60
60
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
@@ -63,6 +63,17 @@ if [ "$_QG_MEMORY" != "true" ]; then
63
63
  exit 0
64
64
  fi
65
65
 
66
+ # Memory area check: while git tracks files in the memory folder of this checkout, or a
67
+ # `.git` entry stands and git does not answer, the memory area is paused, and this hook
68
+ # makes no folder, writes no backup and bootstraps no working memory. The check asks once,
69
+ # in node mode, so a git that hangs is cut (queue_tracked_area), at the project root.
70
+ queue_tracked_area "$PROJECT_ROOT" memory node
71
+ if [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
72
+ log "Memory paused: git tracks files in the memory folder, or did not answer; no backup or working memory written"
73
+ dbg "EXIT: memory area paused"
74
+ exit 0
75
+ fi
76
+
66
77
  # Auto-create .devflow/ and ensure .gitignore entries (idempotent after first run)
67
78
  source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
68
79
 
@@ -112,27 +123,18 @@ if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
112
123
  dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
113
124
  fi
114
125
 
115
- # Snapshot current WORKING-MEMORY.md (preserves session context through compaction).
116
- # The snapshot goes into the backup the next session start injects, so a working
117
- # memory a symbolic link leads to is treated as absent (D-HOOKS-NO-SYMLINK).
118
- MEMORY_SNAPSHOT=""
119
- if df_file_below "$PROJECT_ROOT" "$MEMORY_DIR/WORKING-MEMORY.md"; then
120
- MEMORY_SNAPSHOT=$(head -c 65536 "$MEMORY_DIR/WORKING-MEMORY.md")
121
- dbg "MEMORY_SNAPSHOT_LENGTH=${#MEMORY_SNAPSHOT}"
122
- fi
123
-
124
126
  # Write backup JSON
125
127
  # D-BACKUP-RENAME: the backup is written whole into a copy beside it, then
126
128
  # renamed over it; it is never truncated and refilled in place. Reason:
127
- # session-start-memory reads it twice, and a read landing between a truncate
128
- # and the write that refills it would meet a partial file; a rename hands every
129
- # read either the previous backup or the new one. SEC-2: the copy is created
129
+ # a read landing between a truncate and the write that refills it would meet a
130
+ # partial file; a rename hands every read either the previous backup or the new
131
+ # one. SEC-2: the copy is created
130
132
  # under umask 077, and only where nothing stands at its name: builtin tests come
131
133
  # first, so an entry already there, a link to a FIFO or a device included, is
132
134
  # never opened (noclobber alone would open one, as no regular file stands there),
133
135
  # and noclobber then makes the create itself exclusive. The rename gives the
134
- # backup the copy's inode and mode, so the backup, which holds the working
135
- # memory, is 0600 whatever the caller's umask (as in queue-append). A write or
136
+ # backup the copy's inode and mode, so the Git-state backup is 0600 whatever
137
+ # the caller's umask (as in queue-append). A write or
136
138
  # rename that fails removes whatever holds the copy's name and keeps the
137
139
  # previous backup; the bootstrap below still runs.
138
140
  #
@@ -157,7 +159,6 @@ elif [ ! -e "$BACKUP_TMP" ] && [ ! -L "$BACKUP_TMP" ] && (umask 077 && set -o no
157
159
  --arg status "$GIT_STATUS" \
158
160
  --arg log "$GIT_LOG" \
159
161
  --arg diff "$GIT_DIFF_STAT" \
160
- --arg memory "$MEMORY_SNAPSHOT" \
161
162
  > "$BACKUP_TMP") && mv "$BACKUP_TMP" "$BACKUP_FILE"; then
162
163
  log "Wrote backup: $BACKUP_FILE"
163
164
  dbg "Wrote backup: $BACKUP_FILE"