devflow-kit 2.4.0 → 2.5.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 (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -3,14 +3,16 @@
3
3
  # rules that govern .devflow/.
4
4
  #
5
5
  # .devflow/ holds per-developer runtime state (memory, learning, docs, locks) —
6
- # local by default. TWO exceptions are shared via git:
6
+ # local by default. THREE exceptions are shared via git:
7
7
  # 1. Feature knowledge bases under .devflow/features/: index.md and every
8
8
  # {slug}/KNOWLEDGE.md (tracked, committed by the Knowledge agent at workflow end).
9
9
  # 2. .devflow/conventions.md — naming-convention authority written by the Git
10
10
  # learn-conventions operation; shared so the team uses one naming source.
11
+ # 3. .devflow/policy.json — the team-owned evidence policy, read from the default
12
+ # branch by resolve-evidence-policy.cjs. Devflow never writes it.
11
13
  #
12
- # Everything else under .devflow/ stays local. A user opts back out by re-adding
13
- # `.devflow/features/` or `.devflow/conventions.md` to their own .gitignore.
14
+ # Everything else under .devflow/ stays local. A user opts back out of the first two
15
+ # by re-adding `.devflow/features/` or `.devflow/conventions.md` to their own .gitignore.
14
16
  #
15
17
  # Re-including files under an ignored tree requires un-ignoring each level with a
16
18
  # `dir/*` + `!dir/keep` pair: a bare `.devflow/` excludes the directory outright,
@@ -23,9 +25,23 @@
23
25
  # Both reach this one writer so the rule is applied identically everywhere; this
24
26
  # decouples git-tracking of .devflow/ from any single feature toggle (avoids PF-014).
25
27
  #
26
- # Idempotent and O(1) after the first run via the .root-gitignore-configured-v3
27
- # marker. The marker is versioned: bumping it (v2 → v3) forces existing installs
28
- # to re-run once and upgrade their block (adds !.devflow/conventions.md line).
28
+ # Idempotent and O(1) after the first run via the .root-gitignore-configured-v5
29
+ # marker under .devflow/ (project-local). The marker is a claim, not proof, so the
30
+ # fast path also requires the block's own sentinel, the policy line AND a
31
+ # .claudeignore entry to be present; a .gitignore that lost any of them (a merge
32
+ # resolution, a hand edit) is healed on the next run. A v4-marked project misses the
33
+ # fast path, gains the policy line and is re-stamped v5.
34
+ #
35
+ # D-GITIGNORE-V5: the block is detected ONLY by its devflow-unique sentinel
36
+ # `!.devflow/conventions.md` — NEVER by `.claudeignore` or `!.devflow/policy.json`,
37
+ # which users legitimately author themselves. Those two are completion lines: each is
38
+ # topped up when missing, in block order (policy, then .claudeignore). Keying presence
39
+ # off a user-authored line inverts both halves of the contract: projects that already
40
+ # carry that line never receive the carve-out (so .devflow/ runtime files leak into
41
+ # git), and re-appending `.claudeignore` after a user's `!.claudeignore` reverses
42
+ # their intent under gitignore's last-match-wins. Hence: a `.claudeignore` OR
43
+ # `!.claudeignore` entry means "the user owns that line" — emit the block without it.
44
+ # All matching is whole-line and whitespace-tolerant; never substring.
29
45
  #
30
46
  # Usage: source ensure-root-gitignore "$PROJECT_ROOT"
31
47
  # Sourced helper: uses `return` (never exit), _ERG_-prefixed locals (never clobbers
@@ -34,31 +50,59 @@
34
50
  [ -z "$1" ] && return 1
35
51
 
36
52
  _ERG_DEVFLOW_DIR="$1/.devflow"
37
- _ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v3"
53
+ _ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v5"
38
54
  _ERG_GITIGNORE="$1/.gitignore"
39
55
 
40
- # Fast-path with verification: marker normally means the block is installed, but
41
- # the marker is a claim, not proof — a merge-conflict resolution may have dropped
42
- # the block. Gate the fast-path return on the v3 sentinel actually being present
43
- # in .gitignore. Idempotent: sentinel present → return 0; sentinel absent → heal.
44
- if [ -f "$_ERG_MARKER" ]; then
45
- grep -qF '!.devflow/conventions.md' "$_ERG_GITIGNORE" 2>/dev/null && return 0
46
- # Sentinel absent — marker is stale; fall through to re-apply the block.
56
+ # Whole-line, whitespace-tolerant matchers. `*` and `.` are escaped so the ERE
57
+ # matches the literal gitignore patterns.
58
+ _ERG_RE_OPTOUT='^[[:space:]]*/\.devflow/[[:space:]]*$'
59
+ _ERG_RE_LEGACY='^[[:space:]]*\.devflow/[[:space:]]*$'
60
+ _ERG_RE_SENTINEL='^[[:space:]]*!\.devflow/conventions\.md[[:space:]]*$'
61
+ _ERG_RE_SENTINEL_V2='^[[:space:]]*!\.devflow/features/\*/KNOWLEDGE\.md[[:space:]]*$'
62
+ _ERG_RE_POLICY='^[[:space:]]*!\.devflow/policy\.json[[:space:]]*$'
63
+ _ERG_RE_CLAUDEIGNORE='^[[:space:]]*!?\.claudeignore[[:space:]]*$'
64
+
65
+ # Drop every legacy marker beside v5 — builtin `[ -e ]` tests, so rm forks only for a
66
+ # marker that exists. Called from the fast path and from the on-success stamp below,
67
+ # so an older devflow's marker is dropped whichever path a run takes.
68
+ # Always returns 0 (never fails the caller under `set -e`): the last iteration's
69
+ # `[ -e ]` test is false whenever the unversioned marker is absent (the common case),
70
+ # and a function call's own exit status is NOT exempt from errexit the way an inlined
71
+ # for-loop's is — unlike this same loop when it sat directly inside the caller's `{ }`.
72
+ _erg_drop_legacy_markers() {
73
+ for _ERG_OLD in -v4 -v3 -v2 ''; do
74
+ [ -e "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" ] \
75
+ && rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" 2>/dev/null
76
+ done
77
+ return 0
78
+ }
79
+
80
+ # Fast path: converged only when the marker is stamped AND the block sentinel is
81
+ # present AND the policy line is present AND a .claudeignore entry exists. Every
82
+ # other state falls through and recomputes.
83
+ if [ -f "$_ERG_MARKER" ] \
84
+ && grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null \
85
+ && grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null \
86
+ && grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null; then
87
+ _erg_drop_legacy_markers
88
+ return 0
47
89
  fi
48
90
 
49
91
  # The marker lives under .devflow/, so the directory must exist before we touch it.
50
92
  # (ensure-devflow-init creates it earlier; session-start-context may reach here first.)
51
93
  mkdir -p "$_ERG_DEVFLOW_DIR" 2>/dev/null || return 1
52
94
 
53
- # The carve-out block, built once into _ERG_BLOCK (emitted on create and append).
54
- # Keep byte-identical to ensureDevflowGitignore in src/targets/claude-code/post-install.ts.
55
- # D-GITIGNORE-V3: v3 adds !.devflow/conventions.md (naming authority, git-tracked).
56
- printf -v _ERG_BLOCK '%s\n' \
95
+ # The carve-out block, built once. _ERG_BLOCK_NO_CI is the block a project that owns
96
+ # its own .claudeignore entry receives; _ERG_BLOCK is that plus the final line.
97
+ # Both trail a newline. Keep byte-identical to DEVFLOW_GITIGNORE_BLOCK and
98
+ # DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE in
99
+ # src/targets/claude-code/post-install.ts.
100
+ printf -v _ERG_BLOCK_NO_CI '%s\n' \
57
101
  '# Devflow runtime data — local by default (memory, learning, docs, locks).' \
58
- '# Two exceptions are shared via git: feature knowledge bases under .devflow/features/' \
59
- '# (index.md and every {slug}/KNOWLEDGE.md) and .devflow/conventions.md (naming' \
60
- '# authority). To stop sharing, re-add `.devflow/features/` or `.devflow/conventions.md`' \
61
- '# to your own .gitignore.' \
102
+ '# Shared via git: feature knowledge bases under .devflow/features/ (index.md and' \
103
+ '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority) and' \
104
+ '# .devflow/policy.json (evidence policy). To stop sharing the first two, re-add' \
105
+ '# `.devflow/features/` or `.devflow/conventions.md` to your own .gitignore.' \
62
106
  '.devflow/*' \
63
107
  '!.devflow/features/' \
64
108
  '.devflow/features/*' \
@@ -66,51 +110,108 @@ printf -v _ERG_BLOCK '%s\n' \
66
110
  '!.devflow/features/*/' \
67
111
  '.devflow/features/*/*' \
68
112
  '!.devflow/features/*/KNOWLEDGE.md' \
69
- '!.devflow/conventions.md'
113
+ '!.devflow/conventions.md' \
114
+ '!.devflow/policy.json'
115
+ printf -v _ERG_LINE_SENTINEL '%s\n' '!.devflow/conventions.md'
116
+ printf -v _ERG_LINE_POLICY '%s\n' '!.devflow/policy.json'
117
+ printf -v _ERG_LINE_CLAUDEIGNORE '%s\n' '.claudeignore'
118
+ _ERG_BLOCK="$_ERG_BLOCK_NO_CI$_ERG_LINE_CLAUDEIGNORE"
70
119
 
71
- _ERG_OK=0
72
- if [ ! -f "$_ERG_GITIGNORE" ]; then
73
- # No .gitignore yet — create it with the block.
74
- printf '%s' "$_ERG_BLOCK" > "$_ERG_GITIGNORE" && _ERG_OK=1
75
- elif grep -qF '!.devflow/conventions.md' "$_ERG_GITIGNORE"; then
76
- # v3 carve-out already present — nothing to do.
77
- _ERG_OK=1
78
- elif grep -qE '^/\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE"; then
120
+ # Does the file already carry the policy line, or a .claudeignore entry of the user's
121
+ # own (either form)? Computed BEFORE the legacy filter below, which rewrites the file.
122
+ _ERG_HAS_POLICY=0
123
+ grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_POLICY=1
124
+ _ERG_HAS_CI=0
125
+ grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_CI=1
126
+
127
+ # The completion lines this file lacks, in block order (policy, then .claudeignore).
128
+ # Mirrors missingCompletionLines in the TS twin.
129
+ _ERG_COMPLETION=''
130
+ if [ "$_ERG_HAS_POLICY" = 0 ]; then
131
+ _ERG_COMPLETION="$_ERG_LINE_POLICY"
132
+ fi
133
+ if [ "$_ERG_HAS_CI" = 0 ]; then
134
+ _ERG_COMPLETION="$_ERG_COMPLETION$_ERG_LINE_CLAUDEIGNORE"
135
+ fi
136
+
137
+ # Classify: noop (nothing to do) | continue (extend an existing block) |
138
+ # block (install a fresh block) | fail (a rewrite failed; leave the file alone).
139
+ _ERG_MODE=noop
140
+ _ERG_APPEND=''
141
+ if grep -qE "$_ERG_RE_OPTOUT" "$_ERG_GITIGNORE" 2>/dev/null; then
79
142
  # User-authored `/.devflow/` (leading slash) — respect it; don't force the carve-out.
80
- # Checked BEFORE the v2 sentinel so a file containing both keeps the user-authored
81
- # entry (matches ensureDevflowGitignore TS order: v3→/.devflow/→v2→wholesale→append).
82
- _ERG_OK=1
83
- elif grep -qF '!.devflow/features/*/KNOWLEDGE.md' "$_ERG_GITIGNORE"; then
84
- # v2→v3 upgrade: v2 sentinel present but conventions.md line absent — append just the
85
- # missing line. A .gitignore whose last byte is not a newline would otherwise fuse the
86
- # appended line onto the last existing one, corrupting BOTH patterns; the sibling append
87
- # branches below get this for free by emitting a leading '\n'. `tail -c 1` yields the
88
- # empty string when the file already ends in a newline (command substitution strips it).
89
- if [ -n "$(tail -c 1 "$_ERG_GITIGNORE" 2>/dev/null)" ]; then
90
- printf '\n' >> "$_ERG_GITIGNORE"
143
+ # Checked FIRST so a file containing both it and a sentinel keeps the user's entry.
144
+ _ERG_MODE=noop
145
+ elif grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null; then
146
+ # Block installed (v3 and later) — top up only the completion lines it lacks.
147
+ if [ -n "$_ERG_COMPLETION" ]; then
148
+ _ERG_MODE=continue
149
+ _ERG_APPEND="$_ERG_COMPLETION"
91
150
  fi
92
- printf '!.devflow/conventions.md\n' >> "$_ERG_GITIGNORE" && _ERG_OK=1
93
- elif grep -qE '^\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE"; then
94
- # Upgrade our legacy wholesale entry: strip the bare `.devflow/` line and our old
95
- # comment, then append the carve-out block. Portable (grep filter + mv; no sed -i,
96
- # which differs on BSD/GNU). An empty filter result is fine — the block is appended
97
- # regardless; a leading blank is added only when prior content survived.
98
- _ERG_TMP="$_ERG_GITIGNORE.devflow-tmp.$$"
99
- grep -vE '^\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE" 2>/dev/null \
100
- | grep -vF '# Devflow runtime data (local by default; remove to share via git)' \
101
- > "$_ERG_TMP" 2>/dev/null
102
- { [ -s "$_ERG_TMP" ] && printf '\n'; printf '%s' "$_ERG_BLOCK"; } >> "$_ERG_TMP" \
103
- && mv "$_ERG_TMP" "$_ERG_GITIGNORE" && _ERG_OK=1
104
- [ "$_ERG_OK" = 1 ] || rm -f "$_ERG_TMP" 2>/dev/null
151
+ elif grep -qE "$_ERG_RE_SENTINEL_V2" "$_ERG_GITIGNORE" 2>/dev/null; then
152
+ # v2 block installed — append the lines it lacks, in block order.
153
+ _ERG_MODE=continue
154
+ _ERG_APPEND="$_ERG_LINE_SENTINEL$_ERG_COMPLETION"
105
155
  else
106
- # .gitignore exists but has no Devflow entry — append the block.
107
- { printf '\n'; printf '%s' "$_ERG_BLOCK"; } >> "$_ERG_GITIGNORE" && _ERG_OK=1
156
+ # No devflow block — install one, omitting the .claudeignore line the user owns.
157
+ _ERG_MODE=block
158
+ if [ "$_ERG_HAS_CI" = 1 ]; then
159
+ _ERG_APPEND="$_ERG_BLOCK_NO_CI"
160
+ else
161
+ _ERG_APPEND="$_ERG_BLOCK"
162
+ fi
163
+ if grep -qE "$_ERG_RE_LEGACY" "$_ERG_GITIGNORE" 2>/dev/null; then
164
+ # Upgrade our legacy wholesale entry: strip the bare `.devflow/` line and our old
165
+ # comment first, then fall through to the shared append below. Portable (grep
166
+ # filter + mv; no sed -i, which differs on BSD/GNU).
167
+ # The pipeline's own status is deliberately ignored: grep -v exits 1 when it
168
+ # emits no lines, which is the correct outcome for a .gitignore whose only
169
+ # content was the legacy entry. The redirect creates _ERG_TMP either way.
170
+ _ERG_TMP="$_ERG_GITIGNORE.devflow-tmp.$$"
171
+ grep -vE "$_ERG_RE_LEGACY" "$_ERG_GITIGNORE" 2>/dev/null \
172
+ | grep -vF '# Devflow runtime data (local by default; remove to share via git)' \
173
+ > "$_ERG_TMP" 2>/dev/null
174
+ if [ -f "$_ERG_TMP" ] && mv "$_ERG_TMP" "$_ERG_GITIGNORE"; then
175
+ :
176
+ else
177
+ rm -f "$_ERG_TMP" 2>/dev/null
178
+ _ERG_MODE=fail
179
+ fi
180
+ fi
108
181
  fi
109
182
 
110
- # On success, stamp the current-format v3 marker and drop legacy markers.
183
+ # The two append shapes, mirrored byte-for-byte by the TS twin's appendLines and
184
+ # appendBlock. `tail -c 1` yields the empty string when the file already ends in a
185
+ # newline (command substitution strips it), so a file whose last byte is not a
186
+ # newline gets one before the appended text fuses onto its last line. Existing
187
+ # trailing newlines are preserved verbatim — no trimming, no blank-line dedupe.
188
+ _ERG_OK=0
189
+ case "$_ERG_MODE" in
190
+ noop)
191
+ _ERG_OK=1
192
+ ;;
193
+ continue)
194
+ if [ -n "$(tail -c 1 "$_ERG_GITIGNORE" 2>/dev/null)" ]; then
195
+ printf '\n' >> "$_ERG_GITIGNORE"
196
+ fi
197
+ printf '%s' "$_ERG_APPEND" >> "$_ERG_GITIGNORE" && _ERG_OK=1
198
+ ;;
199
+ block)
200
+ if [ ! -s "$_ERG_GITIGNORE" ]; then
201
+ # Absent or empty — the block is the whole file, with no leading blank line.
202
+ printf '%s' "$_ERG_APPEND" > "$_ERG_GITIGNORE" && _ERG_OK=1
203
+ else
204
+ if [ -n "$(tail -c 1 "$_ERG_GITIGNORE" 2>/dev/null)" ]; then
205
+ printf '\n' >> "$_ERG_GITIGNORE"
206
+ fi
207
+ { printf '\n'; printf '%s' "$_ERG_APPEND"; } >> "$_ERG_GITIGNORE" && _ERG_OK=1
208
+ fi
209
+ ;;
210
+ esac
211
+
212
+ # On success, stamp the current-format v5 marker and drop legacy markers.
111
213
  [ "$_ERG_OK" = 1 ] && {
112
214
  touch "$_ERG_MARKER"
113
- rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v2" 2>/dev/null
114
- rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured" 2>/dev/null
215
+ _erg_drop_legacy_markers
115
216
  }
