devflow-kit 2.5.0 → 3.0.1

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 (158) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +44 -19
  3. package/dist/agents/git.md +13 -15
  4. package/dist/cli/commands/ambient.js +160 -145
  5. package/dist/cli/commands/capture.js +29 -55
  6. package/dist/cli/commands/compliance.js +32 -61
  7. package/dist/cli/commands/context.js +17 -32
  8. package/dist/cli/commands/debug.js +65 -26
  9. package/dist/cli/commands/flags.js +3 -3
  10. package/dist/cli/commands/hud.js +34 -10
  11. package/dist/cli/commands/init-seed.js +40 -4
  12. package/dist/cli/commands/init.js +249 -271
  13. package/dist/cli/commands/install-report.js +10 -15
  14. package/dist/cli/commands/knowledge/index.js +1 -1
  15. package/dist/cli/commands/knowledge/toggle.js +11 -3
  16. package/dist/cli/commands/learning.js +52 -37
  17. package/dist/cli/commands/legacy-hooks.js +11 -14
  18. package/dist/cli/commands/memory.js +67 -78
  19. package/dist/cli/commands/proxy.js +23 -41
  20. package/dist/cli/commands/security.js +5 -13
  21. package/dist/cli/commands/skills.js +21 -3
  22. package/dist/cli/commands/tracker.js +100 -228
  23. package/dist/cli/commands/uninstall.js +343 -138
  24. package/dist/commands/bug-analysis.md +38 -12
  25. package/dist/commands/code-review.md +70 -21
  26. package/dist/commands/debug.md +37 -7
  27. package/dist/commands/dynamic-build.md +66 -17
  28. package/dist/commands/dynamic-plan.md +19 -8
  29. package/dist/commands/dynamic-profile.md +24 -10
  30. package/dist/commands/dynamic-tickets.md +22 -11
  31. package/dist/commands/explore.md +37 -7
  32. package/dist/commands/implement.md +96 -32
  33. package/dist/commands/plan.md +62 -19
  34. package/dist/commands/release.md +2 -2
  35. package/dist/commands/research.md +34 -8
  36. package/dist/commands/resolve.md +65 -17
  37. package/dist/commands/self-review.md +45 -9
  38. package/dist/core/compliance-compose.js +27 -27
  39. package/dist/core/evidence-policy.js +240 -24
  40. package/dist/core/feature-config.js +94 -25
  41. package/dist/core/feature-switch.js +1 -1
  42. package/dist/core/flags.js +30 -2
  43. package/dist/core/fs-atomic.js +27 -0
  44. package/dist/core/hook-log-dirs.js +104 -0
  45. package/dist/core/learning-tuning-config.js +5 -3
  46. package/dist/core/ledger-root.js +102 -0
  47. package/dist/core/manifest.js +6 -4
  48. package/dist/core/mds-variants.js +34 -97
  49. package/dist/core/migrations.js +49 -23
  50. package/dist/core/plugins.js +5 -4
  51. package/dist/core/project-paths.js +0 -17
  52. package/dist/core/same-location.js +25 -0
  53. package/dist/core/tracker.js +226 -139
  54. package/dist/hud/components/config-counts.js +15 -4
  55. package/dist/hud/components/learning-counts.js +14 -0
  56. package/dist/hud/config.js +2 -1
  57. package/dist/hud/cost-history.js +2 -4
  58. package/dist/hud/git.js +52 -7
  59. package/dist/hud/index.js +7 -9
  60. package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
  61. package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
  62. package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
  63. package/dist/skills/git/references/tracker/_mcp.md +1 -1
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
  65. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
  66. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
  67. package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
  68. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
  70. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
  71. package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
  74. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
  76. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
  77. package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
  80. package/dist/targets/claude-code/claude-paths.js +59 -57
  81. package/dist/targets/claude-code/compliance-install.js +49 -65
  82. package/dist/targets/claude-code/hooks.js +108 -3
  83. package/dist/targets/claude-code/installer.js +30 -57
  84. package/dist/targets/claude-code/post-install.js +246 -139
  85. package/dist/targets/claude-code/tracker-install.js +38 -65
  86. package/package.json +5 -4
  87. package/src/assets/agents/code.md +4 -3
  88. package/src/assets/agents/design.md +1 -0
  89. package/src/assets/agents/git.mds +55 -57
  90. package/src/assets/agents/knowledge.md +2 -2
  91. package/src/assets/agents/review.md +3 -1
  92. package/src/assets/agents/tracker.md +37 -30
  93. package/src/assets/commands/_partials/_compliance.mds +19 -1
  94. package/src/assets/commands/_partials/_decisions.mds +15 -3
  95. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  96. package/src/assets/commands/_partials/_engine.mds +2 -2
  97. package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
  98. package/src/assets/commands/_partials/_factory.mds +1 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  100. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  101. package/src/assets/commands/_partials/_preamble.mds +1 -1
  102. package/src/assets/commands/_partials/_publication.mds +6 -2
  103. package/src/assets/commands/_partials/_settings.mds +28 -0
  104. package/src/assets/commands/_partials/_ticket_template.mds +3 -3
  105. package/src/assets/commands/_partials/_tracker.mds +4 -4
  106. package/src/assets/commands/_partials/_wave.mds +4 -4
  107. package/src/assets/commands/bug-analysis.mds +19 -17
  108. package/src/assets/commands/code-review.mds +39 -33
  109. package/src/assets/commands/debug.mds +4 -5
  110. package/src/assets/commands/dynamic-build.mds +75 -53
  111. package/src/assets/commands/dynamic-plan.mds +20 -15
  112. package/src/assets/commands/dynamic-profile.mds +24 -11
  113. package/src/assets/commands/dynamic-tickets.mds +25 -20
  114. package/src/assets/commands/explore.mds +4 -5
  115. package/src/assets/commands/implement.mds +58 -45
  116. package/src/assets/commands/plan.mds +34 -29
  117. package/src/assets/commands/release.md +2 -2
  118. package/src/assets/commands/research.mds +11 -9
  119. package/src/assets/commands/resolve.mds +41 -39
  120. package/src/assets/commands/self-review.mds +24 -25
  121. package/src/assets/mds/git/_pr.mds +61 -61
  122. package/src/assets/mds/git/_references.mds +19 -19
  123. package/src/assets/mds/tracker/_common.mds +8 -8
  124. package/src/assets/mds/tracker/_github.mds +71 -71
  125. package/src/assets/mds/tracker/_jira.mds +74 -74
  126. package/src/assets/mds/tracker/_linear.mds +75 -75
  127. package/src/assets/mds/tracker/_mcp.mds +23 -17
  128. package/src/assets/scripts/hooks/background-memory-update +35 -19
  129. package/src/assets/scripts/hooks/capture-prompt +18 -12
  130. package/src/assets/scripts/hooks/capture-question +18 -12
  131. package/src/assets/scripts/hooks/capture-turn +27 -17
  132. package/src/assets/scripts/hooks/debug-trace +11 -6
  133. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  134. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  135. package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
  136. package/src/assets/scripts/hooks/git-marker +48 -0
  137. package/src/assets/scripts/hooks/json-helper.cjs +6 -1
  138. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
  139. package/src/assets/scripts/hooks/log-paths +80 -0
  140. package/src/assets/scripts/hooks/memory-worker +17 -15
  141. package/src/assets/scripts/hooks/pre-compact-memory +41 -16
  142. package/src/assets/scripts/hooks/queue-append +104 -30
  143. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  144. package/src/assets/scripts/hooks/session-start-context +289 -122
  145. package/src/assets/scripts/hooks/session-start-memory +35 -16
  146. package/src/assets/scripts/lib/project-config.cjs +633 -0
  147. package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
  148. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  149. package/src/assets/scripts/verify-evidence.cjs +1 -1
  150. package/src/assets/skills/compliance/SKILL.md +2 -2
  151. package/src/assets/skills/docs-framework/SKILL.md +6 -7
  152. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  153. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  154. package/src/assets/skills/git/references/github-api.md +9 -9
  155. package/src/assets/skills/git/references/patterns.md +1 -1
  156. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  157. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  158. package/src/targets/claude-code/templates/managed-settings.json +25 -9
