devflow-kit 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +479 -1
  2. package/README.md +15 -15
  3. package/dist/agents/code.md +6 -8
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +7 -8
  6. package/dist/agents/git.md +3 -3
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +2 -4
  9. package/dist/agents/review.md +4 -4
  10. package/dist/agents/skim.md +2 -2
  11. package/dist/agents/triage.md +9 -2
  12. package/dist/cli/commands/capture.js +1 -1
  13. package/dist/cli/commands/curation-move-io.js +102 -0
  14. package/dist/cli/commands/curation-move-step.js +374 -0
  15. package/dist/cli/commands/curation.js +808 -0
  16. package/dist/cli/commands/gate-level-prompts.js +428 -0
  17. package/dist/cli/commands/gates.js +35 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +244 -57
  20. package/dist/cli/commands/memory.js +60 -13
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +157 -17
  23. package/dist/cli.js +2 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +40 -44
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +26 -25
  28. package/dist/commands/dynamic-plan.md +5 -5
  29. package/dist/commands/dynamic-profile.md +5 -5
  30. package/dist/commands/dynamic-tickets.md +5 -5
  31. package/dist/commands/explore.md +6 -6
  32. package/dist/commands/implement.md +53 -82
  33. package/dist/commands/plan.md +6 -6
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +39 -41
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/area-pause-notice.js +44 -0
  39. package/dist/core/assets.js +20 -20
  40. package/dist/core/curate-tuning-config.js +5 -6
  41. package/dist/core/curation-move-notice.js +84 -0
  42. package/dist/core/curation-move-skip.js +35 -0
  43. package/dist/core/curation-move.js +35 -0
  44. package/dist/core/curation-queue-cleanup.js +135 -0
  45. package/dist/core/curation-store.js +99 -0
  46. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  47. package/dist/core/echo-value.js +11 -5
  48. package/dist/core/evidence-policy.js +11 -9
  49. package/dist/core/feature-switch.js +95 -19
  50. package/dist/core/fs-atomic.js +1 -1
  51. package/dist/core/gate-level.js +10 -2
  52. package/dist/core/git.js +16 -14
  53. package/dist/core/ledger-root.js +82 -22
  54. package/dist/core/linked-path.js +4 -4
  55. package/dist/core/manifest-lock.js +238 -0
  56. package/dist/core/manifest.js +58 -14
  57. package/dist/core/migrations.js +1 -13
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +19 -19
  60. package/dist/core/pool.js +26 -0
  61. package/dist/core/project-paths.js +103 -34
  62. package/dist/core/queue-drain.js +2 -2
  63. package/dist/core/tracked-areas.js +159 -0
  64. package/dist/core/tracker.js +1 -1
  65. package/dist/{learning-off → curation-off}/agents/code.md +6 -8
  66. package/dist/{learning-off → curation-off}/agents/fix.md +7 -8
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  68. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +8 -1
  70. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  71. package/dist/{learning-off → curation-off}/commands/code-review.md +37 -41
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +23 -22
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +50 -79
  76. package/dist/{learning-off → curation-off}/commands/plan.md +2 -2
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +36 -38
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +63 -17
  80. package/dist/hud/config.js +1 -1
  81. package/dist/hud/index.js +6 -6
  82. package/dist/hud/render.js +3 -3
  83. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  84. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  85. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  86. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  87. package/dist/skills/git/references/publication-gate.md +6 -7
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  89. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  90. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  91. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  92. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  94. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  95. package/dist/targets/claude-code/installer.js +15 -15
  96. package/dist/targets/claude-code/language-stamp.js +5 -5
  97. package/dist/targets/claude-code/post-install.js +2 -2
  98. package/dist/targets/claude-code/tracker-servers.js +1 -1
  99. package/package.json +3 -2
  100. package/src/assets/agents/_partials/_code.mds +11 -11
  101. package/src/assets/agents/code.mds +6 -6
  102. package/src/assets/agents/curate.md +8 -8
  103. package/src/assets/agents/debug.mds +6 -6
  104. package/src/assets/agents/design.mds +18 -18
  105. package/src/assets/agents/evaluate.md +6 -12
  106. package/src/assets/agents/fix.mds +7 -6
  107. package/src/assets/agents/git.mds +3 -3
  108. package/src/assets/agents/harden.mds +7 -7
  109. package/src/assets/agents/knowledge.mds +23 -23
  110. package/src/assets/agents/refactor.mds +2 -2
  111. package/src/assets/agents/research.mds +15 -15
  112. package/src/assets/agents/review.mds +43 -43
  113. package/src/assets/agents/skim.mds +11 -11
  114. package/src/assets/agents/triage.mds +29 -22
  115. package/src/assets/agents/validate.md +30 -50
  116. package/src/assets/commands/_partials/_decisions.mds +22 -20
  117. package/src/assets/commands/_partials/_engine.mds +8 -8
  118. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  119. package/src/assets/commands/_partials/_preamble.mds +4 -4
  120. package/src/assets/commands/_partials/_settings.mds +3 -3
  121. package/src/assets/commands/_partials/_wave.mds +7 -7
  122. package/src/assets/commands/bug-analysis.mds +37 -48
  123. package/src/assets/commands/code-review.mds +53 -57
  124. package/src/assets/commands/debug.mds +14 -14
  125. package/src/assets/commands/dynamic-build.mds +38 -37
  126. package/src/assets/commands/dynamic-plan.mds +22 -22
  127. package/src/assets/commands/dynamic-profile.mds +2 -2
  128. package/src/assets/commands/dynamic-tickets.mds +8 -8
  129. package/src/assets/commands/explore.mds +20 -20
  130. package/src/assets/commands/implement.mds +77 -98
  131. package/src/assets/commands/plan.mds +27 -27
  132. package/src/assets/commands/release.mds +12 -12
  133. package/src/assets/commands/research.mds +19 -19
  134. package/src/assets/commands/resolve.mds +66 -60
  135. package/src/assets/commands/self-review.mds +20 -20
  136. package/src/assets/mds/git/_ops.mds +9 -2
  137. package/src/assets/mds/git/_ops_pr.mds +21 -11
  138. package/src/assets/mds/git/_ops_steps.mds +20 -4
  139. package/src/assets/mds/git/_pr.mds +21 -18
  140. package/src/assets/mds/git/_references.mds +6 -7
  141. package/src/assets/mds/tracker/_github.mds +1 -4
  142. package/src/assets/mds/tracker/_jira.mds +1 -4
  143. package/src/assets/mds/tracker/_linear.mds +1 -4
  144. package/src/assets/rules/quality.md +1 -0
  145. package/src/assets/scripts/ci-wait.cjs +389 -92
  146. package/src/assets/scripts/hooks/background-memory-update +81 -3
  147. package/src/assets/scripts/hooks/capture-prompt +12 -12
  148. package/src/assets/scripts/hooks/capture-question +11 -11
  149. package/src/assets/scripts/hooks/capture-turn +12 -12
  150. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  151. package/src/assets/scripts/hooks/curation-move +293 -0
  152. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  153. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  154. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  155. package/src/assets/scripts/hooks/git-marker +81 -0
  156. package/src/assets/scripts/hooks/json-helper.cjs +176 -51
  157. package/src/assets/scripts/hooks/json-parse +1 -1
  158. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  159. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +308 -122
  160. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  161. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  162. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  163. package/src/assets/scripts/hooks/lib/render-decisions.cjs +42 -22
  164. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +452 -0
  165. package/src/assets/scripts/hooks/memory-permission +10 -2
  166. package/src/assets/scripts/hooks/memory-worker +12 -1
  167. package/src/assets/scripts/hooks/pre-compact-memory +17 -16
  168. package/src/assets/scripts/hooks/queue-append +444 -102
  169. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  170. package/src/assets/scripts/hooks/session-start-context +198 -72
  171. package/src/assets/scripts/hooks/session-start-memory +42 -43
  172. package/src/assets/scripts/lib/project-config.cjs +12 -5
  173. package/src/assets/scripts/pr-evidence.cjs +5 -2
  174. package/src/assets/scripts/resolve-base.cjs +1043 -0
  175. package/src/assets/scripts/resolve-settings.cjs +283 -45
  176. package/src/assets/scripts/verify-evidence.cjs +307 -75
  177. package/src/assets/skills/agent-references/SKILL.md +11 -2
  178. package/src/assets/skills/agent-references/references/commands/implement-levels.md +29 -0
  179. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  180. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +25 -0
  181. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  182. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  183. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  184. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  185. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  186. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  187. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  188. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  189. package/templates/lint-pack/.lint-pack.conf +86 -0
  190. package/templates/lint-pack/README.md +190 -0
  191. package/templates/lint-pack/lint-pack.sh +905 -0
  192. package/dist/cli/commands/learning.js +0 -554
  193. package/dist/core/learning-queue-cleanup.js +0 -27
  194. package/dist/core/learning-store.js +0 -61
  195. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  196. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  197. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  198. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  199. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  200. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  201. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  202. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  203. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  204. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  205. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -1,19 +1,41 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { writeFileAtomicExclusive } from './fs-atomic.js';
