devflow-kit 3.8.0 → 3.10.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 (211) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +6 -6
  3. package/dist/agents/code.md +10 -10
  4. package/dist/agents/fix.md +7 -7
  5. package/dist/agents/git.md +2 -2
  6. package/dist/agents/harden.md +7 -6
  7. package/dist/agents/refactor.md +6 -6
  8. package/dist/agents/skim.md +1 -1
  9. package/dist/agents/triage.md +7 -0
  10. package/dist/cli/commands/ambient.js +10 -2
  11. package/dist/cli/commands/compliance-prompts.js +9 -4
  12. package/dist/cli/commands/compliance.js +171 -24
  13. package/dist/cli/commands/curation.js +79 -23
  14. package/dist/cli/commands/gate-level-prompts.js +39 -23
  15. package/dist/cli/commands/gates.js +6 -7
  16. package/dist/cli/commands/init.js +150 -145
  17. package/dist/cli/commands/install-report.js +34 -0
  18. package/dist/cli/commands/legacy-sweep.js +64 -0
  19. package/dist/cli/commands/memory.js +59 -12
  20. package/dist/cli/commands/proxy.js +6 -1
  21. package/dist/cli/commands/rules.js +29 -12
  22. package/dist/cli/commands/skills.js +42 -9
  23. package/dist/cli/commands/tracker.js +9 -3
  24. package/dist/cli/commands/uninstall.js +354 -60
  25. package/dist/commands/bug-analysis.md +71 -122
  26. package/dist/commands/code-review.md +11 -11
  27. package/dist/commands/debug.md +2 -2
  28. package/dist/commands/dynamic-build.md +25 -25
  29. package/dist/commands/dynamic-plan.md +2 -2
  30. package/dist/commands/dynamic-profile.md +2 -2
  31. package/dist/commands/dynamic-tickets.md +2 -2
  32. package/dist/commands/explore.md +2 -2
  33. package/dist/commands/implement.md +48 -34
  34. package/dist/commands/plan.md +2 -2
  35. package/dist/commands/release.md +3 -3
  36. package/dist/commands/research.md +2 -2
  37. package/dist/commands/resolve.md +50 -46
  38. package/dist/commands/self-review.md +10 -9
  39. package/dist/compliance-off/agents/git.md +120 -0
  40. package/dist/compliance-off/agents/triage.md +180 -0
  41. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  42. package/dist/compliance-off/commands/code-review.md +520 -0
  43. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  44. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  45. package/dist/compliance-off/commands/implement.md +736 -0
  46. package/dist/compliance-off/commands/plan.md +671 -0
  47. package/dist/compliance-off/commands/release.md +309 -0
  48. package/dist/compliance-off/commands/resolve.md +742 -0
  49. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  50. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  51. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  52. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  53. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  54. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  55. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  56. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  57. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  58. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  59. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  60. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  61. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  62. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  63. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  64. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  65. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  66. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  67. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  68. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  69. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  70. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  71. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  72. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  73. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  74. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  75. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  76. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  77. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  78. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  79. package/dist/core/agent-models.js +19 -4
  80. package/dist/core/area-pause-notice.js +44 -0
  81. package/dist/core/assets.js +126 -17
  82. package/dist/core/compliance-off-files.js +27 -0
  83. package/dist/core/curation-move-skip.js +4 -5
  84. package/dist/core/curation-queue-cleanup.js +6 -5
  85. package/dist/core/curation-store.js +1 -0
  86. package/dist/core/curation-variants.js +460 -112
  87. package/dist/core/evidence-policy.js +10 -8
  88. package/dist/core/flags.js +2 -2
  89. package/dist/core/installed-variants.js +63 -0
  90. package/dist/core/manifest.js +48 -5
  91. package/dist/core/mds-variants.js +700 -3
  92. package/dist/core/orphan-sweep.js +11 -2
  93. package/dist/core/plugins.js +69 -12
  94. package/dist/core/tracked-areas.js +138 -14
  95. package/dist/curation-compliance-off/agents/triage.md +176 -0
  96. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  97. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  98. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  99. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  100. package/dist/curation-compliance-off/commands/implement.md +689 -0
  101. package/dist/curation-compliance-off/commands/plan.md +631 -0
  102. package/dist/curation-compliance-off/commands/release.md +279 -0
  103. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  104. package/dist/curation-off/agents/code.md +10 -10
  105. package/dist/curation-off/agents/fix.md +7 -7
  106. package/dist/curation-off/agents/harden.md +7 -6
  107. package/dist/curation-off/agents/refactor.md +6 -6
  108. package/dist/curation-off/agents/triage.md +7 -0
  109. package/dist/curation-off/commands/bug-analysis.md +69 -119
  110. package/dist/curation-off/commands/code-review.md +11 -11
  111. package/dist/curation-off/commands/debug.md +2 -2
  112. package/dist/curation-off/commands/dynamic-build.md +25 -25
  113. package/dist/curation-off/commands/explore.md +2 -2
  114. package/dist/curation-off/commands/implement.md +48 -34
  115. package/dist/curation-off/commands/plan.md +2 -2
  116. package/dist/curation-off/commands/release.md +1 -1
  117. package/dist/curation-off/commands/resolve.md +50 -46
  118. package/dist/curation-off/commands/self-review.md +10 -9
  119. package/dist/hud/components/curation-counts.js +27 -0
  120. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  121. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  122. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  123. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  124. package/dist/skills/git/references/publication-gate.md +6 -7
  125. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  126. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  127. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  128. package/dist/targets/claude-code/behavior-install.js +23 -9
  129. package/dist/targets/claude-code/charter-render.js +374 -0
  130. package/dist/targets/claude-code/compliance-install.js +32 -12
  131. package/dist/targets/claude-code/compliance-off-files.js +245 -0
  132. package/dist/targets/claude-code/curation-install.js +570 -100
  133. package/dist/targets/claude-code/installer.js +467 -62
  134. package/dist/targets/claude-code/language-stamp.js +6 -0
  135. package/dist/targets/claude-code/linked-folder.js +182 -0
  136. package/dist/targets/claude-code/reference-converge.js +118 -0
  137. package/dist/targets/claude-code/tracker-install.js +21 -8
  138. package/dist/targets/claude-code/tracker-servers.js +20 -6
  139. package/dist/targets/claude-code/workflow-folder-install.js +231 -0
  140. package/dist/variant-axes.json +109 -0
  141. package/package.json +1 -1
  142. package/src/assets/agents/_partials/_code.mds +6 -6
  143. package/src/assets/agents/code.mds +4 -4
  144. package/src/assets/agents/evaluate.md +7 -13
  145. package/src/assets/agents/fix.mds +1 -1
  146. package/src/assets/agents/git.mds +34 -2
  147. package/src/assets/agents/harden.mds +7 -6
  148. package/src/assets/agents/skim.mds +1 -1
  149. package/src/assets/agents/test.md +21 -12
  150. package/src/assets/agents/triage.mds +9 -0
  151. package/src/assets/agents/validate.md +27 -34
  152. package/src/assets/commands/_partials/_compliance.mds +5 -3
  153. package/src/assets/commands/_partials/_decisions.mds +10 -8
  154. package/src/assets/commands/_partials/_engine.mds +9 -8
  155. package/src/assets/commands/_partials/_publication.mds +10 -1
  156. package/src/assets/commands/_partials/_settings.mds +2 -2
  157. package/src/assets/commands/bug-analysis.mds +80 -126
  158. package/src/assets/commands/code-review.mds +52 -9
  159. package/src/assets/commands/dynamic-build.mds +109 -15
  160. package/src/assets/commands/dynamic-tickets.mds +13 -0
  161. package/src/assets/commands/implement.mds +145 -33
  162. package/src/assets/commands/plan.mds +19 -0
  163. package/src/assets/commands/release.mds +50 -1
  164. package/src/assets/commands/resolve.mds +229 -44
  165. package/src/assets/commands/self-review.mds +8 -7
  166. package/src/assets/mds/git/_ops.mds +29 -1
  167. package/src/assets/mds/git/_ops_pr.mds +20 -10
  168. package/src/assets/mds/git/_ops_steps.mds +27 -0
  169. package/src/assets/mds/git/_pr.mds +15 -15
  170. package/src/assets/mds/git/_references.mds +12 -7
  171. package/src/assets/mds/tracker/_common.mds +4 -0
  172. package/src/assets/mds/tracker/_github.mds +43 -4
  173. package/src/assets/mds/tracker/_jira.mds +44 -4
  174. package/src/assets/mds/tracker/_linear.mds +44 -4
  175. package/src/assets/rules/quality.md +2 -0
  176. package/src/assets/scripts/ci-wait.cjs +493 -118
  177. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  178. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  179. package/src/assets/scripts/hooks/background-memory-update +79 -1
  180. package/src/assets/scripts/hooks/json-helper.cjs +44 -2
  181. package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
  182. package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
  183. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
  184. package/src/assets/scripts/hooks/memory-permission +10 -2
  185. package/src/assets/scripts/hooks/memory-worker +11 -0
  186. package/src/assets/scripts/hooks/pre-compact-memory +11 -0
  187. package/src/assets/scripts/hooks/preamble +18 -8
  188. package/src/assets/scripts/hooks/queue-append +218 -1
  189. package/src/assets/scripts/hooks/session-start-context +89 -37
  190. package/src/assets/scripts/hooks/session-start-memory +39 -0
  191. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  192. package/src/assets/scripts/pr-evidence.cjs +46 -4
  193. package/src/assets/scripts/resolve-settings.cjs +244 -30
  194. package/src/assets/scripts/verify-evidence.cjs +708 -124
  195. package/src/assets/skills/agent-references/SKILL.md +7 -1
  196. package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
  197. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  198. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  199. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
  200. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  201. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  202. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  203. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  204. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  205. package/src/assets/skills/git/SKILL.md +3 -3
  206. package/src/assets/skills/git/references/github-api.md +2 -139
  207. package/src/assets/skills/git/references/github-posting.md +158 -0
  208. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  209. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  210. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  211. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -2,9 +2,11 @@