@@ -2,9 +2,11 @@
2
2
 
3
3
  # SessionStart Hook: Cross-Feature Context Injection
4
4
  # Always-on hook that injects learning context as additionalContext. Sections
5
- # 1–2 are gated by the machine-wide `features.learning` switch in
6
- # ~/.devflow/manifest.json (D-FEATURES-MACHINE-WIDE) — this hook itself is never
7
- # disabled.
5
+ # 1–2 are gated by the `features.learning` switch in ~/.devflow/manifest.json,
6
+ # which this checkout's project.json / config.json can only narrow
7
+ # (D-FEATURES-NARROW-ONLY) — this hook itself is never disabled. They and the .gitignore carve-out run only in a git project that is
8
+ # not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
9
+ # the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
8
10
  #
9
11
  # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines).
10
12
  # Section 2: Learning maintenance directive — when captured turns are pending in
@@ -12,10 +14,14 @@
12
14
  # the main model to spawn the background Learning agent with the resolved model.
13
15
  # The agent claims the queue itself and queue emptiness is the natural gate,
14
16
  # so there is no throttle here.
15
- # Section 3: Tracker setup directive — when the machine's manifest names a
16
- # non-GitHub issue tracker and no ~/.devflow/tracker.md has been inferred for it
17
- # yet, instructs the main model to spawn the background Tracker agent. Gated on a
18
- # zero-byte presence sentinel so a GitHub user pays one stat and zero forks.
17
+ # Section 3: Tracker setup directive — when the issue tracker in effect for this
18
+ # project is not GitHub and no ~/.devflow/tracker/{provider}.md has been inferred
19
+ # for it yet, instructs the main model to spawn the background Tracker agent. The
20
+ # machine provider comes from a sentinel read with a builtin, so a GitHub user —
21
+ # and a jira machine whose conventions are learned — forks nothing.
22
+ # Section 4: Legacy install notice — one line when this repository still carries
23
+ # a retired project-local devflow install (its .claude/settings.json registers
24
+ # devflow hooks), pointing at `devflow uninstall --scope local`.
19
25
 
20
26
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
21
27
  dbg() { :; }
@@ -52,36 +58,53 @@ devflow_debug_set_cwd "$CWD"
52
58
  dbg "CWD=$CWD"
53
59
 
54
60
  # Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
55
- # hook runs with a CWD inside .devflow/...). Empty → fall back to CWD.
61
+ # hook runs with a CWD inside .devflow/...). One git call yields both roots
62
+ # (resolve-project-root): PROJECT_ROOT is this checkout's toplevel, LEDGER_ROOT the
63
+ # main worktree when this is a linked worktree whose main checkout already has a
64
+ # .devflow (D-LEDGER-MAIN-WORKTREE). Empty → fall back to CWD.
56
65
  source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
