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
@@ -2,8 +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
- # are gated by the `learning` field in feature config (config-only, ADR-001) —
6
- # this hook itself is never 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).
7
10
  #
8
11
  # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines).
9
12
  # Section 2: Learning maintenance directive — when captured turns are pending in
@@ -11,6 +14,14 @@
11
14
  # the main model to spawn the background Learning agent with the resolved model.
12
15
  # The agent claims the queue itself and queue emptiness is the natural gate,
13
16
  # so there is no throttle here.
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`.
14
25
 
15
26
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
16
27
  dbg() { :; }
@@ -47,37 +58,147 @@ devflow_debug_set_cwd "$CWD"
47
58
  dbg "CWD=$CWD"
48
59
 
49
60
  # Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
50
- # 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.
51
65
  source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
52
- PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
53
- [ -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
54
84
 
55
85
  # Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
56
- # session regardless of feature toggles, so memory-off projects (learning/knowledge
57
- # only) still get .devflow/ ignored — this is the memory-independent path that fixes
58
- # the gitignore/memory coupling (PF-014). Single source of truth: ensure-root-gitignore.
59
- # Soft-fail: a gitignore write must never block context injection. Marker keeps it O(1).
60
- [ -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
61
92
 
62
93
  CONTEXT=""
63
94
 
64
- DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
65
- LEARNING_DIR="$DEVFLOW_DIR/learning"
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"
99
+
100
+ # The shape gate for every path this hook interpolates into a directive.
101
+ #
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".
108
+ #
109
+ # Positive, not a denylist of the characters that are known to hurt. A denylist
110
+ # enumerates the injections someone thought of — `"` closes the prompt string, `\`
111
+ # reads as an escape, LF and CR put the rest of the path on its own line as free
112
+ # text — and admits every one that was not on the list. An allowlist admits only
113
+ # what is known to be inert, so the next escape nobody has thought of is refused
114
+ # by construction rather than by a later amendment.
115
+ #
116
+ # The narrowing is real and deliberate: a project root containing a space, a
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.
128
+ #
129
+ # What the range does NOT promise is "ASCII only". A `case` bracket range
130
+ # collates under LC_COLLATE, so `A-Za-z0-9` admits an accented letter under a
131
+ # UTF-8 locale and refuses it under C. Every byte the gate exists to refuse —
132
+ # quote, backslash, CR, LF, space, backtick, `$`, `;` — is outside the range in
133
+ # both, so the security property holds either way; only the exact width of the
134
+ # admitted set is locale-dependent, and no rule here rests on it.
135
+ #
136
+ # Checked ONCE, here, where every value is resolved and above every section that
137
+ # interpolates them, so no sink can embed a value no gate saw (PF-023 — the
138
+ # invariant belongs at the convergence point all callers pass through, not in
139
+ # whichever section someone remembered). `case` is a shell builtin, so the GitHub
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".
142
+ #
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
+
160
+ DIRECTIVE_ROOT_SAFE="yes"
161
+ case "$PROJECT_ROOT" in
162
+ ''|*[!A-Za-z0-9/._+-]*)
163
+ DIRECTIVE_ROOT_SAFE=""
164
+ ;;
165
+ esac
166
+
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.
170
+ DIRECTIVE_PATHS_SAFE="$DIRECTIVE_ROOT_SAFE"
171
+ case "$TRACKER_DEVFLOW_DIR" in
172
+ ''|*[!A-Za-z0-9/._+-]*)
173
+ DIRECTIVE_PATHS_SAFE=""
174
+ ;;
175
+ esac
176
+
177
+ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
178
+ LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
179
+ LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
66
180
 
67
181
  # Normal logging
68
182
  source "$SCRIPT_DIR/hook-log-init" "session-start-context"
69
183
 
70
- # --- Learning gate (config-only) ---
71
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
72
- LEARNING_ENABLED="true"
73
- if [ -f "$FEATURE_CONFIG" ]; then
74
- LEARNING_ENABLED=$(json_field_file "$FEATURE_CONFIG" "learning" "true")
75
- fi
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).
192
+ source "$SCRIPT_DIR/queue-append" || { echo "session-start-context: failed to source queue-append" >&2; exit 1; }
193
+ queue_read_gates "$TRACKER_DEVFLOW_DIR/manifest.json" "$PROJECT_ROOT"
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"
76
197
 
77
198
  # --- Section 1: Project Decisions TL;DR ---
78
199
  if [ "$LEARNING_ENABLED" = "true" ]; then
79
200
  # Heal older installs that have .devflow/ but not .devflow/learning/