2
2
  //
3
3
  // Does git track any entry under one of devflow's data folders at a root? One
4
4
  // `git ls-files` call over a fixed set of pathspecs answers `tracked`, `untracked` or
5
- // `unknown`. The one-time ledger move and the backup sweep ask it before they lock, read
6
- // or rename anything: a folder that git tracks is the repository's content, so they leave
7
- // it alone.
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.
8
10
  //
9
11
  // DESIGN: a leaf. It loads node built-ins and project-paths.cjs (which loads node
10
12
  // built-ins only) and requires neither the store nor the move module (the move requires
@@ -13,14 +15,30 @@
13
15
  // every failure is the answer `unknown`, with a cause from the closed set UNKNOWN_CAUSES.
14
16
  //
15
17
  // The folder names come from project-paths.cjs, which owns them: AREA_FOLDERS lists the
16
- // data folders and the backup prefix names the folders the move leaves behind. To cover
17
- // another folder, add its name to AREA_FOLDERS.
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.
18
21
  //
19
22
  // The decision is the repository's index, never the disk. A root with no `.git` entry
20
23
  // (a file or a folder) is not a git working tree and gets no git call. A root with one
21
24
  // gets exactly one call, and every failure of that call (git missing, a timeout, a
22
25
  // non-zero exit with nothing listed) is `unknown`: the caller cannot tell what git