57
- PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
58
- [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
66
+ df_resolve_roots "$CWD" 2>/dev/null || true
67
+ PROJECT_ROOT="${DF_ROOT:-$CWD}"
68
+ LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
69
+
70
+ # D-HOOKS-GIT-ONLY (git-marker): per-project work — the .gitignore carve-out and
71
+ # Sections 1–2 — happens only for a git project that is not the home directory.
72
+ # Outside one there is no repository for the carve-out to protect, so writing it
73
+ # would litter whatever directory Claude Code was launched from, and at `~` the
74
+ # project data would land inside the machine root. df_is_project_root is two
75
+ # builtin walks, so this gate forks nothing. Section 3 keeps its own marker gate below: tracker
76
+ # setup is machine-scoped and is not project scaffolding.
77
+ source "$SCRIPT_DIR/git-marker" 2>/dev/null || true
78
+ PROJECT_OK=""
79
+ if df_is_project_root "$PROJECT_ROOT" 2>/dev/null; then
80
+ PROJECT_OK="yes"
81
+ else
82
+ dbg "project work skipped: not a git project, or the project root is HOME"
83
+ fi
59
84
 
60
85
  # Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
61
- # session regardless of feature toggles, so memory-off projects (learning/knowledge
62
- # only) still get .devflow/ ignored — this is the memory-independent path that fixes
63
- # the gitignore/memory coupling (PF-014). Single source of truth: ensure-root-gitignore.
64
- # Soft-fail: a gitignore write must never block context injection. Marker keeps it O(1).
65
- [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ] && source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
86
+ # git-project session regardless of feature toggles, so memory-off projects
87
+ # (learning/knowledge only) still get .devflow/ ignored — this is the
88
+ # memory-independent path that fixes the gitignore/memory coupling (PF-014). Single
89
+ # source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
90
+ # block context injection. Marker keeps it O(1).
91
+ [ -n "$PROJECT_OK" ] && [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ] && source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
66
92
 
67
93
  CONTEXT=""
68
94
 
69
- # The devflow-GLOBAL root (~/.devflow), honouring the DEVFLOW_DIR env override —
70
- # the ensure-proxy:58-60 idiom. Captured HERE, above the project-scoped
71
- # DEVFLOW_DIR assignment that shadows the inherited value, because Section 3's
72
- # tracker files and the manifest the learning gate reads are user-scope and not
73
- # project-scope. (Known divergence,
74
- # deliberately not propagated: the global learning.json read in Section 2
75
- # hardcodes $HOME/.devflow and ignores this override.)
76
- TRACKER_DEVFLOW_DIR="${DEVFLOW_DIR:-$HOME/.devflow}"
95
+ # The devflow machine root, always $HOME/.devflow (D-ONE-HOME): Section 3's
96
+ # tracker files and the manifest the learning gate reads are machine-wide, not
97
+ # project-scoped (the project's own .devflow is PROJECT_DEVFLOW_DIR below).
98
+ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
77
99
 
78
100
  # The shape gate for every path this hook interpolates into a directive.
79
101
  #
80
- # Sections 2 and 3 embed $PROJECT_ROOT — and Section 3 also $TRACKER_DEVFLOW_DIR —
81
- # inside a double-quoted `prompt: "..."` string the model reads out of
82
- # additionalContext. The provider and model TOKENS in those directives are admitted
83
- # by positive allowlists, and so are these: the gate is the POSITIVE shape
84
- # ^[A-Za-z0-9/._-]+$, expressed as "rejects if any character falls outside it".
102
+ # Section 2 embeds $LEDGER_ROOT; Section 3 embeds $PROJECT_ROOT and
103
+ # $TRACKER_DEVFLOW_DIR — each inside a double-quoted `prompt: "..."` string the
104
+ # model reads out of additionalContext. The provider and model TOKENS in those
105
+ # directives are admitted by positive allowlists, and so are these: the gate is the
106
+ # POSITIVE shape ^[A-Za-z0-9/._+-]+$, expressed as "rejects if any character falls
107
+ # outside it".
85
108
  #
86
109
  # Positive, not a denylist of the characters that are known to hurt. A denylist
87
110
  # enumerates the injections someone thought of — `"` closes the prompt string, `\`
@@ -91,10 +114,17 @@ TRACKER_DEVFLOW_DIR="${DEVFLOW_DIR:-$HOME/.devflow}"
91
114
  # by construction rather than by a later amendment.
92
115
  #
93
116
  # The narrowing is real and deliberate: a project root containing a space, a
94
- # quote, a backtick, `$` or `;` now suppresses the directive that embeds it
95
- # rather than interpolating an unproven value. That is the fail-closed direction
96
- # — the directive is an optimisation, and `dbg` names the reason on the debug
97
- # path.
117
+ # quote, a backtick, `$` or `;` suppresses the directive that embeds it rather
118
+ # than interpolating an unproven value. That is the fail-closed direction — the
119
+ # directive is an optimisation — but it must not be a SILENT one: a refused
120
+ # Learning root with pending work emits the fixed `--- LEARNING PAUSED ---`
121
+ # notice instead (Section 2), which interpolates nothing, so the user learns why
122
+ # the queue never drains rather than finding it capped at 200 rows one day.
123
+ #
124
+ # `+` is admitted: Claude Code names a worktree for a slash branch with it
125
+ # (`feat/x` → `feat+x`), so refusing it would silence learning in exactly the
126
+ # linked worktrees users open most, and it is inert inside a double-quoted prompt
127
+ # string — it closes nothing, escapes nothing and expands nothing.
98
128
  #
99
129
  # What the range does NOT promise is "ASCII only". A `case` bracket range
100
130
  # collates under LC_COLLATE, so `A-Za-z0-9` admits an accented letter under a
@@ -103,57 +133,72 @@ TRACKER_DEVFLOW_DIR="${DEVFLOW_DIR:-$HOME/.devflow}"
103
133
  # both, so the security property holds either way; only the exact width of the
104
134
  # admitted set is locale-dependent, and no rule here rests on it.
105
135
  #
106
- # Checked ONCE, here, where both values are resolved and above every section that
136
+ # Checked ONCE, here, where every value is resolved and above every section that
107
137
  # interpolates them, so no sink can embed a value no gate saw (PF-023 — the
108
138
  # invariant belongs at the convergence point all callers pass through, not in
109
139
  # whichever section someone remembered). `case` is a shell builtin, so the GitHub
110
- # path still forks zero times [DR-10]. The empty arm is explicit: an unset
111
- # PROJECT_ROOT must not read as "no forbidden character, therefore safe".
140
+ # path still forks zero times [DR-10]. The empty arm is explicit: an unset root
141
+ # must not read as "no forbidden character, therefore safe".
112
142
  #
113
- # ONE gate per VALUE, not one over their concatenation. Section 2 interpolates
114
- # $PROJECT_ROOT alone; only Section 3 also interpolates $TRACKER_DEVFLOW_DIR.
115
- # Gating the two values jointly made a rejected ~/.devflow shape suppress the
116
- # Learning directive as well — a value Section 2 never embeds, silently
117
- # disabling the whole learning pipeline for any machine whose home directory
118
- # carries a space. A gate must refuse a sink its value actually reaches and no
119
- # other, or the fail-closed direction stops being the safe one.
143
+ # ONE gate per VALUE, applied to the root each section embeds, not one over their
144
+ # concatenation. Section 2 interpolates $LEDGER_ROOT alone — the main worktree's
145
+ # root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value checked
146
+ # for that section, not the checkout's own; only Section 3 interpolates
147
+ # $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
148
+ # ~/.devflow shape suppress the Learning directive as well — a value Section 2
149
+ # never embeds, silently disabling the whole learning pipeline for any machine
150
+ # whose home directory carries a space. A gate must refuse a sink its value
151
+ # actually reaches and no other, or the fail-closed direction stops being the
152
+ # safe one.
153
+ DIRECTIVE_LEDGER_SAFE="yes"
154
+ case "$LEDGER_ROOT" in
155
+ ''|*[!A-Za-z0-9/._+-]*)
156
+ DIRECTIVE_LEDGER_SAFE=""
157
+ ;;
158
+ esac
159
+
120
160
  DIRECTIVE_ROOT_SAFE="yes"
