devflow-kit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -66,11 +66,8 @@ elif [[ "$HEAD" == "Implement the following plan:"* ]]; then
66
66
  elif [[ "$HEAD" == "/"* ]]; then
67
67
  dbg "EXIT: slash command — no reminder"
68
68
  else
69
- # Model-tier taxonomy (haiku=mechanical, sonnet=defined execution, opus=analysis/design/research)
70
- # is cross-referenced with the full routing table in orchestrator-charter.md.
71
- # Update both together if routing changes.
72
69
  dbg "ORCHESTRATOR_REMINDER injected"
73
- json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds, multi-file reads, and debug loops via the Agent tool (haiku=mechanical, sonnet=defined execution, opus=analysis/design/research) or the matching devflow workflow skill.
70
+ json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds, multi-file reads, and debug loops to the fitting roster agent (Agent tool) or the matching devflow workflow skill.
74
71
  Keep only judgment work mainline: conversation, decisions, routing, synthesis of agent reports."
75
72
  fi
76
73
 
@@ -22,18 +22,41 @@
22
22
  # queue_append_both <memory_queue> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
23
23
  # Calls queue_append_row for each queue whose *_enabled flag is "true". Each
24
24
  # queue is gated independently -- callers compute memory_enabled/learning_enabled
25
- # from feature config themselves (see queue_read_gates below for the one-fork
26
- # combined read that keeps this to a single config subprocess per hook).
25
+ # themselves (see queue_read_gates below for the at-most-one-fork read of both).
27
26
  #
28
- # queue_read_gates <feature_config_path>
29
- # Reads BOTH the "memory" and "learning" fields from .devflow/config.json in a
30
- # SINGLE subprocess fork (AC-P1 -- exactly one config-read fork per capture
31
- # hook, not two). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false") in the
32
- # caller's scope. Missing config file -> both default "true". The two values
33
- # are newline-separated rather than using a control-character delimiter:
34
- # they are always the literal strings "true"/"false", never arbitrary
35
- # content, so a plain newline split is unambiguous and easy to review (no
36
- # invisible bytes hiding in the source).
27
+ # queue_read_gates <manifest_path>
28
+ # Reads BOTH `features.memory` and `features.learning` from the devflow-global
29
+ # ~/.devflow/manifest.json in at most ONE subprocess fork (AC-P1 -- at most one
30
+ # gate-read fork per capture hook, not two; none when D-GATES-FAST-PATH
31
+ # settles it). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false") in the
32
+ # caller's scope. The two values are newline-separated
33
+ # rather than using a control-character delimiter: they are always the literal
34
+ # strings "true"/"false", never arbitrary content, so a plain newline split is
35
+ # unambiguous and easy to review (no invisible bytes hiding in the source).
36
+ #
37
+ # D-FEATURES-MACHINE-WIDE (src/core/feature-switch.ts): memory and learning are
38
+ # switched for the WHOLE MACHINE by the manifest alone -- `devflow init` and
39
+ # `devflow memory|learning --enable/--disable` both write it -- so the answer is
40
+ # the same in every repo and every non-git cwd. The per-repo
41
+ # .devflow/config.json is never read here: its old memory/learning keys are
42
+ # retired, and reading them is what let a feature keep running in every repo
43
+ # but the one `init --no-<feature>` ran in (#378). Only an explicit boolean
44
+ # `false` switches a feature off -- an absent, unreadable or malformed manifest,
45
+ # a missing key, or a non-boolean value leaves it ON (fail-open, ADR-028), the
46
+ # same rule isMachineFeatureOn() applies in the CLI.
47
+ #
48
+ # D-LEARNING-LEGACY-DECISIONS: learning reads `features.learning` when it is a
49
+ # boolean, else the pre-rename `features.decisions` (ADR-011) -- readManifest's
50
+ # migration precedence exactly, so a legacy `decisions: false` switches
51
+ # learning off before any command has healed the file. Only a boolean `false`
52
+ # in whichever key decides is off, as above.
53
+ #
54
+ # Every hook that gates on memory or learning reads it through here: the
55
+ # capture hooks, session-start-context (Sections 1-2), memory-worker,
56
+ # session-start-memory, pre-compact-memory and background-memory-update. The
57
+ # callers resolve <manifest_path> from ${DEVFLOW_DIR:-$HOME/.devflow} BEFORE
58
+ # they shadow DEVFLOW_DIR with the project-scoped .devflow (memory-worker hands
59
+ # its resolved path to the background worker it spawns).
37
60
 
