devflow-kit 2.4.0 → 3.0.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 (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -22,18 +22,55 @@
22
22
  # queue_append_both <memory_queue> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
23
23
  # Calls queue_append_row for each queue whose *_enabled flag is "true". Each
24
24
  # queue is gated independently -- callers compute memory_enabled/learning_enabled
25
- # from feature config themselves (see queue_read_gates below for the one-fork
26
- # combined read that keeps this to a single config subprocess per hook).
25
+ # themselves (see queue_read_gates below for the at-most-one-fork read of both).
27
26
  #
28
- # queue_read_gates <feature_config_path>
29
- # Reads BOTH the "memory" and "learning" fields from .devflow/config.json in a
30
- # SINGLE subprocess fork (AC-P1 -- exactly one config-read fork per capture
31
- # hook, not two). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false") in the
32
- # caller's scope. Missing config file -> both default "true". The two values
33
- # are newline-separated rather than using a control-character delimiter:
34
- # they are always the literal strings "true"/"false", never arbitrary
35
- # content, so a plain newline split is unambiguous and easy to review (no
36
- # invisible bytes hiding in the source).
27
+ # queue_read_gates <manifest_path> [<root>]
28
+ # Reads BOTH switches, `memory` and `learning`, in at most ONE subprocess fork
29
+ # (AC-P1 -- at most one gate-read fork per capture hook, not two; none when
30
+ # the fast paths settle it). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false")
31
+ # in the caller's scope. The two values are newline-separated
32
+ # rather than using a control-character delimiter: they are always the literal
33
+ # strings "true"/"false", never arbitrary content, so a plain newline split is
34
+ # unambiguous and easy to review (no invisible bytes hiding in the source).
35
+ #
36
+ # D-FEATURES-NARROW-ONLY (src/core/feature-switch.ts): a switch is on iff the
37
+ # machine switch is on AND neither repository file sets `features.<name>` to
38
+ # the literal `false` -- the rule resolve-settings.cjs folds, held to the same
39
+ # answers by the shared table tests/fixtures/settings-switch-table.ts (TP-49).
40
+ # A repository can switch memory or learning off for itself, never back on.
41
+ # machine <manifest_path>, the ~/.devflow/manifest.json `devflow init` and
42
+ # `devflow memory|learning --enable/--disable` write. Only an
43
+ # explicit boolean `false` switches a feature off -- an absent,
44
+ # unreadable or malformed manifest, a missing key, or a non-boolean
45
+ # value leaves it ON (fail-open, ADR-028), the rule
46
+ # isMachineFeatureOn() applies in the CLI.
47
+ # project <root>/.devflow/project.json (team-committed) and
48
+ # personal <root>/.devflow/config.json (this worktree's own), read ONLY under
49
+ # `features.memory` / `features.learning`. The retired top-level
50
+ # keys (`memory`, `learning`, `decisions`) and `features.decisions`
51
+ # narrow nothing (#378: reading them let a feature run in every repo
52
+ # but one). An invalid file -- unparseable, over 4096 bytes, a BOM,
53
+ # a symlink, a duplicated key -- narrows nothing.
54
+ # <root> is the checkout's toplevel (DF_ROOT, which every caller holds as
55
+ # $PROJECT_ROOT), for learning too: project.json is committed per branch and
56
+ # config.json lives per worktree, so both are read where the session runs, as
57
+ # resolve-settings.cjs reads them -- never at DF_LEDGER_ROOT, which only says
58
+ # where the queue lands. Without <root> (or with an empty one) no repository
59
+ # file is read.
60
+ #
61
+ # D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, learning reads
62
+ # `features.learning` when it is a boolean, else the pre-rename
63
+ # `features.decisions` (ADR-011) -- readManifest's migration precedence
64
+ # exactly, so a legacy `decisions: false` switches learning off before any
65
+ # command has healed the file. Only a boolean `false` in whichever key decides
66
+ # is off, as above.
67
+ #
68
+ # Every hook that gates on memory or learning reads it through here: the
69
+ # capture hooks, session-start-context (Sections 1-2), memory-worker,
70
+ # session-start-memory, pre-compact-memory and background-memory-update. The
71
+ # callers pass <manifest_path> as $HOME/.devflow/manifest.json, the machine
72
+ # root (D-ONE-HOME); memory-worker hands its resolved path to the background
73
+ # worker it spawns.
37
74
 
38
75
  queue_append_row() {
39
76
  local _qar_file="$1" _qar_role="$2" _qar_content="$3" _qar_ts="$4"
@@ -83,34 +120,115 @@ queue_append_both() {
83
120
  fi
84
121
  }
85
122
 
123
+ # _qg_repo_file_can_narrow <file> -- succeeds when <file> COULD set
124
+ # features.memory or features.learning to false, so only the parser can decide
125
+ # it; fails when it certainly cannot. Builtins only: no fork.
126
+ #
127
+ # D-GATES-FAST-PATH (repository files): the read is bounded -- `-n 4097`, which
128
+ # bash 3.2 supports -- and `read -d ''` succeeds only when it stops short of the
129
+ # end of the file: at a NUL byte (never valid in JSON) or at 4097 characters,
130
+ # which is more than MAX_CONFIG_BYTES (4096) bytes. Either way the parser would
131
+ # call the file invalid, and an invalid file narrows nothing. `-f` follows a
132
+ # symlink and refuses a directory or FIFO, so nothing here can block; the parser
133
+ # itself refuses the symlink. A `\u` escape is the only JSON spelling of a
134
+ # letter other than the letter itself, so without one a narrowing file shows
135
+ # `"features"`, then `{`, then `"memory"` or `"learning"`, then `false`,
136
+ # literally and in that order. `.` spans newlines (POSIX ERE without
137
+ # REG_NEWLINE), so the match can only over-report -- an over-report costs one
138
+ # fork and the parser's answer, never a wrong one. No `"decisions"` and no
139
+ # top-level key before `features` is ever matched.
140
+ _qg_repo_file_can_narrow() {
141
+ local _qgf_text=""
142
+ local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|learning)"[[:space:]]*:[[:space:]]*false'
143
+ [ -f "$1" ] || return 1
144
+ if IFS= read -r -d '' -n 4097 _qgf_text 2>/dev/null < "$1"; then return 1; fi
145
+ case "$_qgf_text" in *'\u'*) return 0 ;; esac
146
+ [[ $_qgf_text =~ $_qgf_re ]]
147
+ }
148
+
86
149
  queue_read_gates() {
87
- local _qg_config="$1"
150
+ local _qg_manifest="${1:-}" _qg_root="${2:-}" _qg_text="" _qg_parse="false" _qg_repo="false" _qg_fields=""
88
151
  _QG_MEMORY="true"
89
152
  _QG_LEARNING="true"
90
153
 
91
- if [ -f "$_qg_config" ]; then
92
- local _qg_fields
154
+ if [ -n "$_qg_manifest" ] && [ -f "$_qg_manifest" ]; then
155
+ # D-GATES-FAST-PATH: every capture hook and every session start pays this
156
+ # read, and on most machines both switches are on. Only a JSON boolean
157
+ # `false` under a "memory", "learning" or "decisions" key can switch anything
158
+ # off, and a `\u` escape is the only JSON spelling of a letter other than the
159
+ # letter itself, so without one such a key appears literally. A text holding
160
+ # no `\u` and no key-then-`false` sequence therefore cannot switch anything
161
+ # off -- valid or not, it reads both-on either way -- and is settled by shell
162
+ # builtins (read, case, [[ =~ ]]) with no jq/node fork; everything else takes
163
+ # the full parse below. `read -d ''` succeeds only on reaching a NUL byte,
164
+ # i.e. before the end of the file, so a text it cut short is parsed in full
165
+ # too. An unreadable file reads empty and fails open, as the parser would.
166
+ local _qg_re='"(memory|learning|decisions)"[[:space:]]*:[[:space:]]*false'
167
+ IFS= read -r -d '' _qg_text 2>/dev/null < "$_qg_manifest" && _qg_parse="true"
168
+ case "$_qg_text" in *'\u'*) _qg_parse="true" ;; esac
169
+ if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
170
+ fi
171
+
172
+ if [ -n "$_qg_root" ]; then
173
+ if _qg_repo_file_can_narrow "$_qg_root/.devflow/project.json" \
174
+ || _qg_repo_file_can_narrow "$_qg_root/.devflow/config.json"; then
175
+ _qg_repo="true"
176
+ fi
177
+ fi
178
+
179
+ if [ "$_qg_repo" = "true" ]; then
180
+ # The ONE fork when a repository file can narrow: node on the resolver's own
181
+ # readRepoLayers + foldSettings (resolve-settings.cjs, a sibling of this
182
+ # hooks/ directory), so every file rule -- symlink, size, BOM, fatal UTF-8,
183
+ # duplicate keys -- is the parser's, not a shell copy of it. It folds the
184
+ # manifest too, read only when the fast path above flagged it (an unflagged
185
+ # manifest is both-on), so a false in all three layers still costs one fork.
186
+ # jq cannot see duplicate keys, so this path is node whatever _HAS_JQ says.
187
+ local _qg_scripts="${BASH_SOURCE[0]%/*}/.." _qg_manifest_arg=""
188
+ [ "$_qg_parse" = "true" ] && _qg_manifest_arg="$_qg_manifest"
189
+ _qg_fields=$(node -e "
190
+ const S = require(require('path').resolve(process.argv[1], 'resolve-settings.cjs'));
191
+ let m;
192
+ if (process.argv[2] !== '') {
193
+ try { m = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8')); } catch (_) { m = undefined; }
194
+ }
195
+ const w = S.foldSettings(Object.assign({ manifest: m }, S.readRepoLayers(process.argv[3]))).switches;
196
+ process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.learning.on));
197
+ " -- "$_qg_scripts" "$_qg_manifest_arg" "$_qg_root" 2>/dev/null) || _qg_fields=""
198
+ fi
199
+
200
+ # The manifest-only parse: when no repository file can narrow, or when the
201
+ # fork above failed (the manifest's answer still stands; the repository's is
202
+ # lost, which fails open like every other unreadable layer).
203
+ if [ -z "$_qg_fields" ] && [ "$_qg_parse" = "true" ]; then
93
204
  if [ "$_HAS_JQ" = "true" ]; then