116
217
  return 0
@@ -29,9 +29,11 @@ LOG_FILE="$LOG_DIR/.${_HOOK_LOG_NAME}.log"
29
29
 
30
30
  # Size guard: truncate log to tail 1MB when it exceeds 2MB (runs once per hook invocation)
31
31
  # stat -f%z (macOS/BSD) → stat -c%s (GNU/Linux) → wc -c (last resort), matching debug-trace pattern.
32
+ # `2>/dev/null` precedes `<`: redirections apply left to right, so the shell's own
33
+ # "No such file or directory" for a missing log (first run for a cwd) lands in /dev/null.
32
34
  _LOG_SIZE=$(stat -f%z "$LOG_FILE" 2>/dev/null) \
33
35
  || _LOG_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null) \
34
- || _LOG_SIZE=$(wc -c < "$LOG_FILE" 2>/dev/null) \
36
+ || _LOG_SIZE=$(wc -c 2>/dev/null < "$LOG_FILE") \
35
37
  || _LOG_SIZE=0
36
38
  if [ -f "$LOG_FILE" ] && [ "$_LOG_SIZE" -gt 2097152 ]; then
37
39
  _LTMP="$LOG_FILE.tmp.$$"
@@ -24,7 +24,12 @@
24
24
  // session-output <context> Build SessionStart output envelope