38
61
  queue_append_row() {
39
62
  local _qar_file="$1" _qar_role="$2" _qar_content="$3" _qar_ts="$4"
@@ -84,27 +107,48 @@ queue_append_both() {
84
107
  }
85
108
 
86
109
  queue_read_gates() {
87
- local _qg_config="$1"
110
+ local _qg_manifest="${1:-}" _qg_text="" _qg_parse="false"
88
111
  _QG_MEMORY="true"
89
112
  _QG_LEARNING="true"
90
113
 
91
- if [ -f "$_qg_config" ]; then
114
+ if [ -n "$_qg_manifest" ] && [ -f "$_qg_manifest" ]; then
115
+ # D-GATES-FAST-PATH: every capture hook and every session start pays this
116
+ # read, and on most machines both switches are on. Only a JSON boolean
117
+ # `false` under a "memory", "learning" or "decisions" key can switch anything
118
+ # off, and a `\u` escape is the only JSON spelling of a letter other than the
119
+ # letter itself, so without one such a key appears literally. A text holding
120
+ # no `\u` and no key-then-`false` sequence therefore cannot switch anything
121
+ # off -- valid or not, it reads both-on either way -- and is settled by shell
122
+ # builtins (read, case, [[ =~ ]]) with no jq/node fork; everything else takes
123
+ # the full parse below. `read -d ''` succeeds only on reaching a NUL byte,
124
+ # i.e. before the end of the file, so a text it cut short is parsed in full
125
+ # too. An unreadable file reads empty and fails open, as the parser would.
126
+ local _qg_re='"(memory|learning|decisions)"[[:space:]]*:[[:space:]]*false'
127
+ IFS= read -r -d '' _qg_text 2>/dev/null < "$_qg_manifest" && _qg_parse="true"
128
+ case "$_qg_text" in *'\u'*) _qg_parse="true" ;; esac
129
+ if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
130
+ fi
131
+
132
+ if [ "$_qg_parse" = "true" ]; then
92
133
  local _qg_fields
93
134
  if [ "$_HAS_JQ" = "true" ]; then
94
- # if/then/else (not //) preserves an explicit `false` -- mirrors json_field_file's
95
- # own documented rationale (jq's // would replace a real `false` with the default).
96
- # The comma produces two raw-output lines (newline-separated) in one jq process.
135
+ # `try` covers a manifest whose top level or `features` is not an object; a
136
+ # parse failure empties the output, which reads as "not switched off". The
137
+ # comma produces two raw-output lines (newline-separated) in one jq process.
97
138
  _qg_fields=$(jq -r '
98
- (if (.memory | type) == "null" then "true" else (.memory | tostring) end),
99
- (if (.learning | type) == "null" then "true" else (.learning | tostring) end)
100
- ' "$_qg_config" 2>/dev/null) || _qg_fields=""
139
+ (if (try .features.memory catch null) == false then "false" else "true" end),
140
+ (if (try (.features | if (.learning | type) == "boolean" then .learning else .decisions end) catch null) == false
141
+ then "false" else "true" end)
142
+ ' "$_qg_manifest" 2>/dev/null) || _qg_fields=""
101
143
  else
102
144
  _qg_fields=$(node -e "
103
- const j = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
104
- const m = j.memory === undefined ? 'true' : String(j.memory);
105
- const d = j.learning === undefined ? 'true' : String(j.learning);
106
- process.stdout.write(m + String.fromCharCode(10) + d);
107
- " -- "$_qg_config" 2>/dev/null) || _qg_fields=""
145
+ const m = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
146
+ const f = m !== null && typeof m === 'object' ? m.features : undefined;
147
+ const isObj = f !== null && typeof f === 'object';
148
+ const on = (k) => !(isObj && f[k] === false);
149
+ const learnKey = isObj && typeof f.learning !== 'boolean' ? 'decisions' : 'learning';
150
+ process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
151
+ " -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
108
152
  fi
109
153
  if [ -n "$_qg_fields" ]; then
110
154
  _QG_MEMORY="${_qg_fields%%$'\n'*}"
@@ -2,8 +2,9 @@
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 machine-wide `features.learning` switch in
6
+ # ~/.devflow/manifest.json (D-FEATURES-MACHINE-WIDE) — this hook itself is never
7
+ # disabled.
7
8
  #
8
9
  # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines).
9
10
  # Section 2: Learning maintenance directive — when captured turns are pending in
@@ -11,6 +12,10 @@
11
12
  # the main model to spawn the background Learning agent with the resolved model.
12
13
  # The agent claims the queue itself and queue emptiness is the natural gate,
13
14
  # 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.
14
19
 
15
20
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
16
21
  dbg() { :; }
@@ -61,18 +66,89 @@ PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
61
66
 
62
67
  CONTEXT=""
63
68
 
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}"
77
+
78
+ # The shape gate for every path this hook interpolates into a directive.
79
+ #
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".
85
+ #
86
+ # Positive, not a denylist of the characters that are known to hurt. A denylist
87
+ # enumerates the injections someone thought of — `"` closes the prompt string, `\`
88
+ # reads as an escape, LF and CR put the rest of the path on its own line as free
89
+ # text — and admits every one that was not on the list. An allowlist admits only
90
+ # what is known to be inert, so the next escape nobody has thought of is refused
91
+ # by construction rather than by a later amendment.
92
+ #
93
+ # 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.
98
+ #
99
+ # What the range does NOT promise is "ASCII only". A `case` bracket range
100
+ # collates under LC_COLLATE, so `A-Za-z0-9` admits an accented letter under a
101
+ # UTF-8 locale and refuses it under C. Every byte the gate exists to refuse —
102
+ # quote, backslash, CR, LF, space, backtick, `$`, `;` — is outside the range in
103
+ # both, so the security property holds either way; only the exact width of the
104
+ # admitted set is locale-dependent, and no rule here rests on it.
105
+ #
106
+ # Checked ONCE, here, where both values are resolved and above every section that
107
+ # interpolates them, so no sink can embed a value no gate saw (PF-023 — the
108
+ # invariant belongs at the convergence point all callers pass through, not in
109
+ # 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".
112
+ #
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.
120
+ DIRECTIVE_ROOT_SAFE="yes"
121
+ case "$PROJECT_ROOT" in
122
+ ''|*[!A-Za-z0-9/._-]*)
123
+ DIRECTIVE_ROOT_SAFE=""
124
+ ;;
125
+ esac
126
+
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.
130
+ DIRECTIVE_PATHS_SAFE="$DIRECTIVE_ROOT_SAFE"
131
+ case "$TRACKER_DEVFLOW_DIR" in
132
+ ''|*[!A-Za-z0-9/._-]*)
133
+ DIRECTIVE_PATHS_SAFE=""
134
+ ;;
135
+ esac
136
+
64
137
  DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