4
+ import { MANIFEST_LOCK_WAIT_MS, withManifestLock } from './manifest-lock.js';
4
5
  function isJsonObject(value) {
5
6
  return typeof value === 'object' && value !== null && !Array.isArray(value);
6
7
  }
8
+ /**
9
+ * The keys a RAW manifest may hold a machine feature under, current names first.
10
+ *
11
+ * `curation` is stored under two current keys, `curation` and `learning`: the
12
+ * feature was renamed, and a manifest written before the rename holds only
13
+ * `learning`. No precedence rule exists between the two (see
14
+ * {@link isMachineFeatureOn}); every write sets both. `memory` and `knowledge`
15
+ * have one current key.
16
+ */
17
+ const CURRENT_KEYS = {
18
+ memory: ['memory'],
19
+ curation: ['curation', 'learning'],
20
+ knowledge: ['knowledge'],
21
+ };
7
22
  /**
8
23
  * The pre-rename key each machine feature was stored under, where one exists:
9
- * `learning` was `decisions` and `knowledge` was `kb`. A manifest no
24
+ * `curation` was `decisions` and `knowledge` was `kb`. A manifest no
10
25
  * command has rewritten since the rename can still hold only the legacy key.
11
26
  * `memory` was never renamed.
12
27
  */
13
28
  const LEGACY_KEYS = {
14
- learning: 'decisions',
29
+ curation: 'decisions',
15
30
  knowledge: 'kb',
16
31
  };