25
25
  // prompt-output <context> Build UserPromptSubmit output envelope
26
26
  // backup-construct Build pre-compact backup JSON from --arg pairs
27
- // assign-anchor <type> <obs_id> Claim next ADR/PF number, render both .md files
27
+ // assign-anchor <type> <obs_id> [--allow-collision]
28
+ // Claim next ADR/PF number, render both .md files.
29
+ // Refuses on a pre-mint citation collision (E4) unless
30
+ // --allow-collision is passed.
31
+ // next-anchor <type> Read-only: print the next candidate ADR/PF id and
32
+ // any pre-mint collision hits; mutates nothing (E4)
28
33
  // retire-anchor <anchor_id> <status> Flip ledger row status, re-render both .md files
29
34
  // refresh-anchor <anchor_id> Re-project log obs onto ledger row, re-render
30
35
  // rotate-observations [<log>] [<arch>] Archive observing rows older than 30 days
@@ -33,6 +38,7 @@
33
38
 
34
39
  const fs = require('fs');
35
40
  const path = require('path');
41
+ const { execFileSync } = require('child_process');
36
42
 
37
43
  const op = process.argv[2];
38
44
  const args = process.argv.slice(3);
@@ -156,6 +162,149 @@ function nextAnchorFromLedger(ledgerRows, type) {
156
162
  return { anchorId: `${prefix}-${nextN}`, nextN };
157
163
  }