65
138
  LEARNING_DIR="$DEVFLOW_DIR/learning"
66
139
 
67
140
  # Normal logging
68
141
  source "$SCRIPT_DIR/hook-log-init" "session-start-context"
69
142
 
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
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.
149
+ 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"
151
+ LEARNING_ENABLED="$_QG_LEARNING"
76
152
 
77
153
  # --- Section 1: Project Decisions TL;DR ---
78
154
  if [ "$LEARNING_ENABLED" = "true" ]; then
@@ -130,6 +206,13 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
130
206
  LEARNING_WORK="queue"
131
207
  fi
132
208
 
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"
213
+ LEARNING_WORK=""
214
+ fi
215
+
133
216
  if [ -n "$LEARNING_WORK" ]; then
134
217
  # Model resolution: project learning.json → global ~/.devflow/learning.json → opus
135
218
  LEARNING_MODEL=""
@@ -164,6 +247,270 @@ ${LEARNING_SECTION}"
164
247
  fi
165
248
  fi
166
249
 
250
+ # --- 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.
255
+ #
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.
263
+ #
264
+ # Also gated on the project root being inside a git repository: the agent
265
+ # infers every repo-derived value from that history, and it writes the
266
+ # 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.
268
+ #
269
+ # Not gated by the learning feature toggle: a user who turned learning off did
270
+ # not turn their issue tracker off.
271
+ 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"
275
+ 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.
278
+ TRACKER_ATTEMPTS_MAX=5
279
+ # Its OWN literal, deliberately NOT shared with Learning's 900 above: one
280
+ # shared constant would make a change to either feature silently reclassify the
281
+ # other's live runs as crashed. 600s is longer than the memory worker's 300s
282
+ # lock (a Tracker run does more — a capability probe plus bounded git scans)
283
+ # and shorter than Learning's 900s (no multi-part curation phase). Too long
284
+ # costs one session's delay before a crashed run retries; too short burns an
285
+ # attempt against the cap.
286
+ TRACKER_PROCESSING_STALE_SECS=600
287
+
288
+ # tracker_gates_open — every gate after the sentinel, cheapest first, each one
289
+ # an early `return 1`.
290
+ #
291
+ # A plain `sh` block has no early exit: a gate there can suppress only by
292
+ # clearing a flag that every later gate must then re-test, which turns the
293
+ # cheapest-first ordering that delivers [DR-10]'s zero-fork guarantee into a
294
+ # stack of re-entries rather than the linear sequence it is. A function supplies
295
+ # the early exit: each gate states its own condition once, and the order below
296
+ # IS the execution order, read top to bottom.
297
+ #
298
+ # TRACKER_ATTEMPTS and TRACKER_PROVIDER are deliberately not `local`: they are
299
+ # this function's output, interpolated by the emitting block below.
300
+ tracker_gates_open() {
301
+ # Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
302
+ #
303
+ # The counter's shape is ONE decimal integer line and nothing else (PF-062 —
304
+ # document the shape of any file that gates an action, and keep absent and
305
+ # malformed distinct from a value). Absent means "no attempt yet" = 0.
306
+ # Malformed self-heals to 0 and is overwritten with a well-formed count on
307
+ # emission below, so a stray byte can never recur: refusing forever would
308
+ # disable inference permanently with no user-visible reason, and treating it as
309
+ # uncapped would defeat the cap. `read` returns non-zero at an EOF with no
310
+ # trailing newline but HAS assigned the variable, so its status is deliberately
311
+ # not consulted — only the value's shape is.
312
+ #
313
+ # `-n 16` bounds the BYTES consumed, which the `case` below cannot: the file is
314
+ # user-scope and hand-editable, and one very long line would otherwise be pulled
315
+ # whole into a shell variable and pattern-matched whole on the SessionStart
316
+ # critical path. Sixteen characters is past every arm's decision point — a
317
+ # well-formed count never reaches two digits, and the out-of-range arm fires at
318
+ # seven — so every arm keeps the verdict it would have reached unbounded.
319
+ # `2>/dev/null` is spelled BEFORE the input redirect for the same reason it is
320
+ # on the write below: redirections apply left to right, and a failed open is
321
+ # reported by the shell itself, so silencing stderr afterwards would be too late.
322
+ TRACKER_ATTEMPTS=""
323
+ if [ -f "$TRACKER_ATTEMPTS_FILE" ]; then
324
+ if [ -r "$TRACKER_ATTEMPTS_FILE" ]; then
325
+ IFS= read -r -n 16 TRACKER_ATTEMPTS 2>/dev/null < "$TRACKER_ATTEMPTS_FILE"
326
+ else
327
+ # Present but unreadable is NOT a fresh start. Leaving the variable empty
328
+ # would take the '' arm below and read as "no attempt yet", so an EACCES on
329
+ # the counter would emit at every startup forever — the unbounded retry the
330
+ # 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
332
+ # re-arm path (devflow init / devflow tracker --set) owns recovery, as it
333
+ # does for any counter the hook leaves sitting at the cap.
334
+ dbg "tracker attempt counter unreadable — treated as at the cap"
335
+ TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
336
+ fi
337
+ fi
338
+ case "$TRACKER_ATTEMPTS" in
339
+ '') TRACKER_ATTEMPTS=0 ;;
340
+ *[!0-9]*)
341
+ dbg "tracker attempt counter malformed — self-healed to 0"
342
+ TRACKER_ATTEMPTS=0
343
+ ;;
344
+ 0) ;; # a bare zero is a well-formed count, not a padded one
345
+ 0*)
346
+ # A zero-padded count is ONE string that this hook's two consumers of it read
347
+ # in DIFFERENT BASES. `[ "$N" -ge "$MAX" ]` parses base 10, so `08` compares
348
+ # as eight; the `$(( N + 1 ))` that writes the next count is shell arithmetic,
349
+ # where a leading `0` means OCTAL and `08` is "value too great for base" — an
350
+ # error that escapes the write's own `2>/dev/null`, because expansion runs
351
+ # before redirection. Nothing in the padded shape says which reading was
352
+ # meant, so it self-heals to 0 with every other malformed value instead of
353
+ # being carried into the disagreement. This hook writes a bare decimal, so
354
+ # padding came from elsewhere. Placed BEFORE the digit-count arm so a
355
+ # six-character `000008` heals rather than reading as "six digits, at the cap".
356
+ dbg "tracker attempt counter zero-padded — self-healed to 0"
357
+ TRACKER_ATTEMPTS=0
358
+ ;;
359
+ ???????*)
360
+ # Bounded before the comparison: `[ "$N" -ge 5 ]` on a value past intmax_t
361
+ # prints "integer expression expected" to stderr and takes the FALSE branch,
362
+ # so an unbounded digit string would fail OPEN — uncapped — and leak a shell
363
+ # error. `-n 16` above already puts that state out of reach; this arm is the
364
+ # semantic half and survives a change to the read bound. It matches SEVEN
365
+ # digits or more (the earlier arms have taken every non-digit and every
366
+ # zero-padded value), which is the shape CLAUDE.md documents as "at the cap",
367
+ # and a count that matters needs one digit, so anything reaching here is past
368
+ # the cap by six orders of magnitude. Six digits and fewer are compared as
369
+ # the integers they are.
370
+ dbg "tracker attempt counter out of range — treated as at the cap"
371
+ TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
372
+ ;;
373
+ esac
374
+ if [ "$TRACKER_ATTEMPTS" -ge "$TRACKER_ATTEMPTS_MAX" ]; then
375
+ dbg "tracker directive suppressed: attempt cap reached ($TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
376
+ return 1
377
+ fi
378
+
379
+ # Gate 2 — the repository. Every repo-derived value the Tracker agent writes
380
+ # comes from the project root's history, and the agent refuses to infer from
381
+ # history at all when the root carries no git marker — outside a checkout it
382
+ # would write its unresolved sentinel into every one of those sections. It
383
+ # writes the conventions file ONCE, create-exclusive, so that degraded file
384
+ # would be this machine's conventions for good, and the attempt that produced
385
+ # it would be spent against the cap.
386
+ #
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
395
+ if ! df_has_git_marker "$PROJECT_ROOT" 2>/dev/null; then
396
+ dbg "tracker directive suppressed: project root is not a git repository"
397
+ return 1
398
+ fi
399
+
400
+ # Gate 3 — source. Only a fresh session (startup) and a cleared one (clear)
401
+ # begin work that needs conventions; resume and compact continue a session that
402
+ # already had its chance, so re-asking there would spawn an agent mid-flight.
403
+ TRACKER_SOURCE=$(printf '%s' "$INPUT" | json_field "source" "")
404
+ case "$TRACKER_SOURCE" in
405
+ startup|clear) ;;
406
+ *)
407
+ dbg "tracker directive suppressed: not a session start"
408
+ return 1
409
+ ;;
410
+ esac
411
+
412
+ # Gate 4 — the claim file, mirroring Section 2's freshness check. A FRESH
413
+ # claim means a live Tracker agent owns the run. A STALE one means a previous
414
+ # run crashed, so re-arm — but never delete it: re-claiming is the agent's job
415
+ # (it touches the file), and a hook that deleted it would race a slow-but-live
416
+ # run. An unreadable mtime falls to the suppressing branch (fail closed).
417
+ if [ -f "$TRACKER_CLAIM_FILE" ]; then
418
+ source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
419
+ _SC_TRACKER_MTIME=$(get_mtime "$TRACKER_CLAIM_FILE" 2>/dev/null || true)
420
+ _SC_TRACKER_NOW=$(date +%s)
421
+ if [ -n "$_SC_TRACKER_MTIME" ] && [ $(( _SC_TRACKER_NOW - _SC_TRACKER_MTIME )) -ge "$TRACKER_PROCESSING_STALE_SECS" ]; then
422
+ dbg "tracker claim file is stale — previous run crashed, re-arming"
423
+ else
424
+ dbg "tracker directive suppressed: fresh .tracker.processing (live agent owns the run)"
425
+ return 1
426
+ fi
427
+ fi
428
+
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
455
+ # allowlisted tokens. Decided once at the top of the file, where both values are
456
+ # resolved; consulted here because this is one of the two sinks that interpolate
457
+ # them, and a control stated once for the file is not a control at a sink that
458
+ # never consults it (PF-023).
459
+ if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
460
+ dbg "tracker directive suppressed: interpolated path shape rejected"
461
+ return 1
462
+ fi
463
+
464
+ # Gate 7 — the increment must LAND. [DR-02] Increment on EMISSION, not on the
465
+ # agent's completion: a crashed agent never reaches its own increment, so without
466
+ # this the crash-loop case stays uncapped even with the agent-side counter. The
467
+ # agent DELETES the counter on a successful write, so a healthy path never
468
+ # accumulates. printf is a builtin and the redirect is the shell's — no fork.
469
+ # `2>/dev/null` is spelled FIRST: redirections are applied left to right, and a
470
+ # failed open on the counter path is reported by the shell itself, so silencing
471
+ # stderr after the failing redirect would be too late to keep the hook quiet.
472
+ #
473
+ # Emission is CONDITIONAL on the write: an increment that cannot persist is a cap
474
+ # 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.
476
+ # Emitting anyway would spawn a background agent at every startup, forever — an
477
+ # unbounded retry driven by the one condition the cap exists to bound.
478
+ if ! printf '%s\n' "$(( TRACKER_ATTEMPTS + 1 ))" 2>/dev/null > "$TRACKER_ATTEMPTS_FILE"; then
479
+ dbg "tracker directive suppressed: attempt counter not writable"
480
+ return 1
481
+ fi
482
+
483
+ return 0
484
+ }
485
+
486
+ if tracker_gates_open; then
487
+ # Allowlisted the same way LEARNING_MODEL is. The tier is
488
+ # a constant today — there is no tracker tuning config — so this `case` is an
489
+ # assertion of the closed domain rather than a sanitiser, and it is the single
490
+ # place the tier is validated, so a later config read cannot be wired in
491
+ # without passing through it. The literal must equal the Tracker agent's
492
+ # frontmatter `model:` (pinned against loadShippedDefaults in shell-hooks).
493
+ TRACKER_MODEL="sonnet"
494
+ case "$TRACKER_MODEL" in
495
+ opus|sonnet|haiku) ;;
496
+ *) TRACKER_MODEL="sonnet" ;;
497
+ esac
498
+
499
+ dbg "tracker directive emitted (provider=$TRACKER_PROVIDER model=$TRACKER_MODEL attempts=$TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
500
+ 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\")
503
+ 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
+ if [ -n "$CONTEXT" ]; then
505
+ CONTEXT="${CONTEXT}
506
+
507
+ ${TRACKER_SECTION}"
508
+ else
509
+ CONTEXT="$TRACKER_SECTION"
510
+ fi
511
+ fi
512
+ fi
513
+
167
514
  # --- Output ---