80
- if [ -d "$DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
201
+ if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
81
202
  mkdir -p "$LEARNING_DIR" 2>/dev/null || true
82
203
  fi
83
204
  if [ -d "$LEARNING_DIR" ]; then
@@ -130,6 +251,25 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
130
251
  LEARNING_WORK="queue"
131
252
  fi
132
253
 
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"
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
271
+ fi
272
+
133
273
  if [ -n "$LEARNING_WORK" ]; then
134
274
  # Model resolution: project learning.json → global ~/.devflow/learning.json → opus
135
275
  LEARNING_MODEL=""
@@ -152,7 +292,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
152
292
  dbg "learning directive emitted (reason=$LEARNING_WORK model=$LEARNING_MODEL)"
153
293
  LEARNING_SECTION="--- LEARNING MAINTENANCE ---
154
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.
155
- 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\")
156
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."
157
297
  if [ -n "$CONTEXT" ]; then
158
298
  CONTEXT="${CONTEXT}
@@ -164,6 +304,380 @@ ${LEARNING_SECTION}"
164
304
  fi
165
305
  fi
166
306
 
307
+ # --- Section 3: Tracker setup directive ---
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.
331
+ #
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.
342
+ #
343
+ # Also gated on the project root being inside a git repository: the agent
344
+ # infers every repo-derived value from that history, and it writes the
345
+ # conventions file once and only once, so a session started outside a checkout
346
+ # would fix this provider's conventions at the unresolved sentinel for good.
347
+ #
348
+ # Not gated by the learning feature toggle: a user who turned learning off did
349
+ # not turn their issue tracker off.
350
+ TRACKER_SENTINEL="$TRACKER_DEVFLOW_DIR/.tracker.enabled"
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"
412
+ TRACKER_CLAIM_FILE="$TRACKER_DEVFLOW_DIR/.tracker.processing"
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.
418
+ TRACKER_ATTEMPTS_MAX=5
419
+ # Its OWN literal, deliberately NOT shared with Learning's 900 above: one
420
+ # shared constant would make a change to either feature silently reclassify the
421
+ # other's live runs as crashed. 600s is longer than the memory worker's 300s
422
+ # lock (a Tracker run does more — a capability probe plus bounded git scans)
423
+ # and shorter than Learning's 900s (no multi-part curation phase). Too long
424
+ # costs one session's delay before a crashed run retries; too short burns an
425
+ # attempt against the cap.
426
+ TRACKER_PROCESSING_STALE_SECS=600
427
+
428
+ # tracker_gates_open — every gate after the provider, cheapest first, each one
429
+ # an early `return 1`.
430
+ #
431
+ # A plain `sh` block has no early exit: a gate there can suppress only by
432
+ # clearing a flag that every later gate must then re-test, which turns the
433
+ # cheapest-first ordering that delivers [DR-10]'s zero-fork guarantee into a
434
+ # stack of re-entries rather than the linear sequence it is. A function supplies
435
+ # the early exit: each gate states its own condition once, and the order below
436
+ # IS the execution order, read top to bottom.
437
+ #
438
+ # TRACKER_ATTEMPTS is deliberately not `local`: it is this function's output,
439
+ # interpolated by the emitting block below.
440
+ tracker_gates_open() {
441
+ # Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
442
+ #
443
+ # The counter's shape is ONE decimal integer line and nothing else (PF-062 —
444
+ # document the shape of any file that gates an action, and keep absent and
445
+ # malformed distinct from a value). Absent means "no attempt yet" = 0.
446
+ # Malformed self-heals to 0 and is overwritten with a well-formed count on
447
+ # emission below, so a stray byte can never recur: refusing forever would
448
+ # disable inference permanently with no user-visible reason, and treating it as
449
+ # uncapped would defeat the cap. `read` returns non-zero at an EOF with no
450
+ # trailing newline but HAS assigned the variable, so its status is deliberately
451
+ # not consulted — only the value's shape is.
452
+ #
453
+ # `-n 16` bounds the BYTES consumed, which the `case` below cannot: the file is
454
+ # user-scope and hand-editable, and one very long line would otherwise be pulled
455
+ # whole into a shell variable and pattern-matched whole on the SessionStart
456
+ # critical path. Sixteen characters is past every arm's decision point — a
457
+ # well-formed count never reaches two digits, and the out-of-range arm fires at
458
+ # seven — so every arm keeps the verdict it would have reached unbounded.
459
+ # `2>/dev/null` is spelled BEFORE the input redirect for the same reason it is
460
+ # on the write below: redirections apply left to right, and a failed open is
461
+ # reported by the shell itself, so silencing stderr afterwards would be too late.
462
+ TRACKER_ATTEMPTS=""
463
+ if [ -f "$TRACKER_ATTEMPTS_FILE" ]; then
464
+ if [ -r "$TRACKER_ATTEMPTS_FILE" ]; then
465
+ IFS= read -r -n 16 TRACKER_ATTEMPTS 2>/dev/null < "$TRACKER_ATTEMPTS_FILE"
466
+ else
467
+ # Present but unreadable is NOT a fresh start. Leaving the variable empty
468
+ # would take the '' arm below and read as "no attempt yet", so an EACCES on
469
+ # the counter would emit at every startup forever — the unbounded retry the
470
+ # cap exists to prevent, triggered by exactly the broken-I/O condition that
471
+ # also stops the agent from ever writing its conventions file. Fails CLOSED; the
472
+ # re-arm path (devflow init / devflow tracker --set) owns recovery, as it
473
+ # does for any counter the hook leaves sitting at the cap.
474
+ dbg "tracker attempt counter unreadable — treated as at the cap"
475
+ TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
476
+ fi
477
+ fi
478
+ case "$TRACKER_ATTEMPTS" in
479
+ '') TRACKER_ATTEMPTS=0 ;;
480
+ *[!0-9]*)
481
+ dbg "tracker attempt counter malformed — self-healed to 0"
482
+ TRACKER_ATTEMPTS=0
483
+ ;;
484
+ 0) ;; # a bare zero is a well-formed count, not a padded one
485
+ 0*)
486
+ # A zero-padded count is ONE string that this hook's two consumers of it read
487
+ # in DIFFERENT BASES. `[ "$N" -ge "$MAX" ]` parses base 10, so `08` compares
488
+ # as eight; the `$(( N + 1 ))` that writes the next count is shell arithmetic,
489
+ # where a leading `0` means OCTAL and `08` is "value too great for base" — an
490
+ # error that escapes the write's own `2>/dev/null`, because expansion runs
491
+ # before redirection. Nothing in the padded shape says which reading was
492
+ # meant, so it self-heals to 0 with every other malformed value instead of
493
+ # being carried into the disagreement. This hook writes a bare decimal, so
494
+ # padding came from elsewhere. Placed BEFORE the digit-count arm so a
495
+ # six-character `000008` heals rather than reading as "six digits, at the cap".
496
+ dbg "tracker attempt counter zero-padded — self-healed to 0"
497
+ TRACKER_ATTEMPTS=0
498
+ ;;
499
+ ???????*)
500
+ # Bounded before the comparison: `[ "$N" -ge 5 ]` on a value past intmax_t
501
+ # prints "integer expression expected" to stderr and takes the FALSE branch,
502
+ # so an unbounded digit string would fail OPEN — uncapped — and leak a shell
503
+ # error. `-n 16` above already puts that state out of reach; this arm is the
504
+ # semantic half and survives a change to the read bound. It matches SEVEN
505
+ # digits or more (the earlier arms have taken every non-digit and every
506
+ # zero-padded value), which is the shape CLAUDE.md documents as "at the cap",
507
+ # and a count that matters needs one digit, so anything reaching here is past
508
+ # the cap by six orders of magnitude. Six digits and fewer are compared as
509
+ # the integers they are.
510
+ dbg "tracker attempt counter out of range — treated as at the cap"
511
+ TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
512
+ ;;
513
+ esac
514
+ if [ "$TRACKER_ATTEMPTS" -ge "$TRACKER_ATTEMPTS_MAX" ]; then
515
+ dbg "tracker directive suppressed: attempt cap reached ($TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
516
+ return 1
517
+ fi
518
+
519
+ # Gate 2 — the repository. Every repo-derived value the Tracker agent writes
520
+ # comes from the project root's history, and the agent refuses to infer from
521
+ # history at all when the root carries no git marker — outside a checkout it
522
+ # would write its unresolved sentinel into every one of those sections. It
523
+ # writes the conventions file ONCE, create-exclusive, so that degraded file
524
+ # would be this machine's conventions for good, and the attempt that produced
525
+ # it would be spent against the cap.
526
+ #
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.
536
+ if ! df_has_git_marker "$PROJECT_ROOT" 2>/dev/null; then
537
+ dbg "tracker directive suppressed: project root is not a git repository"
538
+ return 1
539
+ fi
540
+
541
+ # Gate 3 — source. Only a fresh session (startup) and a cleared one (clear)
542
+ # begin work that needs conventions; resume and compact continue a session that
543
+ # already had its chance, so re-asking there would spawn an agent mid-flight.
544
+ TRACKER_SOURCE=$(printf '%s' "$INPUT" | json_field "source" "")
545
+ case "$TRACKER_SOURCE" in
546
+ startup|clear) ;;
547
+ *)
548
+ dbg "tracker directive suppressed: not a session start"
549
+ return 1
550
+ ;;
551
+ esac
552
+
553
+ # Gate 4 — the claim file, mirroring Section 2's freshness check. A FRESH
554
+ # claim means a live Tracker agent owns the run. A STALE one means a previous
555
+ # run crashed, so re-arm — but never delete it: re-claiming is the agent's job
556
+ # (it touches the file), and a hook that deleted it would race a slow-but-live
557
+ # run. An unreadable mtime falls to the suppressing branch (fail closed).
558
+ if [ -f "$TRACKER_CLAIM_FILE" ]; then
559
+ source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
560
+ _SC_TRACKER_MTIME=$(get_mtime "$TRACKER_CLAIM_FILE" 2>/dev/null || true)
561
+ _SC_TRACKER_NOW=$(date +%s)
562
+ if [ -n "$_SC_TRACKER_MTIME" ] && [ $(( _SC_TRACKER_NOW - _SC_TRACKER_MTIME )) -ge "$TRACKER_PROCESSING_STALE_SECS" ]; then
563
+ dbg "tracker claim file is stale — previous run crashed, re-arming"
564
+ else
565
+ dbg "tracker directive suppressed: fresh .tracker.processing (live agent owns the run)"
566
+ return 1
567
+ fi
568
+ fi
569
+
570
+ # Gate 5 — the shape of the two PATHS the directive carries alongside the two
571
+ # allowlisted tokens. Decided once at the top of the file, where both values are
572
+ # resolved; consulted here because this is one of the two sinks that interpolate
573
+ # them, and a control stated once for the file is not a control at a sink that
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.
577
+ if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
578
+ dbg "tracker directive suppressed: interpolated path shape rejected"
579
+ return 1
580
+ fi
581
+
582
+ # Gate 6 — the increment must LAND. [DR-02] Increment on EMISSION, not on the
583
+ # agent's completion: a crashed agent never reaches its own increment, so without
584
+ # this the crash-loop case stays uncapped even with the agent-side counter. The
585
+ # agent DELETES the counter on a successful write, so a healthy path never
586
+ # accumulates. printf is a builtin and the redirect is the shell's — no fork.
587
+ # `2>/dev/null` is spelled FIRST: redirections are applied left to right, and a
588
+ # failed open on the counter path is reported by the shell itself, so silencing
589
+ # stderr after the failing redirect would be too late to keep the hook quiet.
590
+ #
591
+ # Emission is CONDITIONAL on the write: an increment that cannot persist is a cap
592
+ # that can never engage, and the very same unwritable ~/.devflow also stops the
593
+ # agent writing its conventions file while leaving the sentinel in place.
594
+ # Emitting anyway would spawn a background agent at every startup, forever — an
595
+ # unbounded retry driven by the one condition the cap exists to bound.
596
+ if ! printf '%s\n' "$(( TRACKER_ATTEMPTS + 1 ))" 2>/dev/null > "$TRACKER_ATTEMPTS_FILE"; then
597
+ dbg "tracker directive suppressed: attempt counter not writable"
598
+ return 1
599
+ fi
600
+
601
+ return 0
602
+ }
603
+
604
+ if tracker_gates_open; then
605
+ # Allowlisted the same way LEARNING_MODEL is. The tier is
606
+ # a constant today — there is no tracker tuning config — so this `case` is an
607
+ # assertion of the closed domain rather than a sanitiser, and it is the single
608
+ # place the tier is validated, so a later config read cannot be wired in
609
+ # without passing through it. The literal must equal the Tracker agent's
610
+ # frontmatter `model:` (pinned against loadShippedDefaults in shell-hooks).
611
+ TRACKER_MODEL="sonnet"
612
+ case "$TRACKER_MODEL" in
613
+ opus|sonnet|haiku) ;;
614
+ *) TRACKER_MODEL="sonnet" ;;
615
+ esac
616
+
617
+ dbg "tracker directive emitted (provider=$TRACKER_PROVIDER model=$TRACKER_MODEL attempts=$TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
618
+ TRACKER_SECTION="--- TRACKER SETUP ---
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\")
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."
622
+ if [ -n "$CONTEXT" ]; then
623
+ CONTEXT="${CONTEXT}
624
+
625
+ ${TRACKER_SECTION}"
626
+ else
627
+ CONTEXT="$TRACKER_SECTION"
628
+ fi
629
+ fi
630
+ fi
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
+
167
681
  # --- Output ---
168
682
 
169
683
  # Only output if we have something to inject