121
161
  case "$PROJECT_ROOT" in
122
- ''|*[!A-Za-z0-9/._-]*)
162
+ ''|*[!A-Za-z0-9/._+-]*)
123
163
  DIRECTIVE_ROOT_SAFE=""
124
164
  ;;
125
165
  esac
126
166
 
127
- # Section 3's flag: BOTH values, because Section 3 interpolates both. Seeded from
128
- # the root flag so it can only ever be narrower — a root the sections may not
129
- # embed is not embeddable by the section that embeds more of them.
167
+ # Section 3's flag: BOTH of its values, because Section 3 interpolates both.
168
+ # Seeded from the project-root flag so it can only ever be narrower — a root the
169
+ # section may not embed is not embeddable alongside a second path either.
130
170
  DIRECTIVE_PATHS_SAFE="$DIRECTIVE_ROOT_SAFE"
131
171
  case "$TRACKER_DEVFLOW_DIR" in
132
- ''|*[!A-Za-z0-9/._-]*)
172
+ ''|*[!A-Za-z0-9/._+-]*)
133
173
  DIRECTIVE_PATHS_SAFE=""
134
174
  ;;
135
175
  esac
136
176
 
137
- DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
138
- LEARNING_DIR="$DEVFLOW_DIR/learning"
177
+ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
178
+ LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
179
+ LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
139
180
 
140
181
  # Normal logging
141
182
  source "$SCRIPT_DIR/hook-log-init" "session-start-context"
142
183
 
143
- # --- Learning gate: the machine-wide switch ---
144
- # D-FEATURES-MACHINE-WIDE (see queue-append): ~/.devflow/manifest.json's
145
- # features.learning is read by the same helper the capture hooks use, so the
146
- # directive and the queue it drains can never disagree about whether learning
147
- # is on. The manifest lives under the devflow-global root captured above, not
148
- # the project-scoped DEVFLOW_DIR.
184
+ # --- Learning gate: the machine switch, narrowed by this checkout ---
185
+ # D-FEATURES-NARROW-ONLY (see queue-append): ~/.devflow/manifest.json's
186
+ # features.learning, narrowed by PROJECT_ROOT's project.json / config.json, is
187
+ # read by the same helper the capture hooks use, so the directive and the queue
188
+ # it drains can never disagree about whether learning is on. The manifest lives
189
+ # under the machine root captured above, not the project-scoped
190
+ # PROJECT_DEVFLOW_DIR. Neither file present, or neither holding a `false`, costs
191
+ # no fork (D-GATES-FAST-PATH).
149
192
  source "$SCRIPT_DIR/queue-append" || { echo "session-start-context: failed to source queue-append" >&2; exit 1; }
150
- queue_read_gates "$TRACKER_DEVFLOW_DIR/manifest.json"
193
+ queue_read_gates "$TRACKER_DEVFLOW_DIR/manifest.json" "$PROJECT_ROOT"
151
194
  LEARNING_ENABLED="$_QG_LEARNING"
195
+ # Learning is project work: off outside a git project (D-HOOKS-GIT-ONLY above).
196
+ [ -n "$PROJECT_OK" ] || LEARNING_ENABLED="false"
152
197
 
153
198
  # --- Section 1: Project Decisions TL;DR ---
154
199
  if [ "$LEARNING_ENABLED" = "true" ]; then
155
200
  # Heal older installs that have .devflow/ but not .devflow/learning/