168
515
 
169
516
  # Only output if we have something to inject
@@ -42,20 +42,24 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
42
42
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
43
43
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
44
44
 
45
+ # The machine-wide manifest (the memory switch, D-FEATURES-MACHINE-WIDE in
46
+ # queue-append) is user-scope: resolve it from the inherited DEVFLOW_DIR (or
47
+ # ~/.devflow) BEFORE the project-scoped assignment below shadows that value.
48
+ DEVFLOW_MANIFEST="${DEVFLOW_DIR:-$HOME/.devflow}/manifest.json"
45
49
  DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
46
50
  MEMORY_DIR="$DEVFLOW_DIR/memory"
47
51
 
48
52
  # Normal logging
49
53
  source "$SCRIPT_DIR/hook-log-init" "session-start-memory"
50
54
 
51
- # Check feature config — single source of truth for memory enabled/disabled (ADR-001).
52
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
53
- if [ -f "$FEATURE_CONFIG" ]; then
54
- MEMORY_ENABLED=$(json_field_file "$FEATURE_CONFIG" "memory" "true")
55
- if [ "$MEMORY_ENABLED" = "false" ]; then
56
- dbg "EXIT: memory disabled in feature config"
57
- exit 0
58
- fi
55
+ # Memory gate: the machine-wide switch, read by the helper every memory/learning
56
+ # gate shares (D-FEATURES-MACHINE-WIDE, see queue-append). The per-repo config
57
+ # is never consulted.
58
+ source "$SCRIPT_DIR/queue-append" || { echo "session-start-memory: failed to source queue-append" >&2; exit 1; }
59
+ queue_read_gates "$DEVFLOW_MANIFEST"
60
+ if [ "$_QG_MEMORY" != "true" ]; then
61
+ dbg "EXIT: memory disabled machine-wide"
62
+ exit 0
59
63
  fi
60
64
 
61
65
  # --- D56c cold-path recovery: orphaned .pending-turns.processing ---