158
164
 
165
+ // ---------------------------------------------------------------------------
166
+ // Pre-mint collision guard (E4).
167
+ //
168
+ // A design doc can cite a design-local number ("PF-017") in tracked source
169
+ // before the ledger ever mints that same number for an unrelated entry — the
170
+ // two silently collide and nothing catches it until a human notices the text
171
+ // doesn't match. This scans the project tree for a whole-word citation of the
172
+ // candidate id BEFORE assign-anchor writes it, and refuses to mint over a hit.
173
+ // The consumer-side counterpart lives in mdl's scripts/verify-ledger-citations.mjs.
174
+ // ---------------------------------------------------------------------------
175
+
176
+ /** Directory names excluded from collision scanning at any depth (E4). */
177
+ const COLLISION_SCAN_EXCLUDED_SEGMENTS = new Set(['.git', 'node_modules', 'target', 'dist']);
178
+
179
+ /** Files larger than this are skipped during collision scanning — bounds the scan (E4). */
180
+ const COLLISION_SCAN_MAX_FILE_BYTES = 5 * 1024 * 1024;
181
+
182
+ /**
183
+ * True when a project-relative path must be excluded from collision scanning:
184
+ * the ledger's own files (`.devflow/learning/**`, self-citation is expected,
185
+ * not a collision) or any of the excluded directory segments.
186
+ *
187
+ * @param {string} relPath - path relative to the project root, either separator style
188
+ * @returns {boolean}
189
+ */
190
+ function isCollisionScanExcluded(relPath) {
191
+ const norm = relPath.split(path.sep).join('/');
192
+ if (norm === '.devflow/learning' || norm.startsWith('.devflow/learning/')) return true;
193
+ return norm.split('/').some(seg => COLLISION_SCAN_EXCLUDED_SEGMENTS.has(seg));
194
+ }
195
+
196
+ /**
197
+ * List tracked files via `git ls-files` (respects .gitignore; args passed as an
198
+ * array — never shelled through a string-built command). Throws when the
199
+ * project root is not a git working tree or the `git` binary is unavailable;
200
+ * callers fall back to `listFsWalkFiles`.
201
+ *
202
+ * @param {string} projectRoot
203
+ * @returns {string[]} project-relative paths
204
+ */
205
+ function listGitTrackedFiles(projectRoot) {
206
+ const out = execFileSync('git', ['ls-files', '-z'], {
207
+ cwd: projectRoot,
208
+ stdio: ['ignore', 'pipe', 'ignore'],
209
+ });
210
+ return out.toString('utf8').split('\0').filter(Boolean);
211
+ }
212
+
213
+ /**
214
+ * Bounded, non-recursing-into-excluded-dirs fs walk — fallback for a project
215
+ * root that is not a git working tree. The walk is bounded by construction:
216
+ * it only descends into directories actually present on disk, and never
217
+ * descends into an excluded directory at all (E4).
218
+ *
219
+ * @param {string} projectRoot
220
+ * @returns {string[]} project-relative paths
221
+ */
222
+ function listFsWalkFiles(projectRoot) {
223
+ const results = [];
224
+ const stack = [''];
225
+ while (stack.length > 0) {
226
+ const relDir = stack.pop();
227
+ const absDir = relDir ? path.join(projectRoot, relDir) : projectRoot;
228
+ let entries;
229
+ try {
230
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
231
+ } catch {
232
+ continue; // unreadable dir — best-effort scan, skip
233
+ }
234
+ for (const entry of entries) {
235
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
236
+ if (isCollisionScanExcluded(relPath)) continue;
237
+ if (entry.isDirectory()) {
238
+ stack.push(relPath);
239
+ } else if (entry.isFile()) {
240
+ results.push(relPath);
241
+ }
242
+ }
243
+ }
244
+ return results;
245
+ }
246
+
247
+ /**
248
+ * Scan the project tree for a whole-word citation of `id` (e.g. `ADR-042`),
249
+ * excluding the ledger's own files and common vendored/build directories.
250
+ * Prefers tracked files (`git ls-files`) when the project root is a git
251
+ * working tree; falls back to a bounded fs walk otherwise. Best-effort:
252
+ * unreadable, binary, or oversized files are skipped rather than failing
253
+ * the scan.
254
+ *
255
+ * @param {string} projectRoot
256
+ * @param {string} id - e.g. 'ADR-042' or 'PF-017'
257
+ * @returns {{ file: string, line: number }[]} hits, empty when no collision
258
+ */
259
+ function scanForAnchorCollision(projectRoot, id) {
260
+ let files;
261
+ try {
262
+ files = listGitTrackedFiles(projectRoot);
263
+ } catch {
264
+ files = listFsWalkFiles(projectRoot);
265
+ }
266
+
267
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
268
+ const pattern = new RegExp(`\\b${escaped}\\b`);
269
+ const hits = [];
270
+ for (const relPath of files) {
271
+ if (isCollisionScanExcluded(relPath)) continue;
272
+ const absPath = path.join(projectRoot, relPath);
273
+ let stat;
274
+ try {
275
+ stat = fs.statSync(absPath);
276
+ } catch {
277
+ continue; // race: listed then removed — best-effort scan, skip
278
+ }
279
+ if (!stat.isFile() || stat.size > COLLISION_SCAN_MAX_FILE_BYTES) continue;
280
+ let content;
281
+ try {
282
+ content = fs.readFileSync(absPath, 'utf8');
283
+ } catch {
284
+ continue; // unreadable or invalid utf8 — best-effort scan, skip
285
+ }
286
+ if (content.includes('\u0000')) continue; // binary heuristic
287
+ const lines = content.split('\n');
288
+ for (let i = 0; i < lines.length; i++) {
289
+ if (pattern.test(lines[i])) {
290
+ hits.push({ file: relPath, line: i + 1 });
291
+ }
292
+ }
293
+ }
294
+ return hits;
295
+ }
296
+
297
+ /**
298
+ * Format collision hits for a stderr/stdout report — one `file:line` per line,
299
+ * two-space indented (E4).
300
+ *
301
+ * @param {{ file: string, line: number }[]} hits
302
+ * @returns {string}
303
+ */
304
+ function formatCollisionHits(hits) {
305
+ return hits.map(h => ` ${h.file}:${h.line}`).join('\n');
306
+ }
307
+
159
308
  /**
160
309
  * Read .decisions-usage.json. Returns {version, entries} or empty default.
161
310
  * @param {string} projectRoot - Path to project root (cwd)
@@ -533,20 +682,36 @@ try {
533
682
  }
534
683
 
535
684
  // -------------------------------------------------------------------------
536
- // assign-anchor <type> <obs_id>
685
+ // assign-anchor <type> <obs_id> [--allow-collision]
537
686
  // AC-A2: Assign next anchor ID for the given type (decision|pitfall) to the
538
687
  // observation identified by obs_id in decisions-log.jsonl. Atomic under a
539
688
  // single .decisions.lock acquisition. Registers usage, re-renders both .md.
540
689
  //
690
+ // E4: before writing, refuses if the candidate id is already cited as a
691
+ // whole word somewhere in tracked source (a pre-mint collision — see
692
+ // scanForAnchorCollision above). --allow-collision skips the scan and
693
+ // mints anyway, for the human-ruled case where the citation should be
694
+ // superseded by the ledger's number.
695
+ //
541
696
  // Locking discipline: holds ONLY .decisions.lock (never .observations.lock).
542
697
  // O(anchored) — single pass for max numeric suffix (AC-P2).
543
698
  // -------------------------------------------------------------------------
544
699
  case 'assign-anchor': {
545
- const assignType = args[0]; // 'decision' or 'pitfall'
546
- const assignObsId = args[1];
700
+ const aaKnownFlags = new Set(['--allow-collision']);
701
+ const aaFlags = args.filter(a => a.startsWith('--'));
702
+ const aaUnknownFlags = aaFlags.filter(f => !aaKnownFlags.has(f));
703
+ if (aaUnknownFlags.length > 0) {
704
+ process.stderr.write(`assign-anchor: unknown flag(s): ${aaUnknownFlags.join(', ')}\n`);
705
+ process.exit(1);
706
+ }
707
+ const aaAllowCollision = aaFlags.includes('--allow-collision');
708
+ const aaPositional = args.filter(a => !a.startsWith('--'));
709
+
710
+ const assignType = aaPositional[0]; // 'decision' or 'pitfall'
711
+ const assignObsId = aaPositional[1];
547
712
 
548
713
  if (!assignType || !assignObsId) {
549
- process.stderr.write('assign-anchor: usage: assign-anchor <type> <obs_id>\n');
714
+ process.stderr.write('assign-anchor: usage: assign-anchor <type> <obs_id> [--allow-collision]\n');
550
715
  process.exit(1);
551
716
  }
552
717
  if (assignType !== 'decision' && assignType !== 'pitfall') {
@@ -565,6 +730,22 @@ try {
565
730
  // Compute next anchor — O(anchored), single pass
566
731
  const { anchorId: aaAnchorId } = nextAnchorFromLedger(aaLedgerRows, assignType);
567
732
 
733
+ // E4: pre-mint collision guard — refuse if the candidate id is already
734
+ // cited (as a whole word) somewhere in tracked source with a different
735
+ // meaning, before any ledger write. Never auto-skip to the next free
736
+ // number — the collision is a human call (rename the citation, or
737
+ // rerun with --allow-collision to mint over it deliberately).
738
+ if (!aaAllowCollision) {
739
+ const aaCollisionHits = scanForAnchorCollision(aaProjectRoot, aaAnchorId);
740
+ if (aaCollisionHits.length > 0) {
741
+ throw new Error(
742
+ `assign-anchor: '${aaAnchorId}' is already cited in source with a different ` +
743
+ `meaning; resolve the collision before minting (or pass --allow-collision):\n` +
744
+ formatCollisionHits(aaCollisionHits)
745
+ );
746
+ }
747
+ }
748
+
568
749
  // Read observation from log
569
750
  let aaLogEntries = parseLedger(aaLogPath);
570
751
  const aaObsIdx = aaLogEntries.findIndex(e => e.id === assignObsId);
@@ -648,6 +829,41 @@ try {
648
829
  break;
649
830
  }
650
831
 
832
+ // -------------------------------------------------------------------------
833
+ // next-anchor <type>
834
+ // E4: Read-only preview of what assign-anchor would mint next — no lock
835
+ // acquired, no file written, no usage entry registered. Prints the
836
+ // candidate id and, when a pre-mint collision guard would fire, its
837
+ // file:line hits — so a caller can check before committing to assign-anchor.
838
+ // -------------------------------------------------------------------------
839
+ case 'next-anchor': {
840
+ const naType = args[0];
841
+
842
+ if (!naType) {
843
+ process.stderr.write('next-anchor: usage: next-anchor <type>\n');
844
+ process.exit(1);
845
+ }
846
+ if (naType !== 'decision' && naType !== 'pitfall') {
847
+ process.stderr.write(`next-anchor: type must be 'decision' or 'pitfall', got '${naType}'\n`);
848
+ process.exit(1);
849
+ }
850
+
851
+ const naProjectRoot = process.cwd();
852
+ const naLedgerRows = parseLedger(getDecisionsLedgerPath(naProjectRoot));
853
+ const { anchorId: naAnchorId } = nextAnchorFromLedger(naLedgerRows, naType);
854
+ const naHits = scanForAnchorCollision(naProjectRoot, naAnchorId);
855
+
856
+ process.stdout.write(naAnchorId + '\n');
857
+ if (naHits.length > 0) {
858
+ process.stderr.write(
859
+ `next-anchor: '${naAnchorId}' is already cited in source — collision hits:\n` +
860
+ formatCollisionHits(naHits) + '\n'
861
+ );
862
+ process.exit(1);
863
+ }
864
+ break;
865
+ }
866
+
651
867
  // -------------------------------------------------------------------------
652
868
  // retire-anchor <anchor_id> <status>
653
869
  // AC-A3, AC-F5, AC-F7: Flip decisions_status on the ledger row. Idempotent.
@@ -906,5 +1122,7 @@ if (typeof module !== 'undefined' && module.exports) {
906
1122
  initDecisionsContent,
907
1123
  nextAnchorFromLedger,
908
1124
  rotateObservations,
1125
+ scanForAnchorCollision,
1126
+ isCollisionScanExcluded,
909
1127
  };
910
1128
  }
@@ -43,7 +43,7 @@ function getDocsDir(projectRoot) {
43
43
  // Feature config (neutral .devflow root — not inside learning/)
44
44
  // ---------------------------------------------------------------------------
45
45
 
46
- /** .devflow/config.json — feature toggles {memory, learning, knowledge} */
46
+ /** .devflow/config.json — per-repo facts {reviewPublication, tracker override} */
47
47
  function getFeatureConfigPath(projectRoot) {
48
48
  return path.join(projectRoot, '.devflow', 'config.json');
49
49
  }