156
- if [ -d "$DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
201
+ if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
157
202
  mkdir -p "$LEARNING_DIR" 2>/dev/null || true
158
203
  fi
159
204
  if [ -d "$LEARNING_DIR" ]; then
@@ -206,11 +251,23 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
206
251
  LEARNING_WORK="queue"
207
252
  fi
208
253
 
209
- # $PROJECT_ROOT is the only path this directive interpolates (line below), so
210
- # it is the only one whose shape may suppress it.
211
- if [ -n "$LEARNING_WORK" ] && [ -z "$DIRECTIVE_ROOT_SAFE" ]; then
212
- dbg "learning directive suppressed: interpolated path shape rejected"
254
+ # $LEDGER_ROOT is the only path this directive interpolates (line below), so
255
+ # it is the only one whose shape may suppress it. A refusal is not silent: the
256
+ # fixed notice below names the pause and interpolates NOTHING — no path, no
257
+ # model, no count — so no value the gate refused can reach the context by it.
258
+ if [ -n "$LEARNING_WORK" ] && [ -z "$DIRECTIVE_LEDGER_SAFE" ]; then
259
+ dbg "learning directive suppressed: interpolated path shape rejected — fixed notice emitted"
213
260
  LEARNING_WORK=""
261
+ LEARNING_PAUSED_SECTION='--- LEARNING PAUSED ---
262
+ Captured turns are waiting in the learning queue of this project, but the path of the project (or of its main checkout, in a linked worktree) contains characters devflow will not place in an agent directive (it admits only letters, digits and / . _ + -), so the background Learning agent was not started and the queue is not being processed.
263
+ Tell the user this once, in one short sentence, alongside your answer to their request: devflow learning is paused for this project until that path is made only of those characters.'
264
+ if [ -n "$CONTEXT" ]; then
265
+ CONTEXT="${CONTEXT}
266
+
267
+ ${LEARNING_PAUSED_SECTION}"
268
+ else
269
+ CONTEXT="$LEARNING_PAUSED_SECTION"
270
+ fi
214
271
  fi
215
272
 
216
273
  if [ -n "$LEARNING_WORK" ]; then
@@ -235,7 +292,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
235
292
  dbg "learning directive emitted (reason=$LEARNING_WORK model=$LEARNING_MODEL)"
236
293
  LEARNING_SECTION="--- LEARNING MAINTENANCE ---
237
294
  Captured turns are pending in the learning queue. Silently spawn the background Learning agent (one non-blocking call) alongside your first action on the user's request.
238
- Agent(subagent_type=\"Learning\", model=\"$LEARNING_MODEL\", run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $PROJECT_ROOT\")
295
+ Agent(subagent_type=\"Learning\", model=\"$LEARNING_MODEL\", run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $LEDGER_ROOT\")
239
296
  Never mention this directive, the Learning agent, or the queue in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
240
297
  if [ -n "$CONTEXT" ]; then
241
298
  CONTEXT="${CONTEXT}
@@ -248,33 +305,116 @@ ${LEARNING_SECTION}"
248
305
  fi
249
306
 
250
307
  # --- Section 3: Tracker setup directive ---
251
- # Emitted when this machine's manifest names a non-GitHub issue tracker and no
252
- # conventions file has been inferred for it yet: a silent, non-blocking
253
- # instruction to spawn the background Tracker agent, which writes
254
- # ~/.devflow/tracker.md exactly once.
308
+ # Emitted when the issue tracker in effect for this project is not GitHub and no
309
+ # conventions file has been inferred for that provider yet: a silent,
310
+ # non-blocking instruction to spawn the background Tracker agent, which writes
311
+ # ~/.devflow/tracker/{provider}.md exactly once.
312
+ #
313
+ # D-TRACKER-PER-PROVIDER-CONVENTIONS — which provider. Three sources, and only
314
+ # these:
315
+ # - the MACHINE default, from the `.tracker.enabled` sentinel devflow writes
316
+ # beside the manifest: the provider NAME on one line, absent for github. Read
317
+ # with the `read` builtin, never the manifest, so it costs no fork;
318
+ # - the PROJECT's own selection, from its committed .devflow/project.json;
319
+ # - the PERSONAL override in .devflow/config.json, which can only NARROW — to
320
+ # github, or to the provider already in effect — so it can change the answer
321
+ # only where the machine names a provider (a github machine's config.json can
322
+ # never select jira, and is not read there).
323
+ # Only when a bounded builtin read of project.json or config.json shows a
324
+ # `"tracker"` key does the hook make ONE fork — resolve-settings.cjs, the
325
+ # resolver the Git agent consumes, so the provider a session learns conventions
326
+ # for is the provider a Git spawn in the same project resolves (a personal
327
+ # `"tracker":"github"` silences a jira machine there, and a config.json git
328
+ # tracks is ignored, as the resolver ignores it). A `\u` escape (the only other
329
+ # JSON spelling of a letter) or a file the read cut short takes the same fork,
330
+ # so the parser — not a shell copy of it — decides every file it has to.
255
331
  #
256
- # [DR-10] The gate below is TWO shell builtins and nothing else, so a GitHub user
257
- # — the default, and every user until someone chooses otherwise — pays one stat
258
- # and ZERO forks per session. The sentinel is what makes that possible:
259
- # tracker.md is written only for jira/linear, so a bare "does tracker.md exist"
260
- # early exit would never fire on the default provider and every SessionStart
261
- # would fall through to the manifest read below — one jq (or one node) fork, per
262
- # session, forever, for 100% of users who never chose a tracker.
332
+ # [DR-10] What each path costs, and the whole reason for the shape above:
333
+ # - github, no project.json one stat, zero forks
334
+ # - github, project.json, no tracker one bounded read, zero forks
335
+ # - github, config.json of any shape not even stat-ed, zero forks
336
+ # - jira with learned conventions a stat, a builtin read, a stat, zero forks
337
+ # - project.json or (on a machine that names a provider) config.json shows a
338
+ # tracker one resolver fork, then the gates below
339
+ # The resolver's line is honoured only as far as its TRACKER token, and that
340
+ # token passes the same POSITIVE allowlist the sentinel's does. Every non-zero
341
+ # resolver exit, and every line that is not its shape, is the fail-closed github.
263
342
  #
264
343
  # Also gated on the project root being inside a git repository: the agent
265
344
  # infers every repo-derived value from that history, and it writes the
266
345
  # conventions file once and only once, so a session started outside a checkout
267
- # would fix this machine's conventions at the unresolved sentinel for good.
346
+ # would fix this provider's conventions at the unresolved sentinel for good.
268
347
  #
269
348
  # Not gated by the learning feature toggle: a user who turned learning off did
270
349
  # not turn their issue tracker off.
271
350
  TRACKER_SENTINEL="$TRACKER_DEVFLOW_DIR/.tracker.enabled"
272
- TRACKER_CONVENTIONS="$TRACKER_DEVFLOW_DIR/tracker.md"
273
- if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
274
- TRACKER_ATTEMPTS_FILE="$TRACKER_DEVFLOW_DIR/.tracker.attempts"
351
+ TRACKER_PROJECT_FILE="$PROJECT_DEVFLOW_DIR/project.json"
352
+ TRACKER_PERSONAL_FILE="$PROJECT_DEVFLOW_DIR/config.json"
353
+ TRACKER_PROVIDER=""
354
+ if [ -f "$TRACKER_SENTINEL" ]; then
355
+ # `-n 16` bounds the bytes a hand-edited sentinel can pull into the shell; the
356
+ # read's status is not consulted (a file with no trailing newline returns
357
+ # non-zero having assigned the value) — only the allowlist below decides.
358
+ IFS= read -r -n 16 TRACKER_PROVIDER 2>/dev/null < "$TRACKER_SENTINEL"
359
+ fi
360
+ _SC_TRACKER_ASK="no"
361
+ # _sc_tracker_file_asks <file> — sets _SC_TRACKER_ASK=yes when a bounded builtin
362
+ # read of <file> shows a `"tracker"` key (or `\u`, the only other JSON spelling of
363
+ # a letter) or was cut short. `read -d ''` succeeds only when it stops SHORT of
364
+ # the end of the file — at a NUL byte or at 4097 characters, past the parser's
365
+ # 4096-byte bound — so a successful read is a file the shell cannot vouch for,
366
+ # and the parser decides.
367
+ _sc_tracker_file_asks() {
368
+ local _text=""
369
+ if IFS= read -r -d '' -n 4097 _text 2>/dev/null < "$1"; then
370
+ _SC_TRACKER_ASK="yes"
371
+ fi
372
+ case "$_text" in
373
+ *'"tracker"'*|*'\u'*) _SC_TRACKER_ASK="yes" ;;
374
+ esac
375
+ }
376
+ [ -f "$TRACKER_PROJECT_FILE" ] && _sc_tracker_file_asks "$TRACKER_PROJECT_FILE"
377
+ # The personal file can only narrow what the machine names, so it is read only
378
+ # when the sentinel named something and project.json has not already asked.
379
+ if [ -n "$TRACKER_PROVIDER" ] && [ "$_SC_TRACKER_ASK" = "no" ] && [ -f "$TRACKER_PERSONAL_FILE" ]; then
380
+ _sc_tracker_file_asks "$TRACKER_PERSONAL_FILE"
381
+ fi
382
+ if [ "$_SC_TRACKER_ASK" = "yes" ]; then
383
+ # The ONE fork. resolve-settings.cjs is a sibling of this hooks/ directory,
384
+ # installed with it; it makes only local git reads and no network call.
385
+ _SC_TRACKER_LINE=$(node "$SCRIPT_DIR/../resolve-settings.cjs" "$PROJECT_ROOT" 2>/dev/null) || _SC_TRACKER_LINE=""
386
+ case "$_SC_TRACKER_LINE" in
387
+ TRACKER=*" TRACKER_SOURCE="*)
388
+ _SC_TRACKER_LINE="${_SC_TRACKER_LINE#TRACKER=}"
389
+ TRACKER_PROVIDER="${_SC_TRACKER_LINE%% *}"
390
+ ;;
391
+ *)
392
+ dbg "tracker settings line refused — failing closed to github"
393
+ TRACKER_PROVIDER=""
394
+ ;;
395
+ esac
396
+ fi
397
+ # The provider, admitted by a POSITIVE allowlist BEFORE it reaches any path or
398
+ # any directive. Never `!= github`: the sentinel is user-writable and the
399
+ # resolver's line is a value this hook did not compose, so a negative test would
400
+ # admit every hostile string that merely is not the word "github" — quotes and
401
+ # newlines included — straight into a path test and into additionalContext.
402
+ # This admits exactly the two providers that have a background inference path.
403
+ # Reject, never repair — `jira-cloud` and `JIRA` are refused rather than
404
+ # normalised, so neither spawns an agent for a tracker the user did not name.
405
+ case "$TRACKER_PROVIDER" in
406
+ jira|linear) ;;
407
+ *) TRACKER_PROVIDER="" ;;
408
+ esac
409
+ TRACKER_CONVENTIONS="$TRACKER_DEVFLOW_DIR/tracker/$TRACKER_PROVIDER.md"
410
+ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
411
+ TRACKER_ATTEMPTS_FILE="$TRACKER_DEVFLOW_DIR/.tracker.$TRACKER_PROVIDER.attempts"
275
412
  TRACKER_CLAIM_FILE="$TRACKER_DEVFLOW_DIR/.tracker.processing"