23
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.
24
42
 
25
43
  'use strict';
26
44
 
@@ -28,11 +46,14 @@ const childProcess = require('child_process');
28
46
  const fs = require('fs');
29
47
  const path = require('path');
30
48
 
31
- const { LEGACY_NAMES, CURATION_DIR_NAME } = require('./project-paths.cjs');
49
+ const { LEGACY_NAMES, CURATION_DIR_NAME, getMemoryDir } = require('./project-paths.cjs');
32
50
 
33
- /** The data folders under `.devflow` that the check covers, by the names the path helpers hold. */
51
+ /** The curation data folders under `.devflow` that the check covers, by the names the path helpers hold. */
34
52
  const AREA_FOLDERS = Object.freeze([LEGACY_NAMES.folder, CURATION_DIR_NAME]);
35
53
 
54
+ /** The memory folder under `.devflow`, by the name getMemoryDir gives it. */
55
+ const MEMORY_FOLDER = path.basename(getMemoryDir(path.sep));
56
+
36
57
  /**
37
58
  * The spellings a case-folding file system opens as `.devflow` beyond ASCII letter case.
38
59
  * git's `icase` folds ASCII letters only, but APFS (the macOS default) compares names by
@@ -42,8 +63,9 @@ const AREA_FOLDERS = Object.freeze([LEGACY_NAMES.folder, CURATION_DIR_NAME]);
42
63
  const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
43
64
 
44
65
  /**
45
- * The pathspecs of the check, each matched without regard to ASCII letter case, whatever
46
- * the file system or `core.ignorecase`, and each spelled every way DEVFLOW_SPELLINGS lists:
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:
47
69
  * - each folder of AREA_FOLDERS, as a literal path: an index entry at the folder, or
48
70
  * anywhere below it, matches
49
71
  * - an entry named like a backup folder (`<backup prefix>*`), and every entry below one
@@ -54,6 +76,8 @@ const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
54
76
  * keeps `glob` from matching as a leading directory, so the many files under a
55
77
  * normal `.devflow/` are not listed. Committed content then stands at the path where
56
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.
57
81
  */