@@ -32,15 +32,19 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
32
32
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
33
33
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
34
34
 
35
+ # The machine-wide manifest (the memory switch, D-FEATURES-MACHINE-WIDE in
36
+ # queue-append) is user-scope: resolve it from the inherited DEVFLOW_DIR (or
37
+ # ~/.devflow) BEFORE the project-scoped assignment below shadows that value.
38
+ DEVFLOW_MANIFEST="${DEVFLOW_DIR:-$HOME/.devflow}/manifest.json"
35
39
  DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
36
40
  MEMORY_DIR="$DEVFLOW_DIR/memory"
37
41
 
38
- # Read feature config -- memory:false gates this hook entirely (ADR-001).
39
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
40
- MEMORY_ENABLED="true"
41
- if [ -f "$FEATURE_CONFIG" ]; then
42
- MEMORY_ENABLED=$(json_field_file "$FEATURE_CONFIG" "memory" "true")
43
- fi
42
+ # Memory gate: the machine-wide switch gates this hook entirely, read by the
43
+ # helper every memory/learning gate shares (D-FEATURES-MACHINE-WIDE, see
44
+ # queue-append). The per-repo config is never consulted.
45
+ source "$SCRIPT_DIR/queue-append" || { echo "memory-worker: failed to source queue-append" >&2; exit 1; }
46
+ queue_read_gates "$DEVFLOW_MANIFEST"
47
+ MEMORY_ENABLED="$_QG_MEMORY"
44
48
 