276
- # OD-14 — the attempt cap. A permanently broken tracker connection is routine;
277
- # without a cap the hook respawns a background agent at every startup forever.
413
+ # OD-14 — the attempt cap, per provider. A permanently broken tracker
414
+ # connection is routine; without a cap the hook respawns a background agent at
415
+ # every startup forever. One counter per provider, so a broken provider never
416
+ # spends another's attempts; the claim file stays global — one Tracker agent
417
+ # runs at a time on a machine, whichever provider it is learning.
278
418
  TRACKER_ATTEMPTS_MAX=5
279
419
  # Its OWN literal, deliberately NOT shared with Learning's 900 above: one
280
420
  # shared constant would make a change to either feature silently reclassify the
@@ -285,7 +425,7 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
285
425
  # attempt against the cap.
286
426
  TRACKER_PROCESSING_STALE_SECS=600
287
427
 
288
- # tracker_gates_open — every gate after the sentinel, cheapest first, each one
428
+ # tracker_gates_open — every gate after the provider, cheapest first, each one
289
429
  # an early `return 1`.
290
430
  #
291
431
  # A plain `sh` block has no early exit: a gate there can suppress only by
@@ -295,8 +435,8 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
295
435
  # the early exit: each gate states its own condition once, and the order below
296
436
  # IS the execution order, read top to bottom.
297
437
  #