94
- # if/then/else (not //) preserves an explicit `false` -- mirrors json_field_file's
95
- # own documented rationale (jq's // would replace a real `false` with the default).
96
- # The comma produces two raw-output lines (newline-separated) in one jq process.
205
+ # `try` covers a manifest whose top level or `features` is not an object; a
206
+ # parse failure empties the output, which reads as "not switched off". The
207
+ # comma produces two raw-output lines (newline-separated) in one jq process.
97
208
  _qg_fields=$(jq -r '
98
- (if (.memory | type) == "null" then "true" else (.memory | tostring) end),
99
- (if (.learning | type) == "null" then "true" else (.learning | tostring) end)
100
- ' "$_qg_config" 2>/dev/null) || _qg_fields=""
209
+ (if (try .features.memory catch null) == false then "false" else "true" end),
210
+ (if (try (.features | if (.learning | type) == "boolean" then .learning else .decisions end) catch null) == false
211
+ then "false" else "true" end)
212
+ ' "$_qg_manifest" 2>/dev/null) || _qg_fields=""
101
213
  else
102
214
  _qg_fields=$(node -e "
103
- const j = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
104
- const m = j.memory === undefined ? 'true' : String(j.memory);
105
- const d = j.learning === undefined ? 'true' : String(j.learning);
106
- process.stdout.write(m + String.fromCharCode(10) + d);
107
- " -- "$_qg_config" 2>/dev/null) || _qg_fields=""
215
+ const m = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
216
+ const f = m !== null && typeof m === 'object' ? m.features : undefined;
217
+ const isObj = f !== null && typeof f === 'object';
218
+ const on = (k) => !(isObj && f[k] === false);
219
+ const learnKey = isObj && typeof f.learning !== 'boolean' ? 'decisions' : 'learning';
220
+ process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
221
+ " -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
108
222
  fi
109
- if [ -n "$_qg_fields" ]; then
223
+ fi
224
+
225
+ # Only the four well-formed answers are taken; anything else is fail-open.
226
+ case "$_qg_fields" in
227
+ true$'\n'true|true$'\n'false|false$'\n'true|false$'\n'false)
110
228
  _QG_MEMORY="${_qg_fields%%$'\n'*}"
111
229
  _QG_LEARNING="${_qg_fields#*$'\n'}"
112
- fi
113
- fi
230
+ ;;
231
+ esac
114
232
 
115
233
  # Explicit return: this function's meaning is "populate _QG_* outputs," not a
116
234
  # success/failure signal, so its exit status must never leak the truthiness of
@@ -9,13 +9,19 @@
9
9
  # side anchors identically.
10
10
  #
11
11
  # Sourced by: the memory/learning/session hooks and ensure-devflow-init.
12
- # Sourced helper: uses `return`-free pure function; _-prefixed locals (never
12
+ # Sourced helper: uses `return`-free pure functions; _-prefixed locals (never
13
13
  # clobbers caller vars). Safe under `set -e` (git failure is guarded with || true).
14
14
  #
15
15
  # Usage:
16
16
  # source resolve-project-root
17
- # PROJECT_ROOT="$(df_resolve_root "$CWD")"
17
+ # PROJECT_ROOT="$(df_resolve_root "$CWD")" # one root
18
+ # df_resolve_roots "$CWD" # sets DF_ROOT and DF_LEDGER_ROOT
18
19
  #
20
+ # df_resolve_roots needs df_is_project_root, so this helper sources its sibling
21
+ # git-marker itself rather than trusting every caller to have done so first.
22
+
23
+ source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
24
+
19
25
  # df_resolve_root <cwd> prints the project root for <cwd>:
20
26
  # 1. git top-level — git walks up to the real repo root even from a
21
27
  # .devflow/-nested subdir, so the nested case is fixed for git repos.
@@ -27,11 +33,99 @@ df_resolve_root() {
27
33
  # the caller (e.g. a non-git path). Empty output then routes to the fallback.
28
34
  _root="$(git -C "$_cwd" rev-parse --show-toplevel 2>/dev/null || true)"
29
35
  if [ -z "$_root" ]; then
30
- case "$_cwd" in
31
- */.devflow/*) _root="${_cwd%%/.devflow/*}" ;;
32
- */.devflow) _root="${_cwd%/.devflow}" ;;
33
- *) _root="$_cwd" ;;
34
- esac
36
+ _df_nongit_root "$_cwd"
37
+ _root="$_DF_NONGIT_ROOT"
35
38
  fi