58
82
  const TRACKED_AREA_PATHSPECS = Object.freeze([
59
83
  ...DEVFLOW_SPELLINGS.flatMap(dir => [
@@ -64,6 +88,30 @@ const TRACKED_AREA_PATHSPECS = Object.freeze([
64
88
  ...DEVFLOW_SPELLINGS.map(dir => ':(icase,glob)[.]' + dir.slice(1)),
65
89
  ]);
66
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
+
67
115
  /**
68
116
  * Why a check answered `unknown`, as one fixed word. The words come from this list
69
117
  * alone: they name a spawn error code, a time out, a signal, an exit status or the step
@@ -108,6 +156,20 @@ const GIT_REDIRECT_VARIABLES = Object.freeze([
108
156
  'GIT_ICASE_PATHSPECS',
109
157
  ]);
110
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
+
111
173
  /**
112
174
  * The environment of the git call: the process environment without the redirecting
113
175
  * variables, with a fixed locale, no optional lock and a ceiling directory. Answers a new
@@ -120,8 +182,7 @@ const GIT_REDIRECT_VARIABLES = Object.freeze([
120
182
  * @returns {NodeJS.ProcessEnv}
121
183
  */
122
184
  function trackedAreasGitEnv(base, ceiling) {
123
- const env = { ...base };
124
- for (const name of GIT_REDIRECT_VARIABLES) delete env[name];
185
+ const env = gitEnvWithoutRedirects(base);
125
186
  env.LC_ALL = 'C';
126
187
  env.GIT_OPTIONAL_LOCKS = '0';
127
188
  env.GIT_CEILING_DIRECTORIES = ceiling;
@@ -193,42 +254,108 @@ function entryExists(target) {
193
254
  }
194
255
 
195
256
  /**
196
- * Whether git tracks any entry under the covered folders at `root`, or an entry named
197
- * `.devflow` itself (a link, a gitlink or a file). The index decides, not the disk, and
198
- * a staged entry counts. The answer carries the cause (a value of UNKNOWN_CAUSES) when
199
- * it is `unknown`, and null otherwise.
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.
200
259
  *
201
- * untracked no `.git` entry at the root (no git call is made), or git listed nothing
202
- * tracked git listed an entry
203
- * unknown a `.git` entry stands and git did not answer (see judgeTrackedAreas),
204
- * the `.git` entry could not be probed, or the root is not an absolute path
205
- * string (no git call is made)
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
206
282
  *
207
- * The call is `git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- <paths>`
208
- * with the root as its working directory: an argv with no shell, a time bound and an
209
- * output bound. `--no-literal-pathspecs` leads the argv, so a forced literal-pathspec
210
- * setting cannot turn the magic off and hide an entry. Reading the index makes git run
211
- * the command a repository's config names in `core.fsmonitor`, so the call turns that off
212
- * for itself and stays a pure read. The folder above the root's real path is a ceiling
213
- * directory (trackedAreasGitEnv); a parent path that holds the list separator can not be
214
- * named as one, and answers `unknown`.
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.
215
290
  *
216
291
  * @param {unknown} root - an absolute path
217
- * @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
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 }}
218
295
  */
219
- function inspectTrackedAreas(root) {
296
+ function trackedAreaCall(root, area, baseEnv = process.env) {
297
+ const pathspecs = pathspecsOf(area);
220
298
  if (typeof root !== 'string' || !path.isAbsolute(root)) return unknownBecause(UNKNOWN_CAUSES.rootNotAbsolute);
299
+ if (pathspecs === null) return unknownBecause(UNKNOWN_CAUSES.checkFailed);
221
300
  try {
222
301
  const gitEntry = entryExists(path.join(root, '.git'));
223
302
  if (gitEntry === null) return unknownBecause(UNKNOWN_CAUSES.gitEntryUnreadable);
224
303
  if (gitEntry === false) return { answer: 'untracked', cause: null };
225
304
  const ceiling = path.dirname(fs.realpathSync(root));
226
305
  if (ceiling.includes(path.delimiter)) return unknownBecause(UNKNOWN_CAUSES.ceilingUnusable);
227
- const result = childProcess.spawnSync('git', ['--no-literal-pathspecs', '-c', 'core.fsmonitor=false', 'ls-files', '-z', '--', ...TRACKED_AREA_PATHSPECS], {
306
+ return {
228
307
  cwd: root,
229
- env: trackedAreasGitEnv(process.env, ceiling),
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,
230
357
  stdio: ['ignore', 'pipe', 'ignore'],
231
- timeout: TRACKED_AREAS_GIT_TIMEOUT_MS,
358
+ timeout: timeoutOf(options),
232
359
  maxBuffer: TRACKED_AREAS_GIT_MAX_BUFFER,
233
360
  windowsHide: true,
234
361
  shell: false,
@@ -239,6 +366,60 @@ function inspectTrackedAreas(root) {
239
366
  }
240
367
  }
241
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
+
242
423
  /**
243
424
  * The answer of {@link inspectTrackedAreas}, without its cause.
244
425
  *
@@ -251,9 +432,21 @@ function checkTrackedAreas(root) {
251
432
 
252
433
  module.exports = {
253
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,
254
442
  UNKNOWN_CAUSES,
443
+ gitEnvWithoutRedirects,
255
444
  judgeTrackedAreas,
256
445
  classifyTrackedAreas,
446
+ trackedAreaCall,
447
+ inspectTrackedArea,
448
+ inspectTrackedAreaOnce,
449
+ checkTrackedArea,
257
450
  inspectTrackedAreas,
258
451
  checkTrackedAreas,
259
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
@@ -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)
@@ -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
 
@@ -3,11 +3,16 @@
3
3
  # Devflow Preamble: UserPromptSubmit Hook
4
4
  # Three behaviors (git repos only — silent outside a git tree):
5
5
  # 1. Plan-handoff fast-path: if prompt begins `Implement the following plan:` (Claude Code's
6
- # native plan-mode handoff), inject a directive to invoke devflow:implement immediately.
7
- # NOTE: the literal prefix is duplicated in orchestrator-charter.md (fallback bullet)
8
- # and src/assets/scripts/hooks/preamble — update both together if Claude Code changes the handoff format.
6
+ # native plan-mode handoff) AND devflow:implement is installed, inject a directive to invoke
7
+ # devflow:implement immediately. devflow writes the file hooks/assets/implement-installed
8
+ # while implement is installed and removes it otherwise; the hook only tests that the file
9
+ # exists. Without it a handoff prompt falls through to the reminder (behavior 3).
10
+ # NOTE: the literal prefix is duplicated in orchestrator-charter.md (plan-handoff bullet,
11
+ # which the installed render drops when implement is absent) and in the dispatch below —
12
+ # update both together if Claude Code changes the handoff format.
9
13
  # 2. Slash skip: no reminder for slash commands (/...).
10
- # 3. Orchestrator reminder (2 lines): all other prompts get a delegation reminder.
14
+ # 3. Orchestrator reminder (2 lines): all other prompts, and a handoff prompt when implement
15
+ # is not installed, get a generic delegation reminder.
11
16
 
12
17
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
13
18
  dbg() { :; }
@@ -52,7 +57,12 @@ if ! df_has_git_marker "$CWD" 2>/dev/null; then dbg "EXIT: not a git repo"; exit
52
57
  # --- Prompt dispatch (pure bash 3.2; no subprocess in this block) ---
53
58
  # The plan-handoff prefix is a Claude Code protocol marker (native "execute plan
54
59
  # in new session" handoff), matched literally — plumbing, not semantic detection.
55
- # Duplicated in orchestrator-charter.md (fallback bullet); update both together.
60
+ # Duplicated in orchestrator-charter.md (plan-handoff bullet); update both together.
61
+ # The handoff branch also needs the implement sentinel file: the directive names
62
+ # devflow:implement, so it fires only while that command is installed. The sentinel
63
+ # check is a builtin file test (no fork) and runs only after the prefix matched.
64
+ # Its path equals HANDOFF_SENTINEL_RELPATH in src/targets/claude-code/charter-render.ts
65
+ # (the hooks directory is SCRIPT_DIR); tests/shell-hooks.test.ts pins the match.
56
66
  # Known bound: prefix must appear within the first 256 bytes after whitespace
57
67
  # strip (native handoffs have zero leading whitespace).
58
68
  HEAD="${PROMPT:0:256}"
@@ -60,7 +70,7 @@ HEAD="${HEAD#"${HEAD%%[![:space:]]*}"}" # strip leading whitespace/newlines
60
70
 
61
71
  if [ -z "$HEAD" ]; then
62
72
  dbg "EXIT: empty prompt"
63
- elif [[ "$HEAD" == "Implement the following plan:"* ]]; then
73
+ elif [[ "$HEAD" == "Implement the following plan:"* ]] && [ -f "$SCRIPT_DIR/assets/implement-installed" ]; then
64
74
  dbg "PLAN_HANDOFF detected — injecting devflow:implement directive"
65
75
  # D-ARGS-ONCE: the directive passes the Skill call NO arguments. The handoff
66
76
  # prompt IS the plan, so it is already in the conversation; handing it to the
@@ -75,8 +85,8 @@ elif [[ "$HEAD" == "/"* ]]; then
75
85
  dbg "EXIT: slash command — no reminder"
76
86
  else
77
87
  dbg "ORCHESTRATOR_REMINDER injected"
78
- json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to the fitting roster agent (Agent tool) or the matching devflow workflow skill. Send debug work to the devflow:debug skill, never to the Debug agent.
79
- Keep only judgment work mainline: conversation, decisions, routing, synthesis of agent reports."
88
+ json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to the fitting roster agent (Agent tool) or the matching devflow workflow skill.
89
+ Keep only judgment work mainline: conversation, decisions, routing, combining a few agent reports."
80
90
  fi
81
91
 
82
92
  dbg "=== HOOK COMPLETE ==="