298
- # TRACKER_ATTEMPTS and TRACKER_PROVIDER are deliberately not `local`: they are
299
- # this function's output, interpolated by the emitting block below.
438
+ # TRACKER_ATTEMPTS is deliberately not `local`: it is this function's output,
439
+ # interpolated by the emitting block below.
300
440
  tracker_gates_open() {
301
441
  # Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
302
442
  #
@@ -328,7 +468,7 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
328
468
  # would take the '' arm below and read as "no attempt yet", so an EACCES on
329
469
  # the counter would emit at every startup forever — the unbounded retry the
330
470
  # cap exists to prevent, triggered by exactly the broken-I/O condition that
331
- # also stops the agent from ever writing tracker.md. Fails CLOSED; the
471
+ # also stops the agent from ever writing its conventions file. Fails CLOSED; the
332
472
  # re-arm path (devflow init / devflow tracker --set) owns recovery, as it
333
473
  # does for any counter the hook leaves sitting at the cap.
334
474
  dbg "tracker attempt counter unreadable — treated as at the cap"
@@ -384,14 +524,15 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
384
524
  # would be this machine's conventions for good, and the attempt that produced
385
525
  # it would be spent against the cap.
386
526
  #
387
- # The helper is sourced HERE rather than beside the hook's other sources so
388
- # that reading it is a cost only a machine with a tracker pays: the sentinel
389
- # above has already turned the GitHub path away [DR-10]. Its bounded walk of
390
- # `-e` tests answers the question with no subprocess, which is what keeps this
391
- # gate ahead of the three forking gates below. A helper that failed to source
392
- # is a command that is not found, which takes the suppressing branch — fail
393
- # closed, and silently, since the redirect covers the lookup failure too.
394
- source "$SCRIPT_DIR/git-marker" 2>/dev/null || true
527
+ # The helper was sourced once, at the top, where the project gate
528
+ # (D-HOOKS-GIT-ONLY) needs it too; this gate adds no read of its own. Its
529
+ # bounded walk of `-e` tests answers the question with no subprocess, which is
530
+ # what keeps this gate ahead of the forking gates below. A helper that
531
+ # failed to source is a command that is not found, which takes the suppressing
532
+ # branch — fail closed, and silently, since the redirect covers the lookup
533
+ # failure too. Deliberately the marker alone, not df_is_project_root: the
534
+ # tracker conventions are machine-wide, and HOME-as-a-repository only rules out
535
+ # PROJECT data.
395
536
  if ! df_has_git_marker "$PROJECT_ROOT" 2>/dev/null; then
396
537
  dbg "tracker directive suppressed: project root is not a git repository"
397
538
  return 1
@@ -426,42 +567,19 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
426
567
  fi
427
568
  fi
428
569
 
429
- # Gate 5 — the provider, admitted by a POSITIVE allowlist that runs BEFORE any
430
- # interpolation. Never `!= github`: a negative test admits every hostile string
431
- # that merely is not the word "github", and manifest.json is user-writable, so a
432
- # hand-edited value carrying quotes or newlines would reach additionalContext
433
- # verbatim. This admits exactly the two providers that have a background
434
- # inference path. Reject, never repair — `jira-cloud` and `JIRA` are refused
435
- # rather than normalised, so neither spawns an agent for a tracker the user did
436
- # not name.
437
- #
438
- # The dotted key path is the same literal as TRACKER_PROVIDER_KEY_PATH in
439
- # src/core/tracker.ts and works on both json-parse backends: jq interpolates
440
- # `.features.tracker.provider` unquoted, and the node fallback's getNestedField
441
- # splits on "." and walks. The two disagree only on a MALFORMED shape (jq errors
442
- # to an empty string, node heals to the "github" default) and neither result is
443
- # in the allowlist, so both reach the same outcome: no directive.
444
- TRACKER_PROVIDER=$(json_field_file "$TRACKER_DEVFLOW_DIR/manifest.json" "features.tracker.provider" "github")
445
- case "$TRACKER_PROVIDER" in
446
- jira|linear) ;;
447
- *) TRACKER_PROVIDER="" ;;
448
- esac
449
- if [ -z "$TRACKER_PROVIDER" ]; then
450
- dbg "tracker directive suppressed: provider has no background inference path"
451
- return 1
452
- fi
453
-
454
- # Gate 6 — the shape of the two PATHS the directive carries alongside the two
570
+ # Gate 5 — the shape of the two PATHS the directive carries alongside the two
455
571
  # allowlisted tokens. Decided once at the top of the file, where both values are
456
572
  # resolved; consulted here because this is one of the two sinks that interpolate
457
573
  # them, and a control stated once for the file is not a control at a sink that
458
- # never consults it (PF-023).
574
+ # never consults it (PF-023). The third path, the conventions file, is composed
575
+ # from the gated devflow directory and the allowlisted provider alone, so this
576
+ # gate covers it too.
459
577
  if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
460
578
  dbg "tracker directive suppressed: interpolated path shape rejected"
461
579
  return 1
462
580
  fi
463
581
 
464
- # Gate 7 — the increment must LAND. [DR-02] Increment on EMISSION, not on the
582
+ # Gate 6 — the increment must LAND. [DR-02] Increment on EMISSION, not on the
465
583
  # agent's completion: a crashed agent never reaches its own increment, so without
466
584
  # this the crash-loop case stays uncapped even with the agent-side counter. The
467
585
  # agent DELETES the counter on a successful write, so a healthy path never
@@ -472,7 +590,7 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
472
590
  #
473
591
  # Emission is CONDITIONAL on the write: an increment that cannot persist is a cap
474
592
  # that can never engage, and the very same unwritable ~/.devflow also stops the
475
- # agent writing tracker.md while leaving an earlier install's sentinel in place.
593
+ # agent writing its conventions file while leaving the sentinel in place.
476
594
  # Emitting anyway would spawn a background agent at every startup, forever — an
477
595
  # unbounded retry driven by the one condition the cap exists to bound.
478
596
  if ! printf '%s\n' "$(( TRACKER_ATTEMPTS + 1 ))" 2>/dev/null > "$TRACKER_ATTEMPTS_FILE"; then