45
49
  dbg "MEMORY_ENABLED=$MEMORY_ENABLED"
46
50
 
@@ -93,8 +97,11 @@ dbg "Throttle passed: TRIGGER_AGE=${TRIGGER_AGE}s >= 120s — spawning worker"
93
97
  # Touch trigger BEFORE spawning (prevents concurrent Stop hooks from double-spawning)
94
98
  touch "$TRIGGER_FILE" 2>/dev/null || true
95
99
 
96
- # Spawn detached worker — nohup + disown so it survives the Stop hook process exit
97
- nohup "$UPDATER" "$CWD" </dev/null >>/dev/null 2>&1 & disown
100
+ # Spawn detached worker — nohup + disown so it survives the Stop hook process exit.
101
+ # The manifest path is handed over explicitly: the worker re-checks the memory
102
+ # switch after spawn, and by now DEVFLOW_DIR names the project .devflow (and is
103
+ # what the worker would inherit, were it exported), not the devflow-global root.
104
+ nohup "$UPDATER" "$CWD" "$DEVFLOW_MANIFEST" </dev/null >>/dev/null 2>&1 & disown
98
105
 
99
106
  log "Spawned background-memory-update worker (CWD=$CWD)"
100
107
  dbg "=== HOOK COMPLETE ==="