32
+ /**
33
+ * The keys a write sets for `feature`: the feature's own key, and for `curation` its
34
+ * pre-rename copy too (the manifest mirror). Opt-in switches have one key.
35
+ */
36
+ function writtenKeys(feature) {
37
+ return feature === 'behavior' ? [feature] : CURRENT_KEYS[feature];
38
+ }
17
39
  /**
18
40
  * Whether a RAW parsed manifest leaves `feature` switched on. Pure.
19
41
  *
@@ -23,13 +45,23 @@ const LEGACY_KEYS = {
23
45
  * shell hooks, so the CLI's status and the runtime never disagree about the
24
46
  * same file.
25
47
  *
48
+ * The machine rule (issue #529), identical in every reader —
49
+ * this function, resolve-settings.cjs and the shell gates:
50
+ * 1. off when ANY current key (`curation`, `learning`) is a boolean `false`;
51
+ * 2. else on when any current key is a boolean `true`;
52
+ * 3. else, when neither current key is a boolean, the pre-rename key decides
53
+ * if it is a boolean (`decisions` for curation, `kb` for knowledge);
54
+ * 4. else ON.
55
+ * A non-boolean value is ignored for that key only. A disagreeing pair
56
+ * therefore resolves to OFF: false wins, so a switch a user turned off never
57
+ * comes back on because the other spelling still says true.
58
+ *
26
59
  * D-LEARNING-LEGACY-DECISIONS (a sub-decision of D-FEATURES-NARROW-ONLY):
27
- * `learning` is read as `features.learning` when that is a boolean, else the
28
- * legacy `features.decisions` when THAT is a boolean, else ON — readManifest's
29
- * migration precedence exactly. The legacy key is otherwise honoured only once
30
- * some command happens to run readManifest and heal it, so a `decisions: false`
31
- * would keep learning running until then. queue_read_gates applies the same
32
- * precedence.
60
+ * the legacy `features.decisions` is read only when neither current key is a
61
+ * boolean — readManifest's migration precedence exactly. The legacy key is
62
+ * otherwise honoured only once some command happens to run readManifest and
63
+ * heal it, so a `decisions: false` would keep curation running until then.
64
+ * queue_read_gates applies the same precedence.
33
65
  *
34
66
  * D-KNOWLEDGE-LEGACY-KB (the same sub-decision for the other renamed key):
35
67
  * `knowledge` is read as `features.knowledge` when that is a boolean, else the
@@ -51,11 +83,13 @@ export function isMachineFeatureOn(rawManifest, feature) {
51
83
  const features = rawManifest.features;
52
84
  if (!isJsonObject(features))
53
85
  return true;
86
+ const current = CURRENT_KEYS[feature]
87
+ .map((key) => features[key])
88
+ .filter((value) => typeof value === 'boolean');
89
+ if (current.length > 0)
90
+ return !current.includes(false);
54
91
  const legacyKey = LEGACY_KEYS[feature];
55
- const value = legacyKey !== undefined && typeof features[feature] !== 'boolean'
56
- ? features[legacyKey]
57
- : features[feature];
58
- return value !== false;
92
+ return legacyKey === undefined || features[legacyKey] !== false;
59
93
  }
60
94
  /**
61
95
  * Set `feature` in a RAW parsed manifest. Pure — returns a new object and never
@@ -63,7 +97,9 @@ export function isMachineFeatureOn(rawManifest, feature) {
63
97
  * is no `features` record to write into).
64
98
  *
65
99
  * Only `features.<feature>` and `updatedAt` change; every other key is carried
66
- * verbatim. Writing `learning` leaves a legacy `decisions` key in place, and
100
+ * verbatim. Writing `curation` sets BOTH current keys (`curation` and its
101
+ * pre-rename copy `learning`) to the one value, so a disagreeing pair is
102
+ * overwritten, never carried. It leaves a legacy `decisions` key in place, and
67
103
  * writing `knowledge` a legacy `kb` key, inert: the boolean just written wins
68
104
  * over it (D-LEARNING-LEGACY-DECISIONS, D-KNOWLEDGE-LEGACY-KB). Going through
69
105
  * readManifest()/writeManifest() instead would refuse a manifest that reader
@@ -74,21 +110,28 @@ export function isMachineFeatureOn(rawManifest, feature) {
74
110
  export function setMachineFeature(rawManifest, feature, enabled, now) {
75
111
  if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
76
112
  return null;
113
+ const written = {};
114
+ for (const key of writtenKeys(feature))
115
+ written[key] = enabled;
77
116
  return {
78
117
  ...rawManifest,
79
- features: { ...rawManifest.features, [feature]: enabled },
118
+ features: { ...rawManifest.features, ...written },
80
119
  updatedAt: now,
81
120
  };
82
121
  }
83
122
  /**
84
123
  * Whether a RAW parsed manifest already records `feature` as exactly `enabled`:
85
- * `features.<feature>` is that boolean. Pure. Stricter than
124
+ * every key a write sets (`writtenKeys`) is that boolean. Pure. Stricter than
86
125
  * {@link isMachineFeatureOn} on purpose — an absent key, a non-boolean value and
87
- * a legacy key all read as a state without being recorded as it, so writing the
88
- * explicit boolean is still a change.
126
+ * a legacy key all read as a state without being recorded as it, and for
127
+ * `curation` a disagreeing pair, or one key alone, is a copy that disagrees or is
128
+ * missing: not recorded, so the write that follows puts both keys right.
89
129
  */