36
39
  printf '%s\n' "$_root"
37
40
  }
41
+
42
+ # _df_nongit_root <cwd> sets _DF_NONGIT_ROOT to the non-git fallback root: <cwd>
43
+ # with its first /.devflow/ segment onward (or a trailing /.devflow) stripped.
44
+ # A variable rather than stdout, so a caller pays no `$(...)` subshell for it.
45
+ _df_nongit_root() {
46
+ case "$1" in
47
+ */.devflow/*) _DF_NONGIT_ROOT="${1%%/.devflow/*}" ;;
48
+ */.devflow) _DF_NONGIT_ROOT="${1%/.devflow}" ;;
49
+ *) _DF_NONGIT_ROOT="$1" ;;
50
+ esac
51
+ }
52
+
53
+ # df_resolve_roots <cwd> sets TWO roots in the caller's scope, from ONE git call:
54
+ # DF_ROOT — the checkout's toplevel: where per-checkout data lives
55
+ # (memory, the .gitignore carve-out, the knowledge bases).
56
+ # DF_LEDGER_ROOT — where the learning ledger and its queue live.
57
+ #
58
+ # D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
59
+ # --worktree`) has its own toplevel, so a ledger anchored there restarts at
60
+ # ADR-001 — main's decisions are invisible in it, and the IDs it mints collide
61
+ # with main's (in a repo whose collision guard refuses, the Learning agent stops
62
+ # for good). The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
63
+ # is the main worktree — the parent of `--git-common-dir` — when that common dir
64
+ # is a `<dir>/.git` directory AND `<dir>/.devflow` already exists; otherwise it is
65
+ # DF_ROOT. The existence test keeps a main checkout that never ran devflow from
66
+ # being scaffolded by a worktree session. A main worktree at HOME (a dotfiles
67
+ # repository) is refused as well (df_is_project_root, D-HOOKS-GIT-ONLY): its
68
+ # `.devflow` is the machine root, which always exists, so the existence test alone
69
+ # would put the ledger inside `~/.devflow`. A common dir that is not `/.git`
70
+ # (a bare repository, `--separate-git-dir`, a submodule's modules/ dir) has no
71
+ # main checkout to name, so it stays per checkout. Memory stays at DF_ROOT on
72
+ # purpose: working memory describes the branch in front of you. A worktree that
73
+ # already grew its own ledger before this rule keeps it — local data is left in
74
+ # place, and the worktree simply stops appending to it.
75
+ #
76
+ # The one call asks for both answers at once:
77
+ # git rev-parse --path-format=absolute --show-toplevel --git-common-dir
78
+ # and its output is accepted only when it is EXACTLY two lines, each an absolute
79
+ # path. `--path-format` arrived in git 2.31; an older git echoes the unknown
80
+ # flag back as a line of its own (three lines, the first not absolute), and that
81
+ # falls back to df_resolve_root, so the ledger stays at the toplevel exactly as
82
+ # before this rule. A failed call prints nothing: `--show-toplevel` itself failed
83
+ # (not a work tree), so asking git again could only fail the same way, and the
84
+ # non-git fallback applies directly. Inside a checkout git refuses (dubious
85
+ # ownership, GIT_CEILING_DIRECTORIES) that fallback is the raw cwd, which may be a
86
+ # subdirectory; df_is_project_root refuses any root without its own `.git`
87
+ # (D-HOOKS-TOPLEVEL-ONLY), so no hook scaffolds there. Parsing is parameter expansion and `case`
88
+ # only: one git call on the success path and on the non-git path alike.
89
+ df_resolve_roots() {
90
+ local _cwd="$1" _out="" _top="" _common="" _main=""
91
+ DF_ROOT=""
92
+ DF_LEDGER_ROOT=""
93
+ _out="$(git -C "$_cwd" rev-parse --path-format=absolute --show-toplevel --git-common-dir 2>/dev/null || true)"
94
+ _top="${_out%%$'\n'*}"
95
+ if [ "$_top" != "$_out" ]; then
96
+ _common="${_out#*$'\n'}"
97
+ fi
98
+ case "$_top" in /*) ;; *) _top="" ;; esac
99
+ case "$_common" in
100
+ *$'\n'*) _common="" ;;
101
+ /*) ;;
102
+ *) _common="" ;;
103
+ esac
104
+ if [ -z "$_out" ]; then
105
+ _df_nongit_root "$_cwd"
106
+ DF_ROOT="$_DF_NONGIT_ROOT"
107
+ [ -n "$DF_ROOT" ] || DF_ROOT="$_cwd"
108
+ DF_LEDGER_ROOT="$DF_ROOT"
109
+ return 0
110
+ fi
111
+ if [ -z "$_top" ] || [ -z "$_common" ]; then
112
+ DF_ROOT="$(df_resolve_root "$_cwd")"
113
+ [ -n "$DF_ROOT" ] || DF_ROOT="$_cwd"
114
+ DF_LEDGER_ROOT="$DF_ROOT"
115
+ return 0
116
+ fi
117
+ DF_ROOT="$_top"
118
+ DF_LEDGER_ROOT="$_top"
119
+ case "$_common" in
120
+ */.git)
121
+ _main="${_common%/.git}"
122
+ # df_is_project_root forks nothing (builtin `cd -P` walks). Unavailable —
123
+ # git-marker failed to source — it is a command not found, which keeps the
124
+ # ledger in this checkout: the conservative answer.
125
+ if [ -n "$_main" ] && [ -d "$_main/.devflow" ] && df_is_project_root "$_main" 2>/dev/null; then
126
+ DF_LEDGER_ROOT="$_main"
127
+ fi
128
+ ;;
129
+ esac
130
+ return 0
131
+ }