@@ -43,20 +43,24 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
43
43
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
44
44
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
45
45
 
46
+ # The machine-wide manifest (the memory switch, D-FEATURES-MACHINE-WIDE in
47
+ # queue-append) is user-scope: resolve it from the inherited DEVFLOW_DIR (or
48
+ # ~/.devflow) BEFORE the project-scoped assignment below shadows that value.
49
+ DEVFLOW_MANIFEST="${DEVFLOW_DIR:-$HOME/.devflow}/manifest.json"
46
50
  DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
47
51
  MEMORY_DIR="$DEVFLOW_DIR/memory"
48
52
 
49
53
  # Normal logging
50
54
  source "$SCRIPT_DIR/hook-log-init" "pre-compact-memory"
51
55
 
52
- # Check feature config — single source of truth for memory enabled/disabled (ADR-001).
53
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
54
- if [ -f "$FEATURE_CONFIG" ]; then
55
- MEMORY_ENABLED=$(json_field_file "$FEATURE_CONFIG" "memory" "true")
56
- if [ "$MEMORY_ENABLED" = "false" ]; then
57
- dbg "EXIT: memory disabled in feature config"
58
- exit 0
59
- fi
56
+ # Memory gate: the machine-wide switch, read by the helper every memory/learning
57
+ # gate shares (D-FEATURES-MACHINE-WIDE, see queue-append). The per-repo config
58
+ # is never consulted.
59
+ source "$SCRIPT_DIR/queue-append" || { echo "pre-compact-memory: failed to source queue-append" >&2; exit 1; }
60
+ queue_read_gates "$DEVFLOW_MANIFEST"
61
+ if [ "$_QG_MEMORY" != "true" ]; then
62
+ dbg "EXIT: memory disabled machine-wide"
63
+ exit 0
60
64
  fi
61
65
 
62
66
  # Auto-create .devflow/ and ensure .gitignore entries (idempotent after first run)