90
130
  function isMachineFeatureRecorded(rawManifest, feature, enabled) {
91
- return isJsonObject(rawManifest) && isJsonObject(rawManifest.features) && rawManifest.features[feature] === enabled;
131
+ if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
132
+ return false;
133
+ const features = rawManifest.features;
134
+ return writtenKeys(feature).every((key) => features[key] === enabled);
92
135
  }
93
136
  async function readRawManifest(devflowDir) {
94
137
  try {
@@ -136,16 +179,49 @@ export async function readOptInFeature(devflowDir, feature) {
136
179
  * D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
137
180
  * nothing. The only bytes such a write could change are `updatedAt`, and that
138
181
  * field means "the manifest's content last changed", so a no-op must not move
139
- * it. This is the one write point of `devflow memory|learning|knowledge|behavior
182
+ * it. This is the one write point of `devflow memory|curation|knowledge|behavior
140
183
  * --enable/--disable`, so the rule holds for all four at once.
184
+ *
185
+ * The read and the write run under the manifest lock (manifest-lock.ts), so the
186
+ * heal in readManifest cannot write an older snapshot over this write. The wait is
187
+ * bounded, and longer than the stale age, so a lock left by a dead process is broken
188
+ * inside it. A lock that is still not taken after that wait cannot be broken, and
189
+ * the lock then protects nothing: the toggle goes ahead without it rather than refuse
190
+ * an explicit switch. Just before its write the toggle checks that the lock directory is
191
+ * still its own; when another caller took it, the toggle takes the lock again, at most
192
+ * {@link MANIFEST_LOCK_ATTEMPTS} times and within the same wait, and then goes ahead
193
+ * without it.
141
194
  */
142
195
  export async function writeMachineFeature(devflowDir, feature, enabled) {
196
+ const deadline = Date.now() + MANIFEST_LOCK_WAIT_MS;
197
+ for (let attempt = 0; attempt < MANIFEST_LOCK_ATTEMPTS; attempt++) {
198
+ const timeoutMs = Math.max(0, deadline - Date.now());
199
+ const locked = await withManifestLock(devflowDir, { timeoutMs }, (lock) => recordMachineFeature(devflowDir, feature, enabled, lock.stillHeld));
200
+ if (!locked.ok)
201
+ break;
202
+ if (locked.value !== LOCK_LOST)
203
+ return locked.value;
204
+ }
205
+ return recordMachineFeature(devflowDir, feature, enabled);
206
+ }
207
+ /**
208
+ * How many times a toggle takes the lock when it finds, just before its write, that
209
+ * another caller took the lock directory from it (manifest-lock.ts). The tries share
210
+ * the one wait, {@link MANIFEST_LOCK_WAIT_MS}. After them the toggle goes ahead without
211
+ * the lock, as it does when the wait ends.
212
+ */
213
+ const MANIFEST_LOCK_ATTEMPTS = 3;
214
+ /** `recordMachineFeature` did not write: the lock was taken from this toggle first. */
215
+ const LOCK_LOST = 'lock-lost';
216
+ async function recordMachineFeature(devflowDir, feature, enabled, stillHeld) {
143
217
  const raw = await readRawManifest(devflowDir);
144
218
  if (isMachineFeatureRecorded(raw, feature, enabled))
145
219
  return { ok: true, value: undefined };
146
220
  const next = setMachineFeature(raw, feature, enabled, new Date().toISOString());
147
221
  if (next === null)
148
222
  return { ok: false, error: 'not-installed' };
223
+ if (stillHeld !== undefined && !(await stillHeld()))
224
+ return LOCK_LOST;
149
225
  await writeFileAtomicExclusive(path.join(devflowDir, 'manifest.json'), JSON.stringify(next, null, 2) + '\n');
150
226
  return { ok: true, value: undefined };
151
227
  }
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
6
6
  *
7
7
  * Call sites: used by the CLI's exclusive-write call sites (migrations, init, post-install,
8
8
  * uninstall, security, ambient, memory, HUD).
9
- * The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/lib/learning-store.cjs`)
9
+ * The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/lib/curation-store.cjs`)
10
10
  * intentionally remains a separate implementation — same semantics, different module
11
11
  * system. Any change to the retry logic here MUST be mirrored there.
12
12
  */
@@ -17,12 +17,20 @@ import { describeEchoedValue } from './echo-value.js';
17
17
  * diff.
18
18
  *
19
19
  * A present value that is not one of the three words is MALFORMED. A reader reads
20
- * it as `full` and leaves it on disk as written: no read and no unrelated write
21
- * heals it to absent, which would lower the level without a word.
20
+ * it as {@link MALFORMED_GATE_LEVEL_READS_AS} and leaves it on disk as written: no
21
+ * read and no unrelated write heals it to absent, which would lower the level
22
+ * without a word.
22
23
  */
23
24
  export const GATE_LEVELS = Object.freeze(['full', 'standard', 'lean']);
24
25
  /** The level when neither the personal file nor the manifest records one. */
25
26
  export const DEFAULT_GATE_LEVEL = 'standard';
27
+ /**
28
+ * The level every reader gives a MALFORMED value: the strictest, so a typo never
29
+ * lowers the checking. `devflow gates` and the init prompt show it from here. The
30
+ * settings resolver (`resolve-settings.cjs`) folds to the same level in its own
31
+ * copy, and a parity test holds the two to each other.
32
+ */
33
+ export const MALFORMED_GATE_LEVEL_READS_AS = 'full';
26
34
  /**
27
35
  * Largest manifest read, in bytes. Pinned by a parity test to the resolver
28
36
  * library's MAX_MANIFEST_BYTES, so the CLI and a prompt never disagree about a
package/dist/core/git.js CHANGED
@@ -24,13 +24,25 @@ export function isTrunkBranch(branch) {
24
24
  return TRUNK_BRANCH_PREFIXES.some(p => branch.startsWith(p));
25
25
  }
26
26
  /**
27
- * Get git repository root directory (async, non-blocking)
28
- * Returns null if not in a git repository
27
+ * The repository root named by the output of `git rev-parse --show-toplevel`, or null
28
+ * when the output is not one clean absolute path.
29
29
  *
30
- * Security: Validates output to prevent command injection
30
+ * Security: validates output to prevent command injection
31
31
  * - Rejects paths with injection characters (newlines, semicolons, shell operators)
32
32
  * - Ensures path is absolute
33
33
  * - Resolves path canonically
34
+ */
35
+ export function parseGitToplevel(stdout) {
36
+ const gitRootRaw = stdout.trim();
37
+ if (!gitRootRaw || gitRootRaw.includes('\n') || gitRootRaw.includes(';') || gitRootRaw.includes('&&')) {
38
+ return null;
39
+ }
40
+ const gitRoot = path.resolve(gitRootRaw);
41
+ return path.isAbsolute(gitRoot) ? gitRoot : null;
42
+ }
43
+ /**
44
+ * Get git repository root directory (async, non-blocking)
45
+ * Returns null if not in a git repository
34
46
  *
35
47
  * @param cwd - Directory to resolve the git root from. Defaults to process.cwd().
36
48
  * Callers that already carry an explicit working directory must pass it, otherwise
@@ -42,17 +54,7 @@ export async function getGitRoot(cwd = process.cwd()) {
42
54
  cwd,
43
55
  encoding: 'utf-8'
44
56
  });
45
- const gitRootRaw = stdout.trim();
46
- // Validate git root path (security: prevent injection)
47
- if (!gitRootRaw || gitRootRaw.includes('\n') || gitRootRaw.includes(';') || gitRootRaw.includes('&&')) {
48
- return null;
49
- }
50
- // Validate it's an absolute path
51
- const gitRoot = path.resolve(gitRootRaw);
52
- if (!path.isAbsolute(gitRoot)) {
53
- return null;
54
- }
55
- return gitRoot;
57
+ return parseGitToplevel(stdout);
56
58
  }
57
59
  catch {
58
60
  return null;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @file ledger-root.ts
3
3
  *
4
- * Where the learning ledger lives for a working directory — the TypeScript twin of
4
+ * Where the curation ledger lives for a working directory — the TypeScript twin of
5
5
  * `df_resolve_roots`' DF_LEDGER_ROOT in src/assets/scripts/hooks/resolve-project-root.
6
6
  *
7
7
  * D-LEDGER-MAIN-WORKTREE: the ledger is one per REPOSITORY, not per checkout. In a
8
8
  * linked worktree the hooks queue and render into the main worktree's
9
- * `.devflow/learning/`, so `devflow learning` and the HUD must read and clear that
9
+ * `.devflow/curation/`, so `devflow curation` and the HUD must read and clear that
10
10
  * same directory — resolving the worktree's own toplevel (getGitRoot) would show an
11
11
  * empty ledger and drain a queue nothing writes to. The rule, from ONE git call:
12
12
  *
@@ -18,8 +18,11 @@
18
18
  * - two absolute lines otherwise → the toplevel;
19
19
  * - git before 2.31 echoes `--path-format=absolute` back as its own line → the
20
20
  * toplevel it printed after it (the hooks fall back to the toplevel too);
21
- * - git failed (not a work tree) → null: the caller keeps its non-git fallback;
22
- * - any other output → getGitRoot(), the hooks' df_resolve_root fallback.
21
+ * - git failed (not a work tree) → null: the caller keeps its non-git fallback; git
22
+ * that did not answer within the caller's time budget → null too (`lookupLedgerRoot`
23
+ * tells the two apart);
24
+ * - any other output → `git rev-parse --show-toplevel` alone, the hooks' df_resolve_root
25
+ * fallback, with the same time budget as the first call.
23
26
  *
24
27
  * Memory is NOT resolved here: working memory stays per checkout (getGitRoot).
25
28
  * Parity with the shell helper is pinned by tests/core/ledger-root.test.ts, which
@@ -29,9 +32,24 @@ import { execFile } from 'child_process';
29
32
  import { promises as fs } from 'fs';
30
33
  import * as os from 'os';
31
34
  import * as path from 'path';
32
- import { getGitRoot } from './git.js';
35
+ import { parseGitToplevel } from './git.js';
36
+ /**
37
+ * The budget of one ledger-root lookup, in ms: the HUD's per-git-command budget
38
+ * (src/hud/git.ts GIT_TIMEOUT). The HUD and the init move pass it as `timeoutMs`, so a
39
+ * project on a hung mount costs a bounded wait (one budget for each of the lookup's two
40
+ * git calls at most) and never stalls the rest.
41
+ */
42
+ export const LEDGER_ROOT_TIMEOUT_MS = 1000;
43
+ /**
44
+ * How long past its timeout a git call may stay unanswered before the lookup stops
45
+ * waiting for it: the timeout's own kill needs the process to die, and a process stuck
46
+ * in I/O on a hung mount may not.
47
+ */
48
+ const KILL_GRACE_MS = 500;
33
49
  /** The one git call, as argv (no shell). */
34
50
  const LEDGER_ROOT_GIT_ARGS = ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir'];
51
+ /** The fallback git call, as argv (no shell): the toplevel alone, as the hooks' df_resolve_root asks. */
52
+ const TOPLEVEL_GIT_ARGS = ['rev-parse', '--show-toplevel'];
35
53
  /** What git before 2.31 prints first: the flag it does not know, echoed back. */
36
54
  const PATH_FORMAT_ECHO = '--path-format=absolute';
37
55
  /** Classify the git call's stdout. Pure. */
@@ -45,12 +63,37 @@ export function parseLedgerRootOutput(stdout) {
45
63
  }
46
64
  return { kind: 'unrecognised' };
47
65
  }
48
- /** stdout of the one git call from `cwd`, or null when git failed. */
49
- function runRevParse(cwd, timeoutMs) {
66
+ /**
67
+ * Run `git <args>` from `cwd`. With a timeout the call is bounded twice: Node kills git
68
+ * at `timeoutMs`, and the lookup stops waiting KILL_GRACE_MS later even if git is still
69
+ * alive (and kills it then), so a process stuck on a hung mount cannot hold the caller.
70
+ * Never rejects.
71
+ */
72
+ function runGit(args, cwd, timeoutMs) {
50
73
  return new Promise((resolve) => {
51
- execFile('git', LEDGER_ROOT_GIT_ARGS, { cwd, timeout: timeoutMs, encoding: 'utf-8' }, (err, stdout) => {
52
- resolve(err ? null : stdout);
74
+ let settled = false;
75
+ let backstop;
76
+ const settle = (run) => {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ if (backstop !== undefined)
81
+ clearTimeout(backstop);
82
+ resolve(run);
83
+ };
84
+ const child = execFile('git', [...args], { cwd, timeout: timeoutMs, encoding: 'utf-8' }, (err, stdout) => {
85
+ if (!err)
86
+ settle({ kind: 'ok', stdout });
87
+ else
88
+ settle(timeoutMs > 0 && err.killed === true ? { kind: 'timed-out' } : { kind: 'failed' });
53
89
  });
90
+ if (timeoutMs > 0) {
91
+ backstop = setTimeout(() => {
92
+ child.kill('SIGKILL');
93
+ settle({ kind: 'timed-out' });
94
+ }, timeoutMs + KILL_GRACE_MS);
95
+ backstop.unref();
96
+ }
54
97
  });
55
98
  }
56
99
  /** A path's physical location, or null when it cannot be resolved. */
@@ -76,27 +119,44 @@ async function mainHoldsLedger(main, home) {
76
119
  return mainReal !== null && mainReal !== homeReal;
77
120
  }
78
121
  /**
79
- * The learning ledger root for `cwd` — the main worktree or this checkout's
80
- * toplevel — or null outside a git work tree.
122
+ * The curation ledger root for `cwd`, saying why when there is none: git named no work
123
+ * tree (`no-root`), or did not answer in time (`timed-out`). Each of the two git calls
124
+ * (the main one and the fallback) gets `options.timeoutMs`; 0 or none waits for ever.
81
125
  */
82
- export async function getLedgerRoot(cwd = process.cwd(), options = {}) {
83
- const stdout = await runRevParse(cwd, options.timeoutMs ?? 0);
84
- if (stdout === null)
85
- return null;
86
- const parsed = parseLedgerRootOutput(stdout);
126
+ export async function lookupLedgerRoot(cwd = process.cwd(), options = {}) {
127
+ const timeoutMs = options.timeoutMs ?? 0;
128
+ const run = await runGit(LEDGER_ROOT_GIT_ARGS, cwd, timeoutMs);
129
+ if (run.kind === 'timed-out')
130
+ return { kind: 'timed-out' };
131
+ if (run.kind === 'failed')
132
+ return { kind: 'no-root' };
133
+ const parsed = parseLedgerRootOutput(run.stdout);
87
134
  switch (parsed.kind) {
88
135
  case 'toplevel':
89
- return parsed.toplevel;
90
- case 'unrecognised':
91
- return getGitRoot(cwd);
136
+ return { kind: 'root', root: parsed.toplevel };
137
+ case 'unrecognised': {
138
+ const fallback = await runGit(TOPLEVEL_GIT_ARGS, cwd, timeoutMs);
139
+ if (fallback.kind === 'timed-out')
140
+ return { kind: 'timed-out' };
141
+ const toplevel = fallback.kind === 'ok' ? parseGitToplevel(fallback.stdout) : null;
142
+ return toplevel === null ? { kind: 'no-root' } : { kind: 'root', root: toplevel };
143
+ }
92
144
  case 'roots': {
93
145
  if (!parsed.commonDir.endsWith('/.git'))
94
- return parsed.toplevel;
146
+ return { kind: 'root', root: parsed.toplevel };
95
147
  const main = parsed.commonDir.slice(0, -'/.git'.length);
96
148
  if (main && await mainHoldsLedger(main, options.home ?? os.homedir()))
97
- return main;
98
- return parsed.toplevel;
149
+ return { kind: 'root', root: main };
150
+ return { kind: 'root', root: parsed.toplevel };
99
151
  }
100
152
  }
101
153
  }
154
+ /**
155
+ * The curation ledger root for `cwd` — the main worktree or this checkout's
156
+ * toplevel — or null outside a git work tree (or when git did not answer in time).
157
+ */
158
+ export async function getLedgerRoot(cwd = process.cwd(), options = {}) {
159
+ const lookup = await lookupLedgerRoot(cwd, options);
160
+ return lookup.kind === 'root' ? lookup.root : null;
161
+ }
102
162
  //# sourceMappingURL=ledger-root.js.map
@@ -12,17 +12,17 @@ import { promises as fs } from 'fs';
12
12
  * D-CLI-NO-SYMLINK: the CLI writes or deletes under a project's `.devflow/` only
13
13
  * where neither `.devflow` nor the entry it acts on through it is a symbolic link:
14
14
  * `devflow init` stamps its carve-out marker, removes the legacy markers and
15
- * writes `.devflow/config.json` only then, `devflow learning --configure` writes
16
- * the project's `learning.json` only then, and the queue drains (`devflow learning
15
+ * writes `.devflow/config.json` only then, `devflow curation --configure` writes
16
+ * the project's `curation.json` only then, and the queue drains (`devflow curation
17
17
  * --clear|--disable`, `devflow memory --disable|--clear` and `devflow init
18
- * --no-learning|--no-memory`) delete nothing when `.devflow` or the queue's folder
18
+ * --no-curation|--no-memory`) delete nothing when `.devflow` or the queue's folder
19
19
  * is one. `devflow uninstall`, removing a legacy local install, deletes or rewrites
20
20
  * nothing under the project's `.devflow` or `.claude` where either, or anything
21
21
  * below it on the way to what it removes, is one (legacyLocalChangeGuard in
22
22
  * uninstall.ts). Reason: a repository can commit `.devflow`, or a folder or file in
23
23
  * it, as a link to any place on the machine, and a write or delete through one lands
24
24
  * wherever it points. The hooks hold the same rule (D-HOOKS-NO-SYMLINK, git-marker)
25
- * and so do the learning ops (D-NO-LINKED-TREE). Only paths below the project root
25
+ * and so do the curation ops (D-NO-LINKED-TREE). Only paths below the project root
26
26
  * are passed here: the root and the folders above it are the user's choice.
27
27
  *
28
28
  * Each path is checked with lstat, so a link is seen rather than followed. Nothing at