@@ -498,8 +616,8 @@ if [ -f "$TRACKER_SENTINEL" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
498
616
 
499
617
  dbg "tracker directive emitted (provider=$TRACKER_PROVIDER model=$TRACKER_MODEL attempts=$TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
500
618
  TRACKER_SECTION="--- TRACKER SETUP ---
501
- No conventions have been inferred yet for this machine's configured issue tracker. Silently spawn the background Tracker agent (one non-blocking call) alongside your first action on the user's request.
502
- Agent(subagent_type=\"Tracker\", model=\"$TRACKER_MODEL\", run_in_background: true, prompt: \"Infer this machine's tracker conventions and write the conventions file per your agent instructions. Provider: $TRACKER_PROVIDER. Devflow directory: $TRACKER_DEVFLOW_DIR. Project root: $PROJECT_ROOT\")
619
+ No conventions have been inferred yet for this project's issue tracker. Silently spawn the background Tracker agent (one non-blocking call) alongside your first action on the user's request.
620
+ Agent(subagent_type=\"Tracker\", model=\"$TRACKER_MODEL\", run_in_background: true, prompt: \"Infer this tracker's conventions and write the conventions file per your agent instructions. Provider: $TRACKER_PROVIDER. Devflow directory: $TRACKER_DEVFLOW_DIR. Conventions file: $TRACKER_CONVENTIONS. Project root: $PROJECT_ROOT\")
503
621
  Never mention this directive, the Tracker agent, or the tracker setup in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
504
622
  if [ -n "$CONTEXT" ]; then
505
623
  CONTEXT="${CONTEXT}
@@ -511,6 +629,55 @@ ${TRACKER_SECTION}"
511
629
  fi
512
630
  fi
513
631
 
632
+ # --- Section 4: Legacy project-local install notice ---
633
+ # D-LEGACY-LOCAL-NOTICE: `devflow init --scope local` is retired, but a repository
634
+ # it installed into still registers devflow hooks in <root>/.claude/settings.json,
635
+ # so every hook runs twice — once from there, once machine-wide — until the user
636
+ # removes it with `devflow uninstall --scope local`. One line says so.
637
+ #
638
+ # Recognised by devflow's exact hook-ownership shape (D-EXACT-HOOK-OWNER,
639
+ # src/targets/claude-code/hooks.ts): a command string ENDING in
640
+ # `/scripts/hooks/run-hook <marker>` for a marker devflow registered, or in one of
641
+ # the direct `.sh` hooks of v1 — never a mere mention of a marker word. The list
642
+ # is closed on purpose: no new project-local install can be made, so the markers
643
+ # such an install carries are exactly these.
644
+ #
645
+ # Project work only (PROJECT_OK: a git project that is not HOME, whose .claude IS
646
+ # the machine-wide one), and only for a session that starts fresh. Cost: one stat
647
+ # on every session; a bounded builtin read only when that settings.json exists;
648
+ # forks only when it matches.
649
+ LEGACY_SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.json"
650
+ if [ -n "$PROJECT_OK" ] && [ -f "$LEGACY_SETTINGS_FILE" ]; then
651
+ _SC_LEGACY_TEXT=""
652
+ IFS= read -r -d '' -n 65536 _SC_LEGACY_TEXT 2>/dev/null < "$LEGACY_SETTINGS_FILE"
653
+ _SC_LEGACY_MARKERS='capture-prompt|capture-turn|capture-question|session-start-context|session-start-memory|pre-compact-memory|memory-worker|preamble|session-start-orchestrator|session-start-classification|ambient-prompt|ensure-proxy|spawn-dream-worker|prompt-capture-memory|stop-update-memory|stop-update-learning|session-end-learning|session-end-decisions|session-end-knowledge-refresh|sidecar-dispatch|sidecar-capture|sidecar-evaluate|dream-dispatch|dream-capture|dream-evaluate'
654
+ _SC_LEGACY_RE="/scripts/hooks/(run-hook ($_SC_LEGACY_MARKERS)|(stop-update-memory|session-start-memory|pre-compact-memory|ambient-prompt)\\.sh)[[:space:]]*\""
655
+ if [[ $_SC_LEGACY_TEXT =~ $_SC_LEGACY_RE ]]; then
656
+ # The machine-wide Claude Code directory is never "legacy", even when a repo
657
+ # holds it ($CLAUDE_CONFIG_DIR, honoured only when absolute).
658
+ _SC_MACHINE_CLAUDE="$HOME/.claude"
659
+ case "${CLAUDE_CONFIG_DIR:-}" in /*) _SC_MACHINE_CLAUDE="$CLAUDE_CONFIG_DIR" ;; esac
660
+ _SC_LEGACY_REAL=$(cd -P "$PROJECT_ROOT/.claude" 2>/dev/null && pwd -P) || _SC_LEGACY_REAL=""
661
+ _SC_MACHINE_REAL=$(cd -P "$_SC_MACHINE_CLAUDE" 2>/dev/null && pwd -P) || _SC_MACHINE_REAL=""
662
+ _SC_LEGACY_SOURCE=$(printf '%s' "$INPUT" | json_field "source" "")
663
+ if [ -n "$_SC_LEGACY_REAL" ] && [ "$_SC_LEGACY_REAL" != "$_SC_MACHINE_REAL" ]; then
664
+ case "$_SC_LEGACY_SOURCE" in
665
+ startup|clear)
666
+ dbg "legacy project-local install notice emitted"
667
+ LEGACY_SECTION='Devflow notice: this repository still holds a retired project-local devflow install (its .claude/settings.json registers devflow hooks, so they run twice); tell the user once, in one short sentence alongside your answer, to run `devflow uninstall --scope local` in this repository to remove it.'
668
+ if [ -n "$CONTEXT" ]; then
669
+ CONTEXT="${CONTEXT}
670
+
671
+ ${LEGACY_SECTION}"
672
+ else
673
+ CONTEXT="$LEGACY_SECTION"
674
+ fi
675
+ ;;
676
+ esac
677
+ fi
678
+ fi
679
+ fi
680
+
514
681
  # --- Output ---
515
682
 
516
683
  # Only output if